From 1eec469f19f765c1ca7f45bfe0e6bf39177e64f1 Mon Sep 17 00:00:00 2001 From: Aaron Judd Date: Wed, 19 Oct 2016 13:14:45 -0700 Subject: [PATCH] release v0.17.0 (#1506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added babelrc and stage-2 preset (#1329) * Meteor 1.4.1.1 (#1331) * METEOR@1.4.1.1 - METEOR@1.4.1.1 - updated Meteor packages, collections2 * version 0.15.2 * move plugin loader from startup to reaction-cli (#1332) * preserve custom profile fields in Accounts.onCreateUser (#1335) * copy deprecated cfs:gridfs to local and update npm deps (#1336) * rounding of total to negate fractions of cents * updated version - release 0.15.3 * Josh acceptance test setup user permissions (#1315) * First round setting up acceptance test capabilities * webdriver working * adding meteor ignore file structure * create tests directory * adding png to gitignore * element maps * More progress on functional test setup * fixed assert * using dot notation * adding chai to dev packages * Begin adding acceptance testing instructions to readme * Simple login test complete * Adding to readme about failed tests screenshots * adding stuff * Core permissions acceptance test * more fixes * went off the deep end. added some low hanging fruit tests * enabling all passing tests for now * fixed userAddress call * removing docs from main readme * linting for config fixed * more cleanup * additional cleanup * using chrome instead of firefox now since ff is borked * adding refund test * a couple more refund tests w/ a couple fixes Will add more assertions later for these tests. * adding new tests to runner * argh * remove screenshot * remove ignore of all .png * updated package.json * updated reaction.json.example * finishing up PR * lint cleanup * temp workaround for missing product in store * A couple locator changes - commented out broken stripe tests * persistent profile language (#1338) * allow multiple languages in Translations - allow array to pass multiple languages in addition to shopLanguage. * updated i18next and Translation load order * remove session handling from i18n - remove “language” session - rely on Meteor.user updates (stored lang in profile) - update i18nchooser helpers to insert user profile lang into selected element class * retain language on anonymous user registration * check admin before setting lang — don’t set lang on initial admin * updated accounts.js - cleanup checks * Josh test speed increase #1347 (#1355) * turning verbosity off when running tests by default * Removing sleeps/pauses when I can * flaky test fix * #1347 cut runtime down by nearly 50% This is still just running synchronously. As tests grows we will need to look into running tests in parallel, however this might require some infrastructure in place, i.e. a hosted QA environment. An environment that can handle the load of 5-10 tests hitting it at a time. * only build Docker image on master/development (#1359) * only build Docker image on master/development * chmod +x * fix CI script typos * load default user from env (#1369) resolves #1368 * remove duplicate translation * remove duplicate translations * Shipping 1320 (#1357) * disabled ranges - TODO ranges are not functional yet. * update shipping form methods * remove settings from registry * update swal version - updated sweetAlert2 version * export Import, Fixture - export Fixture independently of Import - refactor Import.shipping to use a modified key - refactor load-data to use Fixture * updated Shipping schema * updated shipping method names - breaking change: shipping method names updated to follow convention of Reaction `updateShippingMethods` becomes `shipping/methods/update`. - adds `shipping/provider/remove` method * add missing import * convert info logging to debug * updated form methods - update form methods - add SWAL2 confirmations * add handling to cart rates - if `handling` has data add `handling` to `rate`. * updated cartTransforms - updated to use own methods and parse float to ensure consistent results. * move shipping from included to core like taxes, the core will include flat rates and the ui handling for additional shipping methods, which will be added to included - in the form of `shipping-shippo` etc. * remove card cursor:pointer (#1384) Resolves #1309 * Product Search (#1350) * Mock Search Files * Mock Search Files * Remove addProduct method * Remove Fixtures to remove mystery Shop * Super limited, even for a fake search * Don't overwrite the descriptions on first run * Larger data set for testing * Don't log unless we are actually adding Search Products * A simple weighted product search * Split out plug ins into "search" and "searchEngine" plugs. * Improve product title search, add search settings panel * Add in variant search * Add more options for weighting. Make the settings panel better. * Add tags to results and search on description * Rename plugin to be consistent with others * Add example config file * New test data with "show store" model with tags * Simple product subscription that returns products and facets (tags) for a searchterm * Only return ids and names of tags (we will eventually only return ids, but names for readability for now) * Filter by tags * We only need one plugin since our decoupling is provided by the use of a publication * Rename plugin * Using Mongo full-text search rather than regex search * Using Mongo full-text search rather than regex search * alphabetize base-style imports * Add in hooks to keep PS collection updated * Move build/rebuild functions into module * New settings for fields and weights * New settings for fields and weights * add in hooks * New settings for fields and weights * Build PS collection dynamically based on configuration * Add in vendor to list of possible fields * initial push of search UI * Move collections to lib so that clients can subscribe * Add shopId to required fields * Move collections to global collections * Use ProductSearch collection on the client side * Filter tags by hashtags in ProductSearch results * Populate template with search results * Product search results sorted by score in Ascending order * Move (re)building search collections to job queue * Handle form with a method so that we can rebuild collection or index when settings change * Queue job to rebuild indexes when weights have changed * Don't run search hooks when ProductSearch collection has not been defined * Add Order search * Namespace product settings * Don't run hooks when under test * Add missing "shopId" field * More fixes to make OrderSearch collection configurable. * Ensure index when inserting product records * testing larger datasets * Make function names less ridiculously long * Pass results without relying on the product already existing in the subscription * Don't add a bunch of products for testing by default * style cleanup * style cleanup * style cleanup * Add handle to product search collection * Clarify log statement * Ensure but don't drop collections when creating an OrderSearch record * No longer build productResults on the client * Cleanup and add `maxResults` * Cleanup settings form * Don't queue search collection jobs when under test * Search result tests * Add price to results. Make results different based upon logged-in status * Add tests for non-published products not showing up for anonymous users * Add tests for non-published products not showing up for anonymous users * Add account search, jobs, etc. * Change account search to not use text search * ui initial push * conflict temporary fix * move style into less * i18n for search * added delay on fadeout to feign smoother transition * sped up tag color transitions * componentize search results content * componentize search results grid * style updates for search modal * get media to show in searchGrid * updated id date for searchGridIcons * Use hashtags not hastags * Change order search to use a regular query plus tests * removed console.log * fixed typo * switched content order * commit of admin buttons (currently not functional) * Add support for text indexes in other supported languages * Make order search results case-insensitive * close button classes * removed outline from nav * removed additional media from search We only need to show one picture in search, for now at least * updated text * added search clear * style updates * Delete all existing records when rebuilding account search * Remove startup test files * Also remove test data files * Make logging less chatty * Allow for transformations outside of the ProductSearch method * convert tags to flexbox * style updates * adding scroll tag container * clean-up of development comments * LESS cleanup * style cleanup * style updates * 96% width input & tags formerly 80% * move some styling from search.less to navbar.less * cleanup & i18n * remove errant button * changed tag active color to ban-active * Add documentation around "supported language" as suggested by CR * added i18n to search * changed search tags to use btn classes * disable currently unused admin controls * rtl support * rtl style adjustments * Exclude anonymous users from accounts publication (#1390) * Exclude anonymous users from accounts publication when logged in as admin * NPM imports first * update deprecated _.pluck - Patch _.pluck = _.map in lodash 4.x - resolves issue introduced in #1390 * metafields -> details for client visibility (#1397) * metafields -> details for client visibility * metafields -> details * Search UI updates (#1410) * add margin-bottom to hovered product for better visibility on last product * removed font-size from .btn.flat * created react flatButton created a new react button component so we aren’t just if/elseing our way through a different type of button to get what we need. * adjustment for #1402 - added overflow toggle on body * removed font size from btn-round * allow ESC to exit modal even when not focused on input * Release 0.16.0 (#1413) * updated README.md * update to v0.16.0 * conflict fixes * updated versions * Better email (#1367) * convert email sending to use Nodemailer * add getShopSettings and getPackageSettings helpers * replace all Meteor accounts-password email methods * remove old email api * rewrite mail config methods to use nodemailer-wellknown * create emails collection for longer term log storage * add mail service to shop mail settings schema * refactor email sending job to use new API * add Meteor methods for email API * add email publication for logs page * build new email dashboard UI plugin for using new API * remove old Blaze email settings * fix alerts arg checks when callback is missing * add new FieldGroup and Loading React components * default theme cleanup * fix variables import order in default theme * dependencies updates * clean up job queue workers * update eslint config for more React details * fix custom email config * fix type handling for email config port * remove email dashboard nav tabs component until needed * add saving state and better error handling to email settings * change email status route name * remove deprecated mail config hook * add unblock to email config verify method * add host/port to conditions for custom email config check * move email methods to base of methods dir * package updates * fix comment typo * update dockerignore to reflect new location * default vals for mail host/port not needed in registry entry * fix optional email save when settings are invalid * fix email settings save button state after errors * don’t run email settings verify method if settings are missing * add log when new email settings are saved * npm updates * fix text overflow in email config panel * fix type checking on email port setting * fix syntax * i18n-ify all new email components * fix sweetalert promise * fix 118n prop * remove password from logs * force immediate retry of email job * add new column in email log table for action buttons * remove unnecessary braces * fix missing email port in UI * package updates * restore searching for email templates from the database * clean up imports method * Check if billing address exists (#1386) Prevents exception if there is no billing address * Hide user menu (#1387) Hide user menu after clicking on any link, including "add product" * Make close button work on tag nav bar in mobile mode (#1393) * Prevent no image being found if no image has priority 0. `"metadata.priority": 0` filter will fail to find an image if none has priority 0, so sort is used instead. (#1392) * Try to prevent inline alert pile (#1391) * updated pull approve to version 2 See: http://docs.pullapprove.com/ and https://blog.pullapprove.com/pullapprove-github-code-review-43761a64ea9a #.enx67rp3f * updated .pullapprove.yml * updated negative reject value * updated reset_on_push * updated pullapprove.yml * update pullapprove.yml -ARRGHHH! * package.json update for nodemailer * Install binary non-npm version of phantomjs. reactioncommerce/reaction#1325 (#1419) * add timeout to taxes tests (#1421) * Renamed field_group to fieldGroup to be consistent with other files (#1417) * Fixes #1337 (#1449) * ro-translation (#1441) * publish authorize-net on npm under @reactioncommerce org (#1451) * Issue/1452 (#1453) * Fixes #1337 * Fixes #1452 * Prevent error in `cartPayerName` helper if buyer name contains non-latin characters. The latter are not accepted by payment gateways, resulting the error in the payment form. (#1432) * PDP Revisions and Revised (#1356) * Initial start for handling product revisions Changes to products / variants are diverted to a draft in the "Revisions" collection. Calling publish updates original products with revision data. * Product visibility and draft reactivity restored Allow products publication to be notified of updates if a revision changes. Expand product visibility to variants. Allow product visibility toggle to be toggled for revisions. * Cleanups and added ability to mark products as deleted * Publishing and diffs Added diffs. Added diff UI. Added publish component. Added translations for react components. * Added ability to publish multiple top level products Draft and publishing now woks from the product grid for one or many products. Clean up. * Fix show diffs toggle * tag create / remove are now properly directed to a revision. * Fixed bug preventing metadata from revision updating Fixed bug that prevented revision updating all together. * Added some tests for product handle and revisions fixed issue causing tests to not run at all. * updated lodash dependencies - should resolve failing test * updating tests for product revisions * Updated more product tests. Updated some catalog implementations to work with revision control. * Fixing more tests * Fixing even more product tests. Adding tests for revisions along with fixing broken tests. * Removing some test expects as they have no chance of every being true * PDP react start of an overhaul of PDP page to fix many outlying bugs and upgrade it to better support revision control and display of revision content. * Variant and edit component updates Added variant list container. Added variant component, updated variant list to use component. Added EditContainer component which provides an edit button based on passed in props to child component. * added child variants to variant list * Added tooltips, cart button and revision object handling * React product detail page updates Metadata for product detail page. Cleaning up react tag components. Cleaning up old react components to be used on new product detail page. * Added tags component to PDP * Updated CSS classes for react PDP media gallery * UI updates for varaints Added visibility buttons to variants and child variants Added currency helpers. Added inventory alerts to varaints * Cleanup of price formatting Price formatting was moved to currency.js. Removing functionality from the helpers file. Fixed only implementation on formatMoney with module. * Provide translation updated to product detail container * React based social component * Added pinterest button Removed generic social button. Other cleanup. * Edit buttons for everything Added edit buttons to all fields, tags, and metadata. Added component for product field. Edit buttons can now display status based on edited field. * product detail editing improvements Auto hide edit button for product fields that remain unchanged. Fixed editing behavior for multiline textfield. * removed template tag * product admin form react Add a new product admin form for managing the react version of the product detail page. * metadata management and style updates Add and remove metadata now works. Updated styles for metafields. * Adding WIP react tag list editor * React product detail page is now primary other various cleanup of PropTypes, and console logs * Remove git ignored config file * Updated textfield className handling * Add ability to define multiple fields on edit button for diffs * Product social container how handles change notifications properly * Selectable variant and child variant Added ability to prevent default action for edit container edit button click. Added functionality to show action view for editing a variant. * Cleanup product detail tags and metadata. Edit buttons for both product tags and metadata now open the ProductAdmin view in the side panel. * React based inline alert components and containers * React based drag and drop. Top variants are now draggable and sortable. Tags are now draggable and sortable. Added DragDropProvider to create a reusable dnd-core instance for react components. Added container container component for sortable items. * Create, update and remove tag functionality added. * Update style of customer facing metadata component Style of the non-editable, customer-facing metadata component now appears like a table, similar in style to the editable counterpart. * Move media gallery to core ui components * removed unused component * Media gallery upload and remove * Media gallery drag and drop sortable * Updated registry entries and media gallery Added registry entries for new product detail page. Disable registry entries for old product detail page. Fixed bug causing crash with Media Gallery and undefined variables. * Fixed erroneous display of edit buttons with permissions * Switch media gallery for ul to div * Fixed exception caused by undefined data * Add publish controls to product admin * Updated publishing workflow. Publishing a product revision now properly sets it as "published" and will allow a new draft revision to be made for any new edits after that point. This allows a history of published changes to be stored. * Updated package.json with react-dnd and react-dropzone dependencies. * Tag updates - Fixes issues related to creating new tags - Fixes issues related to updating existing tags - Fixes issue where duplicated tags are added - Adds ability to exculde tags from suggestions - Adds new midifier for hashtags in product revision update hook - Updated styling of editable tags. Edidatbe tags in admin view are now full width - Removed dead code from revision hooks * Updated some references to lodash isArray to use built in Array.isArray * Fixed broken multi-file uploads for media gallery. Cleanup of unused code. * Added add button to add images using the file picker. * Fixed missing card titled for product admin * Renamed classname to match file name. * Added card group component. * Updated product admin with card group component * Updated product admin fields to be multiline * Updated metadata component Removed local state from metadata component. Parent container should handle all updates to state. Other various cleanup. * Fix broken tag test Skip publish test, revisit its usefulness later * Re-Enabled and updated product publication tests. updated publication collector package. * clean up product publication tests * Fixed slowdown caused by social buttons. Added helper to create settings for social buttons. * Fix tag submit on enter key * Updated tags Fixed issues with drag and drop re-ordering. Fixed drag handle; now only the handle is draggable instead of the entire tag. Sortable item no longer auto wraps elements, instead passes helpers functions to composed component. * Updated drag-n-drop for variants Drag and drop now only allowed if user has edit permissions. cleanup. * Updated media drag-n-drop Drag and drop is now enabled only if the media item is editable. Drag preview now shows up properly. * Update meta field styles and RTL styles * Add metadata (details) placeholders with translations * Load and display child variant media. * Fixed issues causing suggestions to change on arrow key input * Add placeholder for tag input * Updated media gallery uploader placeholder display In edit mode, media gallery now only placeholder item in the first slot, until a media item is uploaded. * space-out social buttons * add link type button * Add basic ability to change product handle manually * Added button toggle text, and fixed translation issues * Product delete callback * ensure product handle is always slug-ified * Removed duplicated delete function Replaced native confirm with a custom alert. * removed console.log * Fix for incorrect product handle being applied to grid items * Props and PropType fixes * Translation component updates. Added i18nKeys for visibility status * Add settings panel for revision control settings * Allow disable of revision control system. Disables collection hooks and product / products publications. * Cleanup and removed unused props. Added parentTag to other callbacks in tags component * Tooltip plugin cleanup * Added ability to disable new product detail page alongside revision control * More css selector specificity * Enable / disable publish controls * Publish revision control enabled status to users. This will allow the old / new PDP pages to be visible non admin users if revision control is enabled / disabled. * Fix legacy PDP media gallery styles. Moved new media gallery styles to a more appropriate place. * Added media gallery hover When enabled, hovering over images in the media gallery will replace the first image with the hovered media item. This effect is most visible on the new product detail page. * Fix safari flex box bug with icon button. Cleanup. * Add max width to PDP container. * Force add icon index file * removed console log in sortableItem * moved revision logging level to debug * Fixed bug that caused any change to a variant to toggle visibility Removed special handling of visibility from the collection hook. Added missing visibility toggle to edit container. * Load media for social buttons * Removed logging statement * Added badge for deleted variants * Fix weird variant selection behavior * Fix for undefined FB reference and cleanup * Fix for broken tests due to revision control on / off state * Updated react-autosuggest to 6.0.4 Updated instances of react-autosuggest to use new API * Fixed typo on visibility button tooltip Fixed issue with button tooltips not being translated if passed as a string. * Clicking a variant while action view is open will now focus it for editing. * Fixed typo in CSS classname * Fixed classname typo * Fix errors caused by change event attached to file upload input * Added ability to restore product variants from trash Top variants, and child variants can now be undeleted, while still in an unpublished draft. Fixed issue with variantForm not being reactively updated when variant was changed. * Don't delete media when variant is deleted. Changed because products are not deleted, only sent to trash. Leaving the media reference is important if you ever want to restore that item from the trash. * Adds ability to restore a deleted product for current draft * Updated alert confirm to SWAL * Fixed issues causing incorrect revision to be displayed for top variant * remove security group from pull approve - we will add back later… * update for 0.17.0 * Don't try to add user to OrderSearch when user doesn't exist (#1460) * Skip adding emails when user doesn't exist * Remove README that's no longer relevant * Set submitting state to handle error case correctly (#1461) * Docker build improvements (#1468) * clean up unused or unnecessary build deps * install all npm deps in first Meteor build step * create new base Docker image for better build caching * install MongoDB in container and use it if no external MONGO_URL is found * move deps versions to Dockerfile only for easier maintenance * fix custom package file script * add build script for full Docker build of base and app * fix lodash/camelcase import that was breaking the production build * npm updates * update CircleCI scripts to use new Docker build * Updated publish controls UX and fixing issues (#1473) * Updated publish controls UX and fixing issues - Fix bug causing error in console when publishing nothing. - Disabled the publish button if there is nothing to publish. - Added alert toasts for successful and unsuccessful publish. - Publish controls now properly finds relevant revisions. * Fixed typo * Fixed typo in comment * Fixed controlled component issue with input (#1469) * Loading 1455 (#1471) * Updated loader component to be consistent with other spinners Added loader for PDP container. Added a basic circular progress component. Removed domkit package. Replaced loader with already available spinner component through CSS. * Renamed loading.jsx to loading.js. JSX extension is unnecessary. * METEOR@1.4.1.2 * Added placeholders product fields (#1472) * Fixed bug that caused visibility button to not save a change. (#1477) * Fixed bug that caused visibility button to not save a change. * Address issue where visibility icon button could only be used once per draft revision * Fix product grid visibility button toggle inconsistencies. * Added comment for legacy publish functionality * Fix visibility toggle for product grid item * Removed tooltip from visibility button (#1479) * browserstack example (#1409) * beginning of getting browserstack working * More configs for browserstack * runner has been moved * creating test suite helper * fixes and moving some stuff around * More fixes. Allow the user to pick and choose what test suites to run. This will make it easier for the user if they are switching test runners frequently. I didn’t like adding all the tests in one array per configuration. This fixes that. Also allows the user to pick and choose what test suites apply to them. * lint fix * Adding reporter for tests, changed admin email, adding npm scripts for running tests * Allow Allure reporting to be optional * Primer for switching login/checkout tests to register/checkout * Login tests, are now register tests This also removes the dependency to having to create a user prior to running * Adding reporting to browserstack conf Also, added npm script for opening report. Another thing I noticed, browserstack tunnel sometimes hangs. So added process killer. * Updating test descriptions * lint fix * updating npm package * Addresses login test with hardcoded user assertion * This allows the user to use dynamic ids in tests * updated login test to pass local * updated test packages * updated from development - use REACTION_USER/AUTH if available. * updated versions * updated user data * Select products not flagged as deleted (#1489) * Selected products not flagged as deleted * Select all products not flagged as deleted * Add nul to second selector * fix order context in orderListSummary (#1493) * Publish 1490 (#1499) * Added a publish alert to the PDP page Updated alert component to support child components * Added toolbar for publish controls * Updated publish controls and product toolbar Added ability to change visibility of product from toolbar. Added better controls for switching view context. Added dropdown component. Updated menu components. Various other fixes. * Added ability to discard draft changes * Cleanup * Added ability to disable menu items * Cleanup * added ability to discard changes added ability to trigger a delete for relevant documents * Removed delete and visibility buttons from product detail form * Hide edit buttons when view as query param is "customer" * clean up * Cleanup * Renamed Product Settings to Product Details Removed commented code in registry entry for product-variant * Hide toolbar for non admin users. Cleanup eslint issues. * Ensure view "as" query param is preserved on route change * Added icons to menu items * Hide tooltip when button is disabled * Fixed type in callback * add ability to pass in original documents as a default * Added styles for disabled menu items Improved spacing divider in menu * add react-addons-create-fragment * fixed typo * Account & Order Search (#1494) * Change `userIsInRole` to `hasPermission` * initial commit * initial commit * i18n updates * breaking templates into smaller files * breaking LESS into smaller files * importing new templates * push for collaboration * i18n updates * file organization * helper functions for orders and accounts * focus input when changing search type * Make email search case-insensitive * push for visibility * update account & order results to tables * removed old accountGrid * Build account search using required fields and transformations * testing "fixed-data-table" * Merge branch 'development' into ek-search-accounts-orders # Conflicts: # imports/plugins/core/ui/client/components/index.js * Make order build dynamic as well (unfinished) * Use transformations for orders and accounts * Add exact search by _id, regex search by name, and match by "cleaned" phone * Refactor to eliminate duplicated code * Merge branch 'development' into ek-search-accounts-orders # Conflicts: # package.json * update table to react-component * updated fields for account resutls * field name correction * updated admin search permissions * renamed LESS files * Search for lastname/firstname in profile subfield * Fix error when profile is null * fixed propTypes error * added new fields to order results * i18n Updates * new search results * style updates * i18n updates * updates to account search * new permissions for search types * file cleanup * file cleanup * file cleanup * add order date to results * added style for shipping status * removed billing status from results display * Handle results when profile is null * add click to orderID shippingStatus panel orderID now goes to the order status / shippingInfo panel on click * fixes for PR comments * Wildcard search on emails in accounts/orders * Don't log so much * Add phone to Account Search * Simple migrations * Configure migrations not to log * Change subdir name to versions so we don't have stuff in migrations/migrations * Integrate migration logging with Reaction logging * Move files into core * Cleanup packages a little * Add a down method * Rename directory * Rename subdirectory * Correct import * fixed hardcoded shop urlSlug * remove unneeded props * accountSearch with user management * Re-namespace Migrations as Versions * killed unused prop * Re-re-structure migrations * Add README with warning about changing API * update migration nstantiation - Run before, not on startup, so that options take affect before Collections are declared. * remove unused lodash * update import order * New Email Template for Order Confirmation (#1501) * confirmation email for orders 95% ready. pushing for access on another machine. * updated media / image code * fix discounts data * localizing social images * update image handling * fixed store logo * update import order * removed un-needed dataForOrderEmail key * added babel-plugin-lodash * fixed single quotes * remove old "new.html" file * [WIP] Updated Translations (#1495) * LingoHub Update :rocket: Manual push by LingoHub User: Aaron Judd. Project: reaction Made with :heart: by https://lingohub.com * LingoHub Update :rocket: Manual push by LingoHub User: Aaron Judd. Project: reaction Made with :heart: by https://lingohub.com * Remove _README.md - remove out of date _README * restore missing rtl * add default en ltr - add default to see if lingo populates through other files. * disable non active default languages - disable languages that are without translators * updated en.json Fix Grammar * cleanup merge - remove deprecated files * remove duplicate key * review cleanup (#1509) * review cleanup - add i18n phrase for Tag in Use - add i18next to tag Alerts - fix typo in edItContainers - updated import order for styleguide adherance - comment WIP debug code for visibility button * add email, revisions dashboard phrases --- .babelrc | 1 + .eslintrc | 4 +- .gitignore | 1 + .meteor/packages | 16 +- .meteor/release | 2 +- .meteor/versions | 54 +- .pullapprove.yml | 11 - .reaction/docker/base.dockerfile | 49 + .reaction/docker/build.sh | 5 + .reaction/docker/reaction.ci.dockerfile | 47 - .reaction/docker/reaction.dev.dockerfile | 46 - .reaction/docker/reaction.prod.dockerfile | 43 - .reaction/docker/scripts/build-meteor.sh | 2 +- .reaction/docker/scripts/build-packages.sh | 6 +- .reaction/docker/scripts/ci-build.sh | 4 +- .reaction/docker/scripts/entrypoint.sh | 14 +- .reaction/docker/scripts/install-deps.sh | 2 +- .reaction/docker/scripts/install-mongo.sh | 27 + .reaction/docker/scripts/install-node.sh | 1 - .reaction/docker/scripts/install-phantom.sh | 9 +- .../docker/scripts/post-build-cleanup.sh | 4 +- .../docker/scripts/post-install-cleanup.sh | 2 +- Dockerfile | 6 +- circle.yml | 9 +- client/modules/i18n/currency.js | 124 ++ client/modules/i18n/helpers.js | 116 +- client/modules/i18n/index.js | 1 + .../core/checkout/client/helpers/cart.js | 12 +- .../templates/layout/alerts/inlineAlerts.js | 2 + .../client/templates/list/ordersList.html | 2 +- .../templates/workflow/shippingInvoice.js | 4 +- .../client/components/publishControls.js | 277 ++++ .../revisions/client/components/settings.js | 52 + .../revisions/client/components/simpleDiff.js | 86 ++ .../client/containers/publishContainer.js | 133 ++ .../client/containers/settingsContainer.js | 77 + .../plugins/core/revisions/client/index.js | 2 + .../revisions/client/templates/settings.html | 5 + .../revisions/client/templates/settings.js | 9 + imports/plugins/core/revisions/index.js | 1 + .../plugins/core/revisions/lib/api/index.js | 1 + .../core/revisions/lib/api/revisions.js | 28 + imports/plugins/core/revisions/register.js | 31 + .../plugins/core/revisions/server/index.js | 2 + .../plugins/core/revisions/server/methods.js | 122 ++ .../core/revisions/server/publications.js | 24 + .../plugins/core/revisions/server/startup.js | 263 ++++ .../core/ui-tagnav/client/helpers/tags.js | 4 +- .../core/ui/client/components/alerts/alert.js | 73 + .../ui/client/components/alerts/alerts.js | 29 + .../core/ui/client/components/alerts/index.js | 2 + .../ui/client/components/button/button.jsx | 170 ++- .../ui/client/components/button/handle.js | 34 + .../ui/client/components/button/iconButton.js | 23 +- .../core/ui/client/components/button/index.js | 5 + .../components/button/visibilityButton.js | 25 + .../components/buttonGroup/buttonGroup.js | 55 +- .../components/buttonGroup/buttonToolbar.js | 23 + .../ui/client/components/buttonGroup/index.js | 2 + .../core/ui/client/components/cards/card.js | 17 + .../ui/client/components/cards/cardBody.js | 17 + .../ui/client/components/cards/cardGroup.js | 17 + .../ui/client/components/cards/cardHeader.js | 34 + .../ui/client/components/cards/cardTitle.js | 29 + .../core/ui/client/components/cards/index.js | 5 + .../ui/client/components/checkbox/checkbox.js | 39 + .../ui/client/components/checkbox/index.js | 1 + .../ui/client/components/divider/divider.js | 44 + .../forms/{field_group.js => fieldGroup.js} | 0 .../core/ui/client/components/icon/icon.jsx | 16 +- .../core/ui/client/components/icon/index.js | 1 + .../core/ui/client/components/index.js | 23 +- .../core/ui/client/components/items/items.js | 19 + .../ui/client/components/loading/loading.js | 14 + .../ui/client/components/loading/loading.jsx | 107 -- .../core/ui/client/components/media/index.js | 2 + .../core/ui/client/components/media/media.js | 102 ++ .../core/ui/client/components/media/media.jsx | 39 - .../client/components/media/mediaGallery.js | 142 ++ .../ui/client/components/menu/dropDownMenu.js | 76 + .../core/ui/client/components/menu/index.js | 3 + .../core/ui/client/components/menu/menu.js | 48 + .../ui/client/components/menu/menuItem.js | 80 + .../ui/client/components/metadata/index.js | 2 + .../ui/client/components/metadata/metadata.js | 124 ++ .../client/components/metadata/metadata.jsx | 124 -- .../client/components/metadata/metafield.js | 128 ++ .../ui/client/components/popover/popover.js | 113 ++ .../components/popover/popoverContent.js | 25 + .../components/progress/circularProgress.js | 27 + .../client/components/separator/separator.js | 30 - .../core/ui/client/components/table/table.js | 27 + .../core/ui/client/components/tags/index.js | 2 + .../core/ui/client/components/tags/tag.jsx | 488 ++++--- .../core/ui/client/components/tags/tagItem.js | 5 +- .../core/ui/client/components/tags/tags.jsx | 422 +++--- .../client/components/textfield/textfield.js | 172 +++ .../client/components/textfield/textfield.jsx | 144 -- .../ui/client/components/toolbar/index.js | 3 + .../ui/client/components/toolbar/toolbar.js | 25 + .../client/components/toolbar/toolbarGroup.js | 27 + .../client/components/toolbar/toolbarText.js | 24 + .../ui/client/components/tooltip/tooltip.js | 62 + .../client/components/translation/currency.js | 18 + .../ui/client/components/translation/index.js | 2 + .../components/translation/translation.js | 24 + .../ui/client/containers/alertContainer.js | 48 + .../ui/client/containers/editContainer.js | 173 +++ .../core/ui/client/containers/index.js | 5 + .../containers/mediaGalleryContainer.js | 179 +++ .../core/ui/client/containers/sortableItem.js | 138 ++ .../ui/client/containers/tagListContainer.js | 276 ++++ .../ui/client/providers/dragDropProvider.js | 45 + .../plugins/core/ui/client/providers/index.js | 3 + .../core/ui/client/providers/translatable.js | 17 + .../client/providers/translationProvider.js | 36 + imports/plugins/core/versions/README.md | 5 + imports/plugins/core/versions/index.js | 1 + imports/plugins/core/versions/register.js | 10 + imports/plugins/core/versions/server/index.js | 2 + ...ld_account_and_order_search_collections.js | 22 + .../core/versions/server/migrations/index.js | 1 + .../plugins/core/versions/server/startup.js | 20 + .../authnet/server/methods/authnet.js | 2 +- .../default-theme/client/styles/base.less | 5 +- .../default-theme/client/styles/button.less | 40 +- .../client/styles/dropdowns.less | 7 + .../default-theme/client/styles/main.less | 5 +- .../default-theme/client/styles/media.less | 118 ++ .../default-theme/client/styles/menu.less | 16 + .../default-theme/client/styles/metadata.less | 170 ++- .../default-theme/client/styles/popover.less | 78 +- .../client/styles/products/attributes.less | 50 +- .../client/styles/products/productDetail.less | 36 +- .../client/styles/products/productGrid.less | 6 - .../styles/products/productImageGallery.less | 1 + .../client/styles/products/variant.less | 4 + .../client/styles/products/variantList.less | 6 +- .../client/styles/search/results.less | 1 - .../styles/search/search-type-toggle.less | 27 + .../client/styles/search/sortable-table.less | 166 +++ .../default-theme/client/styles/tags.less | 15 +- .../client/styles/textfield.less | 12 +- .../default-theme/client/styles/toolbar.less | 32 + .../default-theme/client/styles/tooltip.less | 4 +- .../client/styles/variables.less | 8 + .../client/checkout/example.js | 3 + .../server/methods/inventory.app-test.js | 29 +- .../product-admin/client/components/index.js | 1 + .../client/components/productAdmin.js | 258 ++++ .../product-admin/client/containers/index.js | 1 + .../containers/productAdminContainer.js | 160 ++ .../included/product-admin/client/index.js | 2 + .../client/templates/productAdmin.html | 5 + .../client/templates/productAdmin.js | 16 + .../included/product-admin/register.js | 0 .../client/components/addToCartButton.js | 42 + .../client/components/childVariant.js | 89 ++ .../client/components/index.js | 7 + .../client/components/metadata.js | 66 + .../client/components/productDetail.js | 205 +++ .../client/components/productField.js | 132 ++ .../client/components/tags.js | 67 + .../client/components/variant.js | 116 ++ .../client/components/variantList.js | 159 ++ .../client/containers/index.js | 3 + .../containers/productDetailContainer.js | 261 ++++ .../client/containers/socialContainer.js | 100 ++ .../client/containers/variantListContainer.js | 213 +++ .../product-detail-simple/client/index.js | 2 + .../client/selectors/variants.js | 39 + .../client/templates/productDetailSimple.html | 11 + .../client/templates/productDetailSimple.js | 11 + .../product-detail-simple/register.js | 37 + .../products/productDetail/attributes.js | 20 +- .../products/productDetail/productDetail.html | 8 +- .../products/productDetail/productDetail.js | 95 +- .../productDetail/variants/variant.html | 1 + .../productDetail/variants/variant.js | 23 +- .../variants/variantForm/childVariant.html | 27 +- .../variants/variantForm/childVariant.js | 46 +- .../variants/variantForm/variantForm.html | 34 +- .../variants/variantForm/variantForm.js | 33 +- .../variants/variantList/variantList.html | 5 +- .../variants/variantList/variantList.js | 22 +- .../products/productGrid/controls.html | 8 + .../templates/products/productGrid/item.html | 2 +- .../templates/products/productGrid/item.js | 28 +- .../products/productGrid/productGrid.js | 6 +- .../productSettings/productSettings.html | 7 + .../productSettings/productSettings.js | 51 +- .../client/templates/products/products.js | 9 +- .../included/product-variant/register.js | 11 - .../plugins/included/search-mongo/README.md | 6 - .../search-mongo/server/hooks/search.js | 6 +- .../server/jobs/buildSearchCollections.js | 7 +- .../server/methods/searchcollections.js | 136 +- .../server/methods/transformations.js | 28 + .../publications/searchresults.app-test.js | 15 +- .../server/publications/searchresults.js | 54 +- .../social/client/components/facebook.js | 112 ++ .../social/client/components/googleplus.js | 85 ++ .../social/client/components/index.js | 5 + .../social/client/components/pinterest.js | 65 + .../social/client/components/socialButtons.js | 84 ++ .../social/client/components/twitter.js | 97 ++ .../client/containers/socialContainer.js | 31 + .../plugins/included/social/lib/helpers.js | 36 + .../included/stripe/client/checkout/stripe.js | 4 + .../stripe/server/methods/stripeapi.js | 12 +- .../included/ui-search/client/index.js | 31 +- .../accountSearch/accountResults.html | 9 + .../templates/accountSearch/accountResults.js | 166 +++ .../templates/orderSearch/orderResults.html | 9 + .../templates/orderSearch/orderResults.js | 158 ++ .../content.html | 0 .../{searchGrid => productSearch}/content.js | 0 .../controls.html | 0 .../{searchGrid => productSearch}/controls.js | 2 - .../{searchGrid => productSearch}/notice.html | 0 .../{searchGrid => productSearch}/notice.js | 0 .../productItem.html} | 2 +- .../item.js => productSearch/productItem.js} | 30 +- .../productResults.html} | 4 +- .../productResults.js} | 8 +- .../productSearch/productSearchTags.html | 15 + .../client/templates/searchModal.html | 36 - .../templates/searchModal/searchInput.html | 8 + .../templates/searchModal/searchModal.html | 21 + .../{ => searchModal}/searchModal.js | 127 +- .../templates/searchModal/searchResults.html | 19 + .../searchModal/searchTypeToggle.html | 13 + lib/api/catalog.js | 29 +- lib/api/products.js | 161 ++- lib/collections/collections.js | 6 + lib/collections/schemas/index.js | 1 + lib/collections/schemas/products.js | 24 +- lib/collections/schemas/revisions.js | 61 + lib/collections/schemas/tags.js | 4 + lib/selectors/tags.js | 7 + lib/selectors/variants.js | 1 + package.json | 73 +- private/data/Products.json | 3 + private/data/Shops.json | 72 +- private/data/i18n/_README.md | 21 - private/data/i18n/ar.json | 927 ++++++++++-- private/data/i18n/bg.json | 25 +- private/data/i18n/cs.json | 187 ++- private/data/i18n/de.json | 1103 ++++++++++---- private/data/i18n/el.json | 13 +- private/data/i18n/en.json | 84 +- private/data/i18n/es.json | 1004 ++++++++++--- private/data/i18n/fr.json | 925 ++++++++++-- private/data/i18n/he.json | 1090 ++++++++++---- private/data/i18n/hr.json | 15 +- private/data/i18n/hu.json | 12 +- private/data/i18n/it.json | 379 +++-- private/data/i18n/my.json | 318 ++-- private/data/i18n/nb.json | 17 +- private/data/i18n/nl.json | 599 ++++---- private/data/i18n/pl.json | 186 ++- private/data/i18n/pt.json | 96 +- private/data/i18n/ro.json | 833 +++++++++++ private/data/i18n/ru.json | 168 ++- private/data/i18n/sl.json | 186 ++- private/data/i18n/sv.json | 186 ++- private/data/i18n/tr.json | 624 ++++---- private/data/i18n/vi.json | 160 +- private/data/i18n/zh.json | 505 ++++++- private/email/templates/orders/new.html | 1282 +++++------------ .../email-templates/facebook-icon.png | Bin 0 -> 1474 bytes .../email-templates/google-plus-icon.png | Bin 0 -> 1610 bytes .../resources/email-templates/shop-logo.png | Bin 0 -> 21270 bytes .../email-templates/twitter-icon.png | Bin 0 -> 1613 bytes server/methods/catalog.app-test.js | 496 ++++++- server/methods/catalog.js | 90 +- server/methods/core/orders.js | 89 +- server/publications/collections/packages.js | 1 + .../product-publications.app-test.js | 271 ++-- server/publications/collections/product.js | 96 +- server/publications/collections/products.js | 95 +- server/publications/collections/revisions.js | 40 + tests/acceptance-tests/config/settings.yml | 8 + .../config/test-suite-config.yml | 17 + tests/acceptance-tests/config/user-data.yml | 11 +- .../acceptance-tests/elements/element-ids.yml | 3 + .../acceptance-tests/elements/element-map.yml | 6 + .../lib/basic-user-actions.js | 25 +- tests/acceptance-tests/lib/get-elements.js | 18 + tests/acceptance-tests/lib/get-xpath.js | 7 - .../authnet}/authorizenet-refund.app-test.js | 6 +- .../authnet}/authorizenet-void.app-test.js | 6 +- .../guest-authorizenet-checkout.app-test.js | 4 +- ...egister-authorizenet-checkout.app-test.js} | 10 +- .../braintree}/braintree-refund.app-test.js | 6 +- .../braintree}/braintree-void.app-test.js | 6 +- .../guest-braintree-checkout.app-test.js | 4 +- .../register-braintree-checkout.app-test.js} | 10 +- .../example-payment-refund.app-test.js | 6 +- ...guest-example-payment-checkout.app-test.js | 4 +- ...ster-example-payment-checkout.app-test.js} | 10 +- .../paypal}/guest-paypal-checkout.app-test.js | 4 +- .../paypal}/paypal-refund.app-test.js | 6 +- .../register-paypal-checkout.app-test.js} | 6 +- .../stripe}/guest-stripe-checkout.app-test.js | 4 +- .../register-stripe-checkout.app-test.js} | 10 +- .../stripe}/stripe-refund.app-test.js | 6 +- .../dashboard-permissions.app-test.js | 4 +- .../shop-settings-permissions.app-test.js | 2 +- .../simple-login.app-test.js | 18 +- tests/runner/browserstack.conf.js | 70 + tests/runner/local.conf.js | 40 + tests/runner/test-suite.js | 48 + wdio.conf.js | 208 --- 314 files changed, 18133 insertions(+), 5804 deletions(-) create mode 100644 .reaction/docker/base.dockerfile create mode 100755 .reaction/docker/build.sh delete mode 100644 .reaction/docker/reaction.ci.dockerfile delete mode 100644 .reaction/docker/reaction.dev.dockerfile delete mode 100644 .reaction/docker/reaction.prod.dockerfile create mode 100644 .reaction/docker/scripts/install-mongo.sh mode change 120000 => 100644 Dockerfile create mode 100644 client/modules/i18n/currency.js create mode 100644 imports/plugins/core/revisions/client/components/publishControls.js create mode 100644 imports/plugins/core/revisions/client/components/settings.js create mode 100644 imports/plugins/core/revisions/client/components/simpleDiff.js create mode 100644 imports/plugins/core/revisions/client/containers/publishContainer.js create mode 100644 imports/plugins/core/revisions/client/containers/settingsContainer.js create mode 100644 imports/plugins/core/revisions/client/index.js create mode 100644 imports/plugins/core/revisions/client/templates/settings.html create mode 100644 imports/plugins/core/revisions/client/templates/settings.js create mode 100644 imports/plugins/core/revisions/index.js create mode 100644 imports/plugins/core/revisions/lib/api/index.js create mode 100644 imports/plugins/core/revisions/lib/api/revisions.js create mode 100644 imports/plugins/core/revisions/register.js create mode 100644 imports/plugins/core/revisions/server/index.js create mode 100644 imports/plugins/core/revisions/server/methods.js create mode 100644 imports/plugins/core/revisions/server/publications.js create mode 100644 imports/plugins/core/revisions/server/startup.js create mode 100644 imports/plugins/core/ui/client/components/alerts/alert.js create mode 100644 imports/plugins/core/ui/client/components/alerts/alerts.js create mode 100644 imports/plugins/core/ui/client/components/alerts/index.js create mode 100644 imports/plugins/core/ui/client/components/button/handle.js create mode 100644 imports/plugins/core/ui/client/components/button/index.js create mode 100644 imports/plugins/core/ui/client/components/button/visibilityButton.js create mode 100644 imports/plugins/core/ui/client/components/buttonGroup/buttonToolbar.js create mode 100644 imports/plugins/core/ui/client/components/buttonGroup/index.js create mode 100644 imports/plugins/core/ui/client/components/cards/card.js create mode 100644 imports/plugins/core/ui/client/components/cards/cardBody.js create mode 100644 imports/plugins/core/ui/client/components/cards/cardGroup.js create mode 100644 imports/plugins/core/ui/client/components/cards/cardHeader.js create mode 100644 imports/plugins/core/ui/client/components/cards/cardTitle.js create mode 100644 imports/plugins/core/ui/client/components/cards/index.js create mode 100644 imports/plugins/core/ui/client/components/checkbox/checkbox.js create mode 100644 imports/plugins/core/ui/client/components/checkbox/index.js create mode 100644 imports/plugins/core/ui/client/components/divider/divider.js rename imports/plugins/core/ui/client/components/forms/{field_group.js => fieldGroup.js} (100%) create mode 100644 imports/plugins/core/ui/client/components/icon/index.js create mode 100644 imports/plugins/core/ui/client/components/items/items.js create mode 100644 imports/plugins/core/ui/client/components/loading/loading.js delete mode 100644 imports/plugins/core/ui/client/components/loading/loading.jsx create mode 100644 imports/plugins/core/ui/client/components/media/index.js create mode 100644 imports/plugins/core/ui/client/components/media/media.js delete mode 100644 imports/plugins/core/ui/client/components/media/media.jsx create mode 100644 imports/plugins/core/ui/client/components/media/mediaGallery.js create mode 100644 imports/plugins/core/ui/client/components/menu/dropDownMenu.js create mode 100644 imports/plugins/core/ui/client/components/menu/index.js create mode 100644 imports/plugins/core/ui/client/components/menu/menu.js create mode 100644 imports/plugins/core/ui/client/components/menu/menuItem.js create mode 100644 imports/plugins/core/ui/client/components/metadata/index.js create mode 100644 imports/plugins/core/ui/client/components/metadata/metadata.js delete mode 100644 imports/plugins/core/ui/client/components/metadata/metadata.jsx create mode 100644 imports/plugins/core/ui/client/components/metadata/metafield.js create mode 100644 imports/plugins/core/ui/client/components/popover/popover.js create mode 100644 imports/plugins/core/ui/client/components/popover/popoverContent.js create mode 100644 imports/plugins/core/ui/client/components/progress/circularProgress.js delete mode 100644 imports/plugins/core/ui/client/components/separator/separator.js create mode 100644 imports/plugins/core/ui/client/components/table/table.js create mode 100644 imports/plugins/core/ui/client/components/tags/index.js create mode 100644 imports/plugins/core/ui/client/components/textfield/textfield.js delete mode 100644 imports/plugins/core/ui/client/components/textfield/textfield.jsx create mode 100644 imports/plugins/core/ui/client/components/toolbar/index.js create mode 100644 imports/plugins/core/ui/client/components/toolbar/toolbar.js create mode 100644 imports/plugins/core/ui/client/components/toolbar/toolbarGroup.js create mode 100644 imports/plugins/core/ui/client/components/toolbar/toolbarText.js create mode 100644 imports/plugins/core/ui/client/components/tooltip/tooltip.js create mode 100644 imports/plugins/core/ui/client/components/translation/currency.js create mode 100644 imports/plugins/core/ui/client/components/translation/index.js create mode 100644 imports/plugins/core/ui/client/components/translation/translation.js create mode 100644 imports/plugins/core/ui/client/containers/alertContainer.js create mode 100644 imports/plugins/core/ui/client/containers/editContainer.js create mode 100644 imports/plugins/core/ui/client/containers/index.js create mode 100644 imports/plugins/core/ui/client/containers/mediaGalleryContainer.js create mode 100644 imports/plugins/core/ui/client/containers/sortableItem.js create mode 100644 imports/plugins/core/ui/client/containers/tagListContainer.js create mode 100644 imports/plugins/core/ui/client/providers/dragDropProvider.js create mode 100644 imports/plugins/core/ui/client/providers/index.js create mode 100644 imports/plugins/core/ui/client/providers/translatable.js create mode 100644 imports/plugins/core/ui/client/providers/translationProvider.js create mode 100644 imports/plugins/core/versions/README.md create mode 100644 imports/plugins/core/versions/index.js create mode 100644 imports/plugins/core/versions/register.js create mode 100644 imports/plugins/core/versions/server/index.js create mode 100644 imports/plugins/core/versions/server/migrations/1_rebuild_account_and_order_search_collections.js create mode 100644 imports/plugins/core/versions/server/migrations/index.js create mode 100644 imports/plugins/core/versions/server/startup.js create mode 100644 imports/plugins/included/default-theme/client/styles/menu.less create mode 100644 imports/plugins/included/default-theme/client/styles/search/search-type-toggle.less create mode 100644 imports/plugins/included/default-theme/client/styles/search/sortable-table.less create mode 100644 imports/plugins/included/default-theme/client/styles/toolbar.less create mode 100644 imports/plugins/included/product-admin/client/components/index.js create mode 100644 imports/plugins/included/product-admin/client/components/productAdmin.js create mode 100644 imports/plugins/included/product-admin/client/containers/index.js create mode 100644 imports/plugins/included/product-admin/client/containers/productAdminContainer.js create mode 100644 imports/plugins/included/product-admin/client/index.js create mode 100644 imports/plugins/included/product-admin/client/templates/productAdmin.html create mode 100644 imports/plugins/included/product-admin/client/templates/productAdmin.js create mode 100644 imports/plugins/included/product-admin/register.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/addToCartButton.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/childVariant.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/index.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/metadata.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/productDetail.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/productField.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/tags.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/variant.js create mode 100644 imports/plugins/included/product-detail-simple/client/components/variantList.js create mode 100644 imports/plugins/included/product-detail-simple/client/containers/index.js create mode 100644 imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js create mode 100644 imports/plugins/included/product-detail-simple/client/containers/socialContainer.js create mode 100644 imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js create mode 100644 imports/plugins/included/product-detail-simple/client/index.js create mode 100644 imports/plugins/included/product-detail-simple/client/selectors/variants.js create mode 100644 imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.html create mode 100644 imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.js create mode 100644 imports/plugins/included/product-detail-simple/register.js delete mode 100644 imports/plugins/included/search-mongo/README.md create mode 100644 imports/plugins/included/social/client/components/facebook.js create mode 100644 imports/plugins/included/social/client/components/googleplus.js create mode 100644 imports/plugins/included/social/client/components/index.js create mode 100644 imports/plugins/included/social/client/components/pinterest.js create mode 100644 imports/plugins/included/social/client/components/socialButtons.js create mode 100644 imports/plugins/included/social/client/components/twitter.js create mode 100644 imports/plugins/included/social/client/containers/socialContainer.js create mode 100644 imports/plugins/included/social/lib/helpers.js create mode 100644 imports/plugins/included/ui-search/client/templates/accountSearch/accountResults.html create mode 100644 imports/plugins/included/ui-search/client/templates/accountSearch/accountResults.js create mode 100644 imports/plugins/included/ui-search/client/templates/orderSearch/orderResults.html create mode 100644 imports/plugins/included/ui-search/client/templates/orderSearch/orderResults.js rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/content.html (100%) rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/content.js (100%) rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/controls.html (100%) rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/controls.js (96%) rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/notice.html (100%) rename imports/plugins/included/ui-search/client/templates/{searchGrid => productSearch}/notice.js (100%) rename imports/plugins/included/ui-search/client/templates/{searchGrid/item.html => productSearch/productItem.html} (96%) rename imports/plugins/included/ui-search/client/templates/{searchGrid/item.js => productSearch/productItem.js} (77%) rename imports/plugins/included/ui-search/client/templates/{searchGrid/searchGrid.html => productSearch/productResults.html} (86%) rename imports/plugins/included/ui-search/client/templates/{searchGrid/searchGrid.js => productSearch/productResults.js} (93%) create mode 100644 imports/plugins/included/ui-search/client/templates/productSearch/productSearchTags.html delete mode 100644 imports/plugins/included/ui-search/client/templates/searchModal.html create mode 100644 imports/plugins/included/ui-search/client/templates/searchModal/searchInput.html create mode 100644 imports/plugins/included/ui-search/client/templates/searchModal/searchModal.html rename imports/plugins/included/ui-search/client/templates/{ => searchModal}/searchModal.js (51%) create mode 100644 imports/plugins/included/ui-search/client/templates/searchModal/searchResults.html create mode 100644 imports/plugins/included/ui-search/client/templates/searchModal/searchTypeToggle.html create mode 100644 lib/collections/schemas/revisions.js create mode 100644 lib/selectors/tags.js create mode 100644 lib/selectors/variants.js delete mode 100644 private/data/i18n/_README.md create mode 100644 private/data/i18n/ro.json create mode 100644 public/resources/email-templates/facebook-icon.png create mode 100644 public/resources/email-templates/google-plus-icon.png create mode 100644 public/resources/email-templates/shop-logo.png create mode 100644 public/resources/email-templates/twitter-icon.png create mode 100644 server/publications/collections/revisions.js create mode 100644 tests/acceptance-tests/config/test-suite-config.yml create mode 100644 tests/acceptance-tests/elements/element-ids.yml create mode 100644 tests/acceptance-tests/lib/get-elements.js delete mode 100644 tests/acceptance-tests/lib/get-xpath.js rename tests/acceptance-tests/test/specs/{ => payment-processors/authnet}/authorizenet-refund.app-test.js (91%) rename tests/acceptance-tests/test/specs/{ => payment-processors/authnet}/authorizenet-void.app-test.js (90%) rename tests/acceptance-tests/test/specs/{ => payment-processors/authnet}/guest-authorizenet-checkout.app-test.js (91%) rename tests/acceptance-tests/test/specs/{logged-in-authorizenet-checkout.app-test.js => payment-processors/authnet/register-authorizenet-checkout.app-test.js} (80%) rename tests/acceptance-tests/test/specs/{ => payment-processors/braintree}/braintree-refund.app-test.js (91%) rename tests/acceptance-tests/test/specs/{ => payment-processors/braintree}/braintree-void.app-test.js (90%) rename tests/acceptance-tests/test/specs/{ => payment-processors/braintree}/guest-braintree-checkout.app-test.js (92%) rename tests/acceptance-tests/test/specs/{logged-in-braintree-checkout.app-test.js => payment-processors/braintree/register-braintree-checkout.app-test.js} (80%) rename tests/acceptance-tests/test/specs/{ => payment-processors/example}/example-payment-refund.app-test.js (90%) rename tests/acceptance-tests/test/specs/{ => payment-processors/example}/guest-example-payment-checkout.app-test.js (92%) rename tests/acceptance-tests/test/specs/{logged-in-example-payment-checkout.app-test.js => payment-processors/example/register-example-payment-checkout.app-test.js} (80%) rename tests/acceptance-tests/test/specs/{ => payment-processors/paypal}/guest-paypal-checkout.app-test.js (92%) rename tests/acceptance-tests/test/specs/{ => payment-processors/paypal}/paypal-refund.app-test.js (89%) rename tests/acceptance-tests/test/specs/{logged-in-paypal-checkout.app-test.js => payment-processors/paypal/register-paypal-checkout.app-test.js} (89%) rename tests/acceptance-tests/test/specs/{ => payment-processors/stripe}/guest-stripe-checkout.app-test.js (91%) rename tests/acceptance-tests/test/specs/{logged-in-stripe-checkout.app-test.js => payment-processors/stripe/register-stripe-checkout.app-test.js} (81%) rename tests/acceptance-tests/test/specs/{ => payment-processors/stripe}/stripe-refund.app-test.js (90%) rename tests/acceptance-tests/test/specs/{ => permissions}/dashboard-permissions.app-test.js (91%) rename tests/acceptance-tests/test/specs/{ => permissions}/shop-settings-permissions.app-test.js (95%) rename tests/acceptance-tests/test/specs/{ => smoke-tests}/simple-login.app-test.js (55%) create mode 100644 tests/runner/browserstack.conf.js create mode 100644 tests/runner/local.conf.js create mode 100644 tests/runner/test-suite.js delete mode 100644 wdio.conf.js diff --git a/.babelrc b/.babelrc index 9ccae96ed05..0d172c5c9e0 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { + "plugins": ["lodash"], "presets": ["stage-2"] } diff --git a/.eslintrc b/.eslintrc index 39a5cdb9f34..b1896755675 100644 --- a/.eslintrc +++ b/.eslintrc @@ -86,9 +86,9 @@ "jsx-quotes": [2, "prefer-double"], // http://eslint.org/docs/rules/jsx-quotes "react/no-deprecated": 1, "react/display-name": 1, - "react/forbid-prop-types": 1, + "react/forbid-prop-types": 0, "react/jsx-boolean-value": 0, - "react/jsx-closing-bracket-location": [1, "after-props"], + "react/jsx-closing-bracket-location": 1, "react/jsx-curly-spacing": 1, "react/jsx-indent-props": [1, 2], "react/jsx-max-props-per-line": [1, { diff --git a/.gitignore b/.gitignore index 15517295466..a0e3fc5c9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ node_modules npm-debug.log pids results +allure-results client/plugins.js server/plugins.js diff --git a/.meteor/packages b/.meteor/packages index e937c7a03ad..9df6d6e8758 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -10,26 +10,26 @@ meteor-base@1.0.4 # Packages every Meteor app needs to have mobile-experience@1.0.4 # Packages for a great mobile UX blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views -es5-shim@4.6.14 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.5.8 # Enable ECMAScript2015+ syntax in app code +es5-shim@4.6.14_1 # ECMAScript 5 compatibility for older browsers. +ecmascript@0.5.8_1 # Enable ECMAScript2015+ syntax in app code audit-argument-checks@1.0.7 # ensure meteor method argument validation browser-policy@1.0.9 # security-related policies enforced by newer browsers juliancwirko:postcss # CSS post-processing plugin (replaces standard-minifier-css) -standard-minifier-js@1.2.0 # a minifier plugin used for Meteor apps by default +standard-minifier-js@1.2.0_1 # a minifier plugin used for Meteor apps by default session@1.1.6 # ReactiveDict whose contents are preserved across Hot Code Push tracker@1.1.0 # Meteor transparent reactive programming library -mongo@1.1.12 +mongo@1.1.12_1 random@1.0.10 reactive-var@1.0.10 reactive-dict@1.1.8 check@1.2.3 -http@1.2.9 +http@1.2.9_1 ddp-rate-limiter@1.0.5 underscore@1.0.9 -logging@1.1.15 +logging@1.1.15_1 reload@1.1.10 ejson@1.0.12 -less@2.7.5 +less@2.7.5_1 service-configuration@1.0.10 amplify mdg:validated-method @@ -75,6 +75,8 @@ risul:moment-timezone tmeasday:publish-counts vsivsi:job-collection react-meteor-data +percolate:migrations +johanbrook:publication-collector@1.0.2 # Testing packages dburles:factory diff --git a/.meteor/release b/.meteor/release index 72980bc2f53..2631a23310e 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.4.1.1 +METEOR@1.4.1.2 diff --git a/.meteor/versions b/.meteor/versions index f7a73a052fe..0b3afb09c72 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,4 +1,4 @@ -accounts-base@1.2.11 +accounts-base@1.2.12_1 accounts-facebook@1.0.10 accounts-google@1.0.10 accounts-oauth@1.1.13 @@ -16,8 +16,8 @@ allow-deny@1.0.5 amplify@1.0.0 audit-argument-checks@1.0.7 autoupdate@1.3.11 -babel-compiler@6.9.1 -babel-runtime@0.1.11 +babel-compiler@6.9.1_1 +babel-runtime@0.1.11_1 base64@1.0.9 binary-heap@1.0.9 blaze@2.1.9 @@ -28,7 +28,7 @@ browser-policy@1.0.9 browser-policy-common@1.0.10 browser-policy-content@1.0.11 browser-policy-framing@1.0.11 -caching-compiler@1.1.7 +caching-compiler@1.1.7_1 caching-html-compiler@1.0.7 callback-hook@1.0.9 cfs:access-point@0.1.49 @@ -53,21 +53,21 @@ cfs:upload-http@0.0.20 cfs:worker@0.1.4 check@1.2.3 chuangbo:cookie@1.1.0 -coffeescript@1.2.4_1 +coffeescript@1.2.4_2 dburles:factory@1.1.0 ddp@1.2.5 -ddp-client@1.3.1 +ddp-client@1.3.1_1 ddp-common@1.2.6 ddp-rate-limiter@1.0.5 -ddp-server@1.3.10 +ddp-server@1.3.10_1 deps@1.0.12 diff-sequence@1.0.6 dispatch:mocha@0.0.9 -ecmascript@0.5.8 -ecmascript-runtime@0.3.14 +ecmascript@0.5.8_1 +ecmascript-runtime@0.3.14_1 ejson@1.0.12 -email@1.1.17 -es5-shim@4.6.14 +email@1.1.17_1 +es5-shim@4.6.14_1 facebook@1.2.9 fastclick@1.0.12 geojson-utils@1.0.9 @@ -75,9 +75,10 @@ google@1.1.14 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 -http@1.2.9 +http@1.2.9_1 id-map@1.0.8 jeremy:stripe@1.6.0 +johanbrook:publication-collector@1.0.2 jparker:crypto-core@0.1.0 jparker:crypto-md5@0.1.1 jparker:gravatar@0.5.1 @@ -89,14 +90,14 @@ kadira:blaze-layout@2.3.0 kadira:dochead@1.5.0 kadira:flow-router-ssr@3.13.0 launch-screen@1.0.12 -less@2.7.5 +less@2.7.5_1 livedata@1.0.18 localstorage@1.0.11 -logging@1.1.15 +logging@1.1.15_1 matb33:collection-hooks@0.8.4 mdg:validated-method@1.1.0 mdg:validation-error@0.5.1 -meteor@1.2.17 +meteor@1.2.17_1 meteor-base@1.0.4 meteorhacks:fast-render@2.16.0 meteorhacks:inject-data@2.0.0 @@ -104,27 +105,28 @@ meteorhacks:meteorx@1.4.1 meteorhacks:picker@1.0.3 meteorhacks:ssr@2.2.0 meteorhacks:subs-manager@1.6.4 -minifier-css@1.2.14 -minifier-js@1.2.14 +minifier-css@1.2.14_1 +minifier-js@1.2.14_1 minimongo@1.0.17 mobile-experience@1.0.4 mobile-status-bar@1.0.12 -modules@0.7.6 -modules-runtime@0.7.6 +modules@0.7.6_1 +modules-runtime@0.7.6_1 momentjs:moment@2.15.1 -mongo@1.1.12 +mongo@1.1.12_5 mongo-id@1.0.5 mongo-livedata@1.0.12 mrt:later@1.6.1 -npm-bcrypt@0.9.1 -npm-mongo@1.5.49 +npm-bcrypt@0.9.1_1 +npm-mongo@2.2.10_1 oauth@1.1.11 oauth-encryption@1.2.0 oauth1@1.1.10 oauth2@1.1.10 -observe-sequence@1.0.12 +observe-sequence@1.0.13 ongoworks:security@2.0.1 ordered-dict@1.0.8 +percolate:migrations@0.9.8 practicalmeteor:chai@2.1.0_1 practicalmeteor:mocha-core@1.0.1 practicalmeteor:sinon@1.14.1_2 @@ -147,18 +149,18 @@ shell-server@0.2.1 spacebars@1.0.13 spacebars-compiler@1.0.13 srp@1.0.9 -standard-minifier-js@1.2.0 +standard-minifier-js@1.2.0_1 templating@1.2.15 templating-compiler@1.2.15 templating-runtime@1.2.15 templating-tools@1.0.5 tmeasday:check-npm-versions@0.3.1 -tmeasday:publish-counts@0.7.3 +tmeasday:publish-counts@0.8.0 tracker@1.1.0 twitter@1.1.12 ui@1.0.12 underscore@1.0.9 url@1.0.10 vsivsi:job-collection@1.4.0 -webapp@1.3.11 +webapp@1.3.11_1 webapp-hashing@1.0.9 diff --git a/.pullapprove.yml b/.pullapprove.yml index d46b6bc09c2..67f479f4865 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -42,14 +42,3 @@ groups: - mikemurray - zenweasel - kieckhafer - security: - author_approval: - ignored: true - reject_value: -100 - required: -1 - reset_on_push: - enabled: true - reset_on_reopened: - enabled: true - users: - - Capt-Slow diff --git a/.reaction/docker/base.dockerfile b/.reaction/docker/base.dockerfile new file mode 100644 index 00000000000..5dc6bca608f --- /dev/null +++ b/.reaction/docker/base.dockerfile @@ -0,0 +1,49 @@ +FROM debian:jessie +MAINTAINER Reaction Commerce + +ENV NODE_VERSION "4.6.0" + +# Install MongoDB +ENV INSTALL_MONGO "true" +ENV MONGO_VERSION "3.2.10" +ENV MONGO_MAJOR "3.2" + +# Install PhantomJS +ENV INSTALL_PHANTOMJS "true" +ENV PHANTOM_VERSION "2.1.1" + +# build directories +ENV APP_SOURCE_DIR "/opt/reaction/src" +ENV APP_BUNDLE_DIR "/opt/reaction/dist" +ENV BUILD_SCRIPTS_DIR "/opt/reaction/build_scripts" + +# Add entrypoint and build scripts +COPY .reaction/docker/scripts $BUILD_SCRIPTS_DIR +RUN chmod -R +x $BUILD_SCRIPTS_DIR + +# install base dependencies and clean up +RUN cd $BUILD_SCRIPTS_DIR && \ + bash $BUILD_SCRIPTS_DIR/install-deps.sh && \ + bash $BUILD_SCRIPTS_DIR/install-node.sh && \ + bash $BUILD_SCRIPTS_DIR/install-mongo.sh && \ + bash $BUILD_SCRIPTS_DIR/install-phantom.sh && \ + bash $BUILD_SCRIPTS_DIR/post-install-cleanup.sh + +# copy the app to the container +ONBUILD COPY . $APP_SOURCE_DIR + +# install Meteor, build app, clean up +ONBUILD RUN cd $APP_SOURCE_DIR && \ + bash $BUILD_SCRIPTS_DIR/install-meteor.sh && \ + bash $BUILD_SCRIPTS_DIR/build-meteor.sh && \ + bash $BUILD_SCRIPTS_DIR/post-build-cleanup.sh + +# set the default port that Node will listen on +ENV PORT 80 +EXPOSE 80 + +WORKDIR $APP_BUNDLE_DIR/bundle + +# start the app +ENTRYPOINT ./entrypoint.sh +CMD [] diff --git a/.reaction/docker/build.sh b/.reaction/docker/build.sh new file mode 100755 index 00000000000..ea1e5a80f0e --- /dev/null +++ b/.reaction/docker/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# build the base container and then the app container +docker build -f .reaction/docker/base.dockerfile -t reactioncommerce/base:latest . +docker build -t reactioncommerce/reaction:latest . diff --git a/.reaction/docker/reaction.ci.dockerfile b/.reaction/docker/reaction.ci.dockerfile deleted file mode 100644 index 70fad2b4133..00000000000 --- a/.reaction/docker/reaction.ci.dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM debian:jessie -MAINTAINER Reaction Commerce - -ENV DEV_BUILD "true" - -ENV NODE_VERSION "4.4.7" - -# Install PhantomJS -ENV INSTALL_PHANTOMJS "true" - -# Meteor environment variables -ENV PORT "80" -ENV ROOT_URL "http://localhost" - -# build script directories -ENV APP_SOURCE_DIR "/var/src" -ENV APP_BUNDLE_DIR "/var/www" -ENV BUILD_SCRIPTS_DIR "/opt/reaction" - -# Install entrypoint and build scripts -COPY .reaction/docker/scripts $BUILD_SCRIPTS_DIR - -RUN chmod -R +x $BUILD_SCRIPTS_DIR - -# install base dependencies, cleanup -RUN bash $BUILD_SCRIPTS_DIR/install-deps.sh && \ - bash $BUILD_SCRIPTS_DIR/install-node.sh && \ - bash $BUILD_SCRIPTS_DIR/install-phantom.sh && \ - bash $BUILD_SCRIPTS_DIR/post-install-cleanup.sh - -# copy the app to the container, build it, cleanup -COPY . $APP_SOURCE_DIR - -RUN cd $APP_SOURCE_DIR && \ - bash $BUILD_SCRIPTS_DIR/install-meteor.sh && \ - bash $BUILD_SCRIPTS_DIR/build-meteor.sh && \ - bash $BUILD_SCRIPTS_DIR/post-build-cleanup.sh - -# switch to production meteor bundle -WORKDIR $APP_BUNDLE_DIR/bundle - -# 80 is the default meteor production port, while 3000 is development mode -EXPOSE 80 - -# start mongo and reaction -ENTRYPOINT ["./entrypoint.sh"] -CMD [] diff --git a/.reaction/docker/reaction.dev.dockerfile b/.reaction/docker/reaction.dev.dockerfile deleted file mode 100644 index 51592f90e3d..00000000000 --- a/.reaction/docker/reaction.dev.dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM debian:jessie -MAINTAINER Reaction Commerce - -ENV DEV_BUILD "true" - -ENV NODE_VERSION "4.4.7" - -# Install PhantomJS -ENV INSTALL_PHANTOMJS "true" - -# Meteor environment variables -ENV PORT "80" -ENV ROOT_URL "http://localhost" - -# build script directories -ENV APP_SOURCE_DIR "/var/src" -ENV APP_BUNDLE_DIR "/var/www" -ENV BUILD_SCRIPTS_DIR "/opt/reaction" - -# Install entrypoint and build scripts -COPY .reaction/docker/scripts $BUILD_SCRIPTS_DIR - -RUN chmod -R +x $BUILD_SCRIPTS_DIR - -# install base dependencies -RUN bash $BUILD_SCRIPTS_DIR/install-deps.sh && \ - bash $BUILD_SCRIPTS_DIR/install-node.sh && \ - bash $BUILD_SCRIPTS_DIR/install-phantom.sh && \ - bash $BUILD_SCRIPTS_DIR/install-meteor.sh - -# copy the app to the container, build it, cleanup -COPY . $APP_SOURCE_DIR - -RUN cd $APP_SOURCE_DIR && \ - bash $BUILD_SCRIPTS_DIR/build-meteor.sh && \ - bash $BUILD_SCRIPTS_DIR/post-build-cleanup.sh - -# switch to production meteor bundle -WORKDIR $APP_BUNDLE_DIR/bundle - -# 80 is the default meteor production port, while 3000 is development mode -EXPOSE 80 - -# start mongo and reaction -ENTRYPOINT ["./entrypoint.sh"] -CMD [] diff --git a/.reaction/docker/reaction.prod.dockerfile b/.reaction/docker/reaction.prod.dockerfile deleted file mode 100644 index a63399a0cb8..00000000000 --- a/.reaction/docker/reaction.prod.dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM debian:jessie -MAINTAINER Reaction Commerce - -ENV NODE_VERSION "4.4.7" - -# Install PhantomJS -ENV INSTALL_PHANTOMJS "true" - -# Meteor environment variables -ENV PORT "80" -ENV ROOT_URL "http://localhost" - -# build script directories -ENV APP_SOURCE_DIR "/var/src" -ENV APP_BUNDLE_DIR "/var/www" -ENV BUILD_SCRIPTS_DIR "/opt/reaction" - -# Install entrypoint and build scripts -COPY .reaction/docker/scripts $BUILD_SCRIPTS_DIR - -RUN chmod -R +x $BUILD_SCRIPTS_DIR - -# copy the app to the container -COPY . $APP_SOURCE_DIR - -# install base dependencies, build app, cleanup -RUN bash $BUILD_SCRIPTS_DIR/install-deps.sh && \ - bash $BUILD_SCRIPTS_DIR/install-node.sh && \ - bash $BUILD_SCRIPTS_DIR/install-phantom.sh && \ - bash $BUILD_SCRIPTS_DIR/install-meteor.sh && \ - cd $APP_SOURCE_DIR && \ - bash $BUILD_SCRIPTS_DIR/build-meteor.sh && \ - bash $BUILD_SCRIPTS_DIR/post-build-cleanup.sh - -# switch to production meteor bundle -WORKDIR $APP_BUNDLE_DIR/bundle - -# 80 is the default meteor production port, while 3000 is development mode -EXPOSE 80 - -# start mongo and reaction -ENTRYPOINT ["./entrypoint.sh"] -CMD [] diff --git a/.reaction/docker/scripts/build-meteor.sh b/.reaction/docker/scripts/build-meteor.sh index 8e5f66c02b9..f72e3f4ad19 100755 --- a/.reaction/docker/scripts/build-meteor.sh +++ b/.reaction/docker/scripts/build-meteor.sh @@ -19,7 +19,7 @@ bash $BUILD_SCRIPTS_DIR/build-packages.sh bash $BUILD_SCRIPTS_DIR/plugin-loader.sh # Install app deps -meteor npm install --production +meteor npm install # build the source mkdir -p $APP_BUNDLE_DIR diff --git a/.reaction/docker/scripts/build-packages.sh b/.reaction/docker/scripts/build-packages.sh index f0575e1c5de..92ca7baf540 100755 --- a/.reaction/docker/scripts/build-packages.sh +++ b/.reaction/docker/scripts/build-packages.sh @@ -1,11 +1,11 @@ #!/bin/bash # -# add bin/docker/packages to use custom build packages +# Add .reaction/docker/packages to use custom +# Meteor packages in the Docker build # -if [ -f bin/docker/packages ]; then +if [ -f .reaction/docker/packages ]; then echo "[-] Using custom Meteor packages file..." cp docker/packages .meteor/packages - exit 0 fi diff --git a/.reaction/docker/scripts/ci-build.sh b/.reaction/docker/scripts/ci-build.sh index 689ac173975..0b56f500a2a 100755 --- a/.reaction/docker/scripts/ci-build.sh +++ b/.reaction/docker/scripts/ci-build.sh @@ -11,8 +11,8 @@ if [[ -e ~/docker/image.tar ]]; then docker load -i ~/docker/image.tar fi -# build new image -docker build -t reactioncommerce/reaction:latest . +# build new base and app images +.reaction/docker/build.sh # if successful, save in cache mkdir -p ~/docker diff --git a/.reaction/docker/scripts/entrypoint.sh b/.reaction/docker/scripts/entrypoint.sh index 12499c6d4ad..da5ab9bc37b 100755 --- a/.reaction/docker/scripts/entrypoint.sh +++ b/.reaction/docker/scripts/entrypoint.sh @@ -7,9 +7,17 @@ # set -e -# set default meteor values if they arent set -: ${PORT:="80"} -: ${ROOT_URL:="http://localhost"} +# start local mongodb if no external MONGO_URL was set +if [[ "${MONGO_URL}" == *"127.0.0.1"* ]]; then + if hash mongod 2>/dev/null; then + mkdir -p /data/db + printf "\n[-] External MONGO_URL not found. Starting local MongoDB...\n\n" + mongod --storageEngine=wiredTiger --fork --logpath /var/log/mongodb.log + else + echo "ERROR: Mongo not installed inside the container. Rebuild with INSTALL_MONGO=true" + exit 1 + fi +fi # Run meteor exec node ./main.js diff --git a/.reaction/docker/scripts/install-deps.sh b/.reaction/docker/scripts/install-deps.sh index 437785c7d6f..99444f82ba0 100755 --- a/.reaction/docker/scripts/install-deps.sh +++ b/.reaction/docker/scripts/install-deps.sh @@ -7,4 +7,4 @@ printf "\n[-] Installing base OS dependencies...\n\n" apt-get update -qq -y -apt-get install -qq -y --no-install-recommends curl ca-certificates bzip2 git build-essential python graphicsmagick +apt-get install -qq -y --no-install-recommends curl ca-certificates bzip2 build-essential python graphicsmagick diff --git a/.reaction/docker/scripts/install-mongo.sh b/.reaction/docker/scripts/install-mongo.sh new file mode 100644 index 00000000000..bff619d6905 --- /dev/null +++ b/.reaction/docker/scripts/install-mongo.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +if [ "${INSTALL_MONGO}" = "true" ]; then + + printf "\n[-] Installing MongoDB ${MONGO_VERSION}...\n\n" + + apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys DFFA3DCF326E302C4787673A01C4E7FAAAB2461C + apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys 42F3E95A2C4F08279C4960ADD68FA50FEA312927 + + echo "deb http://repo.mongodb.org/apt/debian jessie/mongodb-org/$MONGO_MAJOR main" > /etc/apt/sources.list.d/mongodb-org.list + + apt-get update + + apt-get install -y \ + mongodb-org=$MONGO_VERSION \ + mongodb-org-server=$MONGO_VERSION \ + mongodb-org-shell=$MONGO_VERSION \ + mongodb-org-mongos=$MONGO_VERSION \ + mongodb-org-tools=$MONGO_VERSION + + rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/mongodb + mv /etc/mongod.conf /etc/mongod.conf.orig + +fi diff --git a/.reaction/docker/scripts/install-node.sh b/.reaction/docker/scripts/install-node.sh index 417d837c783..3d02ca4f8f8 100755 --- a/.reaction/docker/scripts/install-node.sh +++ b/.reaction/docker/scripts/install-node.sh @@ -2,7 +2,6 @@ set -e -: ${NODE_VERSION:=4.4.7} : ${NODE_ARCH:=x64} printf "\n[-] Installing Node ${NODE_VERSION}...\n\n" diff --git a/.reaction/docker/scripts/install-phantom.sh b/.reaction/docker/scripts/install-phantom.sh index 86add804a76..be4b1148e76 100755 --- a/.reaction/docker/scripts/install-phantom.sh +++ b/.reaction/docker/scripts/install-phantom.sh @@ -6,19 +6,20 @@ if [ "${INSTALL_PHANTOMJS}" = "true" ]; then printf "\n[-] Installing Phantom.js...\n\n" - PHANTOM_VERSION="2.1.1" PHANTOM_JS="phantomjs-$PHANTOM_VERSION-linux-x86_64" apt-get update - apt-get install build-essential wget chrpath libssl-dev libxft-dev -y + apt-get install -y wget chrpath libssl-dev libxft-dev - cd ~ + cd /tmp wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOM_VERSION/$PHANTOM_JS.tar.bz2 tar xvjf $PHANTOM_JS.tar.bz2 mv $PHANTOM_JS /usr/local/share ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/share/phantomjs ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin/phantomjs ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/bin/phantomjs - + + apt-get -y purge wget + phantomjs -v fi diff --git a/.reaction/docker/scripts/post-build-cleanup.sh b/.reaction/docker/scripts/post-build-cleanup.sh index 63a09506f5b..99fc0e5073a 100755 --- a/.reaction/docker/scripts/post-build-cleanup.sh +++ b/.reaction/docker/scripts/post-build-cleanup.sh @@ -29,10 +29,10 @@ rm -rf /opt/nodejs/bin/npm rm -rf /opt/nodejs/lib/node_modules/npm/ # remove meteor -rm -rf /usr/bin/meteor +rm -rf /usr/local/bin/meteor rm -rf /root/.meteor # remove os dependencies -apt-get -qq -y purge ca-certificates curl git bzip2 +apt-get -qq -y purge ca-certificates curl bzip2 apt-get -qq -y autoremove rm -rf /var/lib/apt/lists/* diff --git a/.reaction/docker/scripts/post-install-cleanup.sh b/.reaction/docker/scripts/post-install-cleanup.sh index cc80d24393e..dc3969eb76a 100755 --- a/.reaction/docker/scripts/post-install-cleanup.sh +++ b/.reaction/docker/scripts/post-install-cleanup.sh @@ -14,5 +14,5 @@ rm -rf /root/.cache /root/.config /root/.local rm -rf /tmp/* # remove os dependencies -apt-get -qq -y autoremove +apt-get -y autoremove rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile b/Dockerfile deleted file mode 120000 index 3513d4dd3aa..00000000000 --- a/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -.reaction/docker/reaction.prod.dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..ff16b37f57a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM reactioncommerce/base:latest + +# Default environment variables +ENV ROOT_URL "http://localhost" +ENV MONGO_URL "mongodb://127.0.0.1:27017/reaction" diff --git a/circle.yml b/circle.yml index 44a2bc9fff2..d9782365e0b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,10 +1,10 @@ machine: node: - version: 4.5.0 + version: 4.6.0 services: - docker pre: - - meteor update || curl https://install.meteor.com | /bin/sh + - hash meteor 2>/dev/null || curl https://install.meteor.com | /bin/sh dependencies: cache_directories: @@ -26,15 +26,20 @@ deployment: prequel: branch: development commands: + - docker tag reactioncommerce/base:latest reactioncommerce/base:devel - docker tag reactioncommerce/reaction:latest reactioncommerce/prequel:latest - docker tag reactioncommerce/reaction:latest reactioncommerce/prequel:$CIRCLE_BUILD_NUM - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - docker push reactioncommerce/base:devel - docker push reactioncommerce/prequel:$CIRCLE_BUILD_NUM - docker push reactioncommerce/prequel:latest release: branch: master commands: + - docker tag reactioncommerce/base:latest reactioncommerce/base:$CIRCLE_BUILD_NUM - docker tag reactioncommerce/reaction:latest reactioncommerce/reaction:$CIRCLE_BUILD_NUM - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS + - docker push reactioncommerce/base:$CIRCLE_BUILD_NUM + - docker push reactioncommerce/base:latest - docker push reactioncommerce/reaction:$CIRCLE_BUILD_NUM - docker push reactioncommerce/reaction:latest diff --git a/client/modules/i18n/currency.js b/client/modules/i18n/currency.js new file mode 100644 index 00000000000..ce4a4ca7cab --- /dev/null +++ b/client/modules/i18n/currency.js @@ -0,0 +1,124 @@ +import accounting from "accounting-js"; +import { Meteor } from "meteor/meteor"; +import { Template } from "meteor/templating"; +import { localeDep, i18nextDep } from "./main"; +import { Reaction, Logger, i18next } from "/client/api"; + +/** + * formatPriceString + * @summary return shop /locale specific formatted price + * also accepts a range formatted with " - " + * @param {String} currentPrice - currentPrice or "xx.xx - xx.xx" formatted String + * @return {String} returns locale formatted and exchange rate converted values + */ +export function formatPriceString(formatPrice) { + const locale = Reaction.Locale.get(); + + if (typeof locale !== "object" || typeof locale.currency !== "object") { + // locale not yet loaded, so we don"t need to return anything. + return false; + } + + if (typeof formatPrice !== "string" && typeof formatPrice !== "number") { + return false; + } + + // for the cases then we have only one price. It is a number. + const currentPrice = formatPrice.toString(); + let price = 0; + const prices = ~currentPrice.indexOf(" - ") ? + currentPrice.split(" - ") : [currentPrice]; + + // basic "for" is faster then "for ...of" for arrays. We need more speed here + const len = prices.length; + for (let i = 0; i < len; i++) { + const originalPrice = prices[i]; + try { + // we know the locale, but we don"t know exchange rate. In that case we + // should return to default shop currency + if (typeof locale.currency.rate !== "number") { + throw new Meteor.Error("exchangeRateUndefined"); + } + prices[i] *= locale.currency.rate; + + price = _formatPrice(price, originalPrice, prices[i], + currentPrice, locale.currency, i, len); + } catch (error) { + Logger.debug("currency error, fallback to shop currency"); + price = _formatPrice(price, originalPrice, prices[i], + currentPrice, locale.shopCurrency, i, len); + } + } + + return price; +} + +export function formatNumber(currentPrice) { + const locale = Reaction.Locale.get(); + let price = currentPrice; + const format = Object.assign({}, locale.currency, { + format: "%v" + }); + const shopFormat = Object.assign({}, locale.shopCurrency, { + format: "%v" + }); + + if (typeof locale.currency === "object" && locale.currency.rate) { + price = currentPrice * locale.currency.rate; + return accounting.formatMoney(price, format); + } + + Logger.debug("currency error, fallback to shop currency"); + return accounting.formatMoney(currentPrice, shopFormat); +} + +/** + * _formatPrice + * private function for formatting locale currency + * @private + * @param {Number} price price + * @param {Number} originalPrice originalPrice + * @param {Number} actualPrice actualPrice + * @param {Number} currentPrice currentPrice + * @param {Number} currency currency + * @param {Number} pos position + * @param {Number} len length + * @return {Number} formatted price + */ +function _formatPrice(price, originalPrice, actualPrice, currentPrice, currency, + pos, len) { + // this checking for locale.shopCurrency mostly + if (typeof currency !== "object") { + return false; + } + + let adjustedPrice = actualPrice; + let formattedPrice; + + // Precision is mis-used in accounting js. Scale is the propery term for number + // of decimal places. Let's adjust it here so accounting.js does not break. + if (currency.scale !== undefined) { + currency.precision = currency.scale; + } + + // If there are no decimal places, in the case of the Japanese Yen, we adjust it here. + if (currency.scale === 0) { + adjustedPrice = actualPrice * 100; + } + + // @param {string} currency.where: If it presents - in situation then two + // prices in string, currency sign will be placed just outside the right price. + // For now it should be manually added to fixtures shop data. + if (typeof currency.where === "string" && currency.where === "right" && + len > 1 && pos === 0) { + const modifiedCurrency = Object.assign({}, currency, { + symbol: "" + }); + formattedPrice = accounting.formatMoney(adjustedPrice, modifiedCurrency); + } else { + // accounting api: http://openexchangerates.github.io/accounting.js/ + formattedPrice = accounting.formatMoney(adjustedPrice, currency); + } + + return price === 0 ? currentPrice.replace(originalPrice, formattedPrice) : price.replace(originalPrice, formattedPrice); +} diff --git a/client/modules/i18n/helpers.js b/client/modules/i18n/helpers.js index ba045644783..4c9453b25c3 100644 --- a/client/modules/i18n/helpers.js +++ b/client/modules/i18n/helpers.js @@ -1,7 +1,6 @@ -import accounting from "accounting-js"; -import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; import { localeDep, i18nextDep } from "./main"; +import { formatPriceString } from "./currency"; import { Reaction, Logger, i18next } from "/client/api"; /** @@ -49,120 +48,9 @@ Template.registerHelper("currencySymbol", function () { */ Template.registerHelper("formatPrice", function (formatPrice) { localeDep.depend(); - - const locale = Reaction.Locale.get(); - - if (typeof locale !== "object" || typeof locale.currency !== "object") { - // locale not yet loaded, so we don"t need to return anything. - return false; - } - - if (typeof formatPrice !== "string" && typeof formatPrice !== "number") { - return false; - } - - // for the cases then we have only one price. It is a number. - const currentPrice = formatPrice.toString(); - let price = 0; - const prices = ~currentPrice.indexOf(" - ") ? - currentPrice.split(" - ") : [currentPrice]; - - // basic "for" is faster then "for ...of" for arrays. We need more speed here - const len = prices.length; - for (let i = 0; i < len; i++) { - const originalPrice = prices[i]; - try { - // we know the locale, but we don"t know exchange rate. In that case we - // should return to default shop currency - if (typeof locale.currency.rate !== "number") { - throw new Meteor.Error("exchangeRateUndefined"); - } - prices[i] *= locale.currency.rate; - - price = _formatPrice(price, originalPrice, prices[i], - currentPrice, locale.currency, i, len); - } catch (error) { - Logger.debug("currency error, fallback to shop currency"); - price = _formatPrice(price, originalPrice, prices[i], - currentPrice, locale.shopCurrency, i, len); - } - } - - return price; + return formatPriceString(formatPrice); }); -Reaction.Currency = {}; - -Reaction.Currency.formatNumber = function (currentPrice) { - const locale = Reaction.Locale.get(); - let price = currentPrice; - const format = Object.assign({}, locale.currency, { - format: "%v" - }); - const shopFormat = Object.assign({}, locale.shopCurrency, { - format: "%v" - }); - - if (typeof locale.currency === "object" && locale.currency.rate) { - price = currentPrice * locale.currency.rate; - return accounting.formatMoney(price, format); - } - - Logger.debug("currency error, fallback to shop currency"); - return accounting.formatMoney(currentPrice, shopFormat); -}; - -/** - * _formatPrice - * private function for formatting locale currency - * @private - * @param {Number} price price - * @param {Number} originalPrice originalPrice - * @param {Number} actualPrice actualPrice - * @param {Number} currentPrice currentPrice - * @param {Number} currency currency - * @param {Number} pos position - * @param {Number} len length - * @return {Number} formatted price - */ -function _formatPrice(price, originalPrice, actualPrice, currentPrice, currency, - pos, len) { - // this checking for locale.shopCurrency mostly - if (typeof currency !== "object") { - return false; - } - - let adjustedPrice = actualPrice; - let formattedPrice; - - // Precision is mis-used in accounting js. Scale is the propery term for number - // of decimal places. Let's adjust it here so accounting.js does not break. - if (currency.scale !== undefined) { - currency.precision = currency.scale; - } - - // If there are no decimal places, in the case of the Japanese Yen, we adjust it here. - if (currency.scale === 0) { - adjustedPrice = actualPrice * 100; - } - - // @param {string} currency.where: If it presents - in situation then two - // prices in string, currency sign will be placed just outside the right price. - // For now it should be manually added to fixtures shop data. - if (typeof currency.where === "string" && currency.where === "right" && - len > 1 && pos === 0) { - const modifiedCurrency = Object.assign({}, currency, { - symbol: "" - }); - formattedPrice = accounting.formatMoney(adjustedPrice, modifiedCurrency); - } else { - // accounting api: http://openexchangerates.github.io/accounting.js/ - formattedPrice = accounting.formatMoney(adjustedPrice, currency); - } - - return price === 0 ? currentPrice.replace(originalPrice, formattedPrice) : price.replace(originalPrice, formattedPrice); -} - Object.assign(Reaction, { /** * translateRegistry diff --git a/client/modules/i18n/index.js b/client/modules/i18n/index.js index 1f59e2ae09b..8bae49692fd 100644 --- a/client/modules/i18n/index.js +++ b/client/modules/i18n/index.js @@ -1,4 +1,5 @@ import i18next, { getBrowserLanguage, i18nextDep, localeDep } from "./main"; +export * from "./currency"; export { i18next, diff --git a/imports/plugins/core/checkout/client/helpers/cart.js b/imports/plugins/core/checkout/client/helpers/cart.js index 2429a35df81..8b9c8556c85 100644 --- a/imports/plugins/core/checkout/client/helpers/cart.js +++ b/imports/plugins/core/checkout/client/helpers/cart.js @@ -71,16 +71,10 @@ Template.registerHelper("cart", function () { * @summary gets current cart billing address / payment name * @return {String} returns cart.billing[0].fullName */ - Template.registerHelper("cartPayerName", function () { const cart = Cart.findOne(); - if (cart) { - if (cart.billing) { - if (cart.billing[0].address) { - if (cart.billing[0].address.fullName) { - return cart.billing[0].address.fullName; - } - } - } + if (cart && cart.billing && cart.billing[0] && cart.billing[0].address && cart.billing[0].address.fullName) { + const name = cart.billing[0].address.fullName; + if (name.replace(/[a-zA-Z ]*/, "").length === 0) return name; } }); diff --git a/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts.js b/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts.js index 763528f4fb5..5b56658eb53 100644 --- a/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts.js +++ b/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts.js @@ -143,3 +143,5 @@ Alerts = { }, collection_: new Mongo.Collection(null) }; + +export default Alerts; diff --git a/imports/plugins/core/orders/client/templates/list/ordersList.html b/imports/plugins/core/orders/client/templates/list/ordersList.html index ed5ddafb808..ceb8c4e2d10 100644 --- a/imports/plugins/core/orders/client/templates/list/ordersList.html +++ b/imports/plugins/core/orders/client/templates/list/ordersList.html @@ -97,7 +97,7 @@

diff --git a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js index 2e7d4bc830f..c391ff77e30 100644 --- a/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js +++ b/imports/plugins/core/orders/client/templates/workflow/shippingInvoice.js @@ -4,7 +4,7 @@ import accounting from "accounting-js"; import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; import { ReactiveVar } from "meteor/reactive-var"; -import { Reaction, i18next, Logger } from "/client/api"; +import { i18next, Logger, formatNumber } from "/client/api"; import { NumericInput } from "/imports/plugins/core/ui/client/components"; import { Media, Orders, Shops } from "/lib/collections"; import _ from "lodash"; @@ -248,7 +248,7 @@ Template.coreOrderShippingInvoice.helpers({ }, money(amount) { - return Reaction.Currency.formatNumber(amount); + return formatNumber(amount); }, disabled() { diff --git a/imports/plugins/core/revisions/client/components/publishControls.js b/imports/plugins/core/revisions/client/components/publishControls.js new file mode 100644 index 00000000000..c5b46e47440 --- /dev/null +++ b/imports/plugins/core/revisions/client/components/publishControls.js @@ -0,0 +1,277 @@ +import React, { Component, PropTypes } from "react"; +import { + Button, + ButtonToolbar, + Divider, + DropDownMenu, + Menu, + MenuItem, + Popover, + Translation +} from "/imports/plugins/core/ui/client/components"; +import SimpleDiff from "./simpleDiff"; +import { Translatable } from "/imports/plugins/core/ui/client/providers"; + +class PublishControls extends Component { + constructor(props) { + super(props); + + this.state = { + showDiffs: false + }; + + this.handleToggleShowChanges = this.handleToggleShowChanges.bind(this); + this.handlePublishClick = this.handlePublishClick.bind(this); + } + + handleToggleShowChanges() { + this.setState({ + showDiffs: !this.state.showDiffs + }); + } + + handlePublishClick() { + if (this.props.onPublishClick) { + this.props.onPublishClick(this.props.revisions); + } + } + + handleVisibilityChange = (event, value) => { + if (this.props.onVisibilityChange) { + let isDocumentVisible = false; + + if (value === "public") { + isDocumentVisible = true; + } + + this.props.onVisibilityChange(event, isDocumentVisible); + } + } + + handleAction = (event, value) => { + if (this.props.onAction) { + this.props.onAction(event, value, this.props.documentIds); + } + } + + get showChangesButtonLabel() { + if (!this.showDiffs) { + return "Show Changes"; + } + + return "Hide Changes"; + } + + get showChangesButtoni18nKeyLabel() { + if (!this.showDiffs) { + return "app.showChanges"; + } + + return "app.hideChanges"; + } + + get revisionIds() { + if (this.hasRevisions) { + return this.props.revisions.map(revision => revision._id); + } + return false; + } + + get hasRevisions() { + return Array.isArray(this.props.revisions) && this.props.revisions.length; + } + + get diffs() { + return this.props.revisions; + } + + get showDiffs() { + return this.diffs && this.state.showDiffs; + } + + get isVisible() { + if (Array.isArray(this.props.revisions) && this.props.revisions.length) { + const primaryRevision = this.props.revisions[0]; + + if (primaryRevision.documentData.isVisible) { + return "public"; + } + } else if (Array.isArray(this.props.documents) && this.props.documents.length) { + const primaryDocument = this.props.documents[0]; + + if (primaryDocument.isVisible) { + return "public"; + } + } + + return "private"; + } + + /** + * Getter hasChanges + * @return {Boolean} one or more revision has changes + */ + get hasChanges() { + // Verify we even have any revision at all + if (this.hasRevisions) { + // Loop through all revisions to determin if they have changes + const diffHasActualChanges = this.props.revisions.map((revision) => { + // We probably do have chnages to publish + // Note: Sometimes "updatedAt" will cause false positives, but just incase, lets + // enable the publish button anyway. + if (Array.isArray(revision.diff) && revision.diff.length) { + return true; + } + + // If all else fails, we will disable the button + return false; + }); + + // If even one revision has changes we should enable the publish button + return diffHasActualChanges.some((element) => { + return element === true; + }); + } + + // No revisions, no publishing + return false; + } + + renderChanges() { + if (this.showDiffs) { + const diffs = this.props.revisions.map((revision) => { + return ; + }); + + return ( +
+ {diffs} +
+ ); + } + return null; + } + + renderDeletionStatus() { + if (this.hasChanges) { + if (this.props.revisions[0].documentData.isDeleted) { + return ( + - ); + const extraProps = {}; + + if (tagName === "a") { + extraProps.href = "#"; + } + + const buttonProps = Object.assign({ + "className": classes, + "data-event-action": eventAction, + "onMouseOut": this.handleButtonMouseOut, + "onMouseOver": this.handleButtonMouseOver, + "onClick": this.handleClick, + "type": "button" + }, attrs, extraProps); + + + // Create a react fragment for all the button children + let buttonChildren; + + if (iconAfter) { + buttonChildren = createFragment({ + label: this.renderLabel(), + icon: this.renderIcon(), + children: this.props.children + }); + } else { + buttonChildren = createFragment({ + icon: this.renderIcon(), + label: this.renderLabel(), + children: this.props.children + }); + } + + // Button with tooltip gets some special treatment + if (tooltip) { + return React.createElement(tagName, buttonProps, + + + {buttonChildren} + + + ); + } + + // Normal button, without tooltip + return React.createElement(tagName, buttonProps, buttonChildren); } } Button.propTypes = { active: PropTypes.bool, children: PropTypes.node, - className: PropTypes.string, + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + disabled: PropTypes.bool, eventAction: PropTypes.string, i18nKeyLabel: PropTypes.string, i18nKeyTitle: PropTypes.string, + i18nKeyToggleOnLabel: PropTypes.string, i18nKeyTooltip: PropTypes.string, icon: PropTypes.string, + iconAfter: PropTypes.bool, label: PropTypes.string, + onClick: PropTypes.func, onIcon: PropTypes.string, + primary: PropTypes.bool, status: PropTypes.string, + tagName: PropTypes.string, title: PropTypes.string, toggle: PropTypes.bool, toggleOn: PropTypes.bool, - tooltip: PropTypes.string + toggleOnLabel: PropTypes.string, + tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.node]), + value: PropTypes.any }; Button.defaultProps = { - toggle: false, - active: false + active: false, + disabled: false, + iconAfter: false, + tagName: "button", + toggle: false }; export default Button; diff --git a/imports/plugins/core/ui/client/components/button/handle.js b/imports/plugins/core/ui/client/components/button/handle.js new file mode 100644 index 00000000000..8a219cf1aea --- /dev/null +++ b/imports/plugins/core/ui/client/components/button/handle.js @@ -0,0 +1,34 @@ +import React, { PropTypes } from "react"; +import { Icon } from "../icon"; + +/** + * Handle is a special type of button used for drag handles. + * It uses the fa-bars icon by default, and does not have click or hover states + * + * Use this button in places where you need a pre-styled button for drag handles + * + * @param {Object} props Props passed into component + * @returns {node} component with pre-configured icon for dragging + */ +const Handle = (props) => { + const handle = ( +
+ +
+ ); + + if (props.connectDragSource) { + return props.connectDragSource(handle); + } + + return handle; +}; + +Handle.propTypes = { + connectDragSource: PropTypes.func +}; + +export default Handle; diff --git a/imports/plugins/core/ui/client/components/button/iconButton.js b/imports/plugins/core/ui/client/components/button/iconButton.js index 776bfc70863..da00e534a66 100644 --- a/imports/plugins/core/ui/client/components/button/iconButton.js +++ b/imports/plugins/core/ui/client/components/button/iconButton.js @@ -1,8 +1,4 @@ -/* eslint no-unused-vars: 1 */ -// -// TODO review PropTypes import in iconButton.js -// -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import classnames from "classnames"; import Button from "./button.jsx"; @@ -15,35 +11,36 @@ class IconButton extends Component { } = this.props; -// this.props.buttonKind === 'flat' -// default should be default, flat is new css that makes the bakcground tarnsparent - + // this.props.buttonKind === 'flat' + // default should be default, flat is new css that makes the bakcground tarnsparent let buttonClassName; if (this.props.kind === "flat") { buttonClassName = classnames({ "rui": true, "button": true, + "icon": true, + "icon-only": true, "flat": true }); - } - else if (this.props.kind === "close") { + } else if (this.props.kind === "close") { buttonClassName = classnames({ "rui": true, "button": true, + "icon-only": true, "close": true }); - } - else { + } else { buttonClassName = classnames({ "rui": true, "button": true, "edit": true, + "icon-only": true, "variant-edit": true }); } - let iconClassName = classnames({ + const iconClassName = classnames({ "fa-lg": true, [icon]: true }); diff --git a/imports/plugins/core/ui/client/components/button/index.js b/imports/plugins/core/ui/client/components/button/index.js new file mode 100644 index 00000000000..03ca42d7a66 --- /dev/null +++ b/imports/plugins/core/ui/client/components/button/index.js @@ -0,0 +1,5 @@ +export { default as Button } from "./button.jsx"; +export { default as IconButton } from "./iconButton"; +export { default as EditButton } from "./editButton"; +export { default as VisibilityButton } from "./visibilityButton"; +export { default as Handle } from "./handle"; diff --git a/imports/plugins/core/ui/client/components/button/visibilityButton.js b/imports/plugins/core/ui/client/components/button/visibilityButton.js new file mode 100644 index 00000000000..27895b02bc7 --- /dev/null +++ b/imports/plugins/core/ui/client/components/button/visibilityButton.js @@ -0,0 +1,25 @@ +import React from "react"; +import IconButton from "./iconButton"; + +/** + * Visibility button is a special type of Icon Button that is toggable by default + * and presents a eye icon in its on state, and a eye-slash icon when it is off. + * + * Use this button in places where you need a pre-styled button for toggling visibility + * states of components. + * + * @param {Object} props Props passed into component + * @returns {IconButton} Retruns an IconButton component with pre-configured icons for visibility + */ +const VisibilityButton = (props) => { + return ( + + ); +}; + +export default VisibilityButton; diff --git a/imports/plugins/core/ui/client/components/buttonGroup/buttonGroup.js b/imports/plugins/core/ui/client/components/buttonGroup/buttonGroup.js index 3f26df3436f..b5bdb36c923 100644 --- a/imports/plugins/core/ui/client/components/buttonGroup/buttonGroup.js +++ b/imports/plugins/core/ui/client/components/buttonGroup/buttonGroup.js @@ -1,32 +1,23 @@ -// import React from "react"; -// import classnames from "classnames"; -// -// const Items = ReactionUI.Components.Items; -// -// class ButtonGroup extends React.Component { -// -// renderButtons() { -// if (this.props.children) { -// const items = this.props.children.map((item, index) => { -// // if (this.props.autoWrap) { return ( {React.cloneElement(item)} ); } -// -// return React.cloneElement(item); -// }); -// -// return items; -// } -// } -// -// render() { -// const classes = classnames({rui: true, buttons: true}) -// -// return ( -//
-// -// {this.renderButtons()} -// -//
-// ); -// } -// } -// export default ButtonGroup; +import React, { Component, PropTypes} from "react"; +import classnames from "classnames"; + +class ButtonGroup extends Component { + render() { + const baseClassName = classnames({ + "rui": true, + "btn-group": true + }); + + return ( +
+ {this.props.children} +
+ ); + } +} + +ButtonGroup.propTypes = { + children: PropTypes.node +}; + +export default ButtonGroup; diff --git a/imports/plugins/core/ui/client/components/buttonGroup/buttonToolbar.js b/imports/plugins/core/ui/client/components/buttonGroup/buttonToolbar.js new file mode 100644 index 00000000000..63de8f8689c --- /dev/null +++ b/imports/plugins/core/ui/client/components/buttonGroup/buttonToolbar.js @@ -0,0 +1,23 @@ +import React, { Component, PropTypes} from "react"; +import classnames from "classnames"; + +class ButtonToolbar extends Component { + render() { + const baseClassName = classnames({ + "rui": true, + "btn-toolbar": true + }); + + return ( +
+ {this.props.children} +
+ ); + } +} + +ButtonToolbar.propTypes = { + children: PropTypes.node +}; + +export default ButtonToolbar; diff --git a/imports/plugins/core/ui/client/components/buttonGroup/index.js b/imports/plugins/core/ui/client/components/buttonGroup/index.js new file mode 100644 index 00000000000..acd0a2e8d91 --- /dev/null +++ b/imports/plugins/core/ui/client/components/buttonGroup/index.js @@ -0,0 +1,2 @@ +export { default as ButtonGroup } from "./buttonGroup"; +export { default as ButtonToolbar } from "./buttonToolbar"; diff --git a/imports/plugins/core/ui/client/components/cards/card.js b/imports/plugins/core/ui/client/components/cards/card.js new file mode 100644 index 00000000000..1143996c010 --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/card.js @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from "react"; + +class Card extends Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +Card.propTypes = { + children: PropTypes.node +}; + +export default Card; diff --git a/imports/plugins/core/ui/client/components/cards/cardBody.js b/imports/plugins/core/ui/client/components/cards/cardBody.js new file mode 100644 index 00000000000..5b6ffe93114 --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/cardBody.js @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from "react"; + +class CardBody extends Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +CardBody.propTypes = { + children: PropTypes.node +}; + +export default CardBody; diff --git a/imports/plugins/core/ui/client/components/cards/cardGroup.js b/imports/plugins/core/ui/client/components/cards/cardGroup.js new file mode 100644 index 00000000000..fb11a741637 --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/cardGroup.js @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from "react"; + +class CardGroup extends Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +CardGroup.propTypes = { + children: PropTypes.node +}; + +export default CardGroup; diff --git a/imports/plugins/core/ui/client/components/cards/cardHeader.js b/imports/plugins/core/ui/client/components/cards/cardHeader.js new file mode 100644 index 00000000000..5ec113657ad --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/cardHeader.js @@ -0,0 +1,34 @@ +import React, { Component, PropTypes } from "react"; +import CardTitle from "./cardTitle"; + +class CardHeader extends Component { + + renderTitle() { + if (this.props.title) { + return ( + + ); + } + return null; + } + + render() { + return ( +
+ {this.renderTitle()} + {this.props.children} +
+ ); + } +} + +CardHeader.propTypes = { + children: PropTypes.node, + i18nKeyTitle: PropTypes.string, + title: PropTypes.string +}; + +export default CardHeader; diff --git a/imports/plugins/core/ui/client/components/cards/cardTitle.js b/imports/plugins/core/ui/client/components/cards/cardTitle.js new file mode 100644 index 00000000000..ebadb7df00e --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/cardTitle.js @@ -0,0 +1,29 @@ +import React, { Component, PropTypes } from "react"; +import { Translation } from "../translation"; + +class CardTitle extends Component { + + render() { + const { element, ...props } = this.props; + + if (element) { + return React.cloneElement(element, props); + } + + return ( +

+ + {this.props.children} +

+ ); + } +} + +CardTitle.propTypes = { + children: PropTypes.node, + element: PropTypes.node, + i18nKeyTitle: PropTypes.string, + title: PropTypes.string +}; + +export default CardTitle; diff --git a/imports/plugins/core/ui/client/components/cards/index.js b/imports/plugins/core/ui/client/components/cards/index.js new file mode 100644 index 00000000000..2f7708ed970 --- /dev/null +++ b/imports/plugins/core/ui/client/components/cards/index.js @@ -0,0 +1,5 @@ +export { default as Card } from "./card"; +export { default as CardHeader } from "./cardHeader"; +export { default as CardTitle } from "./cardTitle"; +export { default as CardBody } from "./cardBody"; +export { default as CardGroup } from "./cardGroup"; diff --git a/imports/plugins/core/ui/client/components/checkbox/checkbox.js b/imports/plugins/core/ui/client/components/checkbox/checkbox.js new file mode 100644 index 00000000000..72b8715a3c7 --- /dev/null +++ b/imports/plugins/core/ui/client/components/checkbox/checkbox.js @@ -0,0 +1,39 @@ +import React, { Component, PropTypes } from "react"; +import { Translation } from "/imports/plugins/core/ui/client/components"; + +class Checkbox extends Component { + handleChange = (event) => { + if (this.props.onChange) { + const isInputChecked = !this.props.checked; + this.props.onChange(event, isInputChecked, this.props.name); + } + } + + render() { + return ( + + ); + } +} + +Checkbox.defaultProps = { + checked: false +}; + +Checkbox.propTypes = { + checked: PropTypes.bool, + i18nKeyLabel: PropTypes.string, + label: PropTypes.string, + name: PropTypes.string, + onChange: PropTypes.func +}; + +export default Checkbox; diff --git a/imports/plugins/core/ui/client/components/checkbox/index.js b/imports/plugins/core/ui/client/components/checkbox/index.js new file mode 100644 index 00000000000..d7ffe3a0e02 --- /dev/null +++ b/imports/plugins/core/ui/client/components/checkbox/index.js @@ -0,0 +1 @@ +export { default as Checkbox } from "./checkbox"; diff --git a/imports/plugins/core/ui/client/components/divider/divider.js b/imports/plugins/core/ui/client/components/divider/divider.js new file mode 100644 index 00000000000..44d2f26e7f6 --- /dev/null +++ b/imports/plugins/core/ui/client/components/divider/divider.js @@ -0,0 +1,44 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import { Translation } from "../"; + +class Divider extends Component { + renderLabel() { + return ( + + ); + } + + render() { + const { label, i18nKeyLabel } = this.props; + const classes = classnames({ + rui: true, + separator: true, + divider: true, + labeled: label || i18nKeyLabel + }); + + if (label) { + return ( +
+
+ + + +
+
+ ); + } + + return ( +
+ ); + } +} + +Divider.propTypes = { + i18nKeyLabel: PropTypes.string, + label: PropTypes.string +}; + +export default Divider; diff --git a/imports/plugins/core/ui/client/components/forms/field_group.js b/imports/plugins/core/ui/client/components/forms/fieldGroup.js similarity index 100% rename from imports/plugins/core/ui/client/components/forms/field_group.js rename to imports/plugins/core/ui/client/components/forms/fieldGroup.js diff --git a/imports/plugins/core/ui/client/components/icon/icon.jsx b/imports/plugins/core/ui/client/components/icon/icon.jsx index 4ab1190d93a..c88282828ad 100644 --- a/imports/plugins/core/ui/client/components/icon/icon.jsx +++ b/imports/plugins/core/ui/client/components/icon/icon.jsx @@ -1,7 +1,7 @@ -import React from "react"; -import classnames from "classnames"; +import React, { Component, PropTypes } from "react"; +import classnames from "classnames/dedupe"; -class Icon extends React.Component { +class Icon extends Component { render() { const { icon } = this.props; let classes; @@ -11,12 +11,17 @@ class Icon extends React.Component { classes = icon; } else { classes = classnames({ - fa: true, + "fa": true, [`fa-${icon}`]: true }); } } + classes = classnames({ + "rui": true, + "font-icon": true, + }, classes, this.props.className); + return ( ); @@ -24,7 +29,8 @@ class Icon extends React.Component { } Icon.propTypes = { - icon: React.PropTypes.string.isRequired + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + icon: PropTypes.string.isRequired }; export default Icon; diff --git a/imports/plugins/core/ui/client/components/icon/index.js b/imports/plugins/core/ui/client/components/icon/index.js new file mode 100644 index 00000000000..313e78e5ebb --- /dev/null +++ b/imports/plugins/core/ui/client/components/icon/index.js @@ -0,0 +1 @@ +export { default as Icon } from "./icon"; diff --git a/imports/plugins/core/ui/client/components/index.js b/imports/plugins/core/ui/client/components/index.js index b7eec7c406c..4317ec9d822 100644 --- a/imports/plugins/core/ui/client/components/index.js +++ b/imports/plugins/core/ui/client/components/index.js @@ -1,11 +1,26 @@ // export ButtonGroup from "./buttonGroup/buttonGroup"; +export { Alerts, Alert } from "./alerts"; export { default as Icon } from "./icon/icon"; -export { default as Seperator } from "./separator/separator"; +export { default as CircularProgress } from "./progress/circularProgress"; +export { default as Divider } from "./divider/divider"; +export { default as Items } from "./items/items"; export { default as Item } from "./items/item"; +export { default as TextField } from "./textfield/textfield"; export { default as NumericInput } from "./numericInput/numericInput"; +export { Button, IconButton, EditButton, VisibilityButton, Handle } from "./button"; +export { Translation, Currency } from "./translation"; +export { default as Tooltip } from "./tooltip/tooltip"; +export { Metadata, Metafield } from "./metadata"; +export { TagList, TagItem } from "./tags"; +export { Card, CardHeader, CardBody, CardGroup, CardTitle } from "./cards"; +export { MediaGallery, MediaItem } from "./media"; export { default as FlatButton } from "./button/flatButton"; -export { default as IconButton } from "./button/iconButton"; -export { default as EditButton } from "./button/editButton"; +export { default as SortableTable } from "./table/table"; +export { Checkbox } from "./checkbox"; export { default as Loading } from "./loading/loading"; -export { default as FieldGroup } from "./forms/field_group"; +export { default as FieldGroup } from "./forms/fieldGroup"; +export * from "./toolbar"; +export { default as Popover } from "./popover/popover"; +export * from "./menu"; +export * from "./buttonGroup"; diff --git a/imports/plugins/core/ui/client/components/items/items.js b/imports/plugins/core/ui/client/components/items/items.js new file mode 100644 index 00000000000..02fed25bbe2 --- /dev/null +++ b/imports/plugins/core/ui/client/components/items/items.js @@ -0,0 +1,19 @@ +import React from "react"; + +class Items extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +Items.displayName = "Items"; + +Items.propTypes = { + children: React.PropTypes.node +}; + +export default Items; diff --git a/imports/plugins/core/ui/client/components/loading/loading.js b/imports/plugins/core/ui/client/components/loading/loading.js new file mode 100644 index 00000000000..1a979b85a4a --- /dev/null +++ b/imports/plugins/core/ui/client/components/loading/loading.js @@ -0,0 +1,14 @@ +import React, { Component } from "react"; +import CircularProgress from "../progress/circularProgress"; + +class Loading extends Component { + render() { + return ( +
+ +
+ ); + } +} + +export default Loading; diff --git a/imports/plugins/core/ui/client/components/loading/loading.jsx b/imports/plugins/core/ui/client/components/loading/loading.jsx deleted file mode 100644 index af09401f892..00000000000 --- a/imports/plugins/core/ui/client/components/loading/loading.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { Component, PropTypes } from "react"; -import assign from "domkit/appendVendorPrefix"; -import insertKeyframesRule from "domkit/insertKeyframesRule"; - -// Loading Animations -// inspired by http://madscript.com/halogen - -class Loading extends Component { - - getBallStyle() { - return { - backgroundColor: this.props.color, - width: this.props.size, - height: this.props.size, - margin: this.props.margin, - borderRadius: "100%", - verticalAlign: this.props.verticalAlign - }; - } - - getAnimationStyle() { - const keyframes = { - "0%": { - transform: "scale(1)" - }, - "50%": { - transform: "scale(0.5)", - opacity: 0.7 - }, - "100%": { - transform: "scale(1)", - opacity: 1 - } - }; - - const random = top => Math.random() * top; - - const animationName = insertKeyframesRule(keyframes); - const animationDuration = ((random(100) / 100) + 0.6) + "s"; - const animationDelay = ((random(100) / 100) - 0.2) + "s"; - - const animation = [animationName, animationDuration, animationDelay, "infinite", "ease"].join(" "); - const animationFillMode = "both"; - - return { - animation: animation, - animationFillMode: animationFillMode - }; - } - - getStyle(i) { - return assign(this.getBallStyle(i), this.getAnimationStyle(), { - display: "inline-block" - }); - } - - renderLoader(loading) { - if (loading) { - const style = { - width: (parseFloat(this.props.size) * 3) + parseFloat(this.props.margin) * 6, - fontSize: 0 - }; - - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
- ); - } - - return null; - } - - render() { - return this.renderLoader(this.props.loading); - } -} - -Loading.propTypes = { - className: PropTypes.string, - color: PropTypes.string, - id: PropTypes.string, - loading: PropTypes.bool, - margin: PropTypes.string, - size: PropTypes.string, - verticalAlign: PropTypes.string -}; - -Loading.defaultProps = { - className: "loader-wrapper", - color: "#666", - loading: true, - margin: "2px", - size: "15px" -}; - -export default Loading; diff --git a/imports/plugins/core/ui/client/components/media/index.js b/imports/plugins/core/ui/client/components/media/index.js new file mode 100644 index 00000000000..bc4380392ab --- /dev/null +++ b/imports/plugins/core/ui/client/components/media/index.js @@ -0,0 +1,2 @@ +export { default as MediaGallery } from "./mediaGallery"; +export { default as MediaItem } from "./media"; diff --git a/imports/plugins/core/ui/client/components/media/media.js b/imports/plugins/core/ui/client/components/media/media.js new file mode 100644 index 00000000000..a2a6b3b1442 --- /dev/null +++ b/imports/plugins/core/ui/client/components/media/media.js @@ -0,0 +1,102 @@ +import React, { Component, PropTypes } from "react"; +import { IconButton } from "../"; +import { SortableItem } from "../../containers"; + + +class MediaItem extends Component { + + handleMouseEnter = (event) => { + if (this.props.onMouseEnter) { + this.props.onMouseEnter(event, this.props.source); + } + } + + handleMouseLeave = (event) => { + if (this.props.onMouseLeave) { + this.props.onMouseLeave(event, this.props.source); + } + } + + handleRemoveMedia = (event) => { + event.stopPropagation(); + + if (this.props.onRemoveMedia) { + this.props.onRemoveMedia(this.props.source); + } + } + + renderControls() { + if (this.props.editable) { + return ( +
+ +
+ ); + } + + return null; + } + + get defaultSource() { + return this.props.defaultSource || "/resources/placeholder.gif"; + } + + get source() { + if (typeof this.props.source === "object" && this.props.source) { + return this.props.source.url() || this.defaultSource; + } + + return this.props.source || this.defaultSource; + } + + renderImage() { + const image = ( + + ); + + return image; + } + + render() { + const mediaElement = ( +
+ {this.renderImage()} + {this.renderControls()} +
+ ); + + if (this.props.editable) { + return this.props.connectDragSource( + this.props.connectDropTarget( + mediaElement + ) + ); + } + + return mediaElement; + } +} + +MediaItem.propTypes = { + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + defaultSource: PropTypes.string, + editable: PropTypes.bool, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onRemoveMedia: PropTypes.func, + source: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) +}; + +export default SortableItem("media", MediaItem); diff --git a/imports/plugins/core/ui/client/components/media/media.jsx b/imports/plugins/core/ui/client/components/media/media.jsx deleted file mode 100644 index 603fe68f7ec..00000000000 --- a/imports/plugins/core/ui/client/components/media/media.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// -// class Media extends React.Component { -// -// /** -// * handleDrop -// * @summary On drop of a file onto this component, upload it -// * @param {Event} event - Event object -// * @return {void} no return value -// */ -// handleDrop = (event) => { -// // Reaction.Media.productFileUpload(event); -// console.log("Drop!", event); -// } -// -// /** -// * renderImage -// * @summary Render an image tag for media type "image" -// * @return {JSX} image -// */ -// renderImage() { -// // TODO: Maybe not hard code this image, unless its part of this package -// const imageUrl = this.props.media || "/resources/placeholder.gif"; -// return ; -// } -// -// /** -// * render -// * @return {JSX} media component -// */ -// render() { -// return ( -//
-// {this.renderImage()} -//
-// ); -// } -// } -// -// ReactionUI.Components.Media = Media diff --git a/imports/plugins/core/ui/client/components/media/mediaGallery.js b/imports/plugins/core/ui/client/components/media/mediaGallery.js new file mode 100644 index 00000000000..76be7307020 --- /dev/null +++ b/imports/plugins/core/ui/client/components/media/mediaGallery.js @@ -0,0 +1,142 @@ +import React, { Component, PropTypes } from "react"; +import Dropzone from "react-dropzone"; +import MediaItem from "./media"; + +class MediaGallery extends Component { + get hasMedia() { + return Array.isArray(this.props.media) && this.props.media.length > 0; + } + + get allowFeaturedMediaHover() { + if (this.props.allowFeaturedMediaHover && this.props.featuredMedia) { + return true; + } + + return false; + } + + get featuredMedia() { + return this.props.featuredMedia; + } + + handleDropClick = () => { + this.refs.dropzone.open(); + } + + renderAddItem() { + if (this.props.editable) { + return ( +
+ +
+ +
+
+ ); + } + + return null; + } + + renderMedia() { + if (this.hasMedia) { + return this.props.media.map((media, index) => { + if (index === 0 && this.allowFeaturedMediaHover) { + return ( + + ); + } + + return ( + + ); + }); + } + + return ( + + ); + } + + renderMediaGalleryUploader() { + let gallery; + + // Only render media only if there is any + if (this.hasMedia) { + gallery = this.renderMedia(); + } + + return ( +
+ +
+ {gallery} + {this.renderAddItem()} +
+
+
+ ); + } + + renderMediaGallery() { + return ( +
+
+ {this.renderMedia()} +
+
+ ); + } + + render() { + if (this.props.editable) { + return this.renderMediaGalleryUploader(); + } + + return this.renderMediaGallery(); + } +} + +MediaGallery.propTypes = { + allowFeaturedMediaHover: PropTypes.bool, + editable: PropTypes.bool, + featuredMedia: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + media: PropTypes.arrayOf(PropTypes.object), + onDrop: PropTypes.func, + onMouseEnterMedia: PropTypes.func, + onMouseLeaveMedia: PropTypes.func, + onMoveMedia: PropTypes.func, + onRemoveMedia: PropTypes.func +}; + +export default MediaGallery; diff --git a/imports/plugins/core/ui/client/components/menu/dropDownMenu.js b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js new file mode 100644 index 00000000000..d60b967e974 --- /dev/null +++ b/imports/plugins/core/ui/client/components/menu/dropDownMenu.js @@ -0,0 +1,76 @@ +import React, { Children, Component, PropTypes } from "react"; +import { + Button, + Menu, + Popover +} from "../"; + +class DropDownMenu extends Component { + constructor(props) { + super(props); + + this.state = { + label: undefined + }; + } + + handleMenuItemChange = (event, value, menuItem) => { + this.setState({ + label: menuItem.props.label || value + }); + + if (this.props.onChange) { + this.props.onChange(event, value); + } + } + + get label() { + let label = this.state.label; + Children.forEach(this.props.children, (element) => { + if (element.props.value === this.props.value) { + label = element.props.label; + } + }); + + if (!label) { + const children = Children.toArray(this.props.children); + if (children.length) { + return children[0].props.label; + } + } + + return label; + } + + render() { + return ( + + } + > + + {this.props.children} + + + ); + } +} + +DropDownMenu.propTypes = { + children: PropTypes.node, + isEnabled: PropTypes.bool, + onChange: PropTypes.func, + onPublishClick: PropTypes.func, + revisions: PropTypes.arrayOf(PropTypes.object), + translation: PropTypes.shape({ + lang: PropTypes.string + }), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]) +}; + +export default DropDownMenu; diff --git a/imports/plugins/core/ui/client/components/menu/index.js b/imports/plugins/core/ui/client/components/menu/index.js new file mode 100644 index 00000000000..63a9f6f7085 --- /dev/null +++ b/imports/plugins/core/ui/client/components/menu/index.js @@ -0,0 +1,3 @@ +export { default as Menu } from "./menu"; +export { default as MenuItem } from "./menuItem"; +export { default as DropDownMenu } from "./dropDownMenu"; diff --git a/imports/plugins/core/ui/client/components/menu/menu.js b/imports/plugins/core/ui/client/components/menu/menu.js new file mode 100644 index 00000000000..87e6ad5a7a6 --- /dev/null +++ b/imports/plugins/core/ui/client/components/menu/menu.js @@ -0,0 +1,48 @@ +import React, { Children, Component, PropTypes } from "react"; +import TetherComponent from "react-tether"; +import classnames from "classnames"; + + +class Menu extends Component { + + handleChange = (event, value, menuItem) => { + if (this.props.onChange) { + this.props.onChange(event, value, menuItem); + } + } + + renderMenuItems() { + if (this.props.children) { + return Children.map(this.props.children, (element) => { + const newChild = React.cloneElement(element, { + onClick: this.handleChange, + active: element.props.value === this.props.value + }); + + return ( +
  • {newChild}
  • + ); + }); + } + } + + render() { + return ( +
      + {this.renderMenuItems()} +
    + ); + } +} + +Menu.propTypes = { + attachment: PropTypes.string, + children: PropTypes.node, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]) +}; + +Menu.defaultProps = { + attachment: "top" +}; + +export default Menu; diff --git a/imports/plugins/core/ui/client/components/menu/menuItem.js b/imports/plugins/core/ui/client/components/menu/menuItem.js new file mode 100644 index 00000000000..1216b4be1e0 --- /dev/null +++ b/imports/plugins/core/ui/client/components/menu/menuItem.js @@ -0,0 +1,80 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames/dedupe"; +import Icon from "../icon/icon.jsx"; +import { Translation } from "../"; + +class MenuItem extends Component { + + handleClick = (event) => { + event.preventDefault(); + if (this.props.onClick && this.props.disabled === false) { + this.props.onClick(event, this.props.value, this); + } + } + + renderIcon() { + if (this.props.icon) { + return ( + + ); + } + return null; + } + + renderLabel() { + if (this.props.label) { + return ( + + ); + } + + return null; + } + + render() { + const baseClassName = classnames({ + "rui": true, + "menu-item": true, + "active": this.props.active, + "disabled": this.props.disabled === true + }, this.props.className); + + return ( + + {this.renderIcon()} + {this.renderLabel()} + + ); + } +} + +MenuItem.propTypes = { + active: PropTypes.bool, + children: PropTypes.node, + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + disabled: PropTypes.bool, + eventAction: PropTypes.string, + i18nKeyLabel: PropTypes.string, + i18nKeySelectedLabel: PropTypes.string, + icon: PropTypes.string, + label: PropTypes.string, + onClick: PropTypes.func, + selectionLabel: PropTypes.string, + value: PropTypes.any +}; + +MenuItem.defaultProps = { + active: false, + disabled: false +}; + +export default MenuItem; diff --git a/imports/plugins/core/ui/client/components/metadata/index.js b/imports/plugins/core/ui/client/components/metadata/index.js new file mode 100644 index 00000000000..7ee3c6c5046 --- /dev/null +++ b/imports/plugins/core/ui/client/components/metadata/index.js @@ -0,0 +1,2 @@ +export { default as Metadata } from "./metadata"; +export { default as Metafield } from "./metafield"; diff --git a/imports/plugins/core/ui/client/components/metadata/metadata.js b/imports/plugins/core/ui/client/components/metadata/metadata.js new file mode 100644 index 00000000000..ade3d33447f --- /dev/null +++ b/imports/plugins/core/ui/client/components/metadata/metadata.js @@ -0,0 +1,124 @@ +import React, { Component, PropTypes } from "react"; +import Metafield from "./metafield"; + +class Metadata extends Component { + /** + * Handle form submit + * @param {Event} event Event object + * @return {void} no return value + */ + handleSubmit = (event) => { + event.preventDefault(); + } + + handleMetaChange = (event, metafield, index) => { + if (this.props.onMetaChange) { + this.props.onMetaChange(event, metafield, index); + } + } + + handleMetaSave = (event, metafield, index) => { + if (this.props.onMetaSave) { + this.props.onMetaSave(event, metafield, index); + } + } + + handleMetaRemove = (event, metafield, index) => { + if (this.props.onMetaRemove) { + this.props.onMetaRemove(event, metafield, index); + } + } + + /** + * Render user readable metadata + * @return {JSX} metadata + */ + renderMetadata() { + if (this.props.metafields) { + return this.props.metafields.map((metadata, index) => { + return ( +
    +
    {metadata.key}
    +
    {metadata.value}
    +
    + ); + }); + } + + return null; + } + + /** + * Render a metadata form + * @return {JSX} metadata forms for each row of metadata + */ + renderMetadataForm() { + if (this.props.metafields) { + return this.props.metafields.map((metadata, index) => { + return ( + + ); + }); + } + + return null; + } + + renderMetadataCreateForm() { + return ( + + ); + } + + /** + * render + * @return {JSX} component + */ + render() { + // Admin editable metadata + if (this.props.editable) { + return ( +
    + {this.renderMetadataForm()} + {this.renderMetadataCreateForm()} +
    + ); + } + + // User readable metadata + return ( +
    + {this.renderMetadata()} +
    + ); + } +} + +Metadata.defaultProps = { + editable: true +}; + +// Prop Types +Metadata.propTypes = { + editable: PropTypes.bool, + metafields: PropTypes.arrayOf(PropTypes.object), + newMetafield: PropTypes.object, + onMetaChange: PropTypes.func, + onMetaRemove: PropTypes.func, + onMetaSave: PropTypes.func +}; + +export default Metadata; diff --git a/imports/plugins/core/ui/client/components/metadata/metadata.jsx b/imports/plugins/core/ui/client/components/metadata/metadata.jsx deleted file mode 100644 index bb5cdd80c6a..00000000000 --- a/imports/plugins/core/ui/client/components/metadata/metadata.jsx +++ /dev/null @@ -1,124 +0,0 @@ -// import React from "react"; -// -// // import TextField from "reaction-ui/textfield" -// // TODO: For now lets pretend we have to do imports -// const TextField = ReactionUI.Components.TextField; -// const Button = ReactionUI.Components.Button; -// const Seperator = ReactionUI.Components.Seperator; -// const Item = ReactionUI.Components.Item; -// const Items = ReactionUI.Components.Items; -// -// class Metadata extends React.Component { -// -// /** -// * Handle form submit -// * @param {Event} event Event object -// * @return {void} no return value -// */ -// handleSubmit = (event) => { -// event.preventDefault(); -// } -// -// handleRemove = (event) => { -// console.log("Remove!!"); -// } -// -// handleSort = (event) => { -// console.log("sort!!!!"); -// } -// -// /** -// * Render user readable metadata -// * @return {JSX} metadata -// */ -// renderMetadata() { -// return this.props.metafields.map((metadata, index) => { -// return ( -//
    -//
    {metadata.key}
    -//
    {metadata.value}
    -//
    -// ); -// }); -// } -// -// /** -// * Render a metadata form -// * @return {JSX} metadata forms for each row of metadata -// */ -// renderMetadataForm() { -// const fields = this.props.metafields.map((metadata, index) => { -// return ( -// -//
    -// -// -// -//
    -//
    -// ); -// }); -// -// // Blank fields for creating new metadata -// // fields.push( -// // -// // ); -// -// return fields; -// } -// -// renderMetadataCreateForm() { -// -// return ( -// -//
    -// -// -// -//
    -// ); -// } -// -// /** -// * Render a tag creation form -// * @return {JSX} blank tag for creating new tags -// */ -// renderBlankEditableTag() { -// return ( -//
    -//
    -//
    -// ); -// } -// -// /** -// * Render component -// * @return {JSX} tag component -// */ -// render() { -// if (this.props.editable) { -// return this.renderEditableTag(); -// } else if (this.props.blank) { -// return this.renderBlankEditableTag(); -// } -// -// return this.renderTag(); -// } -// } -// -// Tag.propTypes = { -// blank: React.PropTypes.bool, -// editable: React.PropTypes.bool, -// -// // Event handelers -// onTagBookmark: React.PropTypes.func, -// onTagCreate: React.PropTypes.func, -// onTagMouseOut: React.PropTypes.func, -// onTagMouseOver: React.PropTypes.func, -// onTagRemove: React.PropTypes.func, -// onTagUpdate: React.PropTypes.func, -// -// parentTag: PropTypes.Tag, -// placeholder: React.PropTypes.string, -// showBookmark: React.PropTypes.bool, -// tag: PropTypes.Tag -// }; -// -// ReactionUI.Components.Tag = Tag; +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import Autosuggest from "react-autosuggest"; +import { Router } from "/client/api"; +import { i18next } from "/client/api"; +import { Button, Handle } from "/imports/plugins/core/ui/client/components"; +import { SortableItem } from "../../containers"; + + +class Tag extends Component { + displayName: "Tag"; + + get tag() { + return this.props.tag || { + name: "" + }; + } + + get inputPlaceholder() { + return i18next.t(this.props.i18nKeyInputPlaceholder || "tags.tagName", { + defaultValue: this.props.inputPlaceholder || "Tag Name" + }); + } + + getSuggestionValue(suggestion) { + return suggestion.label; + } + + saveTag(event) { + if (this.props.onTagSave) { + this.props.onTagSave(event, this.props.tag); + } + } + + /** + * Handle tag form submit events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagFormSubmit = (event) => { + event.preventDefault(); + this.saveTag(event); + }; + + /** + * Handle tag remove events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagRemove = () => { + if (this.props.onTagRemove) { + this.props.onTagRemove(this.props.tag); + } + }; + + /** + * Handle tag update events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagUpdate = (event) => { + if (this.props.onTagUpdate && event.keyCode === 13) { + this.props.onTagUpdate(this.props.tag._id, event.target.value); + } + }; + + handleTagKeyDown = (event) => { + if (event.keyCode === 13) { + this.saveTag(event); + } + } + + /** + * Handle tag mouse out events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagMouseOut = (event) => { + // event.preventDefault(); + if (this.props.onTagMouseOut) { + this.props.onTagMouseOut(event, this.props.tag); + } + }; + + /** + * Handle tag mouse over events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagMouseOver = (event) => { + if (this.props.onTagMouseOver) { + this.props.onTagMouseOver(event, this.props.tag); + } + }; + + /** + * Handle tag inout blur events and pass them up the component chain + * @param {Event} event Event object + * @return {void} no return value + */ + handleTagInputBlur = (event) => { + if (this.props.onTagInputBlur) { + this.props.onTagInputBlur(event, this.props.tag); + } + }; + + handleInputChange = (event, { newValue }) => { + if (this.props.onTagUpdate) { + const updatedTag = Object.assign({}, this.props.tag, { + name: newValue + }); + this.props.onTagUpdate(event, updatedTag); + } + } + + handleSuggestionsUpdateRequested = (suggestion) => { + if (this.props.onGetSuggestions) { + this.props.onGetSuggestions(suggestion); + } + } + + handleSuggestionsClearRequested = () => { + if (this.props.onClearSuggestions) { + this.props.onClearSuggestions(); + } + } + + /** + * Render a simple tag for display purposes only + * @return {JSX} simple tag + */ + renderTag() { + const url = Router.pathFor("tag", { + hash: { + slug: this.props.tag.slug + } + }); + + const baseClassName = classnames({ + "rui": true, + "tag": true, + "link": true, + "full-width": this.props.fullWidth + }); + + return ( + + {this.props.tag.name} + + ); + } + + /** + * Render an admin editable tag + * @return {JSX} editable tag + */ + renderEditableTag() { + const baseClassName = classnames({ + "rui": true, + "tag": true, + "edit": true, + "full-width": this.props.fullWidth + }); + + return ( + this.props.connectDropTarget( +
    +
    + + {this.renderAutosuggestInput()} +
    + ) + ); + } + + /** + * Render a tag creation form + * @return {JSX} blank tag for creating new tags + */ + renderBlankEditableTag() { + const baseClassName = classnames({ + "rui": true, + "tag": true, + "edit": true, + "create": true, + "full-width": this.props.fullWidth + }); + + return ( +
    +
    +
    + ); + } + + renderSuggestion(suggestion) { + return ( + {suggestion.label} + ); + } + + renderAutosuggestInput() { + return ( + + ); + } + + /** + * Render component + * @return {JSX} tag component + */ + render() { + if (this.props.editable) { + return this.renderEditableTag(); + } else if (this.props.blank) { + return this.renderBlankEditableTag(); + } + + return this.renderTag(); + } +} + +Tag.propTypes = { + blank: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + editable: PropTypes.bool, + fullWidth: PropTypes.bool, + i18nKeyInputPlaceholder: PropTypes.string, + index: PropTypes.number, + inputPlaceholder: PropTypes.string, + onGetSuggestions: PropTypes.func, + onTagInputBlur: PropTypes.func, + onTagMouseOut: PropTypes.func, + onTagMouseOver: PropTypes.func, + onTagRemove: PropTypes.func, + onTagSave: PropTypes.func, + onTagUpdate: PropTypes.func, + parentTag: PropTypes.object, + suggestions: PropTypes.arrayOf(PropTypes.object), + tag: PropTypes.object +}; + +export default SortableItem("tag", Tag); diff --git a/imports/plugins/core/ui/client/components/tags/tagItem.js b/imports/plugins/core/ui/client/components/tags/tagItem.js index 0facd3fb7dd..9e618128aae 100644 --- a/imports/plugins/core/ui/client/components/tags/tagItem.js +++ b/imports/plugins/core/ui/client/components/tags/tagItem.js @@ -12,9 +12,12 @@ function createAutosuggestInput(templateInstance, options) { suggestions: templateInstance.state.get("suggestions"), getSuggestionValue: getSuggestionValue, renderSuggestion: renderSuggestion, - onSuggestionsUpdateRequested({ value }) { + onSuggestionsFetchRequested({ value }) { templateInstance.state.set("suggestions", getSuggestions(value)); }, + onSuggestionsClearRequested() { + templateInstance.state.set("suggestions", []); + }, inputProps: { placeholder: i18next.t(options.i18nPlaceholderKey, { defaultValue: options.i18nPlaceholderValue}), value: templateInstance.state.get("inputValue"), diff --git a/imports/plugins/core/ui/client/components/tags/tags.jsx b/imports/plugins/core/ui/client/components/tags/tags.jsx index 4d8541d5298..bd3b6df88e8 100644 --- a/imports/plugins/core/ui/client/components/tags/tags.jsx +++ b/imports/plugins/core/ui/client/components/tags/tags.jsx @@ -1,260 +1,162 @@ -// /* eslint no-extra-parens: 0 */ -// import React from "react"; -// import { PropTypes } from "/lib/api"; -// const Tag = ReactionUI.Components.Tag; -// const classnames = ReactionUI.Lib.classnames; -// const Sortable = ReactionUI.Lib.Sortable; -// -// class Tags extends React.Component { -// displayName = "Tag List (Tags)"; -// -// constructor(props) { -// super(props); -// this.state = { -// isEditing: true, -// tags: props.tags, -// tagIds: props.tags.map((tag) => tag._id) -// }; -// } -// -// componentDidMount() { -// if (this.props.editable) { -// this._sortable = Sortable.create(this.refs.tags, { -// group: "tags", -// onSort: this.handleDragSort, -// onAdd: this.handleDragAdd, -// onRemove: this.handleDragRemove -// }); -// } -// } -// -// componentWillReceiveProps(props) { -// this.setState({ -// tags: this.props.tags, -// tagIds: this.props.tags.map((tag) => tag._id) -// }); -// -// if (props.editable && this.state.isEditing) { -// if (this._sortable) { -// // this._sortable.option("disabled", false); -// } else { -// this._sortable = Sortable.create(this.refs.tags, { -// group: "tags", -// onSort: this.handleDragSort, -// onAdd: this.handleDragAdd, -// onRemove: this.handleDragRemove -// }); -// } -// } -// } -// -// handleDragAdd = (event) => { -// const toListId = event.to.dataset.id; -// const movedTagId = event.item.dataset.id; -// -// this.setState({ -// tagIds: [ -// ...this.state.tagsIds, -// movedTagId -// ] -// }); -// -// if (this.props.onTagDragAdd) { -// this.props.onTagDragAdd(movedTagId, toListId, event.newIndex, this.props.tags); -// } -// }; -// -// handleDragRemove = (event) => { -// const movedTagId = event.item.dataset.id; -// -// if (this.props.onTagRemove) { -// let foundTag = _.find(this.props.tags, (tag) => { -// return tag._id === movedTagId; -// }); -// -// this.props.onTagRemove(foundTag, this.props.parentTag); -// } -// }; -// -// handleDragSort = (event) => { -// let newTagsOrder = this.move(this.state.tagIds, event.oldIndex, event.newIndex); -// -// if (newTagsOrder) { -// if (this.props.onTagSort) { -// this.props.onTagSort(newTagsOrder, this.props.parentTag); -// } -// } -// }; -// -// move(array, from, to) { -// let fromIndex = from; -// let toIndex = to; -// -// if (!_.isArray(array)) { -// return null; -// } -// -// while (fromIndex < 0) { -// fromIndex += array.length; -// } -// while (toIndex < 0) { -// toIndex += array.length; -// } -// if (toIndex >= this.length) { -// let k = toIndex - array.length; -// while ((k--) + 1) { -// array.push(undefined); -// } -// } -// -// array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); -// -// return array; -// } -// -// handleNewTagSubmit = (event) => { -// event.preventDefault(); -// if (this.props.onTagCreate) { -// this.props.onTagCreate(event.target.tag.value, this.props.parentTag); -// } -// }; -// -// handleTagCreate = (tagId, tagName) => { -// if (this.props.onTagCreate) { -// this.props.onTagCreate(tagId, tagName); -// } -// }; -// -// handleTagRemove = (tag) => { -// if (this.props.onTagRemove) { -// this.props.onTagRemove(tag, this.props.parentTag); -// } -// }; -// -// /** -// * Handle tag mouse out events and pass them up the component chain -// * @param {Event} event Event object -// * @param {Tag} tag Reaction.Schemas.Tag - a tag object -// * @return {void} no return value -// */ -// handleTagMouseOut = (event, tag) => { -// if (this.props.onTagMouseOut) { -// this.props.onTagMouseOut(event, tag); -// } -// }; -// -// /** -// * Handle tag mouse over events and pass them up the component chain -// * @param {Event} event Event object -// * @param {Tag} tag Reaction.Schemas.Tag - a tag object -// * @return {void} no return value -// */ -// handleTagMouseOver = (event, tag) => { -// if (this.props.onTagMouseOver) { -// this.props.onTagMouseOver(event, tag); -// } -// }; -// -// -// handleTagUpdate = (tagId, tagName) => { -// if (this.props.onTagUpdate) { -// let parentTagId; -// if (this.props.parentTag) { -// parentTagId = this.props.parentTag._id; -// } -// this.props.onTagUpdate(tagId, tagName, parentTagId); -// } -// }; -// -// handleTagBookmark = (event) => { -// event; -// // handle event -// }; -// -// renderTags() { -// if (_.isArray(this.state.tags)) { -// const tags = this.state.tags.map((tag, index) => { -// if (tag) { -// return ( -// -// ); -// } -// }); -// -// // Render an blank tag for creating new tags -// if (this.props.editable && this.props.enableNewTagForm) { -// tags.push( -// -// ); -// } -// -// return tags; -// } -// } -// -// render() { -// if (this.state.isEditing === false && this._sortable) { -// this._sortable.option("disabled", true); -// } -// -// const classes = classnames({ -// rui: true, -// tags: true, -// edit: this.props.editable -// }); -// -// return ( -//
    -// {this.renderTags()} -//
    -// ); -// } -// } -// -// // Default Props -// Tags.defaultProps = { -// parentTag: {} -// }; -// -// // Prop Types -// Tags.propTypes = { -// editable: React.PropTypes.bool, -// enableNewTagForm: React.PropTypes.bool, -// -// // Event handelers -// onTagBookmark: React.PropTypes.func, -// onTagCreate: React.PropTypes.func, -// onTagDragAdd: React.PropTypes.func, -// onTagMouseOut: React.PropTypes.func, -// onTagMouseOver: React.PropTypes.func, -// onTagRemove: React.PropTypes.func, -// onTagSort: React.PropTypes.func, -// onTagUpdate: React.PropTypes.func, -// -// parentTag: PropTypes.Tag, -// placeholder: React.PropTypes.string, -// showBookmark: React.PropTypes.bool, -// // tag: PropTypes.Tag -// tags: PropTypes.arrayOfTags -// }; -// -// // Export -// ReactionUI.Components.Tags = Tags; +import React, { Component, PropTypes } from "react"; +import { PropTypes as ReactionPropTypes } from "/lib/api"; +import { TagItem } from "./"; +import classnames from "classnames"; + +class Tags extends Component { + displayName = "Tag List (Tags)"; + + handleNewTagSave = (event, tag) => { + event.preventDefault(); + if (this.props.onNewTagSave) { + this.props.onNewTagSave(tag, this.props.parentTag); + } + }; + + handleNewTagUpdate = (event, tag) => { + if (this.props.onNewTagUpdate) { + this.props.onNewTagUpdate(tag, this.props.parentTag); + } + } + + handleTagSave = (event, tag) => { + if (this.props.onTagSave) { + this.props.onTagSave(tag, this.props.parentTag); + } + }; + + handleTagRemove = (tag) => { + if (this.props.onTagRemove) { + this.props.onTagRemove(tag, this.props.parentTag); + } + }; + + /** + * Handle tag mouse out events and pass them up the component chain + * @param {Event} event Event object + * @param {Tag} tag Reaction.Schemas.Tag - a tag object + * @return {void} no return value + */ + handleTagMouseOut = (event, tag) => { + if (this.props.onTagMouseOut) { + this.props.onTagMouseOut(event, tag, this.props.parentTag); + } + }; + + /** + * Handle tag mouse over events and pass them up the component chain + * @param {Event} event Event object + * @param {Tag} tag Reaction.Schemas.Tag - a tag object + * @return {void} no return value + */ + handleTagMouseOver = (event, tag) => { + if (this.props.onTagMouseOver) { + this.props.onTagMouseOver(event, tag, this.props.parentTag); + } + }; + + + handleTagUpdate = (event, tag) => { + if (this.props.onTagUpdate) { + this.props.onTagUpdate(tag, this.props.parentTag); + } + }; + + renderTags() { + if (_.isArray(this.props.tags)) { + const tags = this.props.tags.map((tag, index) => { + return ( + + ); + }); + + // Render an blank tag for creating new tags + if (this.props.editable && this.props.enableNewTagForm) { + tags.push( + + ); + } + + return tags; + } + + return null; + } + + render() { + const classes = classnames({ + rui: true, + tags: true, + edit: this.props.editable + }); + + return ( +
    + {this.renderTags()} +
    + ); + } +} + +// Default Props +Tags.defaultProps = { + parentTag: {} +}; + +// Prop Types +Tags.propTypes = { + editable: PropTypes.bool, + enableNewTagForm: PropTypes.bool, + newTag: PropTypes.object, + onClearSuggestions: PropTypes.func, + onGetSuggestions: PropTypes.func, + onMoveTag: PropTypes.func, + onNewTagSave: PropTypes.func, + onNewTagUpdate: PropTypes.func, + onTagMouseOut: PropTypes.func, + onTagMouseOver: PropTypes.func, + onTagRemove: PropTypes.func, + onTagSave: PropTypes.func, + onTagSort: PropTypes.func, + onTagUpdate: PropTypes.func, + parentTag: ReactionPropTypes.Tag, + showBookmark: PropTypes.bool, + suggestions: PropTypes.arrayOf(PropTypes.object), + tagProps: PropTypes.object, + tags: ReactionPropTypes.arrayOfTags +}; + +// Export +export default Tags; diff --git a/imports/plugins/core/ui/client/components/textfield/textfield.js b/imports/plugins/core/ui/client/components/textfield/textfield.js new file mode 100644 index 00000000000..56afd3131eb --- /dev/null +++ b/imports/plugins/core/ui/client/components/textfield/textfield.js @@ -0,0 +1,172 @@ +import React, { Component, PropTypes} from "react"; +import classnames from "classnames"; +import TextareaAutosize from "react-textarea-autosize"; +import { Translation } from "../translation"; +import { i18next } from "/client/api"; + +class TextField extends Component { + /** + * Getter: value + * @return {String} value for text input + */ + get value() { + return this.props.value || ""; + } + + /** + * onValueChange + * @summary set the state when the value of the input is changed + * @param {Event} event Event object + * @return {void} + */ + onChange = (event) => { + if (this.props.onChange) { + this.props.onChange(event, event.target.value, this.props.name); + } + } + + /** + * onBlur + * @summary set the state when the value of the input is changed + * @param {Event} event Event object + * @return {void} + */ + onBlur = (event) => { + if (this.props.onBlur) { + this.props.onBlur(event, event.target.value, this.props.name); + } + } + + /** + * Render a multiline input (textarea) + * @return {JSX} jsx + */ + renderMultilineInput() { + const placeholder = i18next.t(this.props.i18nKeyPlaceholder, { + defaultValue: this.props.placeholder + }); + + return ( + + ); + } + + /** + * Render a singleline input + * @return {JSX} jsx + */ + renderSingleLineInput() { + const inputClassName = classnames({ + [`${this.props.name || "text"}-edit-input`]: true + }, this.props.className); + + const placeholder = i18next.t(this.props.i18nKeyPlaceholder, { + defaultValue: this.props.placeholder + }); + + return ( + + ); + } + + /** + * Render either a multiline (textarea) or singleline (input) + * @return {JSX} jsx template + */ + renderField() { + if (this.props.multiline === true) { + return this.renderMultilineInput(); + } + + return this.renderSingleLineInput(); + } + + renderLabel() { + if (this.props.label) { + return ( + + ); + } + + return null; + } + + renderHelpText() { + if (this.props.helpText) { + return ( + + + + ); + } + + return null; + } + + /** + * Render Component + * @return {JSX} component + */ + render() { + const classes = classnames({ + // Base + "rui": true, + "textfield": true, + "form-group": true, + + // Alignment + "center": this.props.align === "center", + "left": this.props.align === "left", + "right": this.props.align === "right" + }); + + return ( +
    + {this.renderLabel()} + {this.renderField()} + {this.renderHelpText()} + +
    + ); + } +} + +TextField.defaultProps = { + +}; + +TextField.propTypes = { + align: PropTypes.oneOf(["left", "center", "right", "justify"]), + className: PropTypes.string, + helpText: PropTypes.string, + i18nKeyHelpText: PropTypes.string, + i18nKeyLabel: PropTypes.string, + i18nKeyPlaceholder: PropTypes.string, + label: PropTypes.string, + multiline: PropTypes.bool, + name: PropTypes.string, + onBlur: PropTypes.func, + onChange: PropTypes.func, + placeholder: PropTypes.string, + value: PropTypes.string +}; + +export default TextField; diff --git a/imports/plugins/core/ui/client/components/textfield/textfield.jsx b/imports/plugins/core/ui/client/components/textfield/textfield.jsx deleted file mode 100644 index 4fff22d9798..00000000000 --- a/imports/plugins/core/ui/client/components/textfield/textfield.jsx +++ /dev/null @@ -1,144 +0,0 @@ -// // TODO: Place holder imports -// // import React from "react" -// const classnames = ReactionUI.Lib.classnames; -// const TextareaAutosize = ReactionUI.Lib.TextareaAutosize; -// -// class TextField extends React.Component { -// state = { -// value: "" -// } -// -// constructor(props) { -// super(props); -// -// this.state = { -// value: props.value -// }; -// } -// -// /** -// * onValueChange -// * @summary set the state when the value of the input is changed -// * @param {Event} event Event object -// * @return {void} -// */ -// onChange = (event) => { -// this.setState({ -// value: event.target.value -// }); -// -// if (this.props.onChange) { -// this.props.onChange(event); -// } -// } -// -// /** -// * onValueChange -// * @summary set the state when the value of the input is changed -// * @param {Event} event Event object -// * @return {void} -// */ -// onValueChange = (event) => { -// this.setState({ -// value: event.target.value -// }); -// -// if (this.props.onValueChange) { -// this.props.onValueChange(event); -// } -// } -// -// /** -// * componentWillReceiveProps - Component Lifecycle -// * @param {Object} props Properties passed from the parent component -// * @return {Void} no return value -// */ -// componentWillReceiveProps(props) { -// if (props) { -// this.setState({ -// value: props.value -// }); -// } -// } -// -// /** -// * Render a multiline input (textarea) -// * @return {JSX} jsx -// */ -// renderMultilineInput() { -// return ( -// -// ); -// } -// -// /** -// * Render a singleline input -// * @return {JSX} jsx -// */ -// renderSingleLineInput() { -// return ( -// -// ); -// } -// -// /** -// * Render either a multiline (textarea) or singleline (input) -// * @return {JSX} jsx template -// */ -// renderField() { -// if (this.props.multiline === true) { -// return this.renderMultilineInput(); -// } -// -// return this.renderSingleLineInput(); -// } -// -// /** -// * Render Component -// * @return {JSX} component -// */ -// render() { -// const classes = classnames({ -// // Base -// rui: true, -// textfield: true, -// -// // Alignment -// center: this.props.align === "center", -// left: this.props.align === "left", -// right: this.props.align === "right" -// }); -// -// return ( -//
    -// {this.renderField()} -// -//
    -// ); -// } -// } -// -// TextField.defaultProps = { -// align: "left" -// }; -// -// TextField.propTypes = { -// align: React.PropTypes.oneOf(["left", "center", "right", "justify"]) -// }; -// -// // Export -// ReactionUI.Components.TextField = TextField; diff --git a/imports/plugins/core/ui/client/components/toolbar/index.js b/imports/plugins/core/ui/client/components/toolbar/index.js new file mode 100644 index 00000000000..517fe5bdba1 --- /dev/null +++ b/imports/plugins/core/ui/client/components/toolbar/index.js @@ -0,0 +1,3 @@ +export { default as Toolbar } from "./toolbar"; +export { default as ToolbarGroup } from "./toolbarGroup"; +export { default as ToolbarText } from "./toolbarText"; diff --git a/imports/plugins/core/ui/client/components/toolbar/toolbar.js b/imports/plugins/core/ui/client/components/toolbar/toolbar.js new file mode 100644 index 00000000000..8db5fccce4f --- /dev/null +++ b/imports/plugins/core/ui/client/components/toolbar/toolbar.js @@ -0,0 +1,25 @@ +import React, { Children, Component, PropTypes } from "react"; +import TetherComponent from "react-tether"; +import classnames from "classnames"; + + +class Toolbar extends Component { + render() { + return ( + + ); + } +} + +Toolbar.propTypes = { + attachment: PropTypes.string, + children: PropTypes.node, +}; + +Toolbar.defaultProps = { + attachment: "top" +}; + +export default Toolbar; diff --git a/imports/plugins/core/ui/client/components/toolbar/toolbarGroup.js b/imports/plugins/core/ui/client/components/toolbar/toolbarGroup.js new file mode 100644 index 00000000000..9431647d869 --- /dev/null +++ b/imports/plugins/core/ui/client/components/toolbar/toolbarGroup.js @@ -0,0 +1,27 @@ +import React, { PropTypes } from "react"; +import classnames from "classnames"; + +/** + * Toobar Text + * @param {Object} props component props + * @return {node} react element node + */ +const ToolbarGroup = (props) => { + const baseClassName = classnames({ + "rui": true, + "toolbar-group": true, + "left": props.firstChild, + "right": props.lastChild + }, props.className); + + return ( +
    {props.children}
    + ); +}; + +ToolbarGroup.propTypes = { + children: PropTypes.node, + className: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) +}; + +export default ToolbarGroup; diff --git a/imports/plugins/core/ui/client/components/toolbar/toolbarText.js b/imports/plugins/core/ui/client/components/toolbar/toolbarText.js new file mode 100644 index 00000000000..ee7d1de87a6 --- /dev/null +++ b/imports/plugins/core/ui/client/components/toolbar/toolbarText.js @@ -0,0 +1,24 @@ +import React, { PropTypes } from "react"; +import classnames from "classnames"; + +/** + * Toobar Text + * @param {Object} props component props + * @return {node} react element node + */ +const ToolbarText = (props) => { + const baseClassName = classnames({ + "navbar-text": true + }, props.className); + + return ( +
    {props.children}
    + ); +}; + +ToolbarText.propTypes = { + children: PropTypes.node, + className: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) +}; + +export default ToolbarText; diff --git a/imports/plugins/core/ui/client/components/tooltip/tooltip.js b/imports/plugins/core/ui/client/components/tooltip/tooltip.js new file mode 100644 index 00000000000..8cccf2b06dd --- /dev/null +++ b/imports/plugins/core/ui/client/components/tooltip/tooltip.js @@ -0,0 +1,62 @@ +import React, { Component, PropTypes } from "react"; +import TetherComponent from "react-tether"; +import classnames from "classnames"; + +class Tooltip extends Component { + + /** + * attachment + * @description Return the attachment for the tooltip or the default + * @return {String} attachment + */ + get attachment() { + return this.props.attachment || Tooltip.defaultProps.attachment; + } + + renderTooltip() { + if (this.props.tooltipContent) { + return ( +
    + {this.props.tooltipContent} +
    + ); + } + + return null; + } + + render() { + return ( + +
    + {this.props.children} +
    + {this.renderTooltip()} +
    + ); + } +} + +Tooltip.propTypes = { + attachment: PropTypes.string, + children: PropTypes.node, + tooltipContent: PropTypes.node +}; + +Tooltip.defaultProps = { + attachment: "bottom center" +}; + +export default Tooltip; diff --git a/imports/plugins/core/ui/client/components/translation/currency.js b/imports/plugins/core/ui/client/components/translation/currency.js new file mode 100644 index 00000000000..bc30222097e --- /dev/null +++ b/imports/plugins/core/ui/client/components/translation/currency.js @@ -0,0 +1,18 @@ +import React, { Component, PropTypes, Children } from "react"; // eslint-disable-line +import { formatPriceString } from "/client/api"; + +class Currency extends Component { + render() { + const amount = formatPriceString(this.props.amount); + + return ( + {amount} + ); + } +} + +Currency.propTypes = { + amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) +}; + +export default Currency; diff --git a/imports/plugins/core/ui/client/components/translation/index.js b/imports/plugins/core/ui/client/components/translation/index.js new file mode 100644 index 00000000000..202ff082069 --- /dev/null +++ b/imports/plugins/core/ui/client/components/translation/index.js @@ -0,0 +1,2 @@ +export { default as Translation } from "./translation"; +export { default as Currency } from "./currency"; diff --git a/imports/plugins/core/ui/client/components/translation/translation.js b/imports/plugins/core/ui/client/components/translation/translation.js new file mode 100644 index 00000000000..a1548e07b27 --- /dev/null +++ b/imports/plugins/core/ui/client/components/translation/translation.js @@ -0,0 +1,24 @@ +import { camelCase } from "lodash"; +import React, { Component, PropTypes } from "react"; +import { i18next } from "/client/api"; + +class Translation extends Component { + render() { + const i18nKey = this.props.i18nKey || camelCase(this.props.defaultValue); + + const translation = i18next.t(i18nKey, { + defaultValue: this.props.defaultValue + }); + + return ( + {translation} + ); + } +} + +Translation.propTypes = { + defaultValue: PropTypes.string, + i18nKey: PropTypes.string +}; + +export default Translation; diff --git a/imports/plugins/core/ui/client/containers/alertContainer.js b/imports/plugins/core/ui/client/containers/alertContainer.js new file mode 100644 index 00000000000..3ba15779115 --- /dev/null +++ b/imports/plugins/core/ui/client/containers/alertContainer.js @@ -0,0 +1,48 @@ +import React, { Component, PropTypes } from "react"; +import { composeWithTracker } from "react-komposer"; +import { Alerts } from "../components"; +import { default as ReactionAlerts } from "/imports/plugins/core/layout/client/templates/layout/alerts/inlineAlerts"; + +class AlertContainer extends Component { + handleAlertRemove(alert) { + ReactionAlerts.collection_.remove(alert._id); + } + + handleAlertSeen(alert) { + ReactionAlerts.collection_.update(alert._id, { + $set: { + seen: true + } + }); + } + + render() { + return ( +
    + +
    + ); + } +} + +function composer(props, onData) { + const alerts = ReactionAlerts.collection_.find({ + "options.placement": props.placement || "", + "options.id": props.id || "" + }).fetch(); + + onData(null, { + alerts: alerts + }); +} + +AlertContainer.propTypes = { + id: PropTypes.string, + placement: PropTypes.string +}; + +export default composeWithTracker(composer)(AlertContainer); diff --git a/imports/plugins/core/ui/client/containers/editContainer.js b/imports/plugins/core/ui/client/containers/editContainer.js new file mode 100644 index 00000000000..112a0601792 --- /dev/null +++ b/imports/plugins/core/ui/client/containers/editContainer.js @@ -0,0 +1,173 @@ +import React, { Children, Component, PropTypes } from "react"; +import { Reaction } from "/client/api"; +import { EditButton, VisibilityButton, Translation } from "/imports/plugins/core/ui/client/components"; +import { composeWithTracker } from "react-komposer"; + +class EditContainer extends Component { + + handleEditButtonClick = (event) => { + const props = this.props; + + if (this.props.onEditButtonClick) { + const returnValue = this.props.onEditButtonClick(event, props); + + if (returnValue === false) { + return returnValue; + } + } + + Reaction.showActionView({ + label: props.label, + i18nKeyLabel: props.i18nKeyLabel, + template: props.editView, + data: props.data + }); + + return true; + } + + handleVisibilityButtonClick = (event) => { + const props = this.props; + + if (this.props.onVisibilityButtonClick) { + const returnValue = this.props.onVisibilityButtonClick(event, props); + + if (returnValue === false) { + return returnValue; + } + } + + return true; + } + + renderVisibilityButton() { + if (this.props.showsVisibilityButton) { + return ( + + ); + } + + return null; + } + + renderEditButton() { + let status; + let tooltip; + let hasChange = false; + + if (this.props.data.__draft && this.props.field) { + const draft = this.props.data.__draft; + + if (Array.isArray(draft.diff)) { + for (const diff of draft.diff) { + let hasChangedField = false; + + if (Array.isArray(this.props.field)) { + if (this.props.field.indexOf(diff.path[0]) >= 0) { + hasChangedField = true; + } + } else if (typeof this.props.field === "string" && this.props.field === diff.path[0]) { + hasChangedField = true; + } + + if (hasChangedField) { + status = "warning"; + + tooltip = ( + + + + ); + + hasChange = true; + } + } + } + } else if (this.props.data.__draft) { + status = "warning"; + + tooltip = ( + + + + ); + } + + if (this.props.autoHideEditButton && hasChange === false) { + return null; + } + + return ( + + ); + } + + render() { + // Display edit button if the permissions allow it. + if (this.props.hasPermission) { + // If children were passed as props to this component, + // copy the children and inject the edit buttons + if (this.props.children) { + return React.cloneElement(this.props.children, { + visibilityButton: this.renderVisibilityButton(), + editButton: this.renderEditButton() + }); + } + + // Otherwise, render a container for the edit buttons + return ( + + {this.renderVisibilityButton()} + {this.renderEditButton()} + + ); + } + + // If permissions don't allow the edit buttons to be shown and there are + // no child elements, then cancel rendering. + if (!this.props.children) { + return null; + } + + // If permissions don't allow the edit buttons to be shown and there are + // child elements, render them normally + return ( + Children.only(this.props.children) + ); + } +} + +EditContainer.propTypes = { + autoHideEditButton: PropTypes.bool, + children: PropTypes.node, + data: PropTypes.object, + field: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + hasPermission: PropTypes.bool, + onEditButtonClick: PropTypes.func, + onVisibilityButtonClick: PropTypes.func, + showsVisibilityButton: PropTypes.bool +}; + +function composer(props, onData) { + let hasPermission; + const viewAs = Reaction.Router.getQueryParam("as"); + + if (props.disabled === true || viewAs === "customer") { + hasPermission = false; + } else { + hasPermission = Reaction.hasPermission(props.premissions); + } + + onData(null, { + hasPermission + }); +} + +export default composeWithTracker(composer)(EditContainer); diff --git a/imports/plugins/core/ui/client/containers/index.js b/imports/plugins/core/ui/client/containers/index.js new file mode 100644 index 00000000000..4acd2988032 --- /dev/null +++ b/imports/plugins/core/ui/client/containers/index.js @@ -0,0 +1,5 @@ +export { default as EditContainer } from "./editContainer"; +export { default as TagListContainer } from "./tagListContainer"; +export { default as AlertContainer } from "./alertContainer"; +export { default as SortableItem } from "./sortableItem"; +export { default as MediaGalleryContainer } from "./mediaGalleryContainer"; diff --git a/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js b/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js new file mode 100644 index 00000000000..f0c1d735a49 --- /dev/null +++ b/imports/plugins/core/ui/client/containers/mediaGalleryContainer.js @@ -0,0 +1,179 @@ +import React, { Component, PropTypes } from "react"; +import update from "react/lib/update"; +import { composeWithTracker } from "react-komposer"; +import { MediaGallery } from "../components"; +import { Reaction } from "/client/api"; +import { ReactionProduct } from "/lib/api"; +import { Media } from "/lib/collections"; + +function uploadHandler(files) { + // TODO: It would be cool to move this logic to common ValidatedMethod, but + // I can't find a way to do this, because of browser's `FileList` collection + // and it `Blob`s which is our event.target.files. + // There is a way to do this: http://stackoverflow.com/a/24003932. but it's too + // tricky + const productId = ReactionProduct.selectedProductId(); + const variant = ReactionProduct.selectedVariant(); + if (typeof variant !== "object") { + return Alerts.add("Please, create new Variant first.", "danger", { + autoHide: true + }); + } + const variantId = variant._id; + const shopId = ReactionProduct.selectedProduct().shopId || Reaction.getShopId(); + const userId = Meteor.userId(); + let count = Media.find({ + "metadata.variantId": variantId + }).count(); + // TODO: we need to mark the first variant images somehow for productGrid. + // But how do we know that this is the first, not second or other variant? + // Question is open. For now if product has more than 1 top variant, everyone + // will have a chance to be displayed + const toGrid = variant.ancestors.length === 1; + + for (const file of files) { + const fileObj = new FS.File(file); + + fileObj.metadata = { + ownerId: userId, + productId: productId, + variantId: variantId, + shopId: shopId, + priority: count, + toGrid: +toGrid // we need number + }; + + Media.insert(fileObj); + count++; + } + + return true; +} + +class MediaGalleryContainer extends Component { + state = { + featuredMedia: undefined + } + + handleDrop = (files) => { + uploadHandler(files); + } + + handleRemoveMedia = (media) => { + const imageUrl = media.url(); + const mediaId = media._id; + + Alerts.alert({ + title: "Remove Media?", + type: "warning", + showCancelButton: true, + imageUrl, + imageHeight: 150 + }, (isConfirm) => { + if (isConfirm) { + Media.remove({ _id: mediaId }, (error) => { + if (error) { + Alerts.toast(error.reason, "warning", { + autoHide: 10000 + }); + } + + // updateImagePriorities(); + }); + } + }); + } + + get media() { + return (this.state && this.state.media) || this.props.media; + } + + handleMouseEnterMedia = (event, media) => { + this.setState({ + featuredMedia: media + }); + } + + handleMouseLeaveMedia = () => { + this.setState({ + featuredMedia: undefined + }); + } + + handleMoveMedia = (dragIndex, hoverIndex) => { + const media = this.props.media[dragIndex]; + + // Apply new sort order to variant list + const newMediaOrder = update(this.props.media, { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, media] + ] + }); + + // Set local state so the component does't have to wait for a round-trip + // to the server to get the updated list of variants + this.setState({ + media: newMediaOrder + }); + + // Save the updated positions + Meteor.defer(() => { + newMediaOrder.forEach((mediaItem, index) => { + Media.update(mediaItem._id, { + $set: { + "metadata.priority": index + } + }); + }); + }); + } + + render() { + return ( + + ); + } +} + +function composer(props, onData) { + let media; + let editable; + const viewAs = Reaction.Router.getQueryParam("as"); + + if (!props.media) { + // Fetch media based on props + } else { + media = props.media; + } + + if (viewAs === "customer") { + editable = false; + } else { + editable = Reaction.hasPermission(props.permission || ["createProduct"]); + } + + onData(null, { + editable, + media + }); +} + +MediaGalleryContainer.propTypes = { + editable: PropTypes.bool, + id: PropTypes.string, + media: PropTypes.arrayOf(PropTypes.object), + placement: PropTypes.string +}; + +export default composeWithTracker(composer)(MediaGalleryContainer); diff --git a/imports/plugins/core/ui/client/containers/sortableItem.js b/imports/plugins/core/ui/client/containers/sortableItem.js new file mode 100644 index 00000000000..25e77ce8ba0 --- /dev/null +++ b/imports/plugins/core/ui/client/containers/sortableItem.js @@ -0,0 +1,138 @@ +import React, { PropTypes } from "react"; +import { findDOMNode } from "react-dom"; +import { DragSource, DropTarget } from "react-dnd"; + +const cardSource = { + beginDrag(props) { + return { + index: props.index + }; + } +}; + +/** + * Specifies the props to inject into your component. + * @param {DragSourceConnector} connect An onject containing functions to assign roles to a component's DOM nodes + * @param {DragSourceMonitor} monitor An object containing functions that return information about drag state + * @return {Object} Props for drag source + */ +function collectDropSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect) { + return { + connectDropTarget: connect.dropTarget() + }; +} + +const cardTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().index; + const hoverIndex = props.index; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return; + } + + // Determine rectangle on screen + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + + // Get horizontal middle + const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + + // Get vertical middle + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + + // Get pixels from left + const hoverClientX = clientOffset.x - hoverBoundingRect.left; + + // Get pixels to the top + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + // Only perform the move when the mouse has crossed half of the items height + // When dragging downwards, only move when the cursor is below 50% + // When dragging upwards, only move when the cursor is above 50% + + // // Dragging to left + // // Don't update position if we are dragging an item to the [left], + // // but have not crossed the middle of the item we are dragging over + // if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) { + // return; + // } + // + // // Dragging to right + // // Don't update position if we are dragging an item to the [right], + // // but have not crossed the middle of the item we are dragging over + // if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX) { + // return; + // } + // + // + // // Dragging downwards + // if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + // return; + // } + // + // // Dragging upwards + // if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + // return; + // } + // + // Move up the list + // if (dragIndex > hoverIndex && (hoverClientX > hoverMiddleX && hoverClientY < hoverMiddleY)) { + // return; + // } + // + // // Move down the list + // if (dragIndex < hoverIndex && (hoverClientX < hoverMiddleX && hoverClientY > hoverMiddleY)) { + // return; + // } + + // + // + // console.log("should update"); + // return + + // Time to actually perform the action + props.onMove(dragIndex, hoverIndex); + + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + monitor.getItem().index = hoverIndex; + } +}; + +export default function ComposeSortableItem(itemType, SortableItemComponent) { + const SortableItem = (props) => { + return ; + }; + + SortableItem.contextTypes = { + dragDropManager: PropTypes.object.isRequired + }; + + SortableItem.propTypes = { + // Injected by React DnD: + connectDragSource: PropTypes.func.isRequired, + connectDropTarget: PropTypes.func.isRequired, + connectDragPreview: PropTypes.func.isRequired, + isDragging: PropTypes.bool.isRequired + }; + + let decoratedComponent = SortableItem; + decoratedComponent = DragSource(itemType, cardSource, collectDropSource)(decoratedComponent); + decoratedComponent = DropTarget(itemType, cardTarget, collectDropTarget)(decoratedComponent); + + return decoratedComponent; +} diff --git a/imports/plugins/core/ui/client/containers/tagListContainer.js b/imports/plugins/core/ui/client/containers/tagListContainer.js new file mode 100644 index 00000000000..07f4290cfbc --- /dev/null +++ b/imports/plugins/core/ui/client/containers/tagListContainer.js @@ -0,0 +1,276 @@ +import React, { Component, PropTypes } from "react"; +import debounce from "lodash/debounce"; +import update from "react/lib/update"; +import { Meteor } from "meteor/meteor"; +import { Reaction, i18next } from "/client/api"; +import { composeWithTracker } from "react-komposer"; +import { TagList } from "../components/tags"; +import { Tags } from "/lib/collections"; +import { getTagIds } from "/lib/selectors/tags"; +import { DragDropProvider } from "/imports/plugins/core/ui/client/providers"; + + +function updateSuggestions(term, { excludeTags }) { + const slug = Reaction.getSlug(term); + + const selector = { + slug: new RegExp(slug, "i") + }; + + if (Array.isArray(excludeTags)) { + selector._id = { + $nin: excludeTags + }; + } + + const tags = Tags.find(selector).map((tag) => { + return { + label: tag.name + }; + }); + + return tags; +} + +class TagListContainer extends Component { + constructor(props) { + super(props); + + this.state = { + tagIds: props.tagIds || [], + tagsByKey: props.tagsByKey || {}, + newTag: { + name: "" + }, + suggestions: [] + }; + + this.debounceUpdateTagOrder = debounce(() => { + Meteor.call( + "products/updateProductField", + this.props.product._id, + "hashtags", + this.state.tagIds + ); + }, 500); + } + + componentWillReceiveProps(nextProps) { + this.setState({ + tagIds: nextProps.tagIds || [], + tagsByKey: nextProps.tagsByKey || {} + }); + } + + get productId() { + if (this.props.product) { + return this.props.product._id; + } + return null; + } + + canSaveTag(tag) { + // Blank tags cannot be saved + if (typeof tag.name === "string" && tag.name.trim().length === 0) { + return false; + } + + // If the tag does not have an id, then allow the save + if (!tag._id) { + return true; + } + + // Get the original tag from the props + // Tags from props are not mutated, and come from an outside source + const originalTag = this.props.tagsByKey[tag._id]; + + if (originalTag && originalTag.name !== tag.name) { + return true; + } + + return false; + } + + handleNewTagSave = (tag) => { + if (this.productId && this.canSaveTag(tag)) { + Meteor.call("products/updateProductTags", this.productId, tag.name, null, (error) => { + if (error) { + return Alerts.toast(i18next.t("productDetail.tagExists"), "error"); + } + + this.setState({ + newTag: { + name: "" + }, + suggestions: [] + }); + + return true; + }); + } + } + + handleNewTagUpdate = (tag) => { + this.setState({ + newTag: tag + }); + } + + handleTagSave = (tag) => { + if (this.productId && this.canSaveTag(tag)) { + Meteor.call("products/updateProductTags", this.productId, tag.name, tag._id, (error) => { + if (error) { + return Alerts.toast(i18next.t("productDetail.tagExists"), "error"); + } + + this.setState({ + suggestions: [] + }); + + return true; + }); + } + } + + handleTagRemove = (tag) => { + if (this.productId) { + Meteor.call("products/removeProductTag", this.productId, tag._id, (error) => { + if (error) { + Alerts.toast(i18next.t("productDetail.tagInUse"), "error"); + } + }); + } + } + + handleTagUpdate = (tag) => { + const newState = update(this.state, { + tagsByKey: { + [tag._id]: { + $set: tag + } + } + }); + + this.setState(newState); + } + + handleMoveTag = (dragIndex, hoverIndex) => { + const tag = this.state.tagIds[dragIndex]; + + // Apply new sort order to variant list + const newState = update(this.state, { + tagIds: { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, tag] + ] + } + }); + + // Set local state so the component does't have to wait for a round-trip + // to the server to get the updated list of variants + this.setState(newState, () => { + // Save the updated positions + if (this.props.product) { + this.debounceUpdateTagOrder(); + } + }); + } + + handleGetSuggestions = (suggestionUpdateRequest) => { + const suggestions = updateSuggestions( + suggestionUpdateRequest.value, + { excludeTags: this.state.tagIds } + ); + + this.setState({ + suggestions: suggestions + }); + } + + handleClearSuggestions = () => { + this.setState({ + suggestions: [] + }); + } + + get tags() { + if (this.props.editable) { + return this.state.tagIds.map((tagId) => this.state.tagsByKey[tagId]); + } + + return this.props.tagsAsArray; + } + + render() { + return ( + + + + ); + } +} + +TagListContainer.propTypes = { + children: PropTypes.node, + editable: PropTypes.bool, + hasPermission: PropTypes.bool, + product: PropTypes.object, + tagIds: PropTypes.arrayOf(PropTypes.string), + tagsAsArray: PropTypes.arrayOf(PropTypes.object), + tagsByKey: PropTypes.object +}; + +function composer(props, onData) { + let tags = props.tags; + + if (props.product) { + if (_.isArray(props.product.hashtags)) { + tags = _.map(props.product.hashtags, function (id) { + return Tags.findOne(id); + }); + } + } + + let isEditable = props.editable; + + if (typeof isEditable !== "boolean") { + isEditable = Reaction.hasPermission(props.premissions); + } + + const tagsByKey = {}; + + if (Array.isArray(tags)) { + for (const tag of tags) { + tagsByKey[tag._id] = tag; + } + } + + onData(null, { + isProductTags: props.product !== undefined, + tagIds: getTagIds({ tags }), + tagsByKey, + tagsAsArray: tags, + editable: isEditable + }); +} + +let decoratedComponent = TagListContainer; +decoratedComponent = composeWithTracker(composer)(decoratedComponent); + +export default decoratedComponent; diff --git a/imports/plugins/core/ui/client/providers/dragDropProvider.js b/imports/plugins/core/ui/client/providers/dragDropProvider.js new file mode 100644 index 00000000000..8af6bf4f4d2 --- /dev/null +++ b/imports/plugins/core/ui/client/providers/dragDropProvider.js @@ -0,0 +1,45 @@ +import React, { Component, PropTypes, Children } from "react"; // eslint-disable-line +import { DragDropManager } from "dnd-core"; +import HTML5Backend from "react-dnd-html5-backend"; + +let defaultManager = new DragDropManager(HTML5Backend); + +// /** +// * This is singleton used to initialize only once dnd in our app. +// * If you initialized dnd and then try to initialize another dnd +// * context the app will break. +// * Here is more info: https://github.com/gaearon/react-dnd/issues/186 +// * +// * The solution is to call Dnd context from this singleton this way +// * all dnd contexts in the app are the same. +// */ +// export default function getDndContext() { +// if (defaultManager) return defaultManager; +// +// defaultManager = new DragDropManager(HTML5Backend); +// +// return defaultManager; +// } + +class DragDropProvider extends Component { + getChildContext() { + return { + dragDropManager: defaultManager + }; + } + + render() { + // `Children.only` enables us not to add a
    for nothing + return Children.only(this.props.children); + } +} + +DragDropProvider.childContextTypes = { + dragDropManager: PropTypes.object.isRequired +}; + +DragDropProvider.propTypes = { + children: PropTypes.node +}; + +export default DragDropProvider; diff --git a/imports/plugins/core/ui/client/providers/index.js b/imports/plugins/core/ui/client/providers/index.js new file mode 100644 index 00000000000..76663a550d2 --- /dev/null +++ b/imports/plugins/core/ui/client/providers/index.js @@ -0,0 +1,3 @@ +export { default as Translatable } from "./translatable"; +export { default as TranslationProvider } from "./translationProvider"; +export { default as DragDropProvider } from "./dragDropProvider"; diff --git a/imports/plugins/core/ui/client/providers/translatable.js b/imports/plugins/core/ui/client/providers/translatable.js new file mode 100644 index 00000000000..e81f8b28c94 --- /dev/null +++ b/imports/plugins/core/ui/client/providers/translatable.js @@ -0,0 +1,17 @@ +import React, { PropTypes } from "react"; + +export default function Translatable() { + return (Component) => { + const TranslatableComponent = (props, context) => { + const { translations } = context; + + return ; + }; + + TranslatableComponent.contextTypes = { + translations: PropTypes.object.isRequired + }; + + return TranslatableComponent; + }; +} diff --git a/imports/plugins/core/ui/client/providers/translationProvider.js b/imports/plugins/core/ui/client/providers/translationProvider.js new file mode 100644 index 00000000000..ee502ced333 --- /dev/null +++ b/imports/plugins/core/ui/client/providers/translationProvider.js @@ -0,0 +1,36 @@ +import React, { Component, PropTypes, Children } from "react"; // eslint-disable-line +import { composeWithTracker } from "react-komposer"; +import { i18next, i18nextDep } from "/client/api"; + +class TranslationProvider extends Component { + getChildContext() { + const { translations } = this.props; + return { translations }; + } + render() { + // `Children.only` enables us not to add a
    for nothing + return Children.only(this.props.children); + } +} + +TranslationProvider.childContextTypes = { + translations: PropTypes.object.isRequired +}; + +TranslationProvider.propTypes = { + children: PropTypes.node, + translations: PropTypes.object.isRequired +}; + +function composer(props, onData) { + i18nextDep.depend(); + + onData(null, { + translations: { + language: Session.get("language") + } + }); +} + + +export default composeWithTracker(composer)(TranslationProvider); diff --git a/imports/plugins/core/versions/README.md b/imports/plugins/core/versions/README.md new file mode 100644 index 00000000000..9e6171a3c99 --- /dev/null +++ b/imports/plugins/core/versions/README.md @@ -0,0 +1,5 @@ +# Note + +This is an experimental and preliminary implementation of migrations +so that we can roll out this feature. However this implementation may +(and probably will) change diff --git a/imports/plugins/core/versions/index.js b/imports/plugins/core/versions/index.js new file mode 100644 index 00000000000..e14bb44673e --- /dev/null +++ b/imports/plugins/core/versions/index.js @@ -0,0 +1 @@ +export { Migrations as Migrations } from "meteor/percolate:migrations"; diff --git a/imports/plugins/core/versions/register.js b/imports/plugins/core/versions/register.js new file mode 100644 index 00000000000..85d437c48d6 --- /dev/null +++ b/imports/plugins/core/versions/register.js @@ -0,0 +1,10 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Migrations", + name: "reaction-migrations", + icon: "fa fa-database", + autoEnable: true, + settings: {}, + registry: [] +}); diff --git a/imports/plugins/core/versions/server/index.js b/imports/plugins/core/versions/server/index.js new file mode 100644 index 00000000000..64e7a701e99 --- /dev/null +++ b/imports/plugins/core/versions/server/index.js @@ -0,0 +1,2 @@ +import "./startup"; +import "./migrations/"; diff --git a/imports/plugins/core/versions/server/migrations/1_rebuild_account_and_order_search_collections.js b/imports/plugins/core/versions/server/migrations/1_rebuild_account_and_order_search_collections.js new file mode 100644 index 00000000000..52e707778e8 --- /dev/null +++ b/imports/plugins/core/versions/server/migrations/1_rebuild_account_and_order_search_collections.js @@ -0,0 +1,22 @@ +import { Migrations } from "/imports/plugins/core/versions"; +import { OrderSearch, AccountSearch } from "/lib/collections"; +import { buildOrderSearch, + buildAccountSearch } from "/imports/plugins/included/search-mongo/server/methods/searchcollections"; + +Migrations.add({ + version: 1, + up: function () { + OrderSearch.remove({}); + AccountSearch.remove(); + buildOrderSearch(); + buildAccountSearch(); + }, + down: function () { + // whether we are going up or down we just want to update the search collections + // to match whatever the current code in the build methods are. + OrderSearch.remove({}); + AccountSearch.remove(); + buildOrderSearch(); + buildAccountSearch(); + } +}); diff --git a/imports/plugins/core/versions/server/migrations/index.js b/imports/plugins/core/versions/server/migrations/index.js new file mode 100644 index 00000000000..238d8131c02 --- /dev/null +++ b/imports/plugins/core/versions/server/migrations/index.js @@ -0,0 +1 @@ +import "./1_rebuild_account_and_order_search_collections"; diff --git a/imports/plugins/core/versions/server/startup.js b/imports/plugins/core/versions/server/startup.js new file mode 100644 index 00000000000..d0177b2e477 --- /dev/null +++ b/imports/plugins/core/versions/server/startup.js @@ -0,0 +1,20 @@ +import _ from "lodash"; +import { Hooks, Logger } from "/server/api"; +import { Migrations } from "/imports/plugins/core/versions"; + +function reactionLogger(opts) { + if (_.includes(["warn", "info", "error"], opts.level)) { + Logger[opts.level](opts.message); + } +} + +Migrations.config({ + logger: reactionLogger, + log: true, + logIfLatest: false, + collectionName: "Migrations" +}); + +Hooks.Events.add("afterCoreInit", () => { + Migrations.migrateTo("latest"); +}); diff --git a/imports/plugins/included/authnet/server/methods/authnet.js b/imports/plugins/included/authnet/server/methods/authnet.js index bbe81e9c86d..cf7fd4d337b 100644 --- a/imports/plugins/included/authnet/server/methods/authnet.js +++ b/imports/plugins/included/authnet/server/methods/authnet.js @@ -6,7 +6,7 @@ import { Meteor } from "meteor/meteor"; import { check, Match } from "meteor/check"; import { Promise } from "meteor/promise"; -import AuthNetAPI from "authorize-net"; +import AuthNetAPI from "@reactioncommerce/authorize-net"; import { Reaction, Logger } from "/server/api"; import { Packages } from "/lib/collections"; import { PaymentMethod } from "/lib/collections/schemas"; diff --git a/imports/plugins/included/default-theme/client/styles/base.less b/imports/plugins/included/default-theme/client/styles/base.less index 76bb54bb7e4..10b1b224b81 100644 --- a/imports/plugins/included/default-theme/client/styles/base.less +++ b/imports/plugins/included/default-theme/client/styles/base.less @@ -4,6 +4,10 @@ html, body { letter-spacing: @letter-spacing; } +body { + // margin-top: 50px; +} + main { min-height: 80vh; // padding: 20px 60px 135px 60px; @@ -77,7 +81,6 @@ h3 { a { cursor: pointer; - &:focus, &:hover { text-decoration: none; } diff --git a/imports/plugins/included/default-theme/client/styles/button.less b/imports/plugins/included/default-theme/client/styles/button.less index e8a66a62a83..efc4009fb97 100644 --- a/imports/plugins/included/default-theme/client/styles/button.less +++ b/imports/plugins/included/default-theme/client/styles/button.less @@ -22,13 +22,33 @@ justify-content: center; align-items: center; border-radius: 50px; - width: 24px; - height: 24px; + width: @btn-icon-size; + height: @btn-icon-size; color: @btn-edit-color; background-color: @btn-edit-bg; margin-left: 5px; } +// .rui.button.edit.btn-default, .btn-edit.btn-default { +// background-color: @btn-default-bg; +// } + +.rui.button.edit.btn-success, .btn-edit.btn-success { + background-color: @btn-success-bg; +} + +.rui.button.edit.btn-warning, .btn-edit.btn-warning { + background-color: @btn-warning-bg; +} + +.rui.button.edit.btn-danger, .btn-edit.btn-danger { + background-color: @btn-danger-bg; +} + +.rui.button.edit.btn-info, .btn-edit.btn-info { + background-color: @btn-info-bg; +} + .rui.button.edit > .icon, .btn-edit > .icon { display: flex; @@ -62,7 +82,6 @@ height: 24px; color: inherit; background-color: transparent; - // margin-left: 5px; } .rui.button.round, .btn-round { @@ -100,3 +119,18 @@ color: @black30; } } + +.rui.button.icon-only i { + width: 100%; +} + +.btn-flat { + .btn-link(); +} + +.btn-flat:hover, +.btn-flat:active, +.btn-flat:focus, +.btn-flat:visited { + text-decoration: none; +} diff --git a/imports/plugins/included/default-theme/client/styles/dropdowns.less b/imports/plugins/included/default-theme/client/styles/dropdowns.less index e39ffa22f34..658229f892d 100644 --- a/imports/plugins/included/default-theme/client/styles/dropdowns.less +++ b/imports/plugins/included/default-theme/client/styles/dropdowns.less @@ -26,3 +26,10 @@ display: block; padding: 3px 20px; } + + +.rui.dropdown-menu { + display: block; + position: static; + float: none; +} diff --git a/imports/plugins/included/default-theme/client/styles/main.less b/imports/plugins/included/default-theme/client/styles/main.less index 38e499c2795..134f708ae84 100644 --- a/imports/plugins/included/default-theme/client/styles/main.less +++ b/imports/plugins/included/default-theme/client/styles/main.less @@ -73,6 +73,7 @@ @import "grid.less"; @import "items.less"; @import "media.less"; +@import "menu.less"; @import "metadata.less"; @import "mixins.less"; @import "navbar.less"; @@ -90,7 +91,7 @@ @import "tagTree.less"; @import "textfield.less"; @import "themeEditor.less"; -@import "tooltip.less"; +@import "toolbar.less"; @import "tooltip.less"; @import "variables.less"; @@ -138,3 +139,5 @@ // Search @import "search/dashboard.less"; @import "search/results.less"; +@import "search/search-type-toggle.less"; +@import "search/sortable-table.less"; diff --git a/imports/plugins/included/default-theme/client/styles/media.less b/imports/plugins/included/default-theme/client/styles/media.less index bf3c4f13a56..0c82c8540b0 100644 --- a/imports/plugins/included/default-theme/client/styles/media.less +++ b/imports/plugins/included/default-theme/client/styles/media.less @@ -1,3 +1,121 @@ +/* media gellery */ +.rui.media-gallery { + max-width: 100%; + text-align: center; + min-height: 150px !important; + max-height: 100%; + border-radius: 6px; + margin-top: 5px; + margin-bottom: 20px; + position: relative; +} + +.gallery-drop-pane .mainImg { + display:block; + top:0; + left:0; + padding:2px; + margin:0; + width:100%; + height:100%; +} + +.rui.media-gallery { + list-style: none; + padding: 0; + width: 100%; +} + +.rui.media-gallery .rui.badge-container { + position: absolute; + top: @badge-offset; + right: @badge-offset; + z-index: 1; +} + +.rui.media-gallery .draggable-media:first-child, +.rui.media-gallery .gallery-image:first-child { + display: block; + top: 0; + left: 0; + padding: 2px; + margin: 0; + width: 100%; + height: 100%; + pointer: auto; +} + +.rui.media-gallery .draggable-media { + display: inline-block; + width: 24%; + height: 24%; +} + +.rui.media-gallery .gallery-image { + position: relative; + display: inline-block; + top: 0; + left: 0; + padding: 2px; + margin: 0; + width: 24%; + height: 24%; + background-color: @list-group-hover-bg; +} + +.rui.media-gallery .gallery-image.gallery-tools { + visibility: hidden; + padding: 10px; + position:absolute; + bottom:0px; + right:0px; +} + +.rui.media-gallery .gallery-image.progress { + position: relative; + height: 5px; + top: -10px; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 0px; + display: block; +} + +.rui.media-gallery .gallery-image:hover { + background-color: @list-group-hover-bg; + opacity: 0.7; + cursor: pointer; +} +.rui.media-gallery .gallery-image:hover .gallery-tools { + visibility: visible; + opacity: 1; +} + +.rui.media-gallery .gallery-image:hover .gallery-tools a { + color: @gray-dark; +} + +.rui.media-gallery .gallery-image .video { + width: 100%; +} + +.rui.media-gallery .gallery-image.add { + position: relative; +} + +.rui.media-gallery .gallery-image.add .badge-container { + position: absolute;; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + padding: 2px; + margin: 0; + width: 100%; + height: 100%; +} + .rui.media img { width: 100%; diff --git a/imports/plugins/included/default-theme/client/styles/menu.less b/imports/plugins/included/default-theme/client/styles/menu.less new file mode 100644 index 00000000000..554710e28ed --- /dev/null +++ b/imports/plugins/included/default-theme/client/styles/menu.less @@ -0,0 +1,16 @@ +.rui.menu .divider { + margin-top: 10px; + margin-bottom: 10px; +} + +.rui.menu-item .font-icon { + .margin-right(10px); +} + +.rui.menu-item.disabled { + color: @black30; +} + +.rui.menu-item.disabled:hover { + background-color: transparent; +} diff --git a/imports/plugins/included/default-theme/client/styles/metadata.less b/imports/plugins/included/default-theme/client/styles/metadata.less index 4f337f4a965..882bba085e3 100644 --- a/imports/plugins/included/default-theme/client/styles/metadata.less +++ b/imports/plugins/included/default-theme/client/styles/metadata.less @@ -4,55 +4,179 @@ } .rui.meta-item { + display: flex; flex: 0 0 auto; width: 100%; - border-bottom: 1px solid @border-color; } -.rui.meta-item:first-child { - border-top: 1px solid @border-color; +.rui.meta-key { + width: 50%; + .padding-right(10px); + border-right: 1px solid @border-color; + width: 50%; + padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal; + background-color: @white; + border: 1px solid @border-color; + border-right: none; + border-bottom: none; +} + +.rui.meta-value { + width: 50%; + padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal; + background-color: @white; + border: 1px solid @border-color; + border-bottom: none; } -.rui.meta-item:first-child button { + + + +.rui.meta-item:first-child .meta-key { + border-radius: @btn-border-radius-base 0 0 0; +} + +.rui.meta-item:first-child .meta-value { border-radius: 0 @btn-border-radius-base 0 0; } -.rui.meta-item button { - border-radius: 0; - flex: 0 0 auto; + +.rui.meta-item:last-child .meta-key { + border-radius: 0 0 0 @btn-border-radius-base; + border-bottom: 1px solid @border-color; } -.rui.meta-item:last-child button { +.rui.meta-item:last-child .meta-value { border-radius: 0 0 @btn-border-radius-base 0; + border-bottom: 1px solid @border-color; +} + +.rui.metafield-list-item, +.rui.metafield-new-item { + padding: 0; + background-color: transparent; + + .row { + padding: 5px 3px 5px; + } + + border: none; + cursor: pointer; + + form { + display: flex; + } + + input { + flex: 1 1 auto; + border-radius: 0; + border: none; + box-shadow: none; + border: 1px solid @border-color; + border-right: none; + width: auto; + min-width: 0; + } + + button { + flex: 0 0 auto; + height: 100%; + width: 44px; + border-radius: 0; + background-color: @white; + border: 1px solid @border-color; + } } -.rui.metadata.edit { - // border: 1px solid @border-color; + + +// First item +.rui.metafield-list-item:first-child .metafield-key-input { + border-radius: @border-radius-base 0 0 0; } -.rui.metadata.edit form { - display: flex; - width: 100%; +.rui.metafield-list-item:first-child .metafield-value-input { + border-radius: 0; + border-left: none; } +.rui.metafield-list-item:first-child button { + border-radius: 0 @border-radius-base 0 0; + border-left: none; +} +// Middle Items +.rui.metafield-list-item .metafield-key-input { + border-radius: 0; +} -.rui.metadata.edit input { - flex: 1 1 auto; - border: none; - border-right: 1px solid @border-color; +.rui.metafield-list-item .metafield-value-input { + border-radius: 0; + border-left: none; +} + +.rui.metafield-list-item button { + border-left: none; +} + +// Last Item +.rui.metafield-list-item:last-child .metafield-key-input { + border-radius: 0 0 0 @border-radius-base; +} + +.rui.metafield-list-item:last-child .metafield-value-input { border-radius: 0; + border-left: none; +} + +.rui.metafield-list-item:last-child button { + border-radius: 0 0 @border-radius-base 0; + border-left: none; +} + +html.rtl { + // First item + .rui.metafield-list-item:first-child .metafield-key-input { + border-radius: 0 @border-radius-base 0 0; + } + + .rui.metafield-list-item:first-child .metafield-value-input { + border-radius: 0; + border-right: none; + } - &:first-child { + .rui.metafield-list-item:first-child button { + border-radius: @border-radius-base 0 0 0; border-left: 1px solid @border-color; } -} -.rui.metadata.edit { + // Middle Items + .rui.metafield-list-item .metafield-key-input { + border-radius: 0; + } - button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + .rui.metafield-list-item .metafield-value-input { + border-radius: 0; + border-right: none; + } + + .rui.metafield-list-item button { + border-left: 1px solid @border-color; + } + + // Last Item + .rui.metafield-list-item:last-child .metafield-key-input { + border-radius: 0 0 @border-radius-base 0; + } + + .rui.metafield-list-item:last-child .metafield-value-input { + border-radius: 0; + border-right: none; + } + + .rui.metafield-list-item:last-child button { + border-radius: 0 0 0 @border-radius-base; + border-left: 1px solid @border-color; } } diff --git a/imports/plugins/included/default-theme/client/styles/popover.less b/imports/plugins/included/default-theme/client/styles/popover.less index 25ccea893bf..52f7389e692 100644 --- a/imports/plugins/included/default-theme/client/styles/popover.less +++ b/imports/plugins/included/default-theme/client/styles/popover.less @@ -1,28 +1,32 @@ -.drop-element, -.drop-element:after, -.drop-element:before, -.drop-element *, -.drop-element *:after, -.drop-element *:before { +.popover-element, +.popover-element:after, +.popover-element:before, +.popover-element *, +.popover-element *:after, +.popover-element *:before { box-sizing: border-box; } -.drop-element { +.popover-element { position: absolute; display: none; z-index: @zindex-popover; } -.drop-element.drop-open { +.rui.popover-content { + padding: 0; +} + +.popover-element.popover-open { display: block; } -.drop-element.drop-theme-arrows { +.popover-element.popover-theme-arrows { max-width: 100%; max-height: 100%; } -.drop-element.drop-theme-arrows .drop-content { +.popover-element.popover-theme-arrows .popover-content { border-radius: 5px; position: relative; font-family: inherit; @@ -33,10 +37,10 @@ line-height: 1.5em; -webkit-transform: translateZ(0); transform: translateZ(0); - -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); - filter:drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); + -webkit-filter: popover-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); + filter:popover-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); } -.drop-element.drop-theme-arrows .drop-content:before { +.popover-element.popover-theme-arrows .popover-content:before { content: ""; display: block; position: absolute; @@ -46,102 +50,102 @@ border-width: 16px; border-style: solid; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-center .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-center .popover-content { margin-bottom: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-center .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-center .popover-content:before { top: 100%; left: 50%; margin-left: -16px; border-top-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-center .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-center .popover-content { margin-top: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-center .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-center .popover-content:before { bottom: 100%; left: 50%; margin-left: -16px; border-bottom-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-right.drop-element-attached-middle .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-right.popover-element-attached-middle .popover-content { margin-right: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-right.drop-element-attached-middle .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-right.popover-element-attached-middle .popover-content:before { left: 100%; top: 50%; margin-top: -16px; border-left-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-left.drop-element-attached-middle .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-left.popover-element-attached-middle .popover-content { margin-left: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-left.drop-element-attached-middle .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-left.popover-element-attached-middle .popover-content:before { right: 100%; top: 50%; margin-top: -16px; border-right-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-left.popover-target-attached-bottom .popover-content { margin-top: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-left.popover-target-attached-bottom .popover-content:before { bottom: 100%; left: 16px; border-bottom-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-right.popover-target-attached-bottom .popover-content { margin-top: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-right.popover-target-attached-bottom .popover-content:before { bottom: 100%; right: 16px; border-bottom-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-left.popover-target-attached-top .popover-content { margin-bottom: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-left.popover-target-attached-top .popover-content:before { top: 100%; left: 16px; border-top-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-right.popover-target-attached-top .popover-content { margin-bottom: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-right.popover-target-attached-top .popover-content:before { top: 100%; right: 16px; border-top-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-right.popover-target-attached-left .popover-content { margin-right: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-right.popover-target-attached-left .popover-content:before { top: 16px; left: 100%; border-left-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-left.popover-target-attached-right .popover-content { margin-left: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-top.popover-element-attached-left.popover-target-attached-right .popover-content:before { top: 16px; right: 100%; border-right-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-right.popover-target-attached-left .popover-content { margin-right: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-right.popover-target-attached-left .popover-content:before { bottom: 16px; left: 100%; border-left-color: #eee; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-left.popover-target-attached-right .popover-content { margin-left: 16px; } -.drop-element.drop-theme-arrows.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content:before { +.popover-element.popover-theme-arrows.popover-element-attached-bottom.popover-element-attached-left.popover-target-attached-right .popover-content:before { bottom: 16px; right: 100%; border-right-color: #eee; diff --git a/imports/plugins/included/default-theme/client/styles/products/attributes.less b/imports/plugins/included/default-theme/client/styles/products/attributes.less index fc54087eb73..3517fd5c66c 100644 --- a/imports/plugins/included/default-theme/client/styles/products/attributes.less +++ b/imports/plugins/included/default-theme/client/styles/products/attributes.less @@ -1,4 +1,5 @@ -.pdp-container { +// Legacy styles for old, Blaze, product detail page +.pdp { .metafield-list-item, .metafield-new-item { padding: 0; background-color: transparent; @@ -71,38 +72,39 @@ .product-attributes { padding: 0; } -} -html.rtl .pdp-container { - .metafield-list-item, .metafield-new-item { - input { - border: 1px solid @border-color; - border-left: none; - } - button { - border-left: 1px solid @border-color; - } - } + html.rtl { + .metafield-list-item, .metafield-new-item { + input { + border: 1px solid @border-color; + border-left: none; + } - .metafield-list-item:first-child { - input:first-child { - border-radius: 0 @border-radius-base 0 0; + button { + border-left: 1px solid @border-color; + } } - button { - border-radius: @border-radius-base 0 0 0; - } + .metafield-list-item:first-child { + input:first-child { + border-radius: 0 @border-radius-base 0 0; + } - } + button { + border-radius: @border-radius-base 0 0 0; + } - .metafield-list-item:last-child { - input:first-child { - border-radius: 0 0 @border-radius-base 0; } - button { - border-radius: 0 0 0 @border-radius-base; + .metafield-list-item:last-child { + input:first-child { + border-radius: 0 0 @border-radius-base 0; + } + + button { + border-radius: 0 0 0 @border-radius-base; + } } } } diff --git a/imports/plugins/included/default-theme/client/styles/products/productDetail.less b/imports/plugins/included/default-theme/client/styles/products/productDetail.less index 2f674672c3a..14bc1ba6c12 100644 --- a/imports/plugins/included/default-theme/client/styles/products/productDetail.less +++ b/imports/plugins/included/default-theme/client/styles/products/productDetail.less @@ -1,4 +1,6 @@ - +.pdp-container { + max-width: 1440px; +} .pdp-content { display: flex; @@ -14,11 +16,12 @@ } // Header -.php.header { +.pdp.header { text-align: center; } -.pdp.header h1 { +.pdp.header h1, +.pdp.header .title-edit-input { text-align: center; font-family: @headings-font-family-h1; font-size: @product-title-font-size; @@ -26,7 +29,8 @@ color: @headings-color-h1; } -.pdp.header h2 { +.pdp.header h2, +.pdp.header .pageTitle-edit-input { text-align: center; font-family: @headings-font-family-h2; font-size: @product-page-title-font-size; @@ -34,6 +38,17 @@ color: @headings-color-h2; } +// Product edit fields +.pdp.product-detail-edit { + position: relative; +} + +.pdp.product-detail-edit .edit-controls { + position: absolute; + top: -@btn-icon-size / 2; + right: -@btn-icon-size / 2; +} + @media only screen and (max-width: @screen-xs-max) { .pdp.header h1 { font-size: 30px; @@ -89,8 +104,11 @@ } } - - +.pdp .rui.social-buttons { + display: flex; + flex: 0 0 auto; + align-items: center; +} // Right Column // @@ -152,6 +170,12 @@ padding: 5px; } +.pdp .tags-header.edit, +.pdp .meta-header.edit { + display: flex; + align-items: center; +} + // Social .pdp .social-media { position: relative; diff --git a/imports/plugins/included/default-theme/client/styles/products/productGrid.less b/imports/plugins/included/default-theme/client/styles/products/productGrid.less index bcdf4ec56a7..7f5a9607952 100644 --- a/imports/plugins/included/default-theme/client/styles/products/productGrid.less +++ b/imports/plugins/included/default-theme/client/styles/products/productGrid.less @@ -136,12 +136,6 @@ padding: 5px; } - .badge { - position: absolute; - top: 20px; - .left(19px); - } - @media @tablet { .product-grid-item-images { height: 225px; diff --git a/imports/plugins/included/default-theme/client/styles/products/productImageGallery.less b/imports/plugins/included/default-theme/client/styles/products/productImageGallery.less index 7f0789b3354..a68ee8d677b 100644 --- a/imports/plugins/included/default-theme/client/styles/products/productImageGallery.less +++ b/imports/plugins/included/default-theme/client/styles/products/productImageGallery.less @@ -1,3 +1,4 @@ +// Legacy product detail page media gallery /* image gallery select/reorder/upload */ .galleryDropPane { max-width: 100%; diff --git a/imports/plugins/included/default-theme/client/styles/products/variant.less b/imports/plugins/included/default-theme/client/styles/products/variant.less index dc60f8ba272..96b1534cd88 100644 --- a/imports/plugins/included/default-theme/client/styles/products/variant.less +++ b/imports/plugins/included/default-theme/client/styles/products/variant.less @@ -70,3 +70,7 @@ background-color: @black20; margin-left: 5px; } + +.variant-deleted { + opacity: 0.8; +} diff --git a/imports/plugins/included/default-theme/client/styles/products/variantList.less b/imports/plugins/included/default-theme/client/styles/products/variantList.less index 032aacb3db3..a64641222fd 100644 --- a/imports/plugins/included/default-theme/client/styles/products/variantList.less +++ b/imports/plugins/included/default-theme/client/styles/products/variantList.less @@ -12,7 +12,7 @@ position: relative; } -.variant-select-option .variant-edit { +.variant-select-option .variant-controls { position: absolute; top: 0; right: 0; @@ -22,9 +22,5 @@ display: flex; justify-content: center; align-items: center; - border-radius: 50px; - width: 24px; - height: 24px; - background-color: @black20; margin-left: 5px; } diff --git a/imports/plugins/included/default-theme/client/styles/search/results.less b/imports/plugins/included/default-theme/client/styles/search/results.less index b285014ce4b..12089497587 100644 --- a/imports/plugins/included/default-theme/client/styles/search/results.less +++ b/imports/plugins/included/default-theme/client/styles/search/results.less @@ -22,7 +22,6 @@ /* -------------------------- Modal Header -------------------------- */ .rui.search-modal-header { width: 100%; - max-height: 250px; padding-top: 40px; padding-bottom: 40px; background: @white; diff --git a/imports/plugins/included/default-theme/client/styles/search/search-type-toggle.less b/imports/plugins/included/default-theme/client/styles/search/search-type-toggle.less new file mode 100644 index 00000000000..a91d5b40ad3 --- /dev/null +++ b/imports/plugins/included/default-theme/client/styles/search/search-type-toggle.less @@ -0,0 +1,27 @@ +.rui.search-type-toggle { + width: 96%; + height: auto; + cursor: pointer; + .display(flex); + border-bottom: solid 1px @black20; + + .search-type-option { + flex: 1; + text-align: center; + padding-top: 10px; + padding-bottom: 7px; + text-transform: uppercase; + border-bottom: 3px solid transparent; + transition: border-color 200ms linear; + + &:hover { + border-bottom: 3px solid fade(@brand-primary-color, 40%); + } + + &.search-type-active { + border-bottom: 3px solid @brand-primary-color; + transition: border-color 200ms linear; + } + + } +} diff --git a/imports/plugins/included/default-theme/client/styles/search/sortable-table.less b/imports/plugins/included/default-theme/client/styles/search/sortable-table.less new file mode 100644 index 00000000000..6d1ad15df8d --- /dev/null +++ b/imports/plugins/included/default-theme/client/styles/search/sortable-table.less @@ -0,0 +1,166 @@ +/* -------------------------- Full Modal -------------------------- */ +.data-table { + width: 96%; + padding-top: 20px; + padding-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + margin-left: auto; + margin-right: auto; + background: @black10; + + thead { + font-weight: bold; + + th { + cursor: pointer; + } + } + + .taco-table { + border-collapse: collapse; + border-spacing: 0; + margin-bottom: 22px; + max-width: 100%; + } + + .taco-table * { + box-sizing: border-box; + } + + .taco-table.table-full-width { + width: 100%; + } + + .taco-table.table-not-full-width { + width: auto; + } + + .taco-table td, + .taco-table th { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 8px; + padding-right: 20px; + line-height: 1.42857; + text-align: left; + } + + .taco-table th { + vertical-align: bottom; + } + + .taco-table td { + vertical-align: top; + } + + .taco-table.table-striped > tbody > tr:nth-child(odd) { + background-color: rgba(0, 0, 0, 0.03); + } + + .taco-table th.sortable { + cursor: pointer; + } + + .taco-table th.sortable:hover { + background-color: rgba(0, 0, 0, 0.03); + } + + .taco-table th.sortable.sorted { + background-color: #f2f7fd; + position: relative; + } + + .taco-table > tbody > tr.row-highlight, + .taco-table.table-striped > tbody > tr:nth-child(odd).row-highlight { + background-color: #eee; + } + + .taco-table .column-highlight { + background-color: rgba(0, 0, 0, 0.05); + } + + .taco-table .data-type-Number, + .taco-table .data-type-NumberOrdinal { + // text-align: right; + } + + .taco-table .group-header { + border-left: 1px solid rgba(0, 0, 0, 0.06); + border-right: 1px solid rgba(0, 0, 0, 0.06); + } + + .taco-table .group-first { + border-left: 1px solid rgba(0, 0, 0, 0.06); + } + + .taco-table .group-last { + border-right: 1px solid rgba(0, 0, 0, 0.06); + } + + .taco-table .sort-indicator.sort-ascending:after { + // content: '▲'; + font-family: FontAwesome; + content: "\f106"; + } + + .taco-table .sort-indicator.sort-descending:after { + // content: '▼'; + font-family: FontAwesome; + content: "\f107"; + } + + .taco-table .sort-indicator:after { + position: absolute; + right: 4px; + top: -1.5px; + } + + .taco-table .highlight-max, + .taco-table .highlight-min { + font-weight: bold; + } + + .taco-table > tbody.bottom-data { + background-color: #f4f4f4; + border-top: 1px solid #ccc; + } + + + .taco-table td.shipping-status span { + + border-radius: 5px; + padding-top: 3px; + padding-bottom: 3px; + padding-left: 8px; + padding-right: 8px; + + &.shipped { + background: #fe8163; + color: #fff; + } + &.packed { + background: #dd5b42; + color: #fff; + } + &.new { + background: #64e192; + color: #fff; + } + + } + + .taco-table td.account-manage span { + + border-radius: 5px; + padding-top: 3px; + padding-bottom: 3px; + padding-left: 8px; + padding-right: 8px; + background: #64e192; + color: #fff; + cursor: pointer; + + } + +} diff --git a/imports/plugins/included/default-theme/client/styles/tags.less b/imports/plugins/included/default-theme/client/styles/tags.less index f7595048c78..f3c3e0df4eb 100644 --- a/imports/plugins/included/default-theme/client/styles/tags.less +++ b/imports/plugins/included/default-theme/client/styles/tags.less @@ -12,6 +12,10 @@ margin: 5px; } +.rui.tag.full-width { + width: 100%; +} + .rui.tag.edit { height: 33px; padding: 0; @@ -29,6 +33,7 @@ } .rui.tag.edit input { + width: 100%; height: 100%; padding: @padding-base-vertical @padding-base-horizontal; border-radius: 0; @@ -38,7 +43,7 @@ border-left: none; } -.rui.tag.edit button { +.rui.tag.edit .btn { border-radius: 0; border: 1px solid @border-color; border-left: none; @@ -46,23 +51,23 @@ color: @text-color; } -.rui.tag.edit button:first-child { +.rui.tag.edit .btn:first-child { border-left: 1px solid @border-color; border-radius: @border-radius-base 0 0 @border-radius-base; } -html.rtl .rui.tag.edit button:first-child { +html.rtl .rui.tag.edit .btn:first-child { border-left: none; border-right: 1px solid @border-color; border-radius: 0 @border-radius-base @border-radius-base 0; } -.rui.tag.edit button:last-child { +.rui.tag.edit .btn:last-child { border-right: 1px solid @border-color; border-radius: 0 @border-radius-base @border-radius-base 0; } -html.rtl .rui.tag.edit button:last-child { +html.rtl .rui.tag.edit .btn:last-child { border-left: 1px solid @border-color; border-radius: @border-radius-base 0 0 @border-radius-base; } diff --git a/imports/plugins/included/default-theme/client/styles/textfield.less b/imports/plugins/included/default-theme/client/styles/textfield.less index af47dec7739..772efe661ef 100644 --- a/imports/plugins/included/default-theme/client/styles/textfield.less +++ b/imports/plugins/included/default-theme/client/styles/textfield.less @@ -1,12 +1,13 @@ .rui.textfield { - display: flex; - flex: 1 1 auto; - border: none; + // display: flex; + // flex: 1 1 auto; + // border: none; } .rui.textfield input { - flex: 1 1 auto; + // flex: 1 1 auto; + width: 100%; padding: @padding-base-vertical @padding-base-horizontal; border: 1px solid @border-color; border-radius: @input-border-radius; @@ -33,7 +34,8 @@ } .rui.textfield textarea { - flex: 1 1 auto; + // flex: 1 1 auto; + width: 100%; padding: @padding-base-vertical @padding-base-horizontal; border: 1px solid @border-color; border-radius: @input-border-radius; diff --git a/imports/plugins/included/default-theme/client/styles/toolbar.less b/imports/plugins/included/default-theme/client/styles/toolbar.less new file mode 100644 index 00000000000..e1bd92a69c6 --- /dev/null +++ b/imports/plugins/included/default-theme/client/styles/toolbar.less @@ -0,0 +1,32 @@ +.rui.toolbar { + // Use bootstrap styles + .navbar(); + .navbar-inverse(); + display: flex; + align-items: center; + padding-left: @navbar-padding-horizontal; + padding-right: @navbar-padding-horizontal; +} + +.rui.toolbar-group { + display: flex; + flex: 0 0 auto; + align-items: center; + padding-left: @navbar-padding-horizontal; + padding-right: @navbar-padding-horizontal; +} + +.rui.toolbar-group.left { + flex: 0 0 auto; + justify-content: flex-start; +} + +.rui.toolbar-group.center { + flex: 0 0 auto; + justify-content: center; +} + +.rui.toolbar-group.right { + flex: 1 1 auto; + justify-content: flex-end; +} diff --git a/imports/plugins/included/default-theme/client/styles/tooltip.less b/imports/plugins/included/default-theme/client/styles/tooltip.less index 6cb536bfcfa..604b46b866b 100644 --- a/imports/plugins/included/default-theme/client/styles/tooltip.less +++ b/imports/plugins/included/default-theme/client/styles/tooltip.less @@ -10,7 +10,7 @@ .tooltip-element { position: absolute; - display: none; + display: none } .tooltip-element.tooltip-open { display: block; @@ -18,7 +18,7 @@ } .tooltip-element.tooltip-theme-arrows { - max-width: 100%; + max-width: 300px; max-height: 100%; } .tooltip-element.tooltip-theme-arrows .tooltip-content { diff --git a/imports/plugins/included/default-theme/client/styles/variables.less b/imports/plugins/included/default-theme/client/styles/variables.less index edcaf5382c8..66c5da13d53 100644 --- a/imports/plugins/included/default-theme/client/styles/variables.less +++ b/imports/plugins/included/default-theme/client/styles/variables.less @@ -126,6 +126,14 @@ @btn-edit-bg: @black20; @btn-active-color: darken(@brand-accent-color, 50%); @btn-active-bg: @brand-accent-color; +@btn-icon-size: 24px; + +// == Badge +@badge-offset: @btn-icon-size / 2; +@badge-offset-top: -@badge-offset / 2; +@badge-offset-right: -@badge-offset / 2; +@badge-offset-bottom: @badge-offset / 2; +@badge-offset-left: @badge-offset / 2; //== Footer @footer-default-bg: transparent; diff --git a/imports/plugins/included/example-paymentmethod/client/checkout/example.js b/imports/plugins/included/example-paymentmethod/client/checkout/example.js index e9a105f7eee..19a980a89d3 100644 --- a/imports/plugins/included/example-paymentmethod/client/checkout/example.js +++ b/imports/plugins/included/example-paymentmethod/client/checkout/example.js @@ -8,6 +8,8 @@ import { ExamplePayment } from "../../lib/collections/schemas"; import "./example.html"; +let submitting = false; + function uiEnd(template, buttonText) { template.$(":input").removeAttr("disabled"); template.$("#btn-complete-order").text(buttonText); @@ -64,6 +66,7 @@ AutoForm.addHooks("example-payment-form", { uiEnd(template, "Resubmit payment"); } else { if (transaction.saved === true) { + submitting = false; paymentMethod = { processor: "Example", storedCard: storedCard, diff --git a/imports/plugins/included/inventory/server/methods/inventory.app-test.js b/imports/plugins/included/inventory/server/methods/inventory.app-test.js index 0b94a63126e..df3ca6e9492 100644 --- a/imports/plugins/included/inventory/server/methods/inventory.app-test.js +++ b/imports/plugins/included/inventory/server/methods/inventory.app-test.js @@ -7,6 +7,7 @@ import { expect } from "meteor/practicalmeteor:chai"; import { sinon } from "meteor/practicalmeteor:sinon"; import { addProduct } from "/server/imports/fixtures/products"; import Fixtures from "/server/imports/fixtures"; +import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions"; Fixtures(); @@ -39,6 +40,7 @@ describe("inventory method", function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + sandbox.stub(RevisionApi, "isRevisionControlEnabled", () => true); // again hack. w/o this we can't remove products from previous spec. Inventory.remove({}); // Empty Inventory }); @@ -62,21 +64,33 @@ describe("inventory method", function () { }); describe("inventory/remove", function () { - it("should remove deleted variants from inventory", function () { - // register inventory (that we'll should delete on variant removal) + // register inventory (that we'll should delete on variant removal) + let qty; + let newQty; + + before(function () { + qty = options[1].inventoryQuantity; + }); + + beforeEach(function () { sandbox.stub(Reaction, "hasPermission", () => true); + }); + + it("should have option quantity greater then 0", function () { // checking our option quantity. It should be greater than zero. - const qty = options[1].inventoryQuantity; expect(qty).to.be.above(0); - // before spec we're cleared collection, so we need to insert all docs - // again and make sure quantity will be equal with `qty` + }); + + it("should have equal quantities", function () { Meteor.call("inventory/register", options[1]); const midQty = Inventory.find({ variantId: options[1]._id }).count(); expect(midQty).to.equal(qty); + }); + + it("should have new quantity equal to 0", function () { // then we are removing option and docs should be automatically removed Meteor.call("products/deleteVariant", options[1]._id); - const newQty = Inventory.find({ variantId: options[1]._id }).count(); - expect(newQty).to.not.equal(qty); + newQty = Inventory.find({ variantId: options[1]._id }).count(); expect(newQty).to.equal(0); }); }); @@ -175,4 +189,3 @@ describe("inventory method", function () { // }); }); }); - diff --git a/imports/plugins/included/product-admin/client/components/index.js b/imports/plugins/included/product-admin/client/components/index.js new file mode 100644 index 00000000000..12d90072da5 --- /dev/null +++ b/imports/plugins/included/product-admin/client/components/index.js @@ -0,0 +1 @@ +export { default as ProductAdmin } from "./productAdmin.js"; diff --git a/imports/plugins/included/product-admin/client/components/productAdmin.js b/imports/plugins/included/product-admin/client/components/productAdmin.js new file mode 100644 index 00000000000..e5b71f0a832 --- /dev/null +++ b/imports/plugins/included/product-admin/client/components/productAdmin.js @@ -0,0 +1,258 @@ +import React, { Component, PropTypes } from "react"; +import { + Button, + Card, + CardHeader, + CardBody, + CardGroup, + Divider, + Metadata, + TextField, + Translation +} from "/imports/plugins/core/ui/client/components"; +import { Router } from "/client/api"; +import { PublishContainer } from "/imports/plugins/core/revisions"; +import { TagListContainer } from "/imports/plugins/core/ui/client/containers"; + +class ProductAdmin extends Component { + handleDeleteProduct = () => { + if (this.props.onDeleteProduct) { + this.props.onDeleteProduct(this.props.product); + } + } + + handleRestoreProduct = () => { + if (this.props.onRestoreProduct) { + this.props.onRestoreProduct(this.props.product); + } + } + + + handleFieldChange = (event, value, field) => { + if (this.props.onFieldChange) { + this.props.onFieldChange(field, value); + } + } + + handleToggleVisibility = () => { + if (this.props.onProductFieldSave) { + this.props.onProductFieldSave(this.product._id, "isVisible", !this.product.isVisible); + } + } + + handleMetaChange = (event, metafield, index) => { + if (this.props.onMetaChange) { + this.props.onMetaChange(metafield, index); + } + } + + handleFieldBlur = (event, value, field) => { + if (this.props.onProductFieldSave) { + this.props.onProductFieldSave(this.product._id, field, value); + } + } + + handleMetaSave = (event, metafield, index) => { + if (this.props.onMetaSave) { + this.props.onMetaSave(this.product._id, metafield, index); + } + } + + handleMetaRemove = (event, metafield, index) => { + if (this.props.onMetaRemove) { + this.props.onMetaRemove(this.product._id, metafield, index); + } + } + + get product() { + return this.props.product || {}; + } + + get permalink() { + if (this.props.product) { + return Router.pathFor("product", { + hash: { + handle: this.props.product.handle + } + }); + } + + return ""; + } + + renderProductVisibilityLabel() { + if (this.product.isVisible) { + return ( + + ); + } + + return ( + + ); + } + + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} + +ProductAdmin.propTypes = { + handleFieldBlur: PropTypes.func, + handleFieldChange: PropTypes.func, + handleProductFieldChange: PropTypes.func, + newMetafield: PropTypes.object, + onDeleteProduct: PropTypes.func, + onFieldChange: PropTypes.func, + onMetaChange: PropTypes.func, + onMetaRemove: PropTypes.func, + onMetaSave: PropTypes.func, + onProductFieldSave: PropTypes.func, + onRestoreProduct: PropTypes.func, + product: PropTypes.object, + revisonDocumentIds: PropTypes.arrayOf(PropTypes.string) +}; + +export default ProductAdmin; diff --git a/imports/plugins/included/product-admin/client/containers/index.js b/imports/plugins/included/product-admin/client/containers/index.js new file mode 100644 index 00000000000..97bc93fc9c7 --- /dev/null +++ b/imports/plugins/included/product-admin/client/containers/index.js @@ -0,0 +1 @@ +export { default as ProductAdminContainer } from "./productAdminContainer"; diff --git a/imports/plugins/included/product-admin/client/containers/productAdminContainer.js b/imports/plugins/included/product-admin/client/containers/productAdminContainer.js new file mode 100644 index 00000000000..684dcee53b0 --- /dev/null +++ b/imports/plugins/included/product-admin/client/containers/productAdminContainer.js @@ -0,0 +1,160 @@ +import React, { Component, PropTypes } from "react"; +import update from "react/lib/update"; +import { composeWithTracker } from "react-komposer"; +import { ReactionProduct } from "/lib/api"; +import { Tags, Media } from "/lib/collections"; +import { ProductAdmin } from "../components"; + +class ProductAdminContainer extends Component { + constructor(props) { + super(props); + + this.state = { + product: props.product, + newMetafield: { + key: "", + value: "" + } + }; + } + + componentWillReceiveProps(nextProps) { + this.setState({ + product: nextProps.product + }); + } + + get product() { + return this.state.product || this.props.product || {}; + } + + handleDeleteProduct = (product) => { + ReactionProduct.maybeDeleteProduct(product || this.product); + } + + handleFieldChange = (field, value) => { + const newState = update(this.state, { + product: { + $merge: { + [field]: value + } + } + }); + + this.setState(newState); + } + + handleProductFieldSave = (productId, fieldName, value) => { + Meteor.call("products/updateProductField", productId, fieldName, value); + } + + + handleMetaChange = (metafield, index) => { + let newState = {}; + + if (index >= 0) { + newState = update(this.state, { + product: { + metafields: { + [index]: { + $set: metafield + } + } + } + }); + } else { + newState = { + newMetafield: metafield + }; + } + + this.setState(newState); + } + + handleMetafieldSave = (productId, metafield, index) => { + // update existing metafield + if (index >= 0) { + Meteor.call("products/updateMetaFields", productId, metafield, index); + } else if (metafield.key && metafield.value) { + Meteor.call("products/updateMetaFields", productId, metafield); + } + + this.setState({ + newMetafield: { + key: "", + value: "" + } + }); + } + + handleMetaRemove = (productId, metafield) => { + Meteor.call("products/removeMetaFields", productId, metafield); + } + + handleProductRestore = (product) => { + Meteor.call("products/updateProductField", product._id, "isDeleted", false); + } + + render() { + return ( + + ); + } +} + + +function composer(props, onData) { + const product = ReactionProduct.selectedProduct(); + let tags; + let media; + let revisonDocumentIds; + + if (product) { + if (_.isArray(product.hashtags)) { + tags = _.map(product.hashtags, function (id) { + return Tags.findOne(id); + }); + } + + const selectedVariant = ReactionProduct.selectedVariant(); + + if (selectedVariant) { + media = Media.find({ + "metadata.variantId": selectedVariant._id + }, { + sort: { + "metadata.priority": 1 + } + }); + } + + revisonDocumentIds = [product._id]; + } + + onData(null, { + product: product, + media, + tags, + revisonDocumentIds + }); +} + +ProductAdminContainer.propTypes = { + product: PropTypes.object, + tags: PropTypes.arrayOf(PropTypes.object) +}; + +// Decorate component and export +export default composeWithTracker(composer)(ProductAdminContainer); diff --git a/imports/plugins/included/product-admin/client/index.js b/imports/plugins/included/product-admin/client/index.js new file mode 100644 index 00000000000..80883aaa9c5 --- /dev/null +++ b/imports/plugins/included/product-admin/client/index.js @@ -0,0 +1,2 @@ +import "./templates/productAdmin.html"; +import "./templates/productAdmin.js"; diff --git a/imports/plugins/included/product-admin/client/templates/productAdmin.html b/imports/plugins/included/product-admin/client/templates/productAdmin.html new file mode 100644 index 00000000000..5abd65ea2c9 --- /dev/null +++ b/imports/plugins/included/product-admin/client/templates/productAdmin.html @@ -0,0 +1,5 @@ + diff --git a/imports/plugins/included/product-admin/client/templates/productAdmin.js b/imports/plugins/included/product-admin/client/templates/productAdmin.js new file mode 100644 index 00000000000..6a8e48dbbdd --- /dev/null +++ b/imports/plugins/included/product-admin/client/templates/productAdmin.js @@ -0,0 +1,16 @@ +import { ProductAdminContainer } from "../containers"; + +Template.ProductAdmin.helpers({ + component() { + const currentData = Template.currentData(); + let data; + + if (currentData && currentData.data) { + data = currentData.data; + } + + return Object.assign({}, data, { + component: ProductAdminContainer + }); + } +}); diff --git a/imports/plugins/included/product-admin/register.js b/imports/plugins/included/product-admin/register.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/imports/plugins/included/product-detail-simple/client/components/addToCartButton.js b/imports/plugins/included/product-detail-simple/client/components/addToCartButton.js new file mode 100644 index 00000000000..5396095e9dc --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/addToCartButton.js @@ -0,0 +1,42 @@ +import React, { Component, PropTypes } from "react"; +import { Translation } from "/imports/plugins/core/ui/client/components"; + + +class AddToCartButton extends Component { + hanleCartQuantityChange = (event) => { + if (this.props.onCartQuantityChange) { + this.props.onCartQuantityChange(event, event.target.value); + } + } + + render() { + return ( +
    + + +
    + ); + } +} + +AddToCartButton.propTypes = { + cartQuantity: PropTypes.number, + onCartQuantityChange: PropTypes.func, + onClick: PropTypes.func +}; + +export default AddToCartButton; diff --git a/imports/plugins/included/product-detail-simple/client/components/childVariant.js b/imports/plugins/included/product-detail-simple/client/components/childVariant.js new file mode 100644 index 00000000000..7f53943d0c3 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/childVariant.js @@ -0,0 +1,89 @@ +import React, { Component, PropTypes} from "react"; +import classnames from "classnames"; +import { Translation } from "/imports/plugins/core/ui/client/components"; +import { MediaItem } from "/imports/plugins/core/ui/client/components"; + +class ChildVariant extends Component { + handleClick = (event) => { + if (this.props.onClick) { + this.props.onClick(event, this.props.variant); + } + } + + get hasMedia() { + return Array.isArray(this.props.media) && this.props.media.length > 0; + } + + get primaryMediaItem() { + if (this.hasMedia) { + return this.props.media[0]; + } + + return null; + } + + renderDeletionStatus() { + if (this.props.variant.isDeleted) { + return ( + + + + ); + } + + return null; + } + + renderMedia() { + if (this.hasMedia) { + const media = this.primaryMediaItem; + + return ( + + ); + } + + return null; + } + + render() { + const variant = this.props.variant; + const classes = classnames({ + "btn": true, + "btn-default": true, + "variant-detail-selected": this.props.isSelected, + "variant-deleted": this.props.variant.isDeleted + }); + + return ( +
    + + +
    + {this.renderDeletionStatus()} + {this.props.visibilityButton} + {this.props.editButton} +
    +
    + ); + } +} + +ChildVariant.propTypes = { + editButton: PropTypes.node, + isSelected: PropTypes.bool, + media: PropTypes.arrayOf(PropTypes.object), + onClick: PropTypes.func, + variant: PropTypes.object, + visibilityButton: PropTypes.node +}; + + +export default ChildVariant; diff --git a/imports/plugins/included/product-detail-simple/client/components/index.js b/imports/plugins/included/product-detail-simple/client/components/index.js new file mode 100644 index 00000000000..aadea499ff2 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/index.js @@ -0,0 +1,7 @@ +export { default as ProductDetail } from "./productDetail"; +export { default as VariantList } from "./variantList"; +export { default as ChildVariant } from "./childVariant"; +export { default as AddToCartButton } from "./addToCartButton"; +export { default as ProductMetadata } from "./metadata"; +export { default as ProductTags } from "./tags"; +export { default as ProductField } from "./productField"; diff --git a/imports/plugins/included/product-detail-simple/client/components/metadata.js b/imports/plugins/included/product-detail-simple/client/components/metadata.js new file mode 100644 index 00000000000..02d6d382d7c --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/metadata.js @@ -0,0 +1,66 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import { Metadata, Translation } from "/imports/plugins/core/ui/client/components/"; +import { EditContainer } from "/imports/plugins/core/ui/client/containers"; + +class ProductMetadata extends Component { + get metafields() { + return this.props.metafields || this.props.product.metafields; + } + + get showEditControls() { + return this.props.product && this.props.editable; + } + + renderEditButton() { + if (this.showEditControls) { + return ( + + + + ); + } + + return null; + } + + render() { + if (Array.isArray(this.metafields) && this.metafields.length > 0) { + const headerClassName = classnames({ + "meta-header": true, + "edit": this.showEditControls + }); + + return ( +
    +

    + + {this.renderEditButton()} +

    + +
    + ); + } + + return null; + } +} + +ProductMetadata.propTypes = { + editContainerProps: PropTypes.object, + editable: PropTypes.bool, + metafields: PropTypes.arrayOf(PropTypes.object), + product: PropTypes.object +}; + +export default ProductMetadata; diff --git a/imports/plugins/included/product-detail-simple/client/components/productDetail.js b/imports/plugins/included/product-detail-simple/client/components/productDetail.js new file mode 100644 index 00000000000..8dbb6dd77ff --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/productDetail.js @@ -0,0 +1,205 @@ +import React, { Component, PropTypes } from "react"; +import { + Button, + Currency, + DropDownMenu, + MenuItem, + Translation, + Toolbar, + ToolbarGroup +} from "/imports/plugins/core/ui/client/components/"; +import { + AddToCartButton, + ProductMetadata, + ProductTags, + ProductField +} from "./"; +import { AlertContainer } from "/imports/plugins/core/ui/client/containers"; +import { PublishContainer } from "/imports/plugins/core/revisions"; + +class ProductDetail extends Component { + get tags() { + return this.props.tags || []; + } + + get product() { + return this.props.product || {}; + } + + get editable() { + return this.props.editable; + } + + handleVisibilityChange = (event, isProductVisible) => { + if (this.props.onProductFieldChange) { + this.props.onProductFieldChange(this.product._id, "isVisible", isProductVisible); + } + } + + handlePublishActions = (event, action) => { + if (action === "delete" && this.props.onDeleteProduct) { + this.props.onDeleteProduct(this.product._id); + } + } + + renderToolbar() { + if (this.props.hasAdminPermission) { + return ( + + + + + + } + onChange={this.props.onViewContextChange} + value={this.props.viewAs} + > + + + + + + + + + ); + } + + return null; + } + + render() { + return ( +
    + {this.renderToolbar()} + +
    + + +
    + } + onProductFieldChange={this.props.onProductFieldChange} + product={this.product} + textFieldProps={{ + i18nKeyPlaceholder: "productDetailEdit.title", + placeholder: "Title" + }} + /> + + } + onProductFieldChange={this.props.onProductFieldChange} + product={this.product} + textFieldProps={{ + i18nKeyPlaceholder: "productDetailEdit.pageTitle", + placeholder: "Subtitle" + }} + /> +
    + + +
    +
    + {this.props.mediaGalleryComponent} + + +
    + +
    + + +
    +
    + + + + + +
    +
    + {this.props.socialComponent} +
    +
    + + +
    + +
    + +
    + +
    + +
    + {this.props.topVariantComponent} +
    +
    +
    + + +
    +
    +
    +
    +
    + ); + } +} + +ProductDetail.propTypes = { + cartQuantity: PropTypes.number, + editable: PropTypes.bool, + hasAdminPermission: PropTypes.bool, + mediaGalleryComponent: PropTypes.node, + onAddToCart: PropTypes.func, + onCartQuantityChange: PropTypes.func, + onDeleteProduct: PropTypes.func, + onProductFieldChange: PropTypes.func, + onViewContextChange: PropTypes.func, + priceRange: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + product: PropTypes.object, + socialComponent: PropTypes.node, + tags: PropTypes.arrayOf(PropTypes.object), + topVariantComponent: PropTypes.node, + viewAs: PropTypes.string +}; + +export default ProductDetail; diff --git a/imports/plugins/included/product-detail-simple/client/components/productField.js b/imports/plugins/included/product-detail-simple/client/components/productField.js new file mode 100644 index 00000000000..bf9c9ad607a --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/productField.js @@ -0,0 +1,132 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import { TextField } from "/imports/plugins/core/ui/client/components/"; +import { EditContainer } from "/imports/plugins/core/ui/client/containers"; + +class ProductField extends Component { + static state = {} + + constructor(props) { + super(props); + + this.state = { + value: this.value + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.product.pageTitle !== this.state.value) { + this.setState({ + value: nextProps.product[this.fieldName] + }); + } + } + + handleChange = (event, value) => { + this.setState({ + value + }); + } + + handleBlur = (event, value) => { + if (this.props.onProductFieldChange) { + this.props.onProductFieldChange(this.props.product._id, this.fieldName, value); + } + } + + get fieldName() { + return this.props.fieldName; + } + + get value() { + return (this.state && this.state.value) || this.props.product[this.fieldName]; + } + + get showEditControls() { + return this.props.product && this.props.editable; + } + + renderEditButton() { + if (this.showEditControls) { + return ( + + + + ); + } + + return null; + } + + renderTextField() { + const baseClassName = classnames({ + "pdp": true, + "product-detail-edit": true, + [`${this.fieldName}-edit`]: this.fieldName + }); + + const textFieldClassName = classnames({ + "pdp": true, + "product-detail-edit": true, + [`${this.fieldName}-edit-input`]: this.fieldName + }); + + return ( +
    + + {this.renderEditButton()} +
    + ); + } + + render() { + if (this.showEditControls) { + return this.renderTextField(); + } + + if (this.props.element) { + return React.cloneElement(this.props.element, { + className: "pdp field", + itemProp: this.props.itemProp, + children: this.value + }); + } + + return ( +
    + {this.value} +
    + ); + } +} + +ProductField.propTypes = { + editContainerProps: PropTypes.object, + editable: PropTypes.bool, + element: PropTypes.node, + fieldName: PropTypes.string, + fieldTitle: PropTypes.string, + itemProp: PropTypes.string, + multiline: PropTypes.bool, + onProductFieldChange: PropTypes.func, + product: PropTypes.object, + textFieldProps: PropTypes.object +}; + +export default ProductField; diff --git a/imports/plugins/included/product-detail-simple/client/components/tags.js b/imports/plugins/included/product-detail-simple/client/components/tags.js new file mode 100644 index 00000000000..1c236b79e6b --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/tags.js @@ -0,0 +1,67 @@ +import React, { Component, PropTypes } from "react"; +import classnames from "classnames"; +import { Translation, TagList } from "/imports/plugins/core/ui/client/components/"; +import { TagListContainer, EditContainer } from "/imports/plugins/core/ui/client/containers"; + +class ProductTags extends Component { + get tags() { + return this.props.tags; + } + + get showEditControls() { + return this.props.product && this.props.editable; + } + + renderEditButton() { + if (this.showEditControls) { + return ( + + + + ); + } + + return null; + } + + render() { + if (Array.isArray(this.tags) && this.tags.length > 0) { + const headerClassName = classnames({ + "tags-header": true, + "edit": this.showEditControls + }); + + return ( +
    +

    + + {this.renderEditButton()} +

    + +
    + ); + } + return null; + } +} + +ProductTags.propTypes = { + editButton: PropTypes.node, + editable: PropTypes.bool, + product: PropTypes.object, + tags: PropTypes.arrayOf(PropTypes.object) +}; + +export default ProductTags; diff --git a/imports/plugins/included/product-detail-simple/client/components/variant.js b/imports/plugins/included/product-detail-simple/client/components/variant.js new file mode 100644 index 00000000000..b977a84a4f0 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/variant.js @@ -0,0 +1,116 @@ +import React, { Component, PropTypes} from "react"; +import classnames from "classnames"; +import { Currency, Translation } from "/imports/plugins/core/ui/client/components"; +import { SortableItem } from "/imports/plugins/core/ui/client/containers"; + +class Variant extends Component { + + handleClick = (event) => { + if (this.props.onClick) { + this.props.onClick(event, this.props.variant); + } + } + + get price() { + return this.props.displayPrice || this.props.variant.price; + } + + renderInventoryStatus() { + const { + inventoryManagement, + inventoryPolicy + } = this.props.variant; + + if (inventoryManagement && this.props.soldOut) { + if (inventoryPolicy) { + return ( + + + + ); + } + + return ( + + + + ); + } + + return null; + } + + renderDeletionStatus() { + if (this.props.variant.isDeleted) { + return ( + + + + ); + } + + return null; + } + + render() { + const variant = this.props.variant; + const classes = classnames({ + "variant-detail": true, + "variant-detail-selected": this.props.isSelected, + "variant-deleted": this.props.variant.isDeleted + }); + + const variantElement = ( +
  • +
    +
    + {variant.title} +
    + +
    + + + +
    + +
    + {this.renderDeletionStatus()} + {this.renderInventoryStatus()} + {this.props.visibilityButton} + {this.props.editButton} +
    +
    +
  • + ); + + if (this.props.editable) { + return this.props.connectDragSource( + this.props.connectDropTarget( + variantElement + ) + ); + } + + return variantElement; + } +} + +Variant.propTypes = { + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + displayPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + editButton: PropTypes.node, + editable: PropTypes.bool, + isSelected: PropTypes.bool, + onClick: PropTypes.func, + soldOut: PropTypes.bool, + variant: PropTypes.object, + visibilityButton: PropTypes.node +}; + +export default SortableItem("product-variant", Variant); diff --git a/imports/plugins/included/product-detail-simple/client/components/variantList.js b/imports/plugins/included/product-detail-simple/client/components/variantList.js new file mode 100644 index 00000000000..b3cd1cdbdf2 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/components/variantList.js @@ -0,0 +1,159 @@ +import React, { Component, PropTypes} from "react"; +import Variant from "./variant"; +import { EditContainer } from "/imports/plugins/core/ui/client/containers"; +import { Divider, Translation } from "/imports/plugins/core/ui/client/components"; +import { ChildVariant } from "./"; + +class VariantList extends Component { + + handleVariantEditClick = (event, editButtonProps) => { + if (this.props.onEditVariant) { + return this.props.onEditVariant(event, editButtonProps.data); + } + return true; + } + + handleVariantVisibilityClick = (event, editButtonProps) => { + if (this.props.onVariantVisibiltyToggle) { + const isVariantVisible = !editButtonProps.data.isVisible; + this.props.onVariantVisibiltyToggle(event, editButtonProps.data, isVariantVisible); + } + } + + handleChildleVariantClick = (event, variant) => { + if (this.props.onVariantClick) { + this.props.onVariantClick(event, variant, 1); + } + } + + handleChildVariantEditClick = (event, editButtonProps) => { + if (this.props.onEditVariant) { + return this.props.onEditVariant(event, editButtonProps.data, 1); + } + return true; + } + + isSoldOut(variant) { + if (this.props.isSoldOut) { + return this.props.isSoldOut(variant); + } + + return false; + } + + renderVariants() { + if (this.props.variants) { + return this.props.variants.map((variant, index) => { + const displayPrice = this.props.displayPrice && this.props.displayPrice(variant._id); + + return ( + + + + ); + }); + } + + return ( +
  • + + {"+"} + +
  • + ); + } + + renderChildVariants() { + if (this.props.childVariants) { + return this.props.childVariants.map((childVariant, index) => { + const media = this.props.childVariantMedia.filter((mediaItem) => { + if (mediaItem.metadata.variantId === childVariant._id) { + return true; + } + return false; + }); + + return ( + + + + ); + }); + } + + return null; + } + + render() { + return ( +
    + +
      + {this.renderVariants()} +
    + +
    + {this.renderChildVariants()} +
    +
    + ); + } +} + +VariantList.propTypes = { + childVariantMedia: PropTypes.arrayOf(PropTypes.any), + childVariants: PropTypes.arrayOf(PropTypes.object), + displayPrice: PropTypes.func, + editable: PropTypes.bool, + isSoldOut: PropTypes.func, + onEditVariant: PropTypes.func, + onMoveVariant: PropTypes.func, + onVariantClick: PropTypes.func, + onVariantVisibiltyToggle: PropTypes.func, + variantIsSelected: PropTypes.func, + variants: PropTypes.arrayOf(PropTypes.object) +}; + +export default VariantList; diff --git a/imports/plugins/included/product-detail-simple/client/containers/index.js b/imports/plugins/included/product-detail-simple/client/containers/index.js new file mode 100644 index 00000000000..850ec7cb65f --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/containers/index.js @@ -0,0 +1,3 @@ +export { default as ProductDetailContainer } from "./productDetailContainer"; +export { default as SocialContainer } from "./socialContainer"; +export { default as VariantListContainer } from "./variantListContainer"; diff --git a/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js b/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js new file mode 100644 index 00000000000..ac3534b5580 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/containers/productDetailContainer.js @@ -0,0 +1,261 @@ +import React, { Component, PropTypes } from "react"; +import { composeWithTracker } from "react-komposer"; +import { Meteor } from "meteor/meteor"; +import { ReactionProduct } from "/lib/api"; +import { Reaction, i18next, Logger } from "/client/api"; +import { Tags, Media } from "/lib/collections"; +import { Loading } from "/imports/plugins/core/ui/client/components"; +import { ProductDetail } from "../components"; +import { SocialContainer, VariantListContainer } from "./"; +import { MediaGalleryContainer } from "/imports/plugins/core/ui/client/containers"; +import { DragDropProvider, TranslationProvider } from "/imports/plugins/core/ui/client/providers"; + +class ProductDetailContainer extends Component { + constructor(props) { + super(props); + + this.state = { + cartQuantity: 1 + }; + } + + handleCartQuantityChange = (event, quantity) => { + this.setState({ + cartQuantity: Math.max(quantity, 1) + }); + } + + handleAddToCart = () => { + let productId; + let quantity; + const currentVariant = ReactionProduct.selectedVariant(); + const currentProduct = ReactionProduct.selectedProduct(); + + if (currentVariant) { + if (currentVariant.ancestors.length === 1) { + const options = ReactionProduct.getVariants(currentVariant._id); + + if (options.length > 0) { + Alerts.inline("Please choose options before adding to cart", "warning", { + placement: "productDetail", + i18nKey: "productDetail.chooseOptions", + autoHide: 10000 + }); + return []; + } + } + + if (currentVariant.inventoryPolicy && currentVariant.inventoryQuantity < 1) { + Alerts.inline("Sorry, this item is out of stock!", "warning", { + placement: "productDetail", + i18nKey: "productDetail.outOfStock", + autoHide: 10000 + }); + return []; + } + + quantity = parseInt(this.state.cartQuantity, 10); + + if (quantity < 1) { + quantity = 1; + } + + if (!currentProduct.isVisible) { + Alerts.inline("Publish product before adding to cart.", "error", { + placement: "productDetail", + i18nKey: "productDetail.publishFirst", + autoHide: 10000 + }); + } else { + productId = currentProduct._id; + + if (productId) { + Meteor.call("cart/addToCart", productId, currentVariant._id, quantity, (error) => { + if (error) { + Logger.error("Failed to add to cart.", error); + return error; + } + // Reset cart quantity on success + this.handleCartQuantityChange(null, 1); + + return true; + }); + } + + // template.$(".variant-select-option").removeClass("active"); + ReactionProduct.setCurrentVariant(null); + // qtyField.val(1); + // scroll to top on cart add + $("html,body").animate({ + scrollTop: 0 + }, 0); + // slide out label + const addToCartText = i18next.t("productDetail.addedToCart"); + const addToCartTitle = currentVariant.title || ""; + $(".cart-alert-text").text(`${quantity} ${addToCartTitle} ${addToCartText}`); + + // Grab and cache the width of the alert to be used in animation + const alertWidth = $(".cart-alert").width(); + const direction = i18next.t("languageDirection") === "rtl" ? "left" : "right"; + const oppositeDirection = i18next.t("languageDirection") === "rtl" ? "right" : "left"; + + // Animate + return $(".cart-alert") + .show() + .css({ + [oppositeDirection]: "auto", + [direction]: -alertWidth + }) + .animate({ + [oppositeDirection]: "auto", + [direction]: 0 + }, 600) + .delay(4000) + .animate({ + [oppositeDirection]: "auto", + [direction]: -alertWidth + }, { + duration: 600, + complete() { + $(".cart-alert").hide(); + } + }); + } + } else { + Alerts.inline("Select an option before adding to cart", "warning", { + placement: "productDetail", + i18nKey: "productDetail.selectOption", + autoHide: 8000 + }); + } + + return null; + } + + handleProductFieldChange = (productId, fieldName, value) => { + Meteor.call("products/updateProductField", productId, fieldName, value); + } + + handleViewContextChange = (event, value) => { + Reaction.Router.setQueryParams({as: value}); + } + + handleDeleteProduct = () => { + ReactionProduct.maybeDeleteProduct(this.props.product); + } + + render() { + return ( + + + } + onAddToCart={this.handleAddToCart} + onCartQuantityChange={this.handleCartQuantityChange} + onViewContextChange={this.handleViewContextChange} + socialComponent={} + topVariantComponent={} + onDeleteProduct={this.handleDeleteProduct} + onProductFieldChange={this.handleProductFieldChange} + {...this.props} + /> + + + ); + } +} + +ProductDetailContainer.propTypes = { + media: PropTypes.arrayOf(PropTypes.object), + product: PropTypes.object +}; + +function composer(props, onData) { + const tagSub = Meteor.subscribe("Tags"); + const productId = Reaction.Router.getParam("handle"); + const variantId = Reaction.Router.getParam("variantId"); + const revisionType = Reaction.Router.getQueryParam("revision"); + const viewProductAs = Reaction.Router.getQueryParam("as"); + + let productSub; + + if (productId) { + productSub = Meteor.subscribe("Product", productId); + } + + if (productSub && productSub.ready() && tagSub.ready()) { + // Get the product + const product = ReactionProduct.setProduct(productId, variantId); + + if (Reaction.hasPermission("createProduct")) { + if (!Reaction.getActionView() && Reaction.isActionViewOpen() === true) { + Reaction.setActionView({ + template: "productAdmin", + data: product + }); + } + } + + // Get the product tags + if (product) { + let tags; + if (_.isArray(product.hashtags)) { + tags = _.map(product.hashtags, function (id) { + return Tags.findOne(id); + }); + } + + let mediaArray = []; + const selectedVariant = ReactionProduct.selectedVariant(); + + if (selectedVariant) { + mediaArray = Media.find({ + "metadata.variantId": selectedVariant._id + }, { + sort: { + "metadata.priority": 1 + } + }).fetch(); + } + + let priceRange; + if (selectedVariant && typeof selectedVariant === "object") { + const childVariants = ReactionProduct.getVariants(selectedVariant._id); + // when top variant has no child variants we display only its price + if (childVariants.length === 0) { + priceRange = selectedVariant.price; + } + // otherwise we want to show child variants price range + priceRange = ReactionProduct.getVariantPriceRange(); + } + + let productRevision; + + if (revisionType === "published") { + productRevision = product.__published; + } + + let editable; + + if (viewProductAs === "customer") { + editable = false; + } else { + editable = Reaction.hasPermission(["createProduct"]); + } + + onData(null, { + product: productRevision || product, + priceRange, + tags, + media: mediaArray, + editable, + viewAs: viewProductAs, + hasAdminPermission: Reaction.hasPermission(["createProduct"]) + }); + } + } +} + +// Decorate component and export +export default composeWithTracker(composer, Loading)(ProductDetailContainer); diff --git a/imports/plugins/included/product-detail-simple/client/containers/socialContainer.js b/imports/plugins/included/product-detail-simple/client/containers/socialContainer.js new file mode 100644 index 00000000000..f5eddf8226e --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/containers/socialContainer.js @@ -0,0 +1,100 @@ +import React, { Component, PropTypes } from "react"; +import { composeWithTracker } from "react-komposer"; +import { ReactionProduct } from "/lib/api"; +import SocialButtons from "/imports/plugins/included/social/client/components/socialButtons"; +import { createSocialSettings } from "/imports/plugins/included/social/lib/helpers"; +import { EditContainer } from "/imports/plugins/core/ui/client/containers"; +import { Media } from "/lib/collections"; + +class ProductSocialContainer extends Component { + render() { + return ( + + + + ); + } +} + +function composer(props, onData) { + const product = ReactionProduct.selectedProduct(); + const selectedVariant = ReactionProduct.selectedVariant() || {}; + let title = product.title; + let mediaUrl; + + if (selectedVariant) { + title = selectedVariant.title; + } + + let description; + + if (typeof product.description === "string") { + description = product.description.substring(0, 254); + } + + const media = Media.findOne({ + "metadata.variantId": { + $in: [ + selectedVariant._id, + product._id + ] + } + }, { + sort: { + "metadata.priority": 1 + } + }); + + if (media) { + mediaUrl = media.url(); + } + + const options = { + data: product, + title: product.title, + description, + placement: "productDetail", + buttonClassName: "fa-lg", + media: mediaUrl, + apps: { + facebook: { + description: product.facebookMsg || description, + media: mediaUrl + }, + twitter: { + description: product.twitterMsg || title, + media: mediaUrl + }, + googleplus: { + itemtype: "Product", + description: product.googleplusMsg || description, + media: mediaUrl + }, + pinterest: { + description: product.pinterestMsg || description, + media: mediaUrl + } + } + }; + + const socialSettings = createSocialSettings(options); + + onData(null, { + data: product, + socialSettings + }); +} + +ProductSocialContainer.propTypes = { + data: PropTypes.object, + socialSettings: PropTypes.object +}; + +export default composeWithTracker(composer)(ProductSocialContainer); diff --git a/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js b/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js new file mode 100644 index 00000000000..78ccc6ba86c --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/containers/variantListContainer.js @@ -0,0 +1,213 @@ +import React, { Component, PropTypes } from "react"; +import { composeWithTracker } from "react-komposer"; +import { ReactionProduct } from "/lib/api"; +import { Reaction } from "/client/api"; +import { VariantList } from "../components"; +import { getChildVariants } from "../selectors/variants"; +import { Products, Media } from "/lib/collections"; +import update from "react/lib/update"; +import { getVariantIds } from "/lib/selectors/variants"; +import { DragDropProvider } from "/imports/plugins/core/ui/client/providers"; + +function variantIsSelected(variantId) { + const current = ReactionProduct.selectedVariant(); + if (current && typeof current === "object" && (variantId === current._id || ~current.ancestors.indexOf(variantId))) { + return true; + } + + return false; +} + +function variantIsInActionView(variantId) { + const actionViewVariant = Reaction.getActionView().data; + + if (actionViewVariant) { + // Check if the variant is selected, and also visible & selected in the action view + return variantIsSelected(variantId) && variantIsSelected(actionViewVariant._id) && Reaction.isActionViewOpen(); + } + + return false; +} + +function getTopVariants() { + let inventoryTotal = 0; + const variants = ReactionProduct.getTopVariants(); + if (variants.length) { + // calculate inventory total for all variants + for (const variant of variants) { + if (variant.inventoryManagement) { + const qty = ReactionProduct.getVariantQuantity(variant); + if (typeof qty === "number") { + inventoryTotal += qty; + } + } + } + // calculate percentage of total inventory of this product + for (const variant of variants) { + const qty = ReactionProduct.getVariantQuantity(variant); + variant.inventoryTotal = inventoryTotal; + if (variant.inventoryManagement && inventoryTotal) { + variant.inventoryPercentage = parseInt(qty / inventoryTotal * 100, 10); + } else { + // for cases when sellers doesn't use inventory we should always show + // "green" progress bar + variant.inventoryPercentage = 100; + } + if (variant.title) { + variant.inventoryWidth = parseInt(variant.inventoryPercentage - + variant.title.length, 10); + } else { + variant.inventoryWidth = 0; + } + } + // sort variants in correct order + variants.sort((a, b) => a.index - b.index); + + return variants; + } + return []; +} + +function isSoldOut(variant) { + return ReactionProduct.getVariantQuantity(variant) < 1; +} + +class VariantListContainer extends Component { + componentWillReceiveProps() { + this.setState({}); + } + + get variants() { + return (this.state && this.state.variants) || this.props.variants; + } + + handleVariantClick = (event, variant, ancestors = -1) => { + if (Reaction.isActionViewOpen()) { + this.handleEditVariant(event, variant, ancestors); + } else { + const selectedProduct = ReactionProduct.selectedProduct(); + + ReactionProduct.setCurrentVariant(variant._id); + Session.set("variant-form-" + variant._id, true); + Reaction.Router.go("product", { + handle: selectedProduct.handle, + variantId: variant._id + }, { + as: Reaction.Router.getQueryParam("as") + }); + } + } + + handleEditVariant = (event, variant, ancestors = -1) => { + const selectedProduct = ReactionProduct.selectedProduct(); + let editVariant = variant; + if (ancestors >= 0) { + editVariant = Products.findOne(variant.ancestors[ancestors]); + } + + ReactionProduct.setCurrentVariant(variant._id); + Session.set("variant-form-" + editVariant._id, true); + Reaction.Router.go("product", { + handle: selectedProduct.handle, + variantId: variant._id + }, { + as: Reaction.Router.getQueryParam("as") + }); + + if (Reaction.hasPermission("createProduct")) { + Reaction.showActionView({ + label: "Edit Variant", + i18nKeyLabel: "productDetailEdit.editVariant", + template: "variantForm", + data: editVariant + }); + } + + // Prevent the default edit button `onEditButtonClick` function from running + return false; + } + + handleVariantVisibilityToggle = (event, variant, variantIsVisible) => { + Meteor.call("products/updateProductField", variant._id, "isVisible", variantIsVisible); + } + + handleMoveVariant = (dragIndex, hoverIndex) => { + const variant = this.props.variants[dragIndex]; + + // Apply new sort order to variant list + const newVariantOrder = update(this.props.variants, { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, variant] + ] + }); + + // Set local state so the component does't have to wait for a round-trip + // to the server to get the updated list of variants + this.setState({ + variants: newVariantOrder + }); + + // Save the updated positions + Meteor.defer(() => { + Meteor.call("products/updateVariantsPosition", getVariantIds(newVariantOrder)); + }); + } + + render() { + return ( + + + + ); + } +} + +function composer(props, onData) { + let childVariantMedia = []; + const childVariants = getChildVariants(); + + if (Array.isArray(childVariants)) { + childVariantMedia = Media.find({ + "metadata.variantId": { + $in: getVariantIds(childVariants) + } + }, { + sort: { + "metadata.priority": 1 + } + }).fetch(); + } + + let editable; + + if (Reaction.Router.getQueryParam("as") === "customer") { + editable = false; + } else { + editable = Reaction.hasPermission(["createProduct"]); + } + + onData(null, { + variants: getTopVariants(), + variantIsSelected, + variantIsInActionView, + childVariants, + childVariantMedia, + displayPrice: ReactionProduct.getVariantPriceRange, + isSoldOut: isSoldOut, + editable + }); +} + +VariantListContainer.propTypes = { + variants: PropTypes.arrayOf(PropTypes.object) +}; + +export default composeWithTracker(composer)(VariantListContainer); diff --git a/imports/plugins/included/product-detail-simple/client/index.js b/imports/plugins/included/product-detail-simple/client/index.js new file mode 100644 index 00000000000..4f4e214b790 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/index.js @@ -0,0 +1,2 @@ +import "./templates/productDetailSimple.html"; +import "./templates/productDetailSimple.js"; diff --git a/imports/plugins/included/product-detail-simple/client/selectors/variants.js b/imports/plugins/included/product-detail-simple/client/selectors/variants.js new file mode 100644 index 00000000000..a7cb9a23ca5 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/selectors/variants.js @@ -0,0 +1,39 @@ +import { ReactionProduct } from "/lib/api"; + +export function getChildVariants() { + const childVariants = []; + const variants = ReactionProduct.getVariants(); + if (variants.length > 0) { + const current = ReactionProduct.selectedVariant(); + + if (!current) { + return []; + } + + if (current.ancestors.length === 1) { + variants.map(variant => { + if (typeof variant.ancestors[1] === "string" && + variant.ancestors[1] === current._id && + variant.optionTitle && + variant.type !== "inventory") { + childVariants.push(variant); + } + }); + } else { + // TODO not sure we need this part... + variants.map(variant => { + if (typeof variant.ancestors[1] === "string" && + variant.ancestors.length === current.ancestors.length && + variant.ancestors[1] === current.ancestors[1] && + variant.optionTitle + ) { + childVariants.push(variant); + } + }); + } + + return childVariants; + } + + return null; +} diff --git a/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.html b/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.html new file mode 100644 index 00000000000..041fe1ea0a0 --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.html @@ -0,0 +1,11 @@ + diff --git a/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.js b/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.js new file mode 100644 index 00000000000..d2b55b438ab --- /dev/null +++ b/imports/plugins/included/product-detail-simple/client/templates/productDetailSimple.js @@ -0,0 +1,11 @@ +import { ProductDetailContainer } from "../containers"; +import { isRevisionControlEnabled } from "/imports/plugins/core/revisions/lib/api"; + +Template.productDetailSimple.helpers({ + isEnabled() { + return isRevisionControlEnabled(); + }, + PDC() { + return ProductDetailContainer; + } +}); diff --git a/imports/plugins/included/product-detail-simple/register.js b/imports/plugins/included/product-detail-simple/register.js new file mode 100644 index 00000000000..da3bd3aaaae --- /dev/null +++ b/imports/plugins/included/product-detail-simple/register.js @@ -0,0 +1,37 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Product Detail Simple", + name: "product-detail-simple", + icon: "fa fa-cubes", + autoEnable: true, + registry: [{ + route: "/product/:handle/:variantId?", + name: "product", + template: "productDetailSimple", + workflow: "coreProductWorkflow" + }, { + label: "Product Details", + provides: "settings", + route: "/product/:handle/:variantId?", + container: "product", + template: "ProductAdmin" + }], + layout: [{ + layout: "coreLayout", + workflow: "coreProductWorkflow", + collection: "Products", + theme: "default", + enabled: true, + structure: { + template: "productDetailSimple", + layoutHeader: "layoutHeader", + layoutFooter: "", + notFound: "productNotFound", + dashboardHeader: "", + dashboardControls: "productDetailDashboardControls", + dashboardHeaderControls: "", + adminControlsFooter: "adminControlsFooter" + } + }] +}); diff --git a/imports/plugins/included/product-variant/client/templates/products/productDetail/attributes.js b/imports/plugins/included/product-variant/client/templates/products/productDetail/attributes.js index 292daff1cc9..7309ebaaefb 100644 --- a/imports/plugins/included/product-variant/client/templates/products/productDetail/attributes.js +++ b/imports/plugins/included/product-variant/client/templates/products/productDetail/attributes.js @@ -34,14 +34,13 @@ Template.metaComponent.events({ "change input": function (event) { const productId = ReactionProduct.selectedProductId(); const updateMeta = { - key: $(event.currentTarget).parent().children( - ".metafield-key-input").val(), - value: $(event.currentTarget).parent().children( - ".metafield-value-input").val() + key: $(event.currentTarget).parent().children(".metafield-key-input").val(), + value: $(event.currentTarget).parent().children(".metafield-value-input").val() }; + if (this.key) { - Meteor.call("products/updateMetaFields", productId, updateMeta, - this); + const index = $(event.currentTarget).closest(".metafield-list-item").index(); + Meteor.call("products/updateMetaFields", productId, updateMeta, index); $(event.currentTarget).animate({ backgroundColor: "#e2f2e2" }).animate({ @@ -51,16 +50,13 @@ Template.metaComponent.events({ } if (updateMeta.value && !updateMeta.key) { - $(event.currentTarget).parent().children(".metafield-key-input").val( - "").focus(); + $(event.currentTarget).parent().children(".metafield-key-input").val("").focus(); } if (updateMeta.key && updateMeta.value) { Meteor.call("products/updateMetaFields", productId, updateMeta); Tracker.flush(); - $(event.currentTarget).parent().children(".metafield-key-input").val( - "").focus(); - return $(event.currentTarget).parent().children( - ".metafield-value-input").val(""); + $(event.currentTarget).parent().children(".metafield-key-input").val("").focus(); + return $(event.currentTarget).parent().children(".metafield-value-input").val(""); } } }); diff --git a/imports/plugins/included/product-variant/client/templates/products/productDetail/productDetail.html b/imports/plugins/included/product-variant/client/templates/products/productDetail/productDetail.html index 02375616b6a..d01d53bb6e6 100644 --- a/imports/plugins/included/product-variant/client/templates/products/productDetail/productDetail.html +++ b/imports/plugins/included/product-variant/client/templates/products/productDetail/productDetail.html @@ -1,6 +1,5 @@