diff --git a/.eslintrc.js b/.eslintrc.js index 254e9b9bf7..eae41e4059 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,22 +5,47 @@ module.exports = { rules: { camelcase: 'off', 'class-methods-use-this': 'off', - 'flowtype/sort-keys': 'off', //fixme + 'consistent-return': 'off', // fixme + 'flowtype/sort-keys': 'off', // fixme + 'jsx-a11y/anchor-is-valid': 'off', // fixme + 'jsx-a11y/click-events-have-key-events': 'off', // fixme 'jsx-a11y/label-has-associated-control': 'off', - 'no-shadow': 'off', //fixme - 'react/default-props-match-prop-types': 'off', //fixme - 'react/destructuring-assignment': 'off', //fixme + 'jsx-a11y/label-has-for': 'off', // fixme + 'jsx-a11y/no-noninteractive-tabindex': 'off', // fixme + 'jsx-a11y/no-static-element-interactions': 'off', // fixme + 'jsx-a11y/no-onchange': 'off', // fixme + 'import/no-extraneous-dependencies': 'off', // fixme + 'import/prefer-default-export': 'off', // fixme + 'no-plusplus': 'off', + 'no-restricted-globals': 'off', // fixme + 'no-shadow': 'off', // fixme + 'no-underscore-dangle': 'off', // fixme + 'no-unused-vars': 'off', // fixme + 'one-var': 'off', // fixme + 'prefer-destructuring': 'off', // fixme + 'prefer-promise-reject-errors': 'off', // fixme + 'react/button-has-type': 'off', // fixme + 'react/default-props-match-prop-types': 'off', // fixme + 'react/destructuring-assignment': 'off', // fixme 'react/display-name': 'off', - 'react/jsx-sort-props': 'off', //fixme - 'react/sort-comp': 'off', //fixme - 'react/no-unused-prop-types': 'off', //fixme - 'react/no-access-state-in-setstate': 'off', //fixme + 'react/forbid-prop-types': 'off', // fixme + 'react/jsx-sort-props': 'off', // fixme + 'react/jsx-no-bind': 'off', // fixme + 'react/sort-comp': 'off', // fixme + 'react/no-unused-prop-types': 'off', // fixme + 'react/no-access-state-in-setstate': 'off', // fixme + 'react/no-array-index-key': 'off', // fixme 'react/no-this-in-sfc': 'off', - 'react/no-unused-state': 'off', //fixme + 'react/no-unused-state': 'off', // fixme + 'react/prefer-stateless-function': 'off', // fixme }, settings: { 'import/resolver': { - 'babel-module': {} - } - } + 'babel-module': {}, + }, + }, + globals: { + shallow: true, + mount: true, + }, }; diff --git a/.flowconfig b/.flowconfig index 3eade2b5d7..a638401143 100644 --- a/.flowconfig +++ b/.flowconfig @@ -13,6 +13,7 @@ .*/node_modules/draft-js* .*/node_modules/findup* .*/node_modules/config-chain* +.*/__tests__/.* [untyped] .*/node_modules/box-react-ui/* @@ -23,6 +24,9 @@ [libs] [options] +esproposal.export_star_as=enable +module.file_ext=.js +module.file_ext=.scss module.system.node.resolve_dirname=node_modules module.system.node.resolve_dirname=./src module.name_mapper.extension='scss' -> '/flow/SCSSFlowStub.js.flow' @@ -35,5 +39,7 @@ module.name_mapper='react-virtualized/dist/es/AutoSizer' -> '/flow module.name_mapper='react-virtualized/dist/es/CellMeasurer' -> '/flow/ReactVirtualizedStub.js.flow' module.name_mapper='draft-js' -> '/flow/DraftJSFlowStub.js.flow' module.name_mapper='draft-js/EditorState' -> '/flow/DraftJSFlowStub.js.flow' +module.name_mapper='draft-js/CompositeDecorator' -> '/flow/DraftJSFlowStub.js.flow' +module.name_mapper='draft-js/Modifier' -> '/flow/DraftJSFlowStub.js.flow' module.name_mapper='react-measure' -> '/flow/ReactMeasure.js.flow' suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe diff --git a/.travis.yml b/.travis.yml index 5ad1e3dd5b..6e6090f727 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ cache: before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0 - export PATH=$HOME/.yarn/bin:$PATH + - export TZ=America/Los_Angeles jobs: include: - name: "Commit Lint" @@ -17,8 +18,10 @@ jobs: script: npm-run-all clean build:i18n lint - name: "Flow Types" script: npm-run-all clean build:i18n && yarn flow - - name: "Tests" - script: yarn test && yarn test:e2e + - name: "Unit Tests" + script: yarn test + - name: "E2E Tests" + script: yarn test:e2e - name: "Build Babel" script: npm-run-all clean build:i18n build:ci:es - name: "Build Webpack" diff --git a/conf/jest/enzyme-adapter.js b/conf/jest/enzyme-adapter.js index cbebdd8812..4781788004 100644 --- a/conf/jest/enzyme-adapter.js +++ b/conf/jest/enzyme-adapter.js @@ -2,7 +2,11 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; import 'raf/polyfill'; -import Enzyme from 'enzyme'; +import Enzyme, { mount, shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); + +// make Enzyme functions available in all test files without importing +global.shallow = shallow; +global.mount = mount; diff --git a/conf/prepush.sh b/conf/prepush.sh index e52bbbe855..bb2b75e289 100755 --- a/conf/prepush.sh +++ b/conf/prepush.sh @@ -3,55 +3,70 @@ # translation properties file PROPERTIES="i18n/en-US.properties" +# Styling variables +red=$"\e[1;31m" +green=$"\e[1;32m" +blue=$"\e[1;34m" +end=$"\e[0m\n" + # commit updated translations if any check_and_commit_updated_translations() { if ! git diff --quiet HEAD $PROPERTIES; then - echo "--------------------------------------------------------" - echo "Committing updated translations" - echo "--------------------------------------------------------" + printf "${red}--------------------------------------------------------${end}" + printf "${red}Committing updated translations${end}" + printf "${red}--------------------------------------------------------${end}" git add $PROPERTIES || exit 1 git commit --amend --no-edit --no-verify || exit 1 - echo "--------------------------------------------------------" - echo "Amended commit with translations, please push again with --no-verify" - echo "--------------------------------------------------------" + printf "${red}--------------------------------------------------------${end}" + printf "${red}Amended commit with translations, please push again with --no-verify${end}" + printf "${red}--------------------------------------------------------${end}" exit 1 + else + printf "${green}--------------------------------------------------------${end}" + printf "${green}en-US.properties is already up to date${end}" + printf "${green}--------------------------------------------------------${end}" fi } # lint, test, and build assets to update translations prepush() { - echo "--------------------------------------------------------" - echo "Build bundles" - echo "--------------------------------------------------------" + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Building bundles${end}" + printf "${blue}-------------------------------------------------------------${end}" yarn build:i18n || exit 1 - echo "--------------------------------------------------------" - echo "Linting" - echo "--------------------------------------------------------" + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Linting${end}" + printf "${blue}-------------------------------------------------------------${end}" yarn lint || exit 1 - echo "--------------------------------------------------------" - echo "Checking flow types" - echo "--------------------------------------------------------" + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Checking flow types${end}" + printf "${blue}-------------------------------------------------------------${end}" yarn flow check || exit 1 - echo "--------------------------------------------------------" - echo "Testing" - echo "--------------------------------------------------------" + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Testing${end}" + printf "${blue}-------------------------------------------------------------${end}" yarn test || exit 1 - echo "--------------------------------------------------------" - echo "Building" - echo "--------------------------------------------------------" - yarn build:dev:dist || exit 1 + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Building all sources, this will update i18n/json${end}" + printf "${blue}-------------------------------------------------------------${end}" + yarn build:prod:es || exit 1 + + printf "${blue}-------------------------------------------------------------${end}" + printf "${blue}Building bundles again, this will update en-US.properties${end}" + printf "${blue}-------------------------------------------------------------${end}" + yarn build:i18n || exit 1 check_and_commit_updated_translations } # Execute this script if ! prepush; then - echo "----------------------------------------------------" - echo "Error: failure in prepush script" - echo "----------------------------------------------------" + printf "${red}---------------------------------------------------------${end}" + printf "${red}Error: failure in prepush script${end}" + printf "${red}---------------------------------------------------------${end}" exit 1 fi diff --git a/conf/styleguide.config.js b/conf/styleguide.config.js index d69cd59195..012f5b4ee2 100644 --- a/conf/styleguide.config.js +++ b/conf/styleguide.config.js @@ -1,22 +1,244 @@ const path = require('path'); const webpackConf = require('./webpack.config.js'); +const webpackConfig = Array.isArray(webpackConf) ? webpackConf[0] : webpackConf; + +const allSections = [ + { + name: 'Elements', + components: () => [ + '../src/elements/content-picker/ContentPicker.js', + '../src/elements/content-explorer/ContentExplorer.js', + '../src/elements/content-uploader/ContentUploader.js', + '../src/elements/content-sidebar/ContentSidebar.js', + '../src/elements/content-preview/ContentPreview.js', + '../src/elements/content-open-with/ContentOpenWith.js', + ], + }, + { + name: 'Colors', + content: '../examples/colors.md', + }, + { + name: 'Typography', + content: '../examples/typography.md', + }, + { + name: 'Components', + components: () => [ + '../src/components/avatar/Avatar.js', + '../src/components/badge/Badge.js', + '../src/components/badgeable/Badgeable.js', + '../src/components/breadcrumb/Breadcrumb.js', + '../src/components/button/Button.js', + '../src/components/button-group/ButtonGroup.js', + '../src/components/checkbox/Checkbox.js', + '../src/components/collapsible/Collapsible.js', + '../src/components/context-menu/ContextMenu.js', + '../src/components/count-badge/CountBadge.js', + '../src/components/datalist-item/DatalistItem.js', + '../src/components/date-picker/DatePicker.js', + '../src/components/draggable-list/DraggableList.js', + '../src/components/dropdown-menu/DropdownMenu.js', + '../src/components/error-mask/ErrorMask.js', + '../src/components/flyout/Flyout.js', + '../src/components/flyout/Overlay.js', + '../src/components/focus-trap/FocusTrap.js', + '../src/components/header/Header.js', + '../src/components/hotkeys/HotkeyFriendlyModal.js', + '../src/components/hotkeys/HotkeyLayer.js', + '../src/components/hotkeys/Hotkeys.js', + '../src/components/infinite-scroll/InfiniteScroll.js', + '../src/components/inline-error/InlineError.js', + '../src/components/inline-notice/InlineNotice.js', + '../src/components/label/Label.js', + '../src/components/link/Link.js', + '../src/components/link/LinkButton.js', + '../src/components/link/LinkGroup.js', + '../src/components/link/LinkPrimaryButton.js', + '../src/components/loading-indicator/LoadingIndicator.js', + '../src/components/loading-indicator/LoadingIndicatorWrapper.js', + '../src/components/logo/Logo.js', + '../src/components/menu/Menu.js', + '../src/components/menu/SelectMenuLinkItem.js', + '../src/components/modal/Modal.js', + '../src/components/modal/ModalActions.js', + '../src/components/modal/ModalDialog.js', + '../src/components/nav-sidebar/NavSidebar.js', + '../src/components/notification/Notification.js', + '../src/components/notification/NotificationsWrapper.js', + '../src/components/pill-cloud/PillCloud.js', + '../src/components/pill-selector-dropdown/PillSelectorDropdown.js', + '../src/components/plain-button/PlainButton.js', + '../src/components/primary-button/PrimaryButton.js', + '../src/components/progress-bar/ProgressBar.js', + '../src/components/radar/RadarAnimation.js', + '../src/components/radio/RadioButton.js', + '../src/components/radio/RadioGroup.js', + '../src/components/time/ReadableTime.js', + '../src/components/scroll-wrapper/ScrollWrapper.js', + '../src/components/search-form/SearchForm.js', + '../src/components/section/Section.js', + '../src/components/select/Select.js', + '../src/components/select-field/MultiSelectField.js', + '../src/components/select-field/SingleSelectField.js', + '../src/components/selector-dropdown/SelectorDropdown.js', + '../src/components/slide-carousel/SlideCarousel.js', + '../src/components/tab-view/TabView.js', + '../src/components/table/Table.js', + '../src/components/text-area/TextArea.js', + '../src/components/text-input/TextInput.js', + '../src/components/text-input-with-copy-button/TextInputWithCopyButton.js', + '../src/components/thumbnail-card/ThumbnailCard.js', + '../src/components/toggle/Toggle.js', + '../src/components/tooltip/Tooltip.js', + ], + }, + { + name: 'Icons', + components: () => [ + '../src/icons/item-icon/ItemIcon.js', + '../src/icons/folder-icon/FolderIcon.js', + '../src/icons/file-icon/FileIcon.js', + '../src/icons/bookmark-icon/BookmarkIcon.js', + '../src/icons/adobe-sign/IconAdobeSign.js', + '../src/icons/google-docs/GoogleDocsIcon.js', + ], + sections: [ + { + name: 'Annotations', + components: '../src/icons/annotations/[A-Z]*.js', + }, + { + name: 'Avatar', + components: '../src/icons/avatars/[A-Z]*.js', + }, + { + name: 'Badges', + components: '../src/icons/badges/[A-Z]*.js', + }, + { + name: 'Box Tools', + components: '../src/icons/box-tools/[A-Z]*.js', + }, + { + name: 'General', + components: '../src/icons/general/[A-Z]*.js', + }, + { + name: 'File', + components: '../src/icons/file/[A-Z]*.js', + }, + { + name: 'Folder', + components: '../src/icons/folder/[A-Z]*.js', + }, + { + name: 'Illustrations', + components: '../src/icons/illustrations/[A-Z]*.js', + }, + { + name: 'iWork', + components: () => [ + '../src/icons/iwork/IconIWorkTrio.js', + '../src/icons/iwork/IWorkIcon.js', + '../src/icons/iwork/IWorkDesktopIcon.js', + ], + }, + { + name: 'Microsoft Office', + components: () => [ + '../src/icons/microsoft-office/IconOfficeWordmark.js', + '../src/icons/microsoft-office/OfficeOnlineIcon.js', + '../src/icons/microsoft-office/OfficeDesktopIcon.js', + ], + }, + { + name: 'Placeholders', + components: '../src/icons/placeholders/[A-Z]*.js', + }, + { + name: 'States', + components: '../src/icons/states/[A-Z]*.js', + }, + { + name: 'Metadata View', + components: '../src/icons/metadata-view/[A-Z]*.js', + }, + ], + }, + { + name: 'Form Elements', + components: () => [ + '../src/components/form-elements/draft-js-mention-selector/DraftJSMentionSelector.js', + '../src/components/form-elements/form/Form.js', + '../src/components/form-elements/text-area/TextArea.js', + '../src/components/form-elements/text-input/TextInput.js', + ], + }, + { + name: 'Features', + components: () => [ + '../src/features/activity-feed/activity-feed/ActivityFeed.js', + '../src/features/in-app-message/create-message-form/CreateMessageForm.js', + '../src/features/in-app-message/targeting-expression-editor/TargetingExpressionEditor.js', + '../src/features/invite-collaborators-modal/InviteCollaboratorsModal.js', + '../src/features/left-sidebar/LeftSidebar.js', + '../src/features/header-flyout/HeaderFlyout.js', + '../src/features/presence/Presence.js', + '../src/features/presence/PresenceLink.js', + '../src/features/security-cloud-game/SecurityCloudGame.js', + '../src/features/share/ShareMenu.js', + '../src/features/shared-link-modal/SharedLink.js', + '../src/features/shared-link-modal/SharedLinkModal.js', + '../src/features/shared-link-settings-modal/SharedLinkSettingsModal.js', + '../src/features/unified-share-modal/UnifiedShareModal.js', + '../src/features/version-history-modal/VersionHistoryModal.js', + ], + sections: [ + { + name: 'Metadata', + components: () => [ + '../src/features/metadata-instance-editor/MetadataInstanceEditor.js', + '../src/features/metadata-view/MetadataView.js', + ], + }, + { + name: 'Content Explorer', + components: () => [ + '../src/features/content-explorer/content-explorer-modal-container/ContentExplorerModalContainer.js', + '../src/features/content-explorer/content-explorer/ContentExplorer.js', + '../src/features/content-explorer/new-folder-modal/NewFolderModal.js', + ], + }, + { + name: 'Item Details', + components: () => [ + '../src/features/item-details/VersionHistoryLink.js', + '../src/features/item-details/ItemExpirationNotice.js', + '../src/features/item-details/SharedLinkExpirationNotice.js', + '../src/features/item-details/ItemProperties.js', + ], + }, + { + name: 'Access Stats', + components: () => ['../src/features/access-stats/AccessStats.js'], + }, + { + name: 'Quick Search', + components: () => [ + '../src/features/quick-search/QuickSearch.js', + '../src/features/quick-search/QuickSearchItem.js', + ], + }, + ], + }, +]; + module.exports = { - webpackConfig: Array.isArray(webpackConf) ? webpackConf[0] : webpackConf, + webpackConfig, styleguideDir: path.join(__dirname, '../styleguide'), - sections: [ - { - name: 'Elements', - components: () => [ - '../src/elements/content-picker/ContentPicker.js', - '../src/elements/content-explorer/ContentExplorer.js', - '../src/elements/content-uploader/ContentUploader.js', - '../src/elements/content-sidebar/ContentSidebar.js', - '../src/elements/content-preview/ContentPreview.js', - '../src/elements/content-open-with/ContentOpenWith.js', - ], - }, - ], + sections: allSections, title: 'Box UI Elements', theme: { color: { diff --git a/conf/webpack.config.js b/conf/webpack.config.js index 569e5e1283..1cacc63975 100644 --- a/conf/webpack.config.js +++ b/conf/webpack.config.js @@ -15,6 +15,7 @@ const isRelease = process.env.NODE_ENV === 'production'; const isDev = process.env.NODE_ENV === 'dev'; const language = process.env.LANGUAGE; const react = process.env.REACT === 'true'; +const examples = process.env.EXAMPLES === 'true'; const token = process.env.TOKEN; // used for examples only const folderId = process.env.FOLDERID; // used for examples only const fileId = process.env.FILEID; // used for examples only @@ -50,11 +51,13 @@ function getConfig(isReactExternalized) { resolve: { modules: ['src', 'node_modules'], alias: { - examples: path.join(__dirname, '../examples/src'), + 'box-ui-elements/es': path.join(__dirname, '../src'), // for examples only + examples: path.join(__dirname, '../examples/src'), // for examples only 'react-intl-locale-data': path.resolve(`node_modules/react-intl/locale-data/${locale}`), 'box-ui-elements-locale-data': path.resolve(`i18n/${language}`), 'box-react-ui-locale-data': path.resolve(`node_modules/box-react-ui/i18n/${language}`), moment: path.resolve('src/utils/MomentShim'), // Hack to leverage Intl instead + 'rsg-components/Wrapper': path.join(__dirname, '../examples/Wrapper'), // for examples only }, }, devServer: { @@ -114,12 +117,14 @@ function getConfig(isReactExternalized) { if (isDev) { config.devtool = 'source-map'; config.plugins.push(Translations); - config.plugins.push( - new CircularDependencyPlugin({ - exclude: /node_modules/, - failOnError: true, - }), - ); + if (!examples) { + config.plugins.push( + new CircularDependencyPlugin({ + exclude: /node_modules/, + failOnError: true, + }), + ); + } } if (isRelease && language === 'en-US') { diff --git a/examples/Wrapper.js b/examples/Wrapper.js new file mode 100644 index 0000000000..bb810753fe --- /dev/null +++ b/examples/Wrapper.js @@ -0,0 +1,22 @@ +// @flow +/** + * Wrapper component for styleguidist examples + */ +// $FlowFixMe +import 'core-js'; // For IE11 +import * as React from 'react'; +import { IntlProvider } from 'react-intl'; + +import './styles/styles.scss'; + +type Props = { + children: React.Node, +}; + +const Wrapper = ({ children }: Props) => ( + + {children} + +); + +export default Wrapper; diff --git a/examples/colors.md b/examples/colors.md new file mode 100644 index 0000000000..a768df3d1a --- /dev/null +++ b/examples/colors.md @@ -0,0 +1,111 @@ +**Box brand blues** + +``` +
+
+
+ +
+
+
+ +
+
+``` + +**Secondary Colors** + +``` +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+``` + +**Tinted Grays** + +``` +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+``` + +**Neutral Grays** + +``` +
+
+
+ +
+
+
+ +
+
+
+ +
+
+``` diff --git a/examples/src/AvatarImageExample.js b/examples/src/AvatarImageExample.js new file mode 100644 index 0000000000..5e402727c5 --- /dev/null +++ b/examples/src/AvatarImageExample.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import Avatar from 'components/avatar'; + +const LevieFace = + 'data:image/png;base64,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'; + +const AvatarImageExample = () => ; + +export default AvatarImageExample; diff --git a/examples/src/ContentExplorerExamples.js b/examples/src/ContentExplorerExamples.js new file mode 100644 index 0000000000..5f02133ddc --- /dev/null +++ b/examples/src/ContentExplorerExamples.js @@ -0,0 +1,133 @@ +import React, { Component } from 'react'; + +import { ContentExplorer } from 'features/content-explorer'; + +const INITIAL_FOLDERS_PATH = [ + { + id: '0', + name: 'All Files', + }, +]; + +const ITEMS = { + 0: [ + { + id: '1', + name: "Can't choose but can drill in", + type: 'folder', + hasCollaborations: true, + isActionDisabled: true, + }, + { + id: '2', + name: 'Choose me or drill in', + type: 'folder', + }, + { + id: '3', + name: 'File.boxnote', + type: 'file', + extension: 'boxnote', + isDisabled: true, + }, + { + id: '4', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + ], + 1: [ + { + id: '11', + name: 'Choose me or drill in', + type: 'folder', + }, + { + id: '12', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + { + id: '13', + name: 'File.ppt', + type: 'file', + extension: 'ppt', + isDisabled: true, + }, + ], + search: [ + { + id: '1', + name: 'Choose me or drill in', + type: 'folder', + }, + ], +}; + +class ContentExplorerExamples extends Component { + constructor(props) { + super(props); + this.state = { + exampleMessage: 'Content Explorer is configured to select a folder.', + folderId: '0', + }; + } + + handleEnterFolder = (enteredFolder, newFoldersPath) => { + this.setState({ + exampleMessage: `Enter folder { id: ${enteredFolder.id}, name: ${enteredFolder.name} }`, + folderId: enteredFolder.id, + }); + }; + + handleChooseItems = chosenItems => { + this.setState({ + exampleMessage: chosenItems.reduce( + (message, chosenItem) => `${message}, Choose { id: ${chosenItem.id}, name: ${chosenItem.name} }`, + '', + ), + }); + }; + + handleSearchSubmit = searchQuery => { + this.setState({ + exampleMessage: `Searched for '${searchQuery}'`, + folderId: 'search', + }); + }; + + handleExitSearch = ({ id }) => { + this.setState({ + exampleMessage: 'Exited search', + folderId: id, + }); + }; + + render() { + const { exampleMessage, folderId } = this.state; + const items = ITEMS[folderId] || []; + + return ( +
+ +
{exampleMessage}
+
+ ); + } +} + +export default ContentExplorerExamples; diff --git a/examples/src/ContentExplorerModalContainerExamples.js b/examples/src/ContentExplorerModalContainerExamples.js new file mode 100644 index 0000000000..496f91abea --- /dev/null +++ b/examples/src/ContentExplorerModalContainerExamples.js @@ -0,0 +1,175 @@ +import React, { Component } from 'react'; + +import Button from 'components/button'; +import { ContentExplorerModalContainer } from 'features/content-explorer'; + +const INITIAL_FOLDERS_PATH = [ + { + id: '0', + name: 'All Files', + }, +]; + +const ITEMS = { + 0: [ + { + id: '1', + name: "Can't choose but can drill in", + type: 'folder', + hasCollaborations: true, + isActionDisabled: true, + }, + { + id: '2', + name: 'Choose me or drill in', + type: 'folder', + }, + { + name: 'File.boxnote', + type: 'file', + extension: 'boxnote', + isDisabled: true, + }, + { + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + ], + 1: [ + { + id: '3', + name: 'Choose me or drill in', + type: 'folder', + }, + { + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + { + name: 'File.ppt', + type: 'file', + extension: 'ppt', + isDisabled: true, + }, + ], + search: [ + { + id: '4', + name: 'Choose me or drill in', + type: 'folder', + }, + ], +}; + +class ContentExplorerModalContainerExamples extends Component { + constructor(props) { + super(props); + this.state = { + exampleMessage: 'Content Explorer is configured to move/copy a file.', + folderId: '0', + foldersPath: INITIAL_FOLDERS_PATH, + isModalOpen: false, + }; + } + + openModal = () => { + this.setState({ isModalOpen: true }); + }; + + closeModal = () => { + this.setState({ + folderId: '0', + foldersPath: INITIAL_FOLDERS_PATH, + isModalOpen: false, + }); + }; + + handleCreateFolderSubmit = folderName => { + const { folderId: currentFolderId, foldersPath } = this.state; + + // Add folder to list of items + const folderId = `${Date.now()}`; + const newFolder = { + id: folderId, + name: folderName, + type: 'folder', + }; + ITEMS[currentFolderId] = ITEMS[currentFolderId] || []; + ITEMS[currentFolderId].push(newFolder); + + // Drill into that folder + this.setState({ + folderId, + foldersPath: foldersPath.concat([newFolder]), + }); + }; + + handleEnterFolder = (enteredFolder, newFoldersPath) => { + this.setState({ folderId: enteredFolder.id }); + }; + + handleMoveItem = destFolder => { + this.setState({ + exampleMessage: `Moved item to { id: ${destFolder.id}, name: ${destFolder.name} }`, + }); + this.closeModal(); + }; + + handleCopyItem = destFolder => { + this.setState({ + exampleMessage: `Copied item to { id: ${destFolder.id}, name: ${destFolder.name} }`, + }); + this.closeModal(); + }; + + handleSearchSubmit = searchQuery => { + this.setState({ + folderId: 'search', + }); + }; + + handleExitSearch = ({ id }) => { + this.setState({ + folderId: id, + }); + }; + + render() { + const { exampleMessage, folderId, foldersPath, isModalOpen } = this.state; + const items = ITEMS[folderId] || []; + + return ( +
+ +
{exampleMessage}
+ {isModalOpen && ( + {}} + listWidth={600} + listHeight={350} + /> + )} +
+ ); + } +} + +export default ContentExplorerModalContainerExamples; diff --git a/examples/src/ContentExplorerMultiSelectExamples.js b/examples/src/ContentExplorerMultiSelectExamples.js new file mode 100644 index 0000000000..7fdb3d368a --- /dev/null +++ b/examples/src/ContentExplorerMultiSelectExamples.js @@ -0,0 +1,189 @@ +import React, { Component } from 'react'; + +import { ContentExplorer } from 'features/content-explorer'; + +import './ContentExplorerMultiSelectExamples.scss'; + +const INITIAL_FOLDERS_PATH = [ + { + id: '0', + name: 'All Files', + }, +]; + +const ITEMS = { + 0: [ + { + id: '1', + name: 'Choose me or drill in', + type: 'folder', + hasCollaborations: true, + isActionDisabled: true, + }, + { + id: '2', + name: 'Choose me or drill in 2', + type: 'folder', + }, + { + id: '3', + name: 'File.boxnote', + type: 'file', + extension: 'boxnote', + isDisabled: false, + }, + { + id: '4', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: false, + }, + { + id: '5', + name: 'Choose me or drill with loading items', + type: 'folder', + }, + { + id: '6', + name: 'File 2.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + ], + 1: [ + { + id: '11', + name: 'Choose me or drill in', + type: 'folder', + }, + { + id: '12', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: false, + }, + { + id: '13', + name: 'File.ppt', + type: 'file', + extension: 'ppt', + isDisabled: false, + }, + ], + 5: [ + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + ], + search: [ + { + id: '2', + name: 'Choose me or drill in', + type: 'folder', + }, + ], + userFilter: [ + { + id: '30', + name: 'Folder owned by the filtered user', + type: 'folder', + }, + { + id: '31', + name: 'File owned by the filtered user', + type: 'file', + }, + ], +}; + +class ContentExplorerMultiSelectExamples extends Component { + constructor(props) { + super(props); + this.state = { + exampleMessage: 'Content Explorer is configured for multi select.', + folderId: '0', + user: null, + }; + } + + handleEnterFolder = (enteredFolder, newFoldersPath) => { + this.setState({ + exampleMessage: `Enter folder { id: ${enteredFolder.id}, name: ${enteredFolder.name} }`, + folderId: enteredFolder.id, + }); + }; + + handleChooseItems = chosenItems => { + this.setState({ + exampleMessage: chosenItems.reduce( + (message, chosenItem) => `${message}, Choose { id: ${chosenItem.id}, name: ${chosenItem.name} }`, + '', + ), + }); + }; + + handleSearchSubmit = searchQuery => { + this.setState({ + exampleMessage: `Searched for '${searchQuery}'`, + folderId: 'search', + }); + }; + + handleExitSearch = ({ id }) => { + this.setState({ + exampleMessage: 'Exited search', + folderId: id, + }); + }; + + handleOnItemAdd = newItem => { + this.setState({ + exampleMessage: `User Filtered: ${newItem.name}`, + folderId: 'userFilter', + }); + }; + + handleOnItemRemove = (item, index) => { + this.setState({ + exampleMessage: 'User Filtered Removed', + folderId: '0', + }); + }; + + renderHeaderActionsAccessory = () => { + return
Accessory
; + }; + + render() { + const { exampleMessage, folderId } = this.state; + const items = ITEMS[folderId] || []; + + return ( +
+ +
{exampleMessage}
+
+ ); + } +} + +export default ContentExplorerMultiSelectExamples; diff --git a/examples/src/ContentExplorerMultiSelectExamples.scss b/examples/src/ContentExplorerMultiSelectExamples.scss new file mode 100644 index 0000000000..9903470d2b --- /dev/null +++ b/examples/src/ContentExplorerMultiSelectExamples.scss @@ -0,0 +1,9 @@ +.content-explorer-example { + .selector-dropdown-wrapper { + top: 7px; + } + + .pill-selector-wrapper div.pill-selector-input-wrapper { + width: 160px; + } +} diff --git a/examples/src/ContentExplorerMultiSelectModalContainerExamples.js b/examples/src/ContentExplorerMultiSelectModalContainerExamples.js new file mode 100644 index 0000000000..8c83280cb4 --- /dev/null +++ b/examples/src/ContentExplorerMultiSelectModalContainerExamples.js @@ -0,0 +1,199 @@ +import React, { Component } from 'react'; + +import Button from 'components/button'; +import { ContentExplorerModalContainer } from 'features/content-explorer'; + +import './ContentExplorerMultiSelectModalContainerExamples.scss'; + +const INITIAL_FOLDERS_PATH = [ + { + id: '0', + name: 'All Files', + }, +]; + +const ITEMS = { + 0: [ + { + id: '1', + name: 'Choose me or drill in', + type: 'folder', + hasCollaborations: true, + isActionDisabled: true, + }, + { + id: '2', + name: 'Choose me or drill in 2', + type: 'folder', + }, + { + id: '3', + name: 'File.boxnote', + type: 'file', + extension: 'boxnote', + isDisabled: false, + }, + { + id: '4', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: false, + }, + { + id: '5', + name: 'Choose me or drill with loading items', + type: 'folder', + }, + { + id: '6', + name: 'File 2.docx', + type: 'file', + extension: 'docx', + isDisabled: true, + }, + ], + 1: [ + { + id: '11', + name: 'Choose me or drill in', + type: 'folder', + }, + { + id: '12', + name: 'File.docx', + type: 'file', + extension: 'docx', + isDisabled: false, + }, + { + id: '13', + name: 'File.ppt', + type: 'file', + extension: 'ppt', + isDisabled: false, + }, + ], + 5: [ + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + { isLoading: true }, + ], + search: [ + { + id: '2', + name: 'Choose me or drill in', + type: 'folder', + }, + ], + userFilter: [ + { + id: '30', + name: 'Folder owned by the filtered user', + type: 'folder', + }, + { + id: '31', + name: 'File owned by the filtered user', + type: 'file', + }, + ], +}; + +class ContentExplorerMultiSelectModalContainerExamples extends Component { + constructor(props) { + super(props); + this.state = { + exampleMessage: 'Content Explorer is configured to selevt multiple file / folder.', + folderId: '0', + foldersPath: INITIAL_FOLDERS_PATH, + isModalOpen: false, + }; + } + + openModal = () => { + this.setState({ isModalOpen: true }); + }; + + closeModal = () => { + this.setState({ + folderId: '0', + foldersPath: INITIAL_FOLDERS_PATH, + isModalOpen: false, + }); + }; + + handleEnterFolder = (enteredFolder, newFoldersPath) => { + this.setState({ folderId: enteredFolder.id }); + }; + + handleSearchSubmit = searchQuery => { + this.setState({ + folderId: 'search', + }); + }; + + handleExitSearch = ({ id }) => { + this.setState({ + folderId: id, + }); + }; + + handleOnItemAdd = newItem => { + this.setState({ + exampleMessage: `User Filtered: ${newItem.name}`, + folderId: 'userFilter', + }); + }; + + handleOnItemRemove = (item, index) => { + this.setState({ + exampleMessage: 'User Filtered Removed', + folderId: '0', + }); + }; + + renderHeaderActionsAccessory = () => { + return
Accessory
; + }; + + render() { + const { exampleMessage, folderId, foldersPath, isModalOpen } = this.state; + const items = ITEMS[folderId] || []; + + return ( +
+ +
{exampleMessage}
+ {isModalOpen && ( + {}} + listWidth={600} + listHeight={350} + /> + )} +
+ ); + } +} + +export default ContentExplorerMultiSelectModalContainerExamples; diff --git a/examples/src/ContentExplorerMultiSelectModalContainerExamples.scss b/examples/src/ContentExplorerMultiSelectModalContainerExamples.scss new file mode 100644 index 0000000000..38a7e03285 --- /dev/null +++ b/examples/src/ContentExplorerMultiSelectModalContainerExamples.scss @@ -0,0 +1,9 @@ +.content-explorer-modal { + .selector-dropdown-wrapper { + top: 7px; + } + + .pill-selector-wrapper div.pill-selector-input-wrapper { + width: 160px; + } +} diff --git a/examples/src/ContextMenuWithSubmenuWithBoundariesElementExample.js b/examples/src/ContextMenuWithSubmenuWithBoundariesElementExample.js new file mode 100644 index 0000000000..e21a14c925 --- /dev/null +++ b/examples/src/ContextMenuWithSubmenuWithBoundariesElementExample.js @@ -0,0 +1,60 @@ +/* eslint-disable no-unused-expressions */ +import * as React from 'react'; + +import { Menu, SubmenuItem, MenuItem } from 'components/menu'; +import ContextMenu from 'components/context-menu'; + +class ContextMenuWithSubmenuWithBoundariesElementExample extends React.Component { + state = { + rightBoundaryElement: null, + bottomBoundaryElement: null, + }; + + render() { + return ( + +
{ + !this.state.rightBoundaryElement && + this.setState({ + rightBoundaryElement: ref, + }); + }} + > + Target Component - right click me +
+ { + !this.state.bottomBoundaryElement && + this.setState({ + bottomBoundaryElement: ref, + }); + }} + > + View Profile + View Profile + {this.state.rightBoundaryElement && ( + + Submenu + + View Profile + View Profile + View Profile + + + )} + Help + +
+ ); + } +} + +export default ContextMenuWithSubmenuWithBoundariesElementExample; diff --git a/examples/src/DraftJSMentionSelectorExamples.js b/examples/src/DraftJSMentionSelectorExamples.js new file mode 100644 index 0000000000..499abb57a2 --- /dev/null +++ b/examples/src/DraftJSMentionSelectorExamples.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { ContentState, EditorState } from 'draft-js'; + +import Avatar from 'components/avatar'; +import Section from 'components/section'; +import PrimaryButton from 'components/primary-button'; +import { DraftMentionDecorator } from 'components/form-elements/draft-js-mention-selector'; + +import MentionSelectorContainer from './MentionSelectorContainer'; + +const CustomSelectorRow = ({ item }) => ( +
+
+ +
+ {item.name} +
+); + +CustomSelectorRow.propTypes = { + item: PropTypes.object, +}; + +class DraftJSMentionSelectorExamples extends Component { + constructor(props) { + super(props); + + this.state = { + exampleExternalEditorState: this.initialEditorState, + }; + } + + onExternalEditorStateChange = newEditorState => { + this.setState({ exampleExternalEditorState: newEditorState }); + }; + + getMentionReplacement(mention) { + return { + component: ({ id, name, ...rest }) => { + const style = { + display: 'inline-block', + verticalAlign: 'middle', + }; + return ( +
+ +
+ ); + }, + data: { + ...mention, + }, + serializedText: () => { + return `@[${mention.id}:${mention.name}]`; + }, + text: '', + }; + } + + initialEditorState = EditorState.createWithContent( + ContentState.createFromText('Is this thing on?'), + DraftMentionDecorator, + ); + + initializeEditorState = () => { + this.setState({ exampleExternalEditorState: this.initialEditorState }); + }; + + render() { + const { exampleExternalEditorState } = this.state; + + return ( +
+

Mention Selector

+
+ + Reset State +
+
+ +
+
+ !event.shiftKey} + /> +
+
+ +
+
+ { + console.log('a change: ', x.toJS()); // eslint-disable-line + }} + onFocus={() => { + console.log('the draft mention selector was focused'); // eslint-disable-line + }} + /> +
+
+ ); + } +} + +export default DraftJSMentionSelectorExamples; diff --git a/examples/src/DraggableListExamples.js b/examples/src/DraggableListExamples.js new file mode 100644 index 0000000000..aec34abfb0 --- /dev/null +++ b/examples/src/DraggableListExamples.js @@ -0,0 +1,75 @@ +// @flow +import React, { Component } from 'react'; +import uniqueId from 'lodash/uniqueId'; + +import Checkbox from '../../src/components/checkbox/Checkbox'; +import DraggableList from '../../src/components/draggable-list'; +import DraggableListItem from '../../src/components/draggable-list/DraggableListItem'; + +import reorder from '../../src/components/draggable-list/draggable-list-utils/reorder'; + +import '../styles/DraggableListExamples.scss'; + +type Props = { + isDraggableViaHandle?: boolean, +}; + +type State = { + items: Array, + listId: string, +}; + +class DraggableListExamples extends Component { + state = { + items: [], + listId: '', + }; + + componentDidMount() { + this.setState({ + items: this.getItems(10), + listId: uniqueId(), + }); + } + + getItems = (count: number): Array => { + return Array.from({ length: count }, (v, k) => k).map(k => ({ + id: uniqueId('item_'), + label: `item ${k}`, + })); + }; + + onDragEnd = (sourceIndex: number, destinationIndex: number) => { + if (!destinationIndex) { + return; + } + + const items = reorder(this.state.items, sourceIndex, destinationIndex); + + this.setState({ + items, + }); + }; + + render() { + const { isDraggableViaHandle } = this.props; + const { items, listId } = this.state; + + return ( + + {items.map((item, index) => ( + + + + ))} + + ); + } +} + +export default DraggableListExamples; diff --git a/examples/src/MentionSelectorContainer.js b/examples/src/MentionSelectorContainer.js new file mode 100644 index 0000000000..98a5ce1547 --- /dev/null +++ b/examples/src/MentionSelectorContainer.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; + +import DraftJSMentionSelector from 'components/form-elements/draft-js-mention-selector'; + +const contactData = [ + { email: 'ken@streetfighter.com', id: '1', name: 'Ken' }, + { email: 'ryu@streetfighter.com', id: '2', name: 'Ryu' }, + { email: 'guile@streetfighter.com', id: '3', name: 'Guile' }, + { email: 'm.bison@streetfighter.com', id: '4', name: 'M. Bison' }, + { email: 'vega@streetfighter.com', id: '5', name: 'Vega' }, + { email: 'blanka@streetfighter.com', id: '6', name: 'Blanka' }, + { email: 'akuma@streetfighter.com', id: '7', name: 'Akuma' }, + { email: 'りゅう@streetfighter.com', id: '8', name: 'りゅう' }, + { email: 'リュー@streetfighter.com', id: '9', name: 'リュー' }, +]; + +class MentionSelectorContainer extends Component { + constructor(props) { + super(props); + this.state = { + contacts: [], + }; + } + + isMatchingContact(mentionString, contact) { + const { email, name } = contact; + return email.toLowerCase().indexOf(mentionString) > -1 || name.toLowerCase().indexOf(mentionString) > -1; + } + + handleMention = mentionString => { + if (!mentionString.length) { + return; + } + const matchingContacts = contactData.reduce((prev, contact) => { + return this.isMatchingContact(mentionString, contact) ? prev.concat([contact]) : prev; + }, []); + + this.setState({ + contacts: matchingContacts, + }); + }; + + render() { + const { ...props } = this.props; + const { contacts } = this.state; + + return ( + ({ + id: contact.id, + name: contact.name, + item: contact, + }))} + label="Comment" + onMention={this.handleMention} + {...props} + /> + ); + } +} + +export default MentionSelectorContainer; diff --git a/examples/src/MetadataExamples.js b/examples/src/MetadataExamples.js new file mode 100644 index 0000000000..1e7255ff9b --- /dev/null +++ b/examples/src/MetadataExamples.js @@ -0,0 +1,1108 @@ +import * as React from 'react'; +import uniqueId from 'lodash/uniqueId'; + +import MetadataInstanceEditor from 'features/metadata-instance-editor/MetadataInstanceEditor'; +import LoadingIndicator from 'components/loading-indicator'; + +// Templates +const template0 = { + id: 'template0', + templateKey: 'template0', + displayName: 'template0 simple', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'somefieldkey', + displayName: 'Description of the field', + }, + ], +}; + +const template1 = { + id: 'template1', + templateKey: 'template1', + displayName: 'template1 title', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + { + id: 'field9', + type: 'multiSelect', + key: 'multiselectfield', + displayName: 'Multi-Select Field', + description: 'example of a multi-select field', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field10', + type: 'multiSelect', + key: 'emptymultiselectfield', + displayName: 'Empty Multi-Select Field', + description: 'example of an empty multi-select field', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field11', + type: 'integer', + key: 'integerfield', + displayName: 'Integer Field', + description: 'example of an integer field', + }, + { + id: 'field12', + type: 'integer', + key: 'emptyintegerfield', + displayName: 'Empty Integer Field', + description: 'example of an empty integer field', + }, + { + id: 'field13', + type: 'string', + key: + 'longlonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + displayName: + 'longlonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + description: + 'longlonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + }, + { + id: 'field14', + type: 'string', + key: + 'long long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + displayName: + 'long long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + description: + 'long long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + }, + { + id: 'field15', + type: 'enum', + key: + 'enumlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + displayName: + 'enumlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + description: + 'enumlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field16', + type: 'multiSelect', + key: + 'multiselectlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + displayName: + 'multiSelectlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + description: + 'multiSelectlonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglong', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field17', + type: 'enum', + key: + 'enumlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + displayName: + 'enumlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + description: + 'enumlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field18', + type: 'multiSelect', + key: + 'multiSelectlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + displayName: + 'multiSelectlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + description: + 'multiSelectlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long longlong long long', + options: [ + { key: 'yes' }, + { key: 'no' }, + { key: 'maybe' }, + { key: 'idk?' }, + { key: 'oh well' }, + { key: 'whatever' }, + ], + }, + { + id: 'field19', + type: 'badfieldtype', + key: 'badfieldtype', + displayName: 'badfieldtype', + description: 'bad field type', + }, + ], +}; + +const template2 = { + id: 'template2', + templateKey: 'template2', + displayName: 'another template2 title', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +const template3 = { + id: 'template3', + templateKey: 'template3', + displayName: 'yet another template3 title', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +const template4 = { + id: 'template4', + templateKey: 'template4', + displayName: 'best template4 title', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +const template5 = { + id: 'template5', + templateKey: 'template5', + displayName: 'good template5 title', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +const properties = { + id: 'properties', + scope: 'global', + templateKey: 'properties', +}; + +const template6 = { + id: 'template6', + templateKey: 'template6', + displayName: 'template with some error', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +const template7 = { + id: 'template7', + templateKey: 'template7', + displayName: 'non editable template', + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + { + id: 'field1', + type: 'string', + key: 'stringfield', + displayName: 'String Field', + description: 'example of a string field', + }, + { + id: 'field2', + type: 'string', + key: 'emptystring', + displayName: 'Empty String Field', + description: 'example of an empty string field', + }, + { + id: 'field3', + type: 'float', + key: 'floatfield', + displayName: 'Float Field', + description: 'example of a float field', + }, + { + id: 'field4', + type: 'float', + key: 'emptyfloat', + displayName: 'Empty Float Field', + description: 'example of an empty float field', + }, + { + id: 'field5', + type: 'enum', + key: 'enumfield', + displayName: 'Enum Field', + description: 'example of a enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field6', + type: 'enum', + key: 'emptyenumfield', + displayName: 'Empty Enum Field', + description: 'example of an empty enum field', + options: [{ key: 'yes' }, { key: 'no' }], + }, + { + id: 'field7', + type: 'date', + key: 'datefield', + displayName: 'Date Field', + description: 'example of a date field', + }, + { + id: 'field8', + type: 'date', + key: 'emptydatefield', + displayName: 'Empty Date Field', + description: 'example of an empty date field', + }, + ], +}; + +// Instances with templates metadata + +const editor0 = { + instance: { + canEdit: true, + id: 'editor0', + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + cascadePolicy: { + canEdit: true, + isEnabled: false, + id: 'some cascading policy id', + }, + }, + template: template0, +}; + +const editor1 = { + instance: { + canEdit: true, + id: 'editor1', + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + integerfield: 3, + multiselectfield: ['no', 'maybe'], + datefield: '2018-06-20T00:00:00.000Z', + }, + cascadePolicy: { + canEdit: true, + isEnabled: false, + id: 'some cascading policy id', + }, + }, + template: template1, +}; + +const editor2 = { + instance: { + canEdit: true, + id: 'editor2', + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + cascadePolicy: { + canEdit: true, + isEnabled: true, + id: 'some cascading policy id', + }, + }, + template: template2, +}; + +const editor3 = { + instance: { + canEdit: true, + id: 'editor3', + data: {}, + cascadePolicy: { + canEdit: true, + isEnabled: true, + id: 'some cascading policy id', + }, + }, + template: template3, +}; + +const editor7 = { + instance: { + id: 'editor7', + canEdit: false, + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + cascadePolicy: { + canEdit: true, + isEnabled: true, + id: 'some cascading policy id', + }, + }, + template: template1, +}; + +const editor8 = { + instance: { + canEdit: false, + id: 'editor8', + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + }, + template: template7, +}; + +const editor9 = { + instance: { + id: 'editor9', + canEdit: true, + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + }, + template: template6, + hasError: true, +}; + +const editor10 = { + instance: { + canEdit: true, + id: 'editor10', + data: { + stringfield: 'some string', + floatfield: 1, + enumfield: 'yes', + datefield: '2018-06-20T00:00:00.000Z', + }, + }, + template: template2, +}; + +// Instances with custom metadata + +const editor4 = { + instance: { + canEdit: true, + id: 'editor4', + data: { + string: 'string', + number: '1', + bool: 'true', + }, + }, + template: properties, +}; + +const editor5 = { + instance: { + canEdit: true, + id: 'editor5', + data: {}, + }, + template: properties, + hasError: true, +}; + +const editor6 = { + instance: { + id: 'editor6', + canEdit: false, + data: { + string: 'string', + number: '1', + bool: 'true', + }, + }, + template: properties, +}; + +const getExtraTemplates = num => + new Array(num).fill(true).map((_, index) => ({ + id: `generatedtemplate-${index}`, + templateKey: `generatedtemplate-${index}`, + displayName: `generated template-${index}`, + scope: 'enterprise_123', + fields: [ + { + id: 'field0', + type: 'string', + key: 'nodescfield', + displayName: 'No Description Field', + }, + ], + })); + +// State of templates from server +const templatesOnServer = [ + template0, + template1, + template2, + template3, + template4, + template5, + properties, + template6, + template7, + ...getExtraTemplates(1), +]; + +// State of instnaces from server +const editorsOnServer = [ + editor0, + editor1, + editor2, + editor3, + editor4, + editor5, + editor6, + editor7, + editor8, + editor9, + editor10, +]; + +class MetadataExamples extends React.PureComponent { + state = {}; + + componentDidMount() { + // Timeout to simulate network call + setTimeout(() => { + this.setState({ + editors: editorsOnServer.map(editor => ({ + hasError: editor.hasError, + template: editor.template, + instance: { + id: editor.instance.id, + canEdit: editor.instance.canEdit, + data: { ...editor.instance.data }, + cascadePolicy: { ...editor.instance.cascadePolicy }, + }, + })), + }); + }, 1000); + + // Timeout to simulate network call + setTimeout(() => { + this.setState({ templates: templatesOnServer.slice(0) }); + }, 1000); + } + + onRemove = id => { + // Timeout to simulate a network call + setTimeout(() => { + // Get rid of the editor whose instance id matches + const editors = this.state.editors.filter(editor => editor.instance.id !== id); + this.setState({ editors: editors.slice(0) }); // slice for react change detection + }, 1000); + }; + + onAdd = template => { + // Timeout to simulate network call + setTimeout(() => { + // The server applies the new instance (or dummy instance) + const newEditor = { + instance: { + canEdit: true, + id: uniqueId('i_'), + data: {}, + }, + template: this.state.templates.find(t => t.templateKey === template.templateKey), + }; + editorsOnServer.push(newEditor); + const editors = this.state.editors.slice(0); + editors.push({ ...newEditor }); + this.setState({ editors }); + }, 1000); + }; + + onSave = (id, ops, cascadePolicy = {}, rawData) => { + // Whats being sent to the server to show in the examples page + document.querySelector('.metadata-operations').textContent = JSON.stringify(ops, null, 2); + document.querySelector('.metadata-cascading-policy').textContent = JSON.stringify(cascadePolicy, null, 2); + + // Timeout to simulate network call + setTimeout(() => { + // Find the editor that needs saving + const editorOnServer = editorsOnServer.find(editor => editor.instance.id === id); + + // Iterate over all ops and apply them + ops.forEach(op => { + const key = op.path.substr(1); + if (op.op === 'replace' || op.op === 'add') { + editorOnServer.instance.data[key] = op.value; + } else if (op.op === 'remove') { + delete editorOnServer.instance.data[key]; + } + }); + + // Apply the cascading policy + if (cascadePolicy) { + if (cascadePolicy.isEnabled && !editorOnServer.instance.cascadePolicy) { + editorOnServer.instance.cascadePolicy = { + id: uniqueId('cascade_policy_id_'), + }; + } else if (!cascadePolicy.isEnabled && editorOnServer.instance.cascadePolicy) { + editorOnServer.instance.cascadePolicy = { + canEdit: true, + }; + } + } + + // Server has saved the new data and will return an updated editor instance + const editorOnClientIndex = this.state.editors.findIndex(editor => editor.instance.id === id); + const editors = this.state.editors.slice(0); // clone for react changes + editors.splice(editorOnClientIndex, 1, { ...editorOnServer }); + this.setState({ editors }); + }, 1000); + }; + + onModification = (id, isDirty) => { + const editorOnClientIndex = this.state.editors.findIndex(editor => editor.instance.id === id); + const editorOnClient = { ...this.state.editors[editorOnClientIndex] }; + editorOnClient.isDirty = isDirty; + const editors = this.state.editors.slice(0); // clone for react changes + editors.splice(editorOnClientIndex, 1, editorOnClient); + this.setState({ editors }); + + // To show on the examples page what all instances are dirty + document.querySelector('.metadata-dirty').textContent = JSON.stringify( + this.state.editors.filter(editor => editor.isDirty), + null, + 2, + ); + }; + + render() { + if (!this.state.templates || !this.state.editors) { + return ; + } + + return ( +
+
+

Instances that are dirty

+
+                    

Data saved

+
+                    

Cascading policy data saved

+
+                
+
+
+
+
+

Editable Metadata with Cascade Policy applicable

+
+ +
+
+
+
+
+
+

Editable Metadata with Cascade Policy not applicable

+
+ Metadata} + /> +
+
+
+

Empty State

+
+ +
+
+
+
+

Add Template Dropdown with No Templates on Server Message

+
+ + {this.state.isBusy && ( +
+ +
+ )} +
+
+
+
+

Data to be saved

+
+                
+
+ ); + } +} + +export default MetadataExamples; diff --git a/examples/src/MetadataViewExamples.js b/examples/src/MetadataViewExamples.js new file mode 100644 index 0000000000..e972a7029c --- /dev/null +++ b/examples/src/MetadataViewExamples.js @@ -0,0 +1,184 @@ +// @flow +import * as React from 'react'; + +import MetadataView from '../../src/features/metadata-view/MetadataView'; + +import type { Template } from '../../src/features/metadata-instance-editor/flowTypes'; + +const totalWidth = 700; +const tableHeight = 300; +const tableHeaderHeight = 40; +const tableRowHeight = 50; +const widths = { + icon: 0.11, + name: 0.3, + lastModified: 0.6, + size: 0.3, + contractValue: 0.9, +}; + +const instances = [ + { + canEdit: true, + id: 'editor1', + data: { + lastModified: '2013-05-20T00:00:00.000Z', + contractValue: 3, + fileType: 'pdf', + name: 'Google', + size: 1, + }, + cascadePolicy: { + canEdit: true, + isEnabled: false, + id: 'some cascading policy id', + }, + }, + { + canEdit: true, + id: 'editor2', + data: { + lastModified: '2017-04-21T00:00:00.000Z', + contractValue: 34588, + fileType: 'powerpoint-presentation', + name: 'Facebook', + size: 127834, + }, + cascadePolicy: { + canEdit: true, + isEnabled: false, + id: 'some cascading policy id', + }, + }, + { + canEdit: true, + id: 'editor3', + data: { + lastModified: '2016-02-22T00:00:00.000Z', + contractValue: 548949829823984983498398, + fileType: 'video', + name: + 'Long name - KJNASDKJNSDKnskdfjnskldjfnskldjnfkjsdngkjdfngamazonKJNASDKJNSDKnskdfjnskldjfnskldjnfkjsdngkjdfngamazonKJNASDKJNSDKnskdfjnskldjfnskldjnfkjsdngkjdfngamazonKJNASDKJNSDKnskdfjnskldjfnskldjnfkjsdngkjdfngamazonKJNASDKJNSDKnskdfjnskldjfnskldjnfkjsdngkjdfng', + size: 14594600037438723487, + }, + cascadePolicy: { + canEdit: true, + isEnabled: false, + id: 'some cascading policy id', + }, + }, +]; + +const fieldTypes = { + FIELD_TYPE_STRING: 'string', + FIELD_TYPE_DATE: 'date', + FIELD_TYPE_FLOAT: 'float', + FIELD_TYPE_ENUM: 'enum', +}; + +export const template1 = { + id: 'template1', + templateKey: 'template1', + displayName: 'template1 title that is super long', + scope: 'enterprise_123', + 'Vendor Name': { + operators: ['is', 'is greater than', 'is less than', 'is not', 'is blank', 'matches any'], + values: ['Google', 'Apple', 'Facebook'], + }, + 'Expiration Month': { + operators: ['is', 'is greater than', 'is less than', 'is not'], + values: ['August 2018', 'September 2018', 'October 2018'], + }, + 'File Type': { + operators: ['is', 'is not'], + values: ['.docx', '.mp3', 'mp4'], + }, + fields: [ + { + id: 'field1', + type: fieldTypes.FIELD_TYPE_STRING, + key: 'name', + displayName: 'Name', + }, + { + id: 'field7', + type: fieldTypes.FIELD_TYPE_DATE, + key: 'lastModified', + displayName: 'Last Modified', + }, + { + id: 'field11', + type: fieldTypes.FIELD_TYPE_FLOAT, + key: 'size', + displayName: 'Size', + description: 'example of an integer field', + }, + { + id: 'field5', + type: fieldTypes.FIELD_TYPE_ENUM, + key: 'contractValue', + displayName: 'Contract Value', + options: [{ key: '$100' }, { key: '$2000' }, { key: '$10000' }, { key: '$200000' }], + }, + ], +}; + +export const template2 = { + id: 'template1', + templateKey: 'template1', + displayName: 'template1 title', + scope: 'enterprise_123', + 'Vendor Name': { + operators: ['is', 'is greater than', 'is less than', 'is not', 'is blank', 'matches any'], + values: ['Google', 'Apple', 'Facebook'], + }, + 'Expiration Month': { + operators: ['is', 'is greater than', 'is less than', 'is not'], + values: ['August 2018', 'September 2018', 'October 2018'], + }, + 'File Type': { + operators: ['is', 'is not'], + values: ['.docx', '.mp3', 'mp4'], + }, + fields: [ + { + id: 'field1', + type: fieldTypes.FIELD_TYPE_STRING, + key: 'name', + displayName: 'Name', + }, + ], +}; + +type Props = { + currentMessage?: string, + template: Template, + templates?: Array