From 9faea69c202376f3f3515ef665be04a12b61a130 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Thu, 3 Mar 2022 02:46:35 -0600 Subject: [PATCH] Set `tabWidth: 2` in Prettier config (#2438) * chore: Update Prettier config, setting tabWidth:2 * style: Apply updated Prettier styles If you need to rebase work past this style change, do as follows: 0. Consider this to be commit `commitA`, replacing that with its id in the following. 1. To make sure mistakes aren't fatal, assign a second branch to your current work. 2. Rebase your branch on the commit immediately before this one, commitA~ 3. Run the following command at the root of the repo: git rebase --strategy-option=theirs \ --exec 'npx prettier --write . && git add -u && git commit --amend --no-edit' \ commitA That will take a short while esp. if you have multiple commits, as it runs Prettier on everything for every commit. If you've deleted files, the rebase may drop down to interactive mode and have you `git rm` as appropriate, then `git rebase --continue`. You should end up with just your changes in your branch, prettily formatted. To validate that, apply the same Prettier config change to your original branch, reformat the files with `npm run prettier`, and then compare the results with the rebased branch. * chore: Clean up lint configs --- .eslintignore | 35 +- .eslintrc.js | 148 +- .github/actions/check-tsc/index.js | 42 +- .prettierignore | 38 +- .pyup.yml | 2 +- CODE_OF_CONDUCT.md | 5 +- Makefile | 4 +- README.md | 23 +- app.json | 143 +- babel.config.json | 9 +- contribute.json | 67 +- docker-compose.yml | 4 +- .../k8s-first-steps/k8s-pontoon-example.yaml | 120 +- docker/k8s-first-steps/readme.md | 239 ++- error_pages/README.md | 1 + error_pages/application-error.html | 91 +- error_pages/css/style.css | 912 ++++++----- error_pages/maintenance-mode.html | 91 +- package.json | 8 +- pontoon/administration/static/css/admin.css | 2 +- .../static/css/admin_project.css | 448 +++--- .../administration/static/js/admin_project.js | 524 +++--- pontoon/api/README.md | 23 +- .../base/static/css/double_list_selector.css | 16 +- pontoon/base/static/css/download_selector.css | 50 +- pontoon/base/static/css/fonts.css | 64 +- pontoon/base/static/css/heading_info.css | 166 +- pontoon/base/static/css/pontoon.css | 52 +- pontoon/base/static/css/sidebar_menu.css | 38 +- pontoon/base/static/css/style.css | 1352 ++++++++-------- pontoon/base/static/css/table.css | 306 ++-- pontoon/base/static/css/terms.css | 18 +- .../base/static/js/double_list_selector.js | 126 +- pontoon/base/static/js/main.js | 813 +++++----- pontoon/base/static/js/progress-chart.js | 120 +- pontoon/base/static/js/sidebar_menu.js | 18 +- pontoon/base/static/js/table.js | 454 +++--- pontoon/base/static/js/tabs.js | 192 +-- .../contributors/static/css/contributor.css | 86 +- .../contributors/static/css/contributors.css | 70 +- .../contributors/static/css/notifications.css | 10 +- pontoon/contributors/static/css/profile.css | 330 ++-- pontoon/contributors/static/css/settings.css | 114 +- pontoon/contributors/static/js/contributor.js | 136 +- .../contributors/static/js/notifications.js | 68 +- pontoon/contributors/static/js/settings.js | 142 +- pontoon/homepage/static/css/homepage.css | 418 ++--- .../homepage/static/css/homepage_admin.css | 4 +- pontoon/homepage/static/js/homepage.js | 288 ++-- pontoon/insights/static/css/insights.css | 244 +-- pontoon/insights/static/js/insights.js | 1421 ++++++++--------- pontoon/machinery/static/css/machinery.css | 22 +- pontoon/machinery/static/js/machinery.js | 1046 ++++++------ .../static/css/manual_notifications.css | 72 +- .../static/js/manual_notifications.js | 106 +- pontoon/static/js/errors/index.js | 8 +- pontoon/sync/README.md | 22 +- pontoon/sync/static/css/sync_logs.css | 112 +- pontoon/teams/static/css/info.css | 40 +- .../static/css/multiple_team_selector.css | 52 +- pontoon/teams/static/css/request.css | 150 +- pontoon/teams/static/css/team.css | 180 +-- pontoon/teams/static/css/team_selector.css | 52 +- pontoon/teams/static/js/bugzilla.js | 327 ++-- pontoon/teams/static/js/info.js | 98 +- .../teams/static/js/multiple_team_selector.js | 96 +- pontoon/teams/static/js/permissions.js | 290 ++-- pontoon/teams/static/js/request.js | 455 +++--- pontoon/teams/static/js/team_selector.js | 12 +- tag-admin/jest.config.js | 20 +- tag-admin/rollup.config.js | 36 +- tag-admin/src/button.js | 28 +- tag-admin/src/button.test.js | 40 +- tag-admin/src/index.js | 14 +- tag-admin/src/manager.js | 64 +- tag-admin/src/manager.test.js | 118 +- tag-admin/src/search.js | 74 +- tag-admin/src/search.test.js | 34 +- tag-admin/src/tag-resources.css | 40 +- tag-admin/src/utils/http-post.js | 116 +- tag-admin/src/utils/http-post.test.js | 110 +- tag-admin/src/widgets/checkbox-table.js | 175 +- tag-admin/src/widgets/checkbox-table.test.js | 284 ++-- tag-admin/src/widgets/checkbox.js | 10 +- tag-admin/src/widgets/checkbox.test.js | 16 +- tag-admin/src/widgets/error-list.css | 10 +- tag-admin/src/widgets/error-list.js | 20 +- tag-admin/src/widgets/error-list.test.js | 18 +- translate/.eslintrc.js | 41 +- translate/README.md | 31 +- translate/jest.config.js | 68 +- translate/package.json | 3 - translate/rollup.config.js | 56 +- translate/src/App.css | 72 +- translate/src/App.test.js | 18 +- translate/src/App.tsx | 183 ++- translate/src/core/api/base.ts | 192 ++- translate/src/core/api/comment.ts | 70 +- translate/src/core/api/entity.ts | 379 +++-- translate/src/core/api/filter.ts | 26 +- translate/src/core/api/index.ts | 46 +- translate/src/core/api/l10n.ts | 10 +- translate/src/core/api/locale.ts | 16 +- translate/src/core/api/machinery.ts | 364 +++-- translate/src/core/api/project.ts | 16 +- translate/src/core/api/resource.ts | 12 +- translate/src/core/api/translation.ts | 204 ++- translate/src/core/api/types.ts | 128 +- translate/src/core/api/user.ts | 169 +- translate/src/core/api/uxaction.ts | 58 +- translate/src/core/comments/actions.ts | 34 +- .../core/comments/components/AddComment.css | 90 +- .../comments/components/AddComment.test.js | 46 +- .../core/comments/components/AddComment.tsx | 998 ++++++------ .../src/core/comments/components/Comment.css | 84 +- .../core/comments/components/Comment.test.js | 116 +- .../src/core/comments/components/Comment.tsx | 192 ++- .../core/comments/components/CommentsList.css | 40 +- .../comments/components/CommentsList.test.js | 36 +- .../core/comments/components/CommentsList.tsx | 144 +- .../core/diff/components/TranslationDiff.css | 18 +- .../diff/components/TranslationDiff.test.js | 32 +- .../core/diff/components/TranslationDiff.ts | 12 +- translate/src/core/diff/withDiff.tsx | 48 +- translate/src/core/editor/actions.ts | 384 +++-- .../components/EditorMainAction.test.js | 164 +- .../editor/components/EditorMainAction.tsx | 186 ++- .../src/core/editor/components/EditorMenu.css | 46 +- .../core/editor/components/EditorMenu.test.js | 122 +- .../src/core/editor/components/EditorMenu.tsx | 146 +- .../core/editor/components/EditorSettings.css | 70 +- .../editor/components/EditorSettings.test.js | 86 +- .../core/editor/components/EditorSettings.tsx | 180 ++- .../core/editor/components/FailedChecks.css | 104 +- .../editor/components/FailedChecks.test.js | 223 ++- .../core/editor/components/FailedChecks.tsx | 195 ++- .../editor/components/KeyboardShortcuts.css | 68 +- .../editor/components/KeyboardShortcuts.tsx | 458 +++--- .../editor/components/TranslationLength.css | 10 +- .../components/TranslationLength.test.js | 182 +-- .../editor/components/TranslationLength.tsx | 104 +- .../editor/hooks/useAddTextToTranslation.ts | 18 +- .../src/core/editor/hooks/useClearEditor.ts | 8 +- .../hooks/useCopyMachineryTranslation.ts | 110 +- .../editor/hooks/useCopyOriginalIntoEditor.ts | 32 +- .../hooks/useCopyOtherLocaleTranslation.ts | 36 +- .../core/editor/hooks/useHandleShortcuts.ts | 276 ++-- .../hooks/useReplaceSelectionContent.ts | 47 +- .../core/editor/hooks/useSendTranslation.ts | 116 +- .../core/editor/hooks/useUpdateTranslation.ts | 18 +- .../hooks/useUpdateTranslationStatus.ts | 84 +- .../editor/hooks/useUpdateUnsavedChanges.ts | 116 +- translate/src/core/editor/reducer.ts | 366 +++-- translate/src/core/editor/selectors.test.js | 268 ++-- translate/src/core/editor/selectors.ts | 140 +- translate/src/core/entities/actions.ts | 174 +- translate/src/core/entities/reducer.ts | 208 +-- translate/src/core/entities/selectors.test.js | 212 +-- translate/src/core/entities/selectors.ts | 81 +- translate/src/core/l10n/actions.ts | 113 +- .../AppLocalizationProvider.test.js | 70 +- .../components/AppLocalizationProvider.tsx | 54 +- translate/src/core/l10n/pseudolocalization.ts | 122 +- translate/src/core/l10n/reducer.test.js | 50 +- translate/src/core/l10n/reducer.ts | 42 +- translate/src/core/lightbox/actions.ts | 24 +- .../src/core/lightbox/components/Lightbox.css | 24 +- .../core/lightbox/components/Lightbox.test.js | 140 +- .../src/core/lightbox/components/Lightbox.tsx | 88 +- translate/src/core/lightbox/reducer.ts | 40 +- translate/src/core/linkify/index.test.js | 147 +- translate/src/core/linkify/index.ts | 14 +- .../loaders/components/SkeletonLoader.css | 38 +- .../loaders/components/SkeletonLoader.tsx | 46 +- .../core/loaders/components/WaveLoader.css | 120 +- .../core/loaders/components/WaveLoader.tsx | 28 +- translate/src/core/locale/actions.ts | 84 +- .../src/core/locale/getPluralExamples.test.js | 62 +- .../src/core/locale/getPluralExamples.ts | 48 +- translate/src/core/locale/reducer.test.js | 62 +- translate/src/core/locale/reducer.ts | 60 +- translate/src/core/navigation/actions.ts | 128 +- .../src/core/navigation/selectors.test.js | 68 +- translate/src/core/navigation/selectors.ts | 86 +- translate/src/core/notification/actions.ts | 46 +- .../components/NotificationPanel.css | 34 +- .../components/NotificationPanel.test.js | 80 +- .../components/NotificationPanel.tsx | 97 +- translate/src/core/notification/messages.tsx | 318 ++-- translate/src/core/notification/reducer.ts | 24 +- .../placeable/components/WithPlaceables.css | 20 +- .../components/WithPlaceables.test.js | 12 +- .../placeable/components/WithPlaceables.ts | 76 +- .../WithPlaceablesForFluent.test.js | 40 +- .../components/WithPlaceablesForFluent.ts | 16 +- .../WithPlaceablesForFluentNoLeadingSpace.ts | 2 +- .../WithPlaceablesNoLeadingSpace.test.js | 34 +- .../WithPlaceablesNoLeadingSpace.ts | 12 +- .../placeable/parsers/altAttribute.test.js | 14 +- .../core/placeable/parsers/altAttribute.tsx | 31 +- .../placeable/parsers/camelCaseString.test.js | 38 +- .../placeable/parsers/camelCaseString.tsx | 25 +- .../placeable/parsers/emailPattern.test.js | 18 +- .../core/placeable/parsers/emailPattern.tsx | 25 +- .../placeable/parsers/escapeSequence.test.js | 14 +- .../core/placeable/parsers/escapeSequence.tsx | 23 +- .../placeable/parsers/filePattern.test.js | 34 +- .../core/placeable/parsers/filePattern.tsx | 25 +- .../placeable/parsers/fluentFunction.test.js | 52 +- .../core/placeable/parsers/fluentFunction.tsx | 23 +- .../parsers/fluentParametrizedTerm.test.js | 55 +- .../parsers/fluentParametrizedTerm.tsx | 32 +- .../placeable/parsers/fluentString.test.js | 42 +- .../core/placeable/parsers/fluentString.tsx | 27 +- .../core/placeable/parsers/fluentTerm.test.js | 40 +- .../src/core/placeable/parsers/fluentTerm.tsx | 20 +- .../parsers/javaFormattingVariable.test.js | 22 +- .../parsers/javaFormattingVariable.tsx | 36 +- .../placeable/parsers/jsonPlaceholder.test.js | 36 +- .../placeable/parsers/jsonPlaceholder.tsx | 23 +- .../placeable/parsers/leadingSpace.test.js | 12 +- .../core/placeable/parsers/leadingSpace.tsx | 23 +- .../placeable/parsers/multipleSpaces.test.js | 14 +- .../core/placeable/parsers/multipleSpaces.tsx | 35 +- .../parsers/narrowNonBreakingSpace.test.js | 14 +- .../parsers/narrowNonBreakingSpace.tsx | 30 +- .../parsers/newlineCharacter.test.js | 14 +- .../placeable/parsers/newlineCharacter.tsx | 35 +- .../placeable/parsers/newlineEscape.test.js | 14 +- .../core/placeable/parsers/newlineEscape.tsx | 23 +- .../parsers/nonBreakingSpace.test.js | 14 +- .../placeable/parsers/nonBreakingSpace.tsx | 27 +- .../placeable/parsers/nsisVariable.test.js | 36 +- .../core/placeable/parsers/nsisVariable.tsx | 25 +- .../placeable/parsers/numberString.test.js | 36 +- .../core/placeable/parsers/numberString.tsx | 25 +- .../placeable/parsers/optionPattern.test.js | 18 +- .../core/placeable/parsers/optionPattern.tsx | 29 +- .../placeable/parsers/punctuation.test.js | 50 +- .../core/placeable/parsers/punctuation.tsx | 55 +- .../parsers/pythonFormatNamedString.test.js | 22 +- .../parsers/pythonFormatNamedString.tsx | 30 +- .../parsers/pythonFormatString.test.js | 24 +- .../placeable/parsers/pythonFormatString.tsx | 30 +- .../parsers/pythonFormattingVariable.test.js | 58 +- .../parsers/pythonFormattingVariable.tsx | 36 +- .../placeable/parsers/qtFormatting.test.js | 20 +- .../core/placeable/parsers/qtFormatting.tsx | 33 +- .../parsers/shortCapitalNumberString.test.js | 34 +- .../parsers/shortCapitalNumberString.tsx | 34 +- .../parsers/stringFormattingVariable.test.js | 72 +- .../parsers/stringFormattingVariable.tsx | 36 +- .../placeable/parsers/tabCharacter.test.js | 14 +- .../core/placeable/parsers/tabCharacter.tsx | 35 +- .../core/placeable/parsers/thinSpace.test.js | 14 +- .../src/core/placeable/parsers/thinSpace.tsx | 20 +- .../placeable/parsers/unusualSpace.test.js | 20 +- .../core/placeable/parsers/unusualSpace.tsx | 23 +- .../core/placeable/parsers/uriPattern.test.js | 40 +- .../src/core/placeable/parsers/uriPattern.tsx | 52 +- .../core/placeable/parsers/xmlEntity.test.js | 20 +- .../src/core/placeable/parsers/xmlEntity.tsx | 22 +- .../src/core/placeable/parsers/xmlTag.test.js | 26 +- .../src/core/placeable/parsers/xmlTag.tsx | 22 +- translate/src/core/plural/actions.ts | 48 +- .../core/plural/components/PluralSelector.css | 48 +- .../plural/components/PluralSelector.test.js | 170 +- .../core/plural/components/PluralSelector.tsx | 141 +- translate/src/core/plural/index.ts | 12 +- translate/src/core/plural/reducer.test.js | 30 +- translate/src/core/plural/reducer.ts | 36 +- translate/src/core/plural/selectors.test.js | 96 +- translate/src/core/plural/selectors.ts | 76 +- translate/src/core/project/actions.ts | 70 +- .../core/project/components/ProjectItem.css | 18 +- .../project/components/ProjectItem.test.js | 62 +- .../core/project/components/ProjectItem.tsx | 38 +- .../core/project/components/ProjectMenu.css | 140 +- .../project/components/ProjectMenu.test.js | 184 ++- .../core/project/components/ProjectMenu.tsx | 399 +++-- .../project/components/ProjectPercent.css | 4 +- .../project/components/ProjectPercent.test.js | 18 +- .../project/components/ProjectPercent.tsx | 14 +- translate/src/core/project/reducer.ts | 60 +- translate/src/core/resource/actions.ts | 84 +- .../core/resource/components/ResourceItem.css | 18 +- .../resource/components/ResourceItem.test.js | 52 +- .../core/resource/components/ResourceItem.tsx | 36 +- .../core/resource/components/ResourceMenu.css | 140 +- .../resource/components/ResourceMenu.test.js | 206 +-- .../core/resource/components/ResourceMenu.tsx | 464 +++--- .../resource/components/ResourcePercent.css | 4 +- .../components/ResourcePercent.test.js | 18 +- .../resource/components/ResourcePercent.tsx | 13 +- translate/src/core/resource/reducer.test.js | 152 +- translate/src/core/resource/reducer.ts | 143 +- translate/src/core/stats/actions.ts | 46 +- translate/src/core/stats/reducer.ts | 30 +- translate/src/core/term/actions.ts | 54 +- translate/src/core/term/components/Term.css | 54 +- .../src/core/term/components/Term.test.js | 134 +- translate/src/core/term/components/Term.tsx | 125 +- .../src/core/term/components/TermsList.css | 6 +- .../core/term/components/TermsList.test.js | 30 +- .../src/core/term/components/TermsList.tsx | 44 +- translate/src/core/term/getMarker.test.js | 190 +-- translate/src/core/term/getMarker.tsx | 70 +- translate/src/core/term/reducer.ts | 50 +- .../components/FluentTranslation.tsx | 48 +- .../components/GenericTranslation.tsx | 42 +- .../components/TranslationProxy.tsx | 23 +- translate/src/core/user/actions.ts | 148 +- .../src/core/user/components/FileUpload.css | 10 +- .../src/core/user/components/FileUpload.tsx | 110 +- translate/src/core/user/components/SignIn.css | 22 +- translate/src/core/user/components/SignIn.tsx | 20 +- .../src/core/user/components/SignInLink.tsx | 24 +- .../src/core/user/components/SignOut.tsx | 30 +- .../user/components/UserAutoUpdater.test.js | 34 +- .../core/user/components/UserAutoUpdater.ts | 36 +- .../src/core/user/components/UserAvatar.tsx | 43 +- .../src/core/user/components/UserControls.css | 2 +- .../core/user/components/UserControls.test.js | 24 +- .../src/core/user/components/UserControls.tsx | 84 +- .../src/core/user/components/UserMenu.css | 108 +- .../src/core/user/components/UserMenu.test.js | 174 +- .../src/core/user/components/UserMenu.tsx | 467 +++--- .../core/user/components/UserNotification.css | 104 +- .../core/user/components/UserNotification.tsx | 284 ++-- .../user/components/UserNotificationsMenu.css | 120 +- .../components/UserNotificationsMenu.test.js | 214 ++- .../user/components/UserNotificationsMenu.tsx | 237 ++- translate/src/core/user/reducer.ts | 255 ++- translate/src/core/user/selectors.test.js | 102 +- translate/src/core/user/selectors.ts | 38 +- translate/src/core/utils/asLocaleString.ts | 2 +- .../components/withActionsDisabled.test.js | 44 +- .../utils/components/withActionsDisabled.tsx | 76 +- .../core/utils/fluent/areSupportedElements.ts | 26 +- .../core/utils/fluent/convertSyntax.test.js | 186 +-- .../src/core/utils/fluent/convertSyntax.ts | 203 ++- .../fluent/extractAccessKeyCandidates.test.js | 106 +- .../fluent/extractAccessKeyCandidates.ts | 132 +- .../core/utils/fluent/flattenMessage.test.js | 192 ++- .../src/core/utils/fluent/flattenMessage.ts | 42 +- .../utils/fluent/flattenPatternElements.ts | 160 +- .../core/utils/fluent/getEmptyMessage.test.js | 231 ++- .../src/core/utils/fluent/getEmptyMessage.ts | 120 +- .../fluent/getReconstructedMessage.test.js | 118 +- .../utils/fluent/getReconstructedMessage.ts | 64 +- .../utils/fluent/getSimplePreview.test.js | 192 +-- .../src/core/utils/fluent/getSimplePreview.ts | 32 +- .../core/utils/fluent/getSyntaxType.test.js | 162 +- .../src/core/utils/fluent/getSyntaxType.ts | 14 +- translate/src/core/utils/fluent/index.ts | 34 +- .../utils/fluent/isPluralExpression.test.js | 72 +- .../core/utils/fluent/isPluralExpression.ts | 20 +- .../src/core/utils/fluent/isSimpleElement.ts | 34 +- .../core/utils/fluent/isSimpleMessage.test.js | 30 +- .../src/core/utils/fluent/isSimpleMessage.ts | 22 +- .../isSimpleSingleAttributeMessage.test.js | 32 +- .../fluent/isSimpleSingleAttributeMessage.ts | 22 +- .../core/utils/fluent/isSupportedMessage.ts | 38 +- translate/src/core/utils/fluent/serialize.ts | 36 +- .../src/core/utils/getOptimizedContent.ts | 18 +- .../src/core/utils/hooks/useOnDiscard.test.js | 86 +- .../src/core/utils/hooks/useOnDiscard.ts | 44 +- translate/src/hooks.ts | 8 +- translate/src/hooks/usePrevious.ts | 10 +- translate/src/index.css | 30 +- translate/src/index.tsx | 16 +- .../components/AddonPromotion.css | 54 +- .../components/AddonPromotion.tsx | 244 ++- translate/src/modules/batchactions/actions.ts | 362 ++--- .../components/ApproveAll.test.js | 194 +-- .../batchactions/components/ApproveAll.tsx | 161 +- .../batchactions/components/BatchActions.css | 122 +- .../components/BatchActions.test.js | 126 +- .../batchactions/components/BatchActions.tsx | 518 +++--- .../batchactions/components/RejectAll.test.js | 224 ++- .../batchactions/components/RejectAll.tsx | 219 ++- .../components/ReplaceAll.test.js | 194 +-- .../batchactions/components/ReplaceAll.tsx | 161 +- translate/src/modules/batchactions/reducer.ts | 182 +-- .../entitieslist/components/EntitiesList.css | 54 +- .../components/EntitiesList.test.js | 168 +- .../entitieslist/components/EntitiesList.tsx | 721 ++++----- .../entitieslist/components/Entity.css | 102 +- .../entitieslist/components/Entity.test.js | 376 ++--- .../entitieslist/components/Entity.tsx | 345 ++-- .../components/ContextIssueButton.css | 22 +- .../components/ContextIssueButton.tsx | 28 +- .../components/EditorSelector.css | 44 +- .../components/EditorSelector.tsx | 24 +- .../components/EntityDetails.css | 20 +- .../components/EntityDetails.test.js | 355 ++-- .../components/EntityDetails.tsx | 942 ++++++----- .../components/EntityNavigation.css | 26 +- .../components/EntityNavigation.test.js | 156 +- .../components/EntityNavigation.tsx | 142 +- .../components/FluentAttribute.tsx | 43 +- .../components/GenericOriginalString.test.js | 68 +- .../components/GenericOriginalString.tsx | 86 +- .../entitydetails/components/Helpers.css | 82 +- .../entitydetails/components/Helpers.tsx | 264 ++- .../entitydetails/components/Metadata.css | 104 +- .../entitydetails/components/Metadata.test.js | 198 ++- .../entitydetails/components/Metadata.tsx | 671 ++++---- .../components/OriginalStringProxy.tsx | 50 +- .../entitydetails/components/Property.tsx | 22 +- .../entitydetails/components/Screenshots.css | 16 +- .../components/Screenshots.test.js | 28 +- .../entitydetails/components/Screenshots.tsx | 40 +- .../entitydetails/components/TermsPopup.css | 16 +- .../entitydetails/components/TermsPopup.tsx | 38 +- .../fluenteditor/components/FluentEditor.css | 22 +- .../components/FluentEditor.test.js | 172 +- .../fluenteditor/components/FluentEditor.tsx | 348 ++-- .../components/rich/RichEditor.tsx | 124 +- .../components/rich/RichTranslationForm.css | 86 +- .../rich/RichTranslationForm.test.js | 262 ++- .../components/rich/RichTranslationForm.tsx | 894 ++++++----- .../components/simple/SimpleEditor.test.js | 109 +- .../components/simple/SimpleEditor.tsx | 143 +- .../components/source/SourceEditor.tsx | 38 +- .../components/FluentOriginalString.tsx | 68 +- .../fluentoriginal/components/RichString.css | 40 +- .../components/RichString.test.js | 134 +- .../fluentoriginal/components/RichString.tsx | 208 ++- .../components/SimpleString.test.js | 42 +- .../components/SimpleString.tsx | 24 +- .../components/SourceString.test.js | 42 +- .../components/SourceString.tsx | 26 +- .../components/GenericEditor.test.js | 130 +- .../components/GenericEditor.tsx | 138 +- .../components/GenericTranslationForm.test.js | 60 +- .../components/GenericTranslationForm.tsx | 172 +- translate/src/modules/history/actions.ts | 371 +++-- .../modules/history/components/History.css | 22 +- .../history/components/History.test.js | 50 +- .../modules/history/components/History.tsx | 136 +- .../history/components/Translation.css | 188 +-- .../history/components/Translation.test.js | 772 +++++---- .../history/components/Translation.tsx | 873 +++++----- translate/src/modules/history/reducer.ts | 93 +- .../components/InteractiveTour.css | 18 +- .../components/InteractiveTour.test.js | 62 +- .../components/InteractiveTour.tsx | 704 ++++---- translate/src/modules/machinery/actions.ts | 192 +-- .../components/ConcordanceSearch.css | 10 +- .../components/ConcordanceSearch.tsx | 96 +- .../machinery/components/Machinery.css | 98 +- .../machinery/components/Machinery.test.js | 88 +- .../machinery/components/Machinery.tsx | 230 ++- .../components/MachineryCount.test.js | 116 +- .../machinery/components/MachineryCount.tsx | 40 +- .../machinery/components/Translation.css | 60 +- .../machinery/components/Translation.test.js | 101 +- .../machinery/components/Translation.tsx | 204 ++- .../components/TranslationSource.test.js | 51 +- .../components/TranslationSource.tsx | 79 +- .../source/CaighdeanTranslation.test.js | 28 +- .../source/CaighdeanTranslation.tsx | 38 +- .../source/GoogleTranslation.test.js | 28 +- .../components/source/GoogleTranslation.tsx | 38 +- .../source/MicrosoftTerminology.test.js | 49 +- .../source/MicrosoftTerminology.tsx | 60 +- .../source/MicrosoftTranslation.test.js | 30 +- .../source/MicrosoftTranslation.tsx | 38 +- .../components/source/SystranTranslation.tsx | 38 +- .../source/TranslationMemory.test.js | 52 +- .../components/source/TranslationMemory.tsx | 60 +- translate/src/modules/machinery/reducer.ts | 191 ++- .../modules/navbar/components/Navigation.css | 34 +- .../navbar/components/Navigation.test.js | 46 +- .../modules/navbar/components/Navigation.tsx | 230 +-- translate/src/modules/otherlocales/actions.ts | 54 +- .../otherlocales/components/Count.test.js | 186 +-- .../modules/otherlocales/components/Count.tsx | 56 +- .../otherlocales/components/OtherLocales.css | 12 +- .../components/OtherLocales.test.js | 112 +- .../otherlocales/components/OtherLocales.tsx | 114 +- .../otherlocales/components/Translation.css | 40 +- .../otherlocales/components/Translation.tsx | 194 ++- translate/src/modules/otherlocales/reducer.ts | 50 +- .../projectinfo/components/ProjectInfo.css | 46 +- .../components/ProjectInfo.test.js | 96 +- .../projectinfo/components/ProjectInfo.tsx | 124 +- .../components/ProgressChart.css | 6 +- .../components/ProgressChart.tsx | 176 +- .../components/ResourceProgress.css | 136 +- .../components/ResourceProgress.test.js | 62 +- .../components/ResourceProgress.tsx | 344 ++-- translate/src/modules/search/actions.ts | 58 +- .../search/components/FiltersPanel.css | 268 ++-- .../search/components/FiltersPanel.test.js | 408 ++--- .../search/components/FiltersPanel.tsx | 887 +++++----- .../modules/search/components/SearchBox.css | 106 +- .../search/components/SearchBox.test.js | 568 +++---- .../modules/search/components/SearchBox.tsx | 865 +++++----- .../search/components/TimeRangeFilter.css | 80 +- .../search/components/TimeRangeFilter.tsx | 660 ++++---- .../search/components/chart-options.ts | 264 +-- translate/src/modules/search/constants.ts | 92 +- translate/src/modules/search/reducer.ts | 46 +- .../src/modules/search/withSearch.test.js | 12 +- translate/src/modules/search/withSearch.tsx | 80 +- translate/src/modules/teamcomments/actions.ts | 94 +- .../teamcomments/components/Count.test.js | 106 +- .../modules/teamcomments/components/Count.tsx | 58 +- .../teamcomments/components/TeamComments.css | 12 +- .../components/TeamComments.test.js | 42 +- .../teamcomments/components/TeamComments.tsx | 88 +- translate/src/modules/teamcomments/reducer.ts | 94 +- .../src/modules/terms/components/Count.tsx | 16 +- .../src/modules/terms/components/Terms.css | 8 +- .../modules/terms/components/Terms.test.js | 44 +- .../src/modules/terms/components/Terms.tsx | 58 +- .../src/modules/unsavedchanges/actions.ts | 70 +- .../components/UnsavedChanges.css | 76 +- .../components/UnsavedChanges.test.js | 96 +- .../components/UnsavedChanges.tsx | 124 +- .../src/modules/unsavedchanges/reducer.ts | 86 +- translate/src/rootReducer.ts | 48 +- translate/src/store.ts | 30 +- translate/src/test/store.jsx | 66 +- translate/src/test/utils.js | 84 +- translate/tsconfig.json | 37 +- 528 files changed, 30406 insertions(+), 31367 deletions(-) diff --git a/.eslintignore b/.eslintignore index 57f7a2523..772425347 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,16 +1,23 @@ -**/*.bundle.js -**/*.js.map -**/dist/** -**/build/** -vendor/** -coverage/** -static/* -**/*.min.js -**/js/lib/**/*.js -**/app/error_pages/**/*.js -**/*blockrain*js -assets/* -**/node_modules/** -docs/ +.vscode/ +tag-admin/dist/ +translate/dist/ +coverage/ +docs/_build/ +docs/venv/ +package-lock.json +specs/ +# Jinja templates pontoon/base/templates/js/pontoon.js +translate/public/translate.html +**/templates/**/*.html + +# Vendored code +error_pages/css/blockrain.css +error_pages/js/ +pontoon/base/static/css/boilerplate.css +pontoon/base/static/css/fontawesome-all.css +pontoon/base/static/css/jquery-ui.css +pontoon/base/static/css/nprogress.css +pontoon/base/static/js/lib/ +pontoon/in_context/static/ diff --git a/.eslintrc.js b/.eslintrc.js index 2ae76017d..f66453086 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,78 +1,78 @@ +/* eslint-env node */ + module.exports = { - "extends": [ - "eslint:recommended", - "plugin:react/recommended" + extends: ['eslint:recommended', 'plugin:react/recommended'], + env: { + es6: true, + browser: true, + jest: true, + }, + parser: '@babel/eslint-parser', + parserOptions: { + ecmaVersion: 2017, + ecmaFeatures: { + jsx: true, + experimentalObjectRestSpread: true, + }, + sourceType: 'module', + babelOptions: { + presets: ['@babel/preset-react'], + }, + requireConfigFile: false, + }, + globals: { + gettext: false, + ngettext: false, + interpolate: false, + l: false, + expect: false, + test: false, + browser: false, + jest: false, + Promise: false, + Set: false, + URLSearchParameters: false, + FormData: false, + require: false, + shortcut: false, + sorttable: false, + $: false, + Pontoon: false, + jQuery: false, + Clipboard: false, + Chart: false, + NProgress: false, + diff_match_patch: false, + Highcharts: false, + Sideshow: false, + editor: false, + DIFF_INSERT: false, + DIFF_EQUAL: false, + DIFF_DELETE: false, + ga: false, + process: false, + generalShortcutsHandler: true, + traversalShortcutsHandler: true, + editorShortcutsHandler: true, + }, + plugins: ['react'], + rules: { + 'react/display-name': 0, + 'react/prefer-es6-class': 1, + 'react/prefer-stateless-function': 0, + 'react/prop-types': 0, + 'react/jsx-key': 0, + 'react/jsx-uses-react': 1, + 'react/jsx-uses-vars': 1, + 'no-unused-vars': [ + 'error', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, ], - env: { - es6: true, - browser: true, - jest: true, + 'no-console': 1, + }, + settings: { + react: { + version: 'detect', }, - parser: "@babel/eslint-parser", - parserOptions: { - ecmaVersion: 2017, - ecmaFeatures: { - jsx: true, - experimentalObjectRestSpread: true - }, - sourceType: 'module', - babelOptions: { - presets: ['@babel/preset-react'], - }, - requireConfigFile: false, - }, - globals: { - gettext: false, - ngettext: false, - interpolate: false, - l: false, - expect: false, - test: false, - browser: false, - jest: false, - Promise: false, - Set: false, - URLSearchParameters: false, - FormData: false, - require: false, - shortcut: false, - sorttable: false, - $: false, - Pontoon: false, - jQuery: false, - Clipboard: false, - Chart: false, - NProgress: false, - diff_match_patch: false, - Highcharts: false, - Sideshow: false, - editor: false, - DIFF_INSERT: false, - DIFF_EQUAL: false, - DIFF_DELETE: false, - ga: false, - process: false, - generalShortcutsHandler: true, - traversalShortcutsHandler: true, - editorShortcutsHandler: true, - }, - plugins: [ - 'react', - ], - rules: { - 'react/display-name': 0, - 'react/prefer-es6-class': 1, - 'react/prefer-stateless-function': 0, - "react/prop-types": 0, - "react/jsx-key": 0, - "react/jsx-uses-react": 1, - 'react/jsx-uses-vars': 1, - "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": true }], - "no-console": 1, - }, - settings: { - 'react': { - 'version': 'detect' - } - } + }, }; diff --git a/.github/actions/check-tsc/index.js b/.github/actions/check-tsc/index.js index ad84a34f7..2a67a3f50 100644 --- a/.github/actions/check-tsc/index.js +++ b/.github/actions/check-tsc/index.js @@ -3,27 +3,27 @@ const { promisify } = require('util'); const { exec } = require('child_process'); async function run() { - console.log('::group::tsc'); - let errors = '0'; - const run = process.env['INPUT_RUN'] || 'npm run types --pretty '; - const cwd = process.env['INPUT_WORKING-DIRECTORY'] || 'translate'; - let stdout, stderr; - try { - ({ stdout, stderr } = await asyncExec(run, { - cwd, - })); - } catch (failed_proc) { - ({ stdout, stderr } = failed_proc); - } - console.log(stdout); - console.log(stderr); - const m = /Found ([0-9]+) errors\./.exec(stdout); - if (m) { - errors = m[1]; - } - console.log('::endgroup::'); - console.log(`\nFound ${errors} errors.\n`); - console.log(`::set-output name=errors::${errors}`); + console.log('::group::tsc'); + let errors = '0'; + const run = process.env['INPUT_RUN'] || 'npm run types --pretty '; + const cwd = process.env['INPUT_WORKING-DIRECTORY'] || 'translate'; + let stdout, stderr; + try { + ({ stdout, stderr } = await asyncExec(run, { + cwd, + })); + } catch (failed_proc) { + ({ stdout, stderr } = failed_proc); + } + console.log(stdout); + console.log(stderr); + const m = /Found ([0-9]+) errors\./.exec(stdout); + if (m) { + errors = m[1]; + } + console.log('::endgroup::'); + console.log(`\nFound ${errors} errors.\n`); + console.log(`::set-output name=errors::${errors}`); } const asyncExec = promisify(exec); diff --git a/.prettierignore b/.prettierignore index 7c133f552..772425347 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,21 +1,23 @@ -# minified code -*.min.js -*.min.css -translate/dist -tag-admin/dist +.vscode/ +tag-admin/dist/ +translate/dist/ coverage/ +docs/_build/ +docs/venv/ +package-lock.json +specs/ -# libraries -**/base/static/js/lib* -**/base/static/css/boilerplate.css -**/base/static/css/fontawesome-all.css -**/base/static/css/jquery-ui.css -**/base/static/css/nprogress.css -**/base/templates/js/pontoon.js -**/in_context/static/css/agency.css -**/in_context/static/js/agency.js +# Jinja templates +pontoon/base/templates/js/pontoon.js +translate/public/translate.html +**/templates/**/*.html -# Prevent VSCode to reformat these files if "Format On Save" enabled -*.html -*.yml -**/package.json* +# Vendored code +error_pages/css/blockrain.css +error_pages/js/ +pontoon/base/static/css/boilerplate.css +pontoon/base/static/css/fontawesome-all.css +pontoon/base/static/css/jquery-ui.css +pontoon/base/static/css/nprogress.css +pontoon/base/static/js/lib/ +pontoon/in_context/static/ diff --git a/.pyup.yml b/.pyup.yml index d76dd93b6..681d897b2 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -1,4 +1,4 @@ -schedule: "every week on monday" +schedule: 'every week on monday' search: False update: insecure requirements: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 498baa3fb..0d424fd88 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,10 +1,11 @@ # Community Participation Guidelines -This repository is governed by Mozilla's code of conduct and etiquette guidelines. +This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the -[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). +[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report + For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. https://pontoon.mydomain.com/ - -### deployment on k8s: - -For a k8s deplyoment example yaml see the k8s-pontoon-example.yaml in this folder - -#### TODO: - -- "_Pontoon sends email when users request projects to be enabled for teams, or teams to be added to projects_" - **e-mails are not yet tested in the image** - ---- - -### Useful commands - -- for git access you need to create the _SSH_KEY_: `ssh-keygen -t rsa -b 4096 -C ""` - - find the public key to be entered in guthub: `cat /root/.ssh/id_rsa.pub` - - the private key to be base64 encoded as _SSH_KEY_: `cat /root/.ssh/id_rsa` -- after the first git sync you also geht the _known_hosts_ file: - - to be base64 encoded as _KNOWN_HOSTS_: `cat /root/.ssh/known_hosts` - -* first manually sync a single project by reading locales from git source code only: `python manage.py sync_projects --projects= --no-commit` -* syncing all projects with writing changes to the soruce code `python manage.py sync_projects`. - This is done by shell script every 30 minutes (evn var SYNC_INTERVAL) -* to see if the sync works look at: http://127.0.0.1:8000/__sync/log/__ - +# Pontoon + +##### (Guide for building and deploying pontoon in a dev environment) + +### Build the container under windows: + +###### get code and prepare the build + +- git checkout: C:\Projekte\pontoon\latest: https://github.com/mozilla/pontoon.git +- three files need to be replaced by those in this folder + +###### build + +- open a powershell in checkout folder and switch to wsl using the `wsl` command +- in WSL: connect windows local docker: `export DOCKER_HOST="tcp://localhost:2375"` +- run the build: `make build` + +### The database setup: + +For a first time setup we recommend starting the container, or at least the database migration script (python manage.py migrate) with a higher privilege user such as the "postgres" superuser. Then for all consecutive runs use the dedicated pontoon user which creation is described below. + +- create a dedicated database + +``` +-- meant to be executed on the mandatory postgress database "postgres" +CREATE DATABASE pontoon; +``` + +- create a poonton database user and roll. Assign the user to its roll + +``` +-- meant to be executed on the mandatory postgress database "postgres" +CREATE USER "pontoon" WITH PASSWORD "h29xlKIN4nrTGyFLsKf1"; +CREATE ROLE "pontoon-all"; +GRANT "pontoon-all" TO "pontoon"; +``` + +- additionally: add the database "postgres" superuser to this roll so he can see the tables easily in SQL-Clients such as pgAdmin. + +``` +-- meant to be executed on the mandatory postgress database "postgres" +GRANT "pontoon-all" TO "postgres"; +``` + +- in case the "postgres" superuser or another higher privilege user was used to initially execute the migration script as mentioned above make sure to change the owner of each table within the poonton database: + +``` +-- meant to be executed on the postgress database "pontoon" +DO $$ +DECLARE + tables CURSOR FOR + SELECT tablename + FROM pg_tables + WHERE tablename NOT LIKE 'pg_%' AND tablename NOT LIKE 'sql_%' + ORDER BY tablename; +BEGIN + FOR table_record IN tables LOOP + EXECUTE format('ALTER TABLE %s OWNER TO "pontoon-all"', table_record.tablename); + -- RAISE NOTICE 'Tablename: %', table_record.tablename; + END LOOP; +END$$; +``` + +### The container: + +###### Test the container locally and init the database + +- tag the container for easier usage: +- for testing run the container like this: + ``` + docker run -d -p 8000:8000 -p 3000:3000 -e DJANGO_LOGIN=true -e DJANGO_DEBUG=false -e DJANGO_DEV=false -e ALLOWED_HOSTS=127.0.0.1 -e CI=true -e DATABASE_URL=postgres://:@:5432/ -e SECRET_KEY=a3cafccbafe39db54f2723f8a6f804c34753679a0f197b5b33050d784129d570 -e SITE_URL=http://127.0.0.1:8000 --name pontoon corp/imagename:pontoon-prod-31.01.20 + ``` + The SSH_KEY and KNOWN_HOSTS environment variables are both base64 encoded, but may be omitted here and set/created manually (without base64 encoding) inside the running container. But for the prod environment they need to be passed to guarantee unattended deployment. + If the environment variable SYNC_INTERVAL is defined a shell script will call sync_projects using this interval in minutes. +- get a bash into the container: `docker exec -it pontoon bash` + useful commands: + + - check open ports (8000 and 3000 should be open): `ss -lntu` + - check running processes: `ps -A` + - show simple startup log of server_run.sh: `cat /app/server_run.log` + + ##### first time only, with newly created database: + + create an admin user: `python ./manage.py createsuperuser --user= --email=` + + ##### for each project: + + - create project using the web ui, see: https://mozilla-l10n.github.io/documentation/tools/pontoon/adding_new_project.html + +- further **administration** via Django: http://127.0.0.1:8000/__a/__ + +#### NOTES: + +- don't try to run pontoon in a subfolder domain, like apigee does. (e.g. https://mydomain.com/pontoon/). It seems only to support running on domain level: **https://mydomain.com/~~pontoon/~~** + you may use subdomains --> https://pontoon.mydomain.com/ + +### deployment on k8s: + +For a k8s deplyoment example yaml see the k8s-pontoon-example.yaml in this folder + +#### TODO: + +- "_Pontoon sends email when users request projects to be enabled for teams, or teams to be added to projects_" + **e-mails are not yet tested in the image** + +--- + +### Useful commands + +- for git access you need to create the _SSH_KEY_: `ssh-keygen -t rsa -b 4096 -C ""` + - find the public key to be entered in guthub: `cat /root/.ssh/id_rsa.pub` + - the private key to be base64 encoded as _SSH_KEY_: `cat /root/.ssh/id_rsa` +- after the first git sync you also geht the _known_hosts_ file: + - to be base64 encoded as _KNOWN_HOSTS_: `cat /root/.ssh/known_hosts` + +* first manually sync a single project by reading locales from git source code only: `python manage.py sync_projects --projects= --no-commit` +* syncing all projects with writing changes to the soruce code `python manage.py sync_projects`. + This is done by shell script every 30 minutes (evn var SYNC_INTERVAL) +* to see if the sync works look at: http://127.0.0.1:8000/__sync/log/__ diff --git a/error_pages/README.md b/error_pages/README.md index 68312d18d..c2f5e1176 100644 --- a/error_pages/README.md +++ b/error_pages/README.md @@ -1,4 +1,5 @@ # Pontoon Error Pages + [Custom Error Pages](https://devcenter.heroku.com/articles/error-pages#customize-pages) for Pontoon deployment on Heroku, featuring Tetris via [blockrain.js](http://aerolab.github.io/blockrain.js/)! Must be hosted outside the main application, which will be down when these pages are displayed. diff --git a/error_pages/application-error.html b/error_pages/application-error.html index bf855b1c9..1b2072d31 100755 --- a/error_pages/application-error.html +++ b/error_pages/application-error.html @@ -1,54 +1,63 @@ - - - + + + - - + + - + - Application Error - + Application Error + - -
-

Application Error

-

Please check back later. Or play some Tetris.

-
+ +
+

Application Error

+

Please check back later. Or play some Tetris.

+
-
-
-
-
- Use only arrows -
-
-
-
-
+
+
+
+
+ Use only arrows +
+
+
+
+
+
+
-
-
-
+ - -
+ + - - - - + + + + - - + + diff --git a/error_pages/css/style.css b/error_pages/css/style.css index 7a794caf8..3137e37f5 100755 --- a/error_pages/css/style.css +++ b/error_pages/css/style.css @@ -1,701 +1,745 @@ -html,body { - background:#333941; - padding:0; - margin:0; - font-family:"Play", "Helvetica Neue", "Arial", sans-serif; - font-weight:normal; - line-height:160%; - font-size:16px; - color:#fff; +html, +body { + background: #333941; + padding: 0; + margin: 0; + font-family: 'Play', 'Helvetica Neue', 'Arial', sans-serif; + font-weight: normal; + line-height: 160%; + font-size: 16px; + color: #fff; text-align: center; } -/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html { - font-family:sans-serif; - -ms-text-size-adjust:100%; - -webkit-text-size-adjust:100% +/*! normalize.css v3.0.1 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } body { - margin:0 + margin: 0; } -article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary { - display:block +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } -audio,canvas,progress,video { - display:inline-block; - vertical-align:baseline +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } audio:not([controls]) { - display:none; - height:0 + display: none; + height: 0; } -[hidden],template { - display:none +[hidden], +template { + display: none; } a { - background:transparent + background: transparent; } -a:active,a:hover { - outline:0 +a:active, +a:hover { + outline: 0; } abbr[title] { - border-bottom:1px dotted + border-bottom: 1px dotted; } -b,strong { - font-weight:bold +b, +strong { + font-weight: bold; } dfn { - font-style:italic + font-style: italic; } h1 { - font-size:2em; - margin:0.67em 0 + font-size: 2em; + margin: 0.67em 0; } mark { - background:#ff0; - color:#000 + background: #ff0; + color: #000; } small { - font-size:80% + font-size: 80%; } -sub,sup { - font-size:75%; - line-height:0; - position:relative; - vertical-align:baseline +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } sup { - top:-0.5em + top: -0.5em; } sub { - bottom:-0.25em + bottom: -0.25em; } img { - border:0 + border: 0; } svg:not(:root) { - overflow:hidden + overflow: hidden; } figure { - margin:1em 40px + margin: 1em 40px; } hr { - -moz-box-sizing:content-box; - box-sizing:content-box; - height:0 + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; } pre { - overflow:auto + overflow: auto; } -code,kbd,pre,samp { - font-family:monospace, monospace; - font-size:1em +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; } -button,input,optgroup,select,textarea { - color:inherit; - font:inherit; - margin:0 +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } button { - overflow:visible + overflow: visible; } -button,select { - text-transform:none +button, +select { + text-transform: none; } -button,html input[type="button"],input[type="reset"],input[type="submit"] { - -webkit-appearance:button; - cursor:pointer +button, +html input[type='button'], +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; + cursor: pointer; } -button[disabled],html input[disabled] { - cursor:default +button[disabled], +html input[disabled] { + cursor: default; } -button::-moz-focus-inner,input::-moz-focus-inner { - border:0; - padding:0 +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } input { - line-height:normal + line-height: normal; } -input[type="checkbox"],input[type="radio"] { - box-sizing:border-box; - padding:0 +input[type='checkbox'], +input[type='radio'] { + box-sizing: border-box; + padding: 0; } -input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button { - height:auto +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + height: auto; } -input[type="search"] { - -webkit-appearance:textfield; - -moz-box-sizing:content-box; - -webkit-box-sizing:content-box; - box-sizing:content-box +input[type='search'] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } -input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration { - -webkit-appearance:none +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-decoration { + -webkit-appearance: none; } fieldset { - border:1px solid #c0c0c0; - margin:0 2px; - padding:0.35em 0.625em 0.75em + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } legend { - border:0; - padding:0 + border: 0; + padding: 0; } textarea { - overflow:auto + overflow: auto; } optgroup { - font-weight:bold + font-weight: bold; } table { - border-collapse:collapse; - border-spacing:0 + border-collapse: collapse; + border-spacing: 0; } -td,th { - padding:0 +td, +th { + padding: 0; } section { - text-align:center; + text-align: center; } section header { - margin-bottom:60px; - padding:0 20px + margin-bottom: 60px; + padding: 0 20px; } section header h1 { - font-weight:normal; - margin-bottom:10px + font-weight: normal; + margin-bottom: 10px; } -section header h1:after,section header h1:before { - content:"**********"; - color:#898989; - padding:0 10px; - font-size:.7em +section header h1:after, +section header h1:before { + content: '**********'; + color: #898989; + padding: 0 10px; + font-size: 0.7em; } section header p { - color:#898989; - font-size:1.2em; - margin-top:10px + color: #898989; + font-size: 1.2em; + margin-top: 10px; } section article { - width:100%; - max-width:840px; - margin:0 auto + width: 100%; + max-width: 840px; + margin: 0 auto; } header { - font-family:"Open Sans", "Helvetica Neue", "Arial", sans-serif; + font-family: 'Open Sans', 'Helvetica Neue', 'Arial', sans-serif; padding: 40px; } header h1 { - color: #EBEBEB; + color: #ebebeb; font-size: 40px; line-height: 50px; margin-bottom: -5px; } header h2 { - color: #7BC876; + color: #7bc876; font-size: 20px; font-style: italic; font-weight: 400; } .btn { - border:2px solid #fff; - color:#fff; - display:inline-block; - padding:12px 25px 12px 15px; - text-decoration:none; - position:relative; - background:#3b3b3b; - margin:0 15px + border: 2px solid #fff; + color: #fff; + display: inline-block; + padding: 12px 25px 12px 15px; + text-decoration: none; + position: relative; + background: #3b3b3b; + margin: 0 15px; } .btn:before { - content:""; - display:inline-block; - position:relative; - top:1px; - height:14px; - width:14px; - margin-right:10px; - background-size:auto 100% + content: ''; + display: inline-block; + position: relative; + top: 1px; + height: 14px; + width: 14px; + margin-right: 10px; + background-size: auto 100%; } .btn.btn-download:before { - background-position:-28px 0 + background-position: -28px 0; } .btn:after { - content:""; - position:absolute; - z-index:-1; - width:100%; - height:100%; - border:2px solid #fff; - bottom:-8px; - right:-8px + content: ''; + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + border: 2px solid #fff; + bottom: -8px; + right: -8px; } section.versus { - text-align:center; - padding-top:100px + text-align: center; + padding-top: 100px; } section.versus .game { - display:inline-block; - margin:0px 20px; - width:200px; - height:500px + display: inline-block; + margin: 0px 20px; + width: 200px; + height: 500px; } #hero { - position:relative; - display:flex; - display:-webkit-flex; - -moz-display:flex; - flex-flow:row wrap; - justify-content:center; - align-content:center; - align-items:center; - -webkit-flex-flow:row wrap; - -webkit-justify-content:center; - -webkit-align-content:center; - -webkit-align-items:center; - -moz-flex-flow:row wrap; - -moz-justify-content:center; - -moz-align-content:center; - -moz-align-items:center; - min-height:500px; - height:90vh; - color:#fff; - padding:0 + position: relative; + display: flex; + display: -webkit-flex; + -moz-display: flex; + flex-flow: row wrap; + justify-content: center; + align-content: center; + align-items: center; + -webkit-flex-flow: row wrap; + -webkit-justify-content: center; + -webkit-align-content: center; + -webkit-align-items: center; + -moz-flex-flow: row wrap; + -moz-justify-content: center; + -moz-align-content: center; + -moz-align-items: center; + min-height: 500px; + height: 90vh; + color: #fff; + padding: 0; } #hero .welcome { - position:relative; - z-index:10; - text-align:center; - margin-bottom:100px + position: relative; + z-index: 10; + text-align: center; + margin-bottom: 100px; } #hero h1 { - font-size:60px; - margin:0 + font-size: 60px; + margin: 0; } #hero h1 img { - width:90%; - max-width:560px + width: 90%; + max-width: 560px; } #hero p { - font-size:1.5em; - margin-bottom:50px + font-size: 1.5em; + margin-bottom: 50px; } #hero:after { - content:""; - display:block; - width:100%; - height:8px; - position:absolute; - bottom:-12px; - left:0 + content: ''; + display: block; + width: 100%; + height: 8px; + position: absolute; + bottom: -12px; + left: 0; } #hero .blockrain-score { - border:2px solid #fff; - text-align:center; - width:120px; - background:#232323 + border: 2px solid #fff; + text-align: center; + width: 120px; + background: #232323; } #hero .blockrain-score .blockrain-score-msg { - border-bottom:2px solid #fff; - text-transform:uppercase; - padding:4px 0; - font-size:.7em + border-bottom: 2px solid #fff; + text-transform: uppercase; + padding: 4px 0; + font-size: 0.7em; } #hero .blockrain-score .blockrain-score-num { - font-size:1.2em; - padding:3px 0 5px + font-size: 1.2em; + padding: 3px 0 5px; } #cover-tetris { - position:absolute; - top:0; - left:0; - right:0; - bottom:0; - z-index:0 + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; } #features { - text-align:center; - padding-bottom:20px + text-align: center; + padding-bottom: 20px; } #features .feature { - width:280px; - height:340px; - float:left; - margin:0; - padding:0 20px; - box-sizing:border-box + width: 280px; + height: 340px; + float: left; + margin: 0; + padding: 0 20px; + box-sizing: border-box; } #features .feature h2 { - font-weight:normal + font-weight: normal; } #features .feature p { - color:#898989 + color: #898989; } #features:after { - content:""; - clear:both; - display:block; - height:0; - width:0 + content: ''; + clear: both; + display: block; + height: 0; + width: 0; } #examples { - background:#333941; - padding-top:0px + background: #333941; + padding-top: 0px; } #examples .buttons .btn { - display:block; - width:35px; - height:63px; - padding:0; - position:absolute; - background-size:auto 100%; - border:none; - top:50%; - margin-top:-30px; - overflow:hidden; - text-indent:-9999px + display: block; + width: 35px; + height: 63px; + padding: 0; + position: absolute; + background-size: auto 100%; + border: none; + top: 50%; + margin-top: -30px; + overflow: hidden; + text-indent: -9999px; } #examples .buttons .btn-prev { - right:100%; - margin-right:70px + right: 100%; + margin-right: 70px; } #examples .buttons .btn-next { - left:100%; - margin-left:70px; - background-position:right + left: 100%; + margin-left: 70px; + background-position: right; } #example-slider { - background:#333941; - width:590px; - height:390px; - position:relative; + background: #333941; + width: 590px; + height: 390px; + position: relative; } .example { - padding:50px 0 0 0; - color:#7BC876; - position:relative + padding: 50px 0 0 0; + color: #7bc876; + position: relative; } .example .game { - display:inline-block; - margin:0px 20px; - width:200px; - height:330px; - position:static + display: inline-block; + margin: 0px 20px; + width: 200px; + height: 330px; + position: static; } .example .game .blockrain-score-holder { - left:100%; - top:-10px; - right:auto; - text-align:left; - margin-left:55px; - font-size:14px; - color:#7BC876; - line-height:20px + left: 100%; + top: -10px; + right: auto; + text-align: left; + margin-left: 55px; + font-size: 14px; + color: #7bc876; + line-height: 20px; } .example .game .blockrain-score-holder .blockrain-score-num { - font-size:24px; - font-weight:normal + font-size: 24px; + font-weight: normal; } -.example .game .blockrain-start,.example .game .blockrain-game-over { - top:auto; - transform:none; - height:100% +.example .game .blockrain-start, +.example .game .blockrain-game-over { + top: auto; + transform: none; + height: 100%; } -.example .game .blockrain-start-msg,.example .game .blockrain-game-over-msg { - color:#EBEBEB; - font-size:14px; - position:absolute; - top:-16px; - width:100% +.example .game .blockrain-start-msg, +.example .game .blockrain-game-over-msg { + color: #ebebeb; + font-size: 14px; + position: absolute; + top: -16px; + width: 100%; } .example .game .blockrain-game-over-msg { - top:50%; - font-size:24px; - margin-top:-24px + top: 50%; + font-size: 24px; + margin-top: -24px; } -.example .game .blockrain-start-btn,.example .game .blockrain-game-over-btn { +.example .game .blockrain-start-btn, +.example .game .blockrain-game-over-btn { cursor: pointer; - position:absolute; - left:100%; - bottom:3px; - border:none; - background:#7BC876; - color:#000; - text-transform:uppercase; - font-weight:bold; - margin-left:55px; - width:120px; - padding:13px 0; - font-size:16px + position: absolute; + left: 100%; + bottom: 3px; + border: none; + background: #7bc876; + color: #000; + text-transform: uppercase; + font-weight: bold; + margin-left: 55px; + width: 120px; + padding: 13px 0; + font-size: 16px; } -.example .game .blockrain-start-btn:before,.example .game .blockrain-start-btn:after,.example .game .blockrain-game-over-btn:before,.example .game .blockrain-game-over-btn:after { - display:none +.example .game .blockrain-start-btn:before, +.example .game .blockrain-start-btn:after, +.example .game .blockrain-game-over-btn:before, +.example .game .blockrain-game-over-btn:after { + display: none; } .example .theme { - position:absolute; - top:36px; - left:20px; - font-size:14px; - line-height:20px; - text-align:left + position: absolute; + top: 36px; + left: 20px; + font-size: 14px; + line-height: 20px; + text-align: left; } .example .theme strong { - display:block; - font-size:24px; - font-weight:normal + display: block; + font-size: 24px; + font-weight: normal; } .example .instructions { - position:absolute; - bottom:10px; - left:20px; - overflow:hidden; - font-size:14px + position: absolute; + bottom: 10px; + left: 20px; + overflow: hidden; + font-size: 14px; } .example .instructions .key { - border:1px solid #7BC876; - display:block; - width:38px; - height:33px; - padding-top:5px; - float:left; - position:relative + border: 1px solid #7bc876; + display: block; + width: 38px; + height: 33px; + padding-top: 5px; + float: left; + position: relative; } .example .instructions .key:before { - content:""; - display:block; - width:14px; - height:14px; - position:absolute; - top:50%; - left:50%; - margin:-7px 0 0 -7px; - background-position:-84px 0; - background-size:auto 100% + content: ''; + display: block; + width: 14px; + height: 14px; + position: absolute; + top: 50%; + left: 50%; + margin: -7px 0 0 -7px; + background-position: -84px 0; + background-size: auto 100%; } .example .instructions .key.key-up { - float:none; - margin-left:39px; - border-bottom:none; - margin-top:20px + float: none; + margin-left: 39px; + border-bottom: none; + margin-top: 20px; } .example .instructions .key.key-up:before { - background-position:-42px 0 + background-position: -42px 0; } .example .instructions .key.key-left:before { - background-position:-56px 0 + background-position: -56px 0; } .example .instructions .key.key-down { - border-left:none; - border-right:none + border-left: none; + border-right: none; } .example .instructions .key.key-down:before { - background-position:-70px 0 + background-position: -70px 0; } #versus-arena { - background:#333941; - width:590px; - height:375px; - border-radius:50%/5%; - position:relative; - padding-top:45px + background: #333941; + width: 590px; + height: 375px; + border-radius: 50%/5%; + position: relative; + padding-top: 45px; } #versus-arena:before { - content:""; - background:#333941; - position:absolute; - top:5%; - bottom:5%; - left:-5%; - right:-5%; - border-radius:5%/50% + content: ''; + background: #333941; + position: absolute; + top: 5%; + bottom: 5%; + left: -5%; + right: -5%; + border-radius: 5%/50%; } #versus-arena .game-holder { - display:inline-block; - vertical-align:middle; - margin:0 15px; - position:relative + display: inline-block; + vertical-align: middle; + margin: 0 15px; + position: relative; } #versus-arena .game { - width:190px; - height:330px; - position:static + width: 190px; + height: 330px; + position: static; } #versus-arena .score { - position:absolute; - right:100%; - margin-right:20px; - color:#7BC876; - font-size:20px; - text-align:right; - line-height:20px + position: absolute; + right: 100%; + margin-right: 20px; + color: #7bc876; + font-size: 20px; + text-align: right; + line-height: 20px; } #versus-arena .score:after { - content:"Won"; - display:block; - font-size:14px + content: 'Won'; + display: block; + font-size: 14px; } #versus-arena .blockrain-score-holder { - color:#7BC876; - line-height:20px + color: #7bc876; + line-height: 20px; } #versus-arena .blockrain-score-holder .blockrain-score-num { - font-weight:normal + font-weight: normal; } #versus-arena #tetris-versus-1 .blockrain-score-holder { - top:auto; - right:100%; - bottom:0px; - margin-right:20px + top: auto; + right: 100%; + bottom: 0px; + margin-right: 20px; } #versus-arena #tetris-versus-2 .blockrain-score-holder { - top:auto; - right:auto; - left:100%; - bottom:0px; - margin-left:20px; - text-align:left + top: auto; + right: auto; + left: 100%; + bottom: 0px; + margin-left: 20px; + text-align: left; } #versus-arena #tetris-versus-2 .score { - right:auto; - left:100%; - text-align:left; - margin-right:0; - margin-left:20px + right: auto; + left: 100%; + text-align: left; + margin-right: 0; + margin-left: 20px; } #footer { - position:relative; - z-index:1; - text-align:center; - padding-bottom:60px + position: relative; + z-index: 1; + text-align: center; + padding-bottom: 60px; } #footer .buttons { - padding:20px 0 + padding: 20px 0; } #footer .buttons .btn { - background:#3b3d3b + background: #3b3d3b; } #footer p { - font-size:.85em + font-size: 0.85em; } #footer p a { - color:#ff7b00 + color: #ff7b00; } #footer p img { - padding:15px 0 + padding: 15px 0; } footer a { - color: #EBEBEB; + color: #ebebeb; font-size: 14px; text-decoration: none; } @@ -704,140 +748,144 @@ footer a { @media (max-width: 840px) { section article { - max-width:560px + max-width: 560px; } } @media (max-width: 780px) { #example-slider { - width:400px + width: 400px; } .example { - height:340px + height: 340px; } .example .game { - float:right + float: right; } - .example .game .blockrain-start-btn,.example .game .blockrain-game-over-btn { - left:50%; - bottom:auto; - top:50%; - margin-left:-60px; - margin-top:-20px + .example .game .blockrain-start-btn, + .example .game .blockrain-game-over-btn { + left: 50%; + bottom: auto; + top: 50%; + margin-left: -60px; + margin-top: -20px; } - .example .game .blockrain-start-msg,.example .game .blockrain-game-over-msg { - top:50%; - margin-top:-75px + .example .game .blockrain-start-msg, + .example .game .blockrain-game-over-msg { + top: 50%; + margin-top: -75px; } } @media (max-width: 640px) { - section header h1:before,section header h1:after { - display:none + section header h1:before, + section header h1:after { + display: none; } #example-slider { - width:240px; - height:460px + width: 240px; + height: 460px; } .example { - padding-top:100px + padding-top: 100px; } .example .game { - float:none + float: none; } .example .instructions { - display:none + display: none; } .example .game .blockrain-score-holder { - left:auto; - right:0; - margin-left:0; - width:100%; - top:-60px; - z-index:1; - text-align:right + left: auto; + right: 0; + margin-left: 0; + width: 100%; + top: -60px; + z-index: 1; + text-align: right; } } @media (max-width: 560px) { section article { - max-width:280px + max-width: 280px; } #example-slider .buttons { - position:relative; - width:60%; - margin:0 auto; - padding-top:40px; - overflow:hidden + position: relative; + width: 60%; + margin: 0 auto; + padding-top: 40px; + overflow: hidden; } #example-slider .buttons .btn { - top:0; - margin:0; - position:relative; - left:auto; - right:auto; - height:50px; - width:28px + top: 0; + margin: 0; + position: relative; + left: auto; + right: auto; + height: 50px; + width: 28px; } #example-slider .buttons .btn.btn-prev { - float:left + float: left; } #example-slider .buttons .btn.btn-next { - float:right + float: right; } } @media (max-width: 400px) { #hero .welcome { - margin-bottom:0; - padding:0 20px + margin-bottom: 0; + padding: 0 20px; } #hero .blockrain-score-holder { - display:none !important + display: none !important; } #footer { - padding:0 20px + padding: 0 20px; } #footer p { - padding:0 20px; - line-height:18px + padding: 0 20px; + line-height: 18px; } #footer .buttons { - padding:10px 0 0 0 + padding: 10px 0 0 0; } - #hero .btn,#footer .btn { - width:100%; - margin:0 0 20px; - box-sizing:border-box + #hero .btn, + #footer .btn { + width: 100%; + margin: 0 0 20px; + box-sizing: border-box; } #hero .game { - opacity:.07 + opacity: 0.07; } section header h1 { - font-size:1.7em + font-size: 1.7em; } section header p { - font-size:1em + font-size: 1em; } } diff --git a/error_pages/maintenance-mode.html b/error_pages/maintenance-mode.html index 26779d45b..7a61d3713 100755 --- a/error_pages/maintenance-mode.html +++ b/error_pages/maintenance-mode.html @@ -1,54 +1,63 @@ - - - + + + - - + + - + - Offline for maintenance - + Offline for maintenance + - -
-

Offline for maintenance

-

Please check back later. Or play some Tetris.

-
+ +
+

Offline for maintenance

+

Please check back later. Or play some Tetris.

+
-
-
-
-
- Use only arrows -
-
-
-
-
+
+
+
+
+ Use only arrows +
+
+
+
+
+
+
-
-
-
+ - -
+ + - - - - + + + + - - + + diff --git a/package.json b/package.json index de15f274f..8df0cc4ae 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "build": "npm run build --workspaces --if-present", "build:prod": "npm run build:prod --workspaces --if-present", "heroku-postbuild": "echo Build is taken care of in ./bin/post_compile", - "prettier": "prettier --write '**/translate/**/*.{js,ts,tsx,css}' '**/pontoon/**/*.{js,css}' '**/tag-admin/**/*.{js,css}'", - "check-prettier": "prettier --check '**/translate/**/*.{js,ts,tsx,css}' '**/pontoon/**/*.{js,css}' '**/tag-admin/**/*.{js,css}'", - "eslint": "eslint 'translate/**/*.{js,ts,tsx}' 'pontoon/**/*.js' 'tag-admin/**/*.js'" + "prettier": "prettier --write .", + "check-prettier": "prettier --check .", + "eslint": "eslint ." }, "workspaces": [ "translate", @@ -92,7 +92,7 @@ "prettier": { "endOfLine": "lf", "trailingComma": "all", - "tabWidth": 4, + "tabWidth": 2, "jsxSingleQuote": true, "singleQuote": true }, diff --git a/pontoon/administration/static/css/admin.css b/pontoon/administration/static/css/admin.css index 24eca81fd..9b01d0fe4 100644 --- a/pontoon/administration/static/css/admin.css +++ b/pontoon/administration/static/css/admin.css @@ -1,3 +1,3 @@ .add { - float: right; + float: right; } diff --git a/pontoon/administration/static/css/admin_project.css b/pontoon/administration/static/css/admin_project.css index 61206631a..4b6d98eaf 100644 --- a/pontoon/administration/static/css/admin_project.css +++ b/pontoon/administration/static/css/admin_project.css @@ -1,80 +1,80 @@ body { - overflow: auto; - position: relative; + overflow: auto; + position: relative; } .select { - padding-left: 0; - text-align: left; + padding-left: 0; + text-align: left; } select { - margin-top: 4px; + margin-top: 4px; } h1 { - color: #ebebeb; - font-size: 48px; - letter-spacing: -1px; - margin-bottom: 40px; - position: relative; + color: #ebebeb; + font-size: 48px; + letter-spacing: -1px; + margin-bottom: 40px; + position: relative; } h1 aside { - bottom: 8px; - position: absolute; - right: 0; + bottom: 8px; + position: absolute; + right: 0; } h1 a { - color: #7bc876; - font-size: 14px; - font-weight: 300; - letter-spacing: 0; - margin-left: 20px; + color: #7bc876; + font-size: 14px; + font-weight: 300; + letter-spacing: 0; + margin-left: 20px; } body > form { - color: #aaaaaa; - font-size: 14px; - padding: 40px 20px 100px; + color: #aaaaaa; + font-size: 14px; + padding: 40px 20px 100px; } form > div { - margin: 20px 0; - text-align: right; + margin: 20px 0; + text-align: right; } form > div.controls { - margin-top: 70px; + margin-top: 70px; } form > section > div { - margin-top: 20px; + margin-top: 20px; } form > div.inline.delete, form > section > div.inline.delete { - opacity: 0.3; + opacity: 0.3; } h3 { - font-size: 30px; - margin-top: 70px; + font-size: 30px; + margin-top: 70px; } h4 { - color: #ebebeb; - font-size: 18px; - font-style: italic; - font-weight: 300; - margin-top: 40px; + color: #ebebeb; + font-size: 18px; + font-style: italic; + font-weight: 300; + margin-top: 40px; } label { - display: block; - padding-bottom: 3px; - text-align: left; + display: block; + padding-bottom: 3px; + text-align: left; } input[type='text'], @@ -82,477 +82,477 @@ input[type='password'], input[type='url'], input[type='number'], textarea { - display: block; - height: 24px; - margin-left: -1px; - width: 975px; + display: block; + height: 24px; + margin-left: -1px; + width: 975px; } .half label { - float: left; - width: 490px; + float: left; + width: 490px; } .half input { - margin-right: 5px; - width: 482px; + margin-right: 5px; + width: 482px; } .half input:last-child { - margin-right: 0; - width: 483px; + margin-right: 0; + width: 483px; } .half .for-name .errorlist { - float: left; + float: left; } .half .for-slug .errorlist { - float: right; + float: right; } .half .errorlist li { - margin: 0; + margin: 0; } form .button { - background: #888888; - border: none; - display: block; - float: right; - font-size: 14px; - height: 31px; - width: 31px; + background: #888888; + border: none; + display: block; + float: right; + font-size: 14px; + height: 31px; + width: 31px; } .button.delete-inline { - color: #333333; - font-size: 20px; - padding: 4px; + color: #333333; + font-size: 20px; + padding: 4px; } #admin-strings-form .controls .button, #admin-form .controls .button { - border-radius: 3px; - font-weight: 400; - text-transform: uppercase; - width: 150px; + border-radius: 3px; + font-weight: 400; + text-transform: uppercase; + width: 150px; } #admin-strings-form .controls .add-inline { - float: left; - margin-left: 0; + float: left; + margin-left: 0; } #admin-strings-form .strings-list .entity textarea:nth-child(3) { - margin-bottom: 20px; + margin-bottom: 20px; } .controls .button.sync, .controls .button.sync:hover { - background: #333941; - color: #aaa; - float: left; + background: #333941; + color: #aaa; + float: left; } .controls .button.pretranslate, .controls .button.pretranslate:hover { - background: #333941; - color: #aaa; - float: right; + background: #333941; + color: #aaa; + float: right; } form a:link, form a:visited { - color: #7bc876; - float: right; - text-transform: uppercase; + color: #7bc876; + float: right; + text-transform: uppercase; } #admin-form a.add-inline, #admin-form a.add-repo { - font-size: 14px; - font-style: normal; - letter-spacing: 0; - margin-top: 17px; + font-size: 14px; + font-style: normal; + letter-spacing: 0; + margin-top: 17px; } .controls a { - margin: 5px 9px; + margin: 5px 9px; } .controls .checkbox { - float: left; - margin: -1px 20px 0 0; - text-transform: uppercase; + float: left; + margin: -1px 20px 0 0; + text-transform: uppercase; } .double-list-selector .locale.select .menu { - border-bottom: 1px solid #5e6475; - margin: 2px 0 -4px -1px; - padding: 10px 0; - width: 295px; + border-bottom: 1px solid #5e6475; + margin: 2px 0 -4px -1px; + padding: 10px 0; + width: 295px; } .double-list-selector .locale.select .menu ul li { - padding-left: 20px; - padding-right: 20px; - position: relative; - width: 100%; + padding-left: 20px; + padding-right: 20px; + position: relative; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .double-list-selector .locale.select.selected label { - text-align: center; + text-align: center; } .double-list-selector .locale.select.selected label .left { - float: left; + float: left; } .double-list-selector .locale.select.readonly label { - text-align: right; + text-align: right; } .double-list-selector .locale.select.readonly label a { - float: left; + float: left; } .locale.select { - float: left; - width: auto; + float: left; + width: auto; } form .locale.select.selected { - float: none; - margin-right: 49px; + float: none; + margin-right: 49px; } .locale.select.readonly { - float: right; + float: right; } .locale.select .menu ul { - height: 170px; - margin-bottom: 0; + height: 170px; + margin-bottom: 0; } .locale.select .menu ul li span.code { - float: right; - width: auto; + float: right; + width: auto; } #id_locales, #id_locales_readonly { - display: none; + display: none; } .can-be-requested { - float: left; - width: 295px; + float: left; + width: 295px; } .copy-locales { - float: left; - margin-left: 48px; - width: 295px; + float: left; + margin-left: 48px; + width: 295px; } .deadline { - float: left; - width: 150px; + float: left; + width: 150px; } .priority { - float: left; - margin-left: 10px; - width: 165px; + float: left; + margin-left: 10px; + width: 165px; } .contact { - float: right; - width: 300px; + float: right; + width: 300px; } .copy-locales select, .priority select, .contact select { - width: 100%; + width: 100%; } .deadline input { - width: 144px; + width: 144px; } .checkbox label { - float: left; - padding: 6px 0 0 15px; - text-transform: uppercase; + float: left; + padding: 6px 0 0 15px; + text-transform: uppercase; } .data-source, .new-strings, .visibility { - position: relative; + position: relative; } .data-source #id_data_source, .new-strings .manage-strings, .visibility #id_visibility { - position: absolute; - right: 0; + position: absolute; + right: 0; } .data-source #id_data_source, .visibility #id_visibility { - top: 5px; + top: 5px; } .new-strings .manage-strings { - top: -3px; + top: -3px; } .repository .delete-wrapper label { - display: inline; + display: inline; } .repository .delete-wrapper input[type='checkbox'] { - float: right; - margin: 2px; + float: right; + margin: 2px; } .checkbox label input { - float: left; - margin-left: -18px; - margin-top: 1px; + float: left; + margin-left: -18px; + margin-top: 1px; } .repository .type-wrapper { - float: left; - margin-right: 6px; + float: left; + margin-right: 6px; } .repository .details-wrapper { - float: left; - width: 552px; + float: left; + width: 552px; } .repository.git .details-wrapper { - width: 768px; + width: 768px; } .repository .details-wrapper input { - width: 908px; + width: 908px; } .repository.git .details-wrapper input { - width: 763px; + width: 763px; } .repository .branch-wrapper { - display: none; - float: left; - margin-left: 6px; - width: 138px; + display: none; + float: left; + margin-left: 6px; + width: 138px; } .repository.git .branch-wrapper { - display: block; + display: block; } .repository .website-wrapper, .repository .prefix-wrapper, .repository .repository-toolbar { - margin-top: 20px; + margin-top: 20px; } .repository .repository-toolbar > section { - display: inline-block; - float: left; + display: inline-block; + float: left; } .repository .repository-toolbar > section.delete-wrapper { - float: right; + float: right; } .inline input[id*='url'] { - float: left; - width: 607px; + float: left; + width: 607px; } .branch-wrapper input[type='text'] { - width: 134px; + width: 134px; } .edit section .bottom { - font-size: 12px; - text-transform: uppercase; + font-size: 12px; + text-transform: uppercase; } .inline[data-count], .entity[data-count], .repository-empty { - display: none; + display: none; } .inline input[id*='name']::-webkit-calendar-picker-indicator { - display: none; /* remove default arrow */ + display: none; /* remove default arrow */ } input#id_configuration_file { - margin-bottom: 5px; + margin-bottom: 5px; } label[for='id_configuration_file'] a { - float: none; - text-transform: none; + float: none; + text-transform: none; } .externalresource .arrow::after { - content: '▾'; - color: #000; - float: left; - margin: 6px 0 0 -20px; - pointer-events: none; + content: '▾'; + color: #000; + float: left; + margin: 6px 0 0 -20px; + pointer-events: none; } .inline label[for*='name'] { - float: left; - width: 325px; + float: left; + width: 325px; } .inline input[id*='name'] { - float: left; - width: 321px; + float: left; + width: 321px; } .inline input[id$='DELETE'], .inline input[id$='obsolete'] { - display: none; + display: none; } .inline label[for*='url'] { - float: left; - width: 649px; + float: left; + width: 649px; } .inline input[id*='name'], .inline label[for*='name'] { - margin-right: 6px; + margin-right: 6px; } textarea { - height: 60px; + height: 60px; } textarea.strings-source { - height: 300px; + height: 300px; } .subtitle { - font-size: 12px; - margin-top: 4px; - text-transform: uppercase; + font-size: 12px; + margin-top: 4px; + text-transform: uppercase; } .errorlist { - color: #f36; - display: inline-block; - font-size: 12px; - line-height: 14px; /* Strange, but needed to keep controls in line when error occures */ - list-style: none; - margin-left: 0; - margin-top: 2px; - text-transform: uppercase; + color: #f36; + display: inline-block; + font-size: 12px; + line-height: 14px; /* Strange, but needed to keep controls in line when error occures */ + list-style: none; + margin-left: 0; + margin-top: 2px; + text-transform: uppercase; } .errorlist li { - display: inline; - margin-left: 5px; + display: inline; + margin-left: 5px; } .locales .errorlist { - margin-top: 10px; - text-align: center; - width: 100%; + margin-top: 10px; + text-align: center; + width: 100%; } .notification { - display: block; - margin-left: auto; - margin-right: auto; + display: block; + margin-left: auto; + margin-right: auto; } .new-strings { - display: none; + display: none; } .manage-strings a { - margin-left: 20px; + margin-left: 20px; } /* Code specific to the manage strings page */ .strings-list .entity { - margin-bottom: 30px; + margin-bottom: 30px; } /* Code specific to the manage tags widget */ .tag.inline label[for*='name'] { - float: left; - width: 330px; + float: left; + width: 330px; } .tag.inline input[id*='name'] { - float: left; - width: 324px; - margin-right: 6px; + float: left; + width: 324px; + margin-right: 6px; } .tag.inline label[for*='slug'] { - float: left; - width: 331px; + float: left; + width: 331px; } .tag.inline input[id*='slug'] { - float: left; - width: 321px; - margin-right: 6px; + float: left; + width: 321px; + margin-right: 6px; } .tag.inline label[for*='priority'] { - float: left; - width: 311px; + float: left; + width: 311px; } .tag.inline input[id*='priority'] { - float: left; - width: 270px; - margin-right: 6px; + float: left; + width: 270px; + margin-right: 6px; } .tag.inline .form-errors { - clear: both; + clear: both; } .tag.inline .form-errors .name-errors { - float: left; - width: 324px; - margin-right: 5px; - min-height: 1px; + float: left; + width: 324px; + margin-right: 5px; + min-height: 1px; } .tag.inline .form-errors .slug-errors { - float: left; - width: 321px; + float: left; + width: 321px; } .tag.inline .form-errors .priority-errors { - float: left; - width: 311px; + float: left; + width: 311px; } .tag.inline select[name^='tag_set-'][name$='-priority'] { - width: 276px; - margin-top: 0; + width: 276px; + margin-top: 0; } diff --git a/pontoon/administration/static/js/admin_project.js b/pontoon/administration/static/js/admin_project.js index a0e75358f..7a469d454 100644 --- a/pontoon/administration/static/js/admin_project.js +++ b/pontoon/administration/static/js/admin_project.js @@ -1,283 +1,283 @@ $(function () { - // Before submitting the form - $('#admin-form').submit(function () { - // Update locales - var locales = [ - { - list: 'selected', - input: $('#id_locales'), - }, - { - list: 'readonly', - input: $('#id_locales_readonly'), - }, - ]; + // Before submitting the form + $('#admin-form').submit(function () { + // Update locales + var locales = [ + { + list: 'selected', + input: $('#id_locales'), + }, + { + list: 'readonly', + input: $('#id_locales_readonly'), + }, + ]; - locales.forEach(function (type) { - var ids = $('.admin-team-selector .locale.' + type.list) - .find('li[data-id]') - .map(function () { - return $(this).data('id'); - }) - .get(); - type.input.val(ids); - }); - - // Update form action - var slug = $('#id_slug').val(); - if (slug.length > 0) { - slug += '/'; - } - $('#admin-form').attr( - 'action', - $('#admin-form').attr('action').split('/projects/')[0] + - '/projects/' + - slug, - ); - }); - - // Submit form with Enter (keyCode === 13) - $('html') - .unbind('keydown.pontoon') - .bind('keydown.pontoon', function (e) { - if ( - $('input[type=text]:focus').length > 0 || - $('input[type=url]:focus').length > 0 - ) { - var key = e.keyCode || e.which; - if (key === 13) { - // A short delay to allow digest of autocomplete before submit - setTimeout(function () { - $('#admin-form').submit(); - }, 1); - return false; - } - } - }); - - // Submit form with button - $('.save').click(function (e) { - e.preventDefault(); - $('#admin-form').submit(); - }); - - // Manually Sync project - $('.sync').click(function (e) { - e.preventDefault(); - - var button = $(this), - title = button.html(); - - if (button.is('.in-progress')) { - return; - } - - button.addClass('in-progress').html('Syncing...'); - - $.ajax({ - url: '/admin/projects/' + $('#id_slug').val() + '/sync/', + locales.forEach(function (type) { + var ids = $('.admin-team-selector .locale.' + type.list) + .find('li[data-id]') + .map(function () { + return $(this).data('id'); }) - .success(function () { - button.html('Started'); - }) - .error(function () { - button.html('Whoops!'); - }) - .complete(function () { - setTimeout(function () { - button.removeClass('in-progress').html(title); - }, 2000); - }); + .get(); + type.input.val(ids); }); - // Manually Pretranslate project - $('.pretranslate').click(function (e) { - e.preventDefault(); - - var button = $(this), - title = button.html(); - - if (button.is('.in-progress')) { - return; - } - - button.addClass('in-progress').html('Pretranslating...'); - - $.ajax({ - url: '/admin/projects/' + $('#id_slug').val() + '/pretranslate/', - }) - .success(function () { - button.html('Started'); - }) - .error(function () { - button.html('Whoops!'); - }) - .complete(function () { - setTimeout(function () { - button.removeClass('in-progress').html(title); - }, 2000); - }); - }); - - // Suggest slugified name for new projects - $('#id_name').blur(function () { - if ($('input[name=pk]').length > 0 || !$('#id_name').val()) { - return; - } - $('#id_slug').attr('placeholder', 'Retrieving...'); - $.ajax({ - url: '/admin/get-slug/', - data: { - name: $('#id_name').val(), - }, - success: function (data) { - var value = data === 'error' ? '' : data; - $('#id_slug').val(value); - }, - error: function () { - $('#id_slug').attr('placeholder', ''); - }, - }); - }); - - $('body').on('blur', '[id^=id_tag_set-][id$=-name]', function () { - var target = $('input#' + $(this).attr('id').replace('-name', '-slug')); - var $this = this; - if (target.val() || !$(this).val()) { - return; - } - target.attr('placeholder', 'Retrieving...'); - $.ajax({ - url: '/admin/get-slug/', - data: { - name: $($this).val(), - }, - success: function (data) { - var value = data === 'error' ? '' : data; - target.val(value); - target.attr('placeholder', ''); - }, - error: function () { - target.attr('placeholder', ''); - }, - }); - }); - - // Copy locales from another project - $('#copy-locales option').on('click', function () { - var projectLocales = []; - - try { - projectLocales = JSON.parse($(this).val()); - } catch (error) { - // No project selected - return; - } - - $('.readonly .move-all').click(); - $('.selected .move-all.left').click(); - - $(projectLocales).each(function (i, id) { - $('.locale.select:first') - .find('[data-id=' + id + ']') - .click(); - }); - }); - - // Show new strings input or link when source type is "database". - function displayNewStringsInput(input) { - if (input.val() === 'database') { - $('.new-strings').show(); - $('.manage-strings').show(); - - // For now, we also hide the entire Repositories section. We might - // want to revisit that behavior later. - $('.repositories').hide(); - } else { - $('.new-strings').hide(); - $('.manage-strings').hide(); - $('.repositories').show(); - } + // Update form action + var slug = $('#id_slug').val(); + if (slug.length > 0) { + slug += '/'; } - var dataSourceInput = $('#id_data_source'); - dataSourceInput.on('change', function () { - displayNewStringsInput(dataSourceInput); + $('#admin-form').attr( + 'action', + $('#admin-form').attr('action').split('/projects/')[0] + + '/projects/' + + slug, + ); + }); + + // Submit form with Enter (keyCode === 13) + $('html') + .unbind('keydown.pontoon') + .bind('keydown.pontoon', function (e) { + if ( + $('input[type=text]:focus').length > 0 || + $('input[type=url]:focus').length > 0 + ) { + var key = e.keyCode || e.which; + if (key === 13) { + // A short delay to allow digest of autocomplete before submit + setTimeout(function () { + $('#admin-form').submit(); + }, 1); + return false; + } + } }); + + // Submit form with button + $('.save').click(function (e) { + e.preventDefault(); + $('#admin-form').submit(); + }); + + // Manually Sync project + $('.sync').click(function (e) { + e.preventDefault(); + + var button = $(this), + title = button.html(); + + if (button.is('.in-progress')) { + return; + } + + button.addClass('in-progress').html('Syncing...'); + + $.ajax({ + url: '/admin/projects/' + $('#id_slug').val() + '/sync/', + }) + .success(function () { + button.html('Started'); + }) + .error(function () { + button.html('Whoops!'); + }) + .complete(function () { + setTimeout(function () { + button.removeClass('in-progress').html(title); + }, 2000); + }); + }); + + // Manually Pretranslate project + $('.pretranslate').click(function (e) { + e.preventDefault(); + + var button = $(this), + title = button.html(); + + if (button.is('.in-progress')) { + return; + } + + button.addClass('in-progress').html('Pretranslating...'); + + $.ajax({ + url: '/admin/projects/' + $('#id_slug').val() + '/pretranslate/', + }) + .success(function () { + button.html('Started'); + }) + .error(function () { + button.html('Whoops!'); + }) + .complete(function () { + setTimeout(function () { + button.removeClass('in-progress').html(title); + }, 2000); + }); + }); + + // Suggest slugified name for new projects + $('#id_name').blur(function () { + if ($('input[name=pk]').length > 0 || !$('#id_name').val()) { + return; + } + $('#id_slug').attr('placeholder', 'Retrieving...'); + $.ajax({ + url: '/admin/get-slug/', + data: { + name: $('#id_name').val(), + }, + success: function (data) { + var value = data === 'error' ? '' : data; + $('#id_slug').val(value); + }, + error: function () { + $('#id_slug').attr('placeholder', ''); + }, + }); + }); + + $('body').on('blur', '[id^=id_tag_set-][id$=-name]', function () { + var target = $('input#' + $(this).attr('id').replace('-name', '-slug')); + var $this = this; + if (target.val() || !$(this).val()) { + return; + } + target.attr('placeholder', 'Retrieving...'); + $.ajax({ + url: '/admin/get-slug/', + data: { + name: $($this).val(), + }, + success: function (data) { + var value = data === 'error' ? '' : data; + target.val(value); + target.attr('placeholder', ''); + }, + error: function () { + target.attr('placeholder', ''); + }, + }); + }); + + // Copy locales from another project + $('#copy-locales option').on('click', function () { + var projectLocales = []; + + try { + projectLocales = JSON.parse($(this).val()); + } catch (error) { + // No project selected + return; + } + + $('.readonly .move-all').click(); + $('.selected .move-all.left').click(); + + $(projectLocales).each(function (i, id) { + $('.locale.select:first') + .find('[data-id=' + id + ']') + .click(); + }); + }); + + // Show new strings input or link when source type is "database". + function displayNewStringsInput(input) { + if (input.val() === 'database') { + $('.new-strings').show(); + $('.manage-strings').show(); + + // For now, we also hide the entire Repositories section. We might + // want to revisit that behavior later. + $('.repositories').hide(); + } else { + $('.new-strings').hide(); + $('.manage-strings').hide(); + $('.repositories').show(); + } + } + var dataSourceInput = $('#id_data_source'); + dataSourceInput.on('change', function () { displayNewStringsInput(dataSourceInput); + }); + displayNewStringsInput(dataSourceInput); - // Suggest public repository website URL - $('body').on('blur', '.repo input', function () { - var val = $(this) - .val() - .replace(/\.git$/, '') - .replace('git@github.com:', 'https://github.com/') - .replace('ssh://', 'https://'); + // Suggest public repository website URL + $('body').on('blur', '.repo input', function () { + var val = $(this) + .val() + .replace(/\.git$/, '') + .replace('git@github.com:', 'https://github.com/') + .replace('ssh://', 'https://'); - $(this).parents('.repository').find('.website-wrapper input').val(val); - }); + $(this).parents('.repository').find('.website-wrapper input').val(val); + }); - // Delete inline form item (e.g. subpage or external resource) - $('body').on('click.pontoon', '.delete-inline', function (e) { - e.preventDefault(); - $(this).parent().toggleClass('delete'); - $(this).next().prop('checked', !$(this).next().prop('checked')); - }); - $('.inline [checked]').click().prev().click(); + // Delete inline form item (e.g. subpage or external resource) + $('body').on('click.pontoon', '.delete-inline', function (e) { + e.preventDefault(); + $(this).parent().toggleClass('delete'); + $(this).next().prop('checked', !$(this).next().prop('checked')); + }); + $('.inline [checked]').click().prev().click(); - // Add inline form item (e.g. subpage or external resource) - var count = { - subpage: $('.subpage:last').data('count'), - externalresource: $('.externalresource:last').data('count'), - entity: $('.entity:last').data('count'), - tag: $('.tag:last').data('count'), - }; - $('.add-inline').click(function (e) { - e.preventDefault(); + // Add inline form item (e.g. subpage or external resource) + var count = { + subpage: $('.subpage:last').data('count'), + externalresource: $('.externalresource:last').data('count'), + entity: $('.entity:last').data('count'), + tag: $('.tag:last').data('count'), + }; + $('.add-inline').click(function (e) { + e.preventDefault(); - var type = $(this).data('type'); - var form = $('.' + type + ':last') - .html() - .replace(/__prefix__/g, count[type]); + var type = $(this).data('type'); + var form = $('.' + type + ':last') + .html() + .replace(/__prefix__/g, count[type]); - $('.' + type + ':last').before( - '
' + form + '
', - ); - count[type]++; + $('.' + type + ':last').before( + '
' + form + '
', + ); + count[type]++; - // These two forms of selectors cover all the cases for django-generated forms we use. - $('#id_' + type + '_set-TOTAL_FORMS').val(count[type]); - $('#id_form-TOTAL_FORMS').val(count[type]); - }); + // These two forms of selectors cover all the cases for django-generated forms we use. + $('#id_' + type + '_set-TOTAL_FORMS').val(count[type]); + $('#id_form-TOTAL_FORMS').val(count[type]); + }); - // Toggle branch input - function toggleBranchInput(element) { - $(element) - .parents('.repository') - .toggleClass('git', $(element).val() === 'git'); - } - // On select change - $('body').on('change', '.repository .type-wrapper select', function () { - toggleBranchInput(this); - }); - // On page load - $('.repository .type-wrapper select').each(function () { - toggleBranchInput(this); - }); + // Toggle branch input + function toggleBranchInput(element) { + $(element) + .parents('.repository') + .toggleClass('git', $(element).val() === 'git'); + } + // On select change + $('body').on('change', '.repository .type-wrapper select', function () { + toggleBranchInput(this); + }); + // On page load + $('.repository .type-wrapper select').each(function () { + toggleBranchInput(this); + }); - // Add repo - var $totalForms = $('#id_repositories-TOTAL_FORMS'); - $('.add-repo').click(function (e) { - e.preventDefault(); - var count = parseInt($totalForms.val(), 10); + // Add repo + var $totalForms = $('#id_repositories-TOTAL_FORMS'); + $('.add-repo').click(function (e) { + e.preventDefault(); + var count = parseInt($totalForms.val(), 10); - var $emptyForm = $('.repository-empty'); - var form = $emptyForm.html().replace(/__prefix__/g, count); - $('.repository:last').after( - '
' + form + '
', - ); + var $emptyForm = $('.repository-empty'); + var form = $emptyForm.html().replace(/__prefix__/g, count); + $('.repository:last').after( + '
' + form + '
', + ); - toggleBranchInput($('.repository:last').find('.type-wrapper select')); + toggleBranchInput($('.repository:last').find('.type-wrapper select')); - $totalForms.val(count + 1); - }); + $totalForms.val(count + 1); + }); }); diff --git a/pontoon/api/README.md b/pontoon/api/README.md index 86a8cf842..e39bb6ba8 100644 --- a/pontoon/api/README.md +++ b/pontoon/api/README.md @@ -1,12 +1,11 @@ # GraphQL API Pontoon exposes some of its data via a public API endpoint. The API is -[GraphQL](http://graphql.org/)-based and available at ``/graphql``. - +[GraphQL](http://graphql.org/)-based and available at `/graphql`. ## Production Deployments -When run in production (``DEV is False``) the API returns ``application/json`` +When run in production (`DEV is False`) the API returns `application/json` responses to GET and POST requests. In case of GET requests, any whitespace in the query must be escaped. @@ -22,14 +21,13 @@ An example POST requests may look like this: $ curl -X POST -d "query={ projects { name } }" https://example.com/graphql ``` - ## Local Development -In a local development setup (``DEV is True``) the endpoint has two modes of +In a local development setup (`DEV is True`) the endpoint has two modes of operation: a JSON one and an HTML one. -When a request is sent, without any headers, with ``Accept: application/json`` or -if it explicitly contains a ``raw`` query argument, the endpoint will behave like +When a request is sent, without any headers, with `Accept: application/json` or +if it explicitly contains a `raw` query argument, the endpoint will behave like a production one, returning JSON responses. The following query in the CLI will return a JSON response: @@ -38,22 +36,21 @@ The following query in the CLI will return a JSON response: $ curl --globoff http://localhost:8000/graphql?query={projects{name}} ``` -If however a request is sent with ``Accept: text/html`` such as is the case when +If however a request is sent with `Accept: text/html` such as is the case when accessing the endpoint in a browser, a GUI query editor and explorer, [GraphiQL](https://github.com/graphql/graphiql), will be served:: http://localhost:8000/graphql?query={projects{name}} -To preview the JSON response in the browser, pass in the ``raw`` query argument:: +To preview the JSON response in the browser, pass in the `raw` query argument:: http://localhost:8000/graphql?query={projects{name}}&raw - ## Query IDE The [GraphiQL](https://github.com/graphql/graphiql) query IDE is available at -``http://localhost:8000/graphql`` when running Pontoon locally and the URL is -accessed with the ``Accept: text/html`` header, e.g. using a browser. +`http://localhost:8000/graphql` when running Pontoon locally and the URL is +accessed with the `Accept: text/html` header, e.g. using a browser. It offers a query editor with: @@ -62,4 +59,4 @@ It offers a query editor with: - real-time error reporting, - results folding, - autogenerated docs on shapes and their fields, -- [introspection](http://docs.graphene-python.org/projects/django/en/latest/debug/) via the ``__debug``. +- [introspection](http://docs.graphene-python.org/projects/django/en/latest/debug/) via the `__debug`. diff --git a/pontoon/base/static/css/double_list_selector.css b/pontoon/base/static/css/double_list_selector.css index f2ccf2175..254b4249b 100644 --- a/pontoon/base/static/css/double_list_selector.css +++ b/pontoon/base/static/css/double_list_selector.css @@ -1,25 +1,25 @@ .double-list-selector .select .menu ul li .arrow { - display: none; - position: absolute; - top: 4px; - right: 2px; + display: none; + position: absolute; + top: 4px; + right: 2px; } .double-list-selector .select .menu ul li.hover .arrow { - display: inline-block; + display: inline-block; } .double-list-selector .select:first-child .menu ul li .arrow:after, .double-list-selector .select:nth-child(2) .menu ul li .arrow:after { - content: ''; + content: ''; } .double-list-selector .select:nth-child(2) .menu ul li.left .arrow:after, .double-list-selector .select:last-child .menu ul li .arrow:after { - content: ''; + content: ''; } .double-list-selector .select:nth-child(2) .menu ul li.left .arrow, .double-list-selector .select:last-child .menu ul li .arrow { - left: 2px; + left: 2px; } diff --git a/pontoon/base/static/css/download_selector.css b/pontoon/base/static/css/download_selector.css index 22e2f2755..afb823964 100644 --- a/pontoon/base/static/css/download_selector.css +++ b/pontoon/base/static/css/download_selector.css @@ -1,48 +1,48 @@ .download-selector { - float: right; + float: right; } .download-selector .selector { - padding-right: 0; - padding-top: 0; + padding-right: 0; + padding-top: 0; } .download-selector .selector .fa { - float: right; - background: #3f4752; - border-radius: 3px; - box-sizing: border-box; - color: #aaa; - font-size: 16px; - height: 32px; - width: 32px; - padding-top: 8px; - text-align: center; + float: right; + background: #3f4752; + border-radius: 3px; + box-sizing: border-box; + color: #aaa; + font-size: 16px; + height: 32px; + width: 32px; + padding-top: 8px; + text-align: center; } .download-selector .selector .fa:hover { - background: #272a2f; + background: #272a2f; } .download-selector.opened .selector .fa { - background: #272a2f; - border-radius: 2px 2px 0 0; + background: #272a2f; + border-radius: 2px 2px 0 0; } .download-selector.opened .menu { - display: block; - top: 32px; - right: 0; - bottom: auto; - width: 220px; + display: block; + top: 32px; + right: 0; + bottom: auto; + width: 220px; } .download-selector.opened .menu li { - padding-bottom: 0; - padding-top: 0; + padding-bottom: 0; + padding-top: 0; } .download-selector.opened .menu li a { - display: block; - line-height: 22px; + display: block; + line-height: 22px; } diff --git a/pontoon/base/static/css/fonts.css b/pontoon/base/static/css/fonts.css index c5c2d288b..45255fe08 100644 --- a/pontoon/base/static/css/fonts.css +++ b/pontoon/base/static/css/fonts.css @@ -1,46 +1,46 @@ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 400; - src: url('../fonts/OpenSans-Regular.eot'); - src: url('../fonts/OpenSans-Regular.eot') format('embedded-opentype'), - url('../fonts/OpenSans-Regular.woff') format('woff'), - url('../fonts/OpenSans-Regular.ttf') format('truetype'); + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: url('../fonts/OpenSans-Regular.eot'); + src: url('../fonts/OpenSans-Regular.eot') format('embedded-opentype'), + url('../fonts/OpenSans-Regular.woff') format('woff'), + url('../fonts/OpenSans-Regular.ttf') format('truetype'); } @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 300; - src: url('../fonts/OpenSans-Light.eot'); - src: url('../fonts/OpenSans-Light.eot') format('embedded-opentype'), - url('../fonts/OpenSans-Light.woff') format('woff'), - url('../fonts/OpenSans-Light.ttf') format('truetype'); + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: url('../fonts/OpenSans-Light.eot'); + src: url('../fonts/OpenSans-Light.eot') format('embedded-opentype'), + url('../fonts/OpenSans-Light.woff') format('woff'), + url('../fonts/OpenSans-Light.ttf') format('truetype'); } @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: url('../fonts/OpenSans-Bold.eot'); - src: url('../fonts/OpenSans-Bold.eot') format('embedded-opentype'), - url('../fonts/OpenSans-Bold.woff') format('woff'), - url('../fonts/OpenSans-Bold.ttf') format('truetype'); + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: url('../fonts/OpenSans-Bold.eot'); + src: url('../fonts/OpenSans-Bold.eot') format('embedded-opentype'), + url('../fonts/OpenSans-Bold.woff') format('woff'), + url('../fonts/OpenSans-Bold.ttf') format('truetype'); } @font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: url('../fonts/OpenSans-Italic.eot'); - src: url('../fonts/OpenSans-Italic.eot') format('embedded-opentype'), - url('../fonts/OpenSans-Italic.woff') format('woff'), - url('../fonts/OpenSans-Italic.ttf') format('truetype'); + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: url('../fonts/OpenSans-Italic.eot'); + src: url('../fonts/OpenSans-Italic.eot') format('embedded-opentype'), + url('../fonts/OpenSans-Italic.woff') format('woff'), + url('../fonts/OpenSans-Italic.ttf') format('truetype'); } @font-face { - font-family: 'Ubuntu Regular'; - font-style: italic; - font-weight: 400; - src: url('../fonts/Ubuntu-RI.ttf'); + font-family: 'Ubuntu Regular'; + font-style: italic; + font-weight: 400; + src: url('../fonts/Ubuntu-RI.ttf'); } diff --git a/pontoon/base/static/css/heading_info.css b/pontoon/base/static/css/heading_info.css index de29b36f4..b4696e73e 100644 --- a/pontoon/base/static/css/heading_info.css +++ b/pontoon/base/static/css/heading_info.css @@ -1,192 +1,192 @@ ul { - list-style: none; - margin: 0; + list-style: none; + margin: 0; } #heading:not(.simple) { - padding: 30px 0; + padding: 30px 0; } #heading:not(.simple) h1 { - padding-bottom: 30px; + padding-bottom: 30px; } #heading h1 a { - color: #ebebeb; - font-weight: bold; - font-size: 32px; + color: #ebebeb; + font-weight: bold; + font-size: 32px; } #heading h1 .small { - color: #7bc876; - font-weight: 300; - padding-left: 5px; + color: #7bc876; + font-weight: 300; + padding-left: 5px; } #heading .details, #heading .legend { - font-weight: 300; + font-weight: 300; } #heading .details li, #heading .legend li { - line-height: 24px; + line-height: 24px; } #heading .details { - float: left; - width: 360px; + float: left; + width: 360px; } #heading .details .title { - color: #aaaaaa; - padding-right: 5px; - position: relative; - text-transform: uppercase; + color: #aaaaaa; + padding-right: 5px; + position: relative; + text-transform: uppercase; } #heading .details .title sup { - position: absolute; - top: -5px; + position: absolute; + top: -5px; } #heading .details .value { - color: #aaa; - float: right; + color: #aaa; + float: right; } #heading .details .value a { - color: #ffffff; + color: #ffffff; } #heading .details .value a:hover { - color: #7bc876; + color: #7bc876; } #heading .details .priority .value { - margin-top: 6px; + margin-top: 6px; } #heading .details .value.overflow { - max-width: 275px; - overflow: hidden; - padding-left: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */ - padding-right: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */ - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; + max-width: 275px; + overflow: hidden; + padding-left: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */ + padding-right: 1px; /* Needed to avoid cutting off overlay due to overflow: hidden; */ + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; } #heading .details .resources .value.overflow { - max-width: 280px; - white-space: normal; + max-width: 280px; + white-space: normal; } #heading .progress { - float: left; - margin: -10px 65px 0; - position: relative; - text-align: center; + float: left; + margin: -10px 65px 0; + position: relative; + text-align: center; } #heading .progress .number { - display: none; - font-size: 50px; - font-weight: bold; - left: 0; - right: 0; - top: 25px; - margin: 0; - position: absolute; + display: none; + font-size: 50px; + font-weight: bold; + left: 0; + right: 0; + top: 25px; + margin: 0; + position: absolute; } #heading .progress .number:after { - content: '%'; - color: #888888; - display: block; - font-size: 20px; - font-weight: 100; - line-height: 10px; + content: '%'; + color: #888888; + display: block; + font-size: 20px; + font-weight: 100; + line-height: 10px; } #heading .legend { - float: left; - width: 190px; + float: left; + width: 190px; } #heading .legend li { - color: #aaaaaa; - padding-left: 25px; - position: relative; - text-align: left; - text-transform: uppercase; + color: #aaaaaa; + padding-left: 25px; + position: relative; + text-align: left; + text-transform: uppercase; } #heading .legend li a { - color: #ffffff; + color: #ffffff; } #heading .legend li:hover a { - color: #7bc876; + color: #7bc876; } #heading .legend li .status.fa { - left: 0; - top: 4px; + left: 0; + top: 4px; } #heading .legend li span.value { - color: #aaa; - float: right; + color: #aaa; + float: right; } #heading .legend li a span.value { - color: #ffffff; + color: #ffffff; } #heading .legend li:hover a span.value { - color: #7bc876; + color: #7bc876; } #heading .non-plottable { - float: right; + float: right; } #heading .non-plottable p { - color: #aaa; - font-weight: 300; - line-height: 24px; - text-align: right; - text-transform: uppercase; + color: #aaa; + font-weight: 300; + line-height: 24px; + text-align: right; + text-transform: uppercase; } #heading .non-plottable a p { - color: #ffffff; + color: #ffffff; } #heading .non-plottable a:hover p { - color: #7bc876; + color: #7bc876; } #heading .non-plottable p.value { - font-size: 20px; + font-size: 20px; } #heading .non-plottable .all { - padding-bottom: 21px; + padding-bottom: 21px; } #heading .non-plottable .unreviewed .status.fa { - left: auto; - position: relative; - top: -1px; + left: auto; + position: relative; + top: -1px; } #heading .non-plottable .unreviewed .status.fa:before { - color: #4d5967; - font-size: 18px; + color: #4d5967; + font-size: 18px; } #heading .non-plottable .unreviewed.pending .status.fa:before { - color: #4fc4f6; + color: #4fc4f6; } diff --git a/pontoon/base/static/css/pontoon.css b/pontoon/base/static/css/pontoon.css index 99664705f..233916a13 100755 --- a/pontoon/base/static/css/pontoon.css +++ b/pontoon/base/static/css/pontoon.css @@ -1,43 +1,43 @@ .pontoon-hovered { - outline: 1px dashed !important; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; + outline: 1px dashed !important; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; } .pontoon-editable-toolbar { - background-color: #ebebeb; - position: absolute; - top: 0; - left: 0; - z-index: 999999999; - display: none; - border-top: 1px dashed #000000; - border-left: 1px dashed #000000; - border-right: 1px dashed #000000; + background-color: #ebebeb; + position: absolute; + top: 0; + left: 0; + z-index: 999999999; + display: none; + border-top: 1px dashed #000000; + border-left: 1px dashed #000000; + border-right: 1px dashed #000000; } .pontoon-editable-toolbar.bottom { - border-top: none; - border-bottom: 1px dashed #000000; + border-top: none; + border-bottom: 1px dashed #000000; } .pontoon-editable-toolbar a { - background: transparent none 0 0 no-repeat; - display: block; - width: 16px; - height: 16px; - float: left; - margin: 2px; + background: transparent none 0 0 no-repeat; + display: block; + width: 16px; + height: 16px; + float: left; + margin: 2px; } .pontoon-editable-toolbar .edit { - background-image: url('../img/edit.png'); + background-image: url('../img/edit.png'); } .pontoon-editable-toolbar .cancel { - background-image: url('../img/cancel.png'); - display: none; + background-image: url('../img/cancel.png'); + display: none; } diff --git a/pontoon/base/static/css/sidebar_menu.css b/pontoon/base/static/css/sidebar_menu.css index f9fdee337..d84fb3395 100644 --- a/pontoon/base/static/css/sidebar_menu.css +++ b/pontoon/base/static/css/sidebar_menu.css @@ -1,54 +1,54 @@ .menu.left-column { - float: left; - width: 300px; + float: left; + width: 300px; } .menu.left-column ul { - max-height: none; + max-height: none; } .menu.left-column li.selected { - background: #3f4752; + background: #3f4752; } .menu.left-column li.selected a { - color: #ffffff; + color: #ffffff; } .menu.left-column li { - padding: 0; + padding: 0; } .menu.left-column li a { - display: block; - font-size: 15px; - padding: 5px; + display: block; + font-size: 15px; + padding: 5px; } .menu.left-column .count { - background: #333941; - float: right; + background: #333941; + float: right; } .menu.right-column { - background: #333941; - float: right; - width: 640px; + background: #333941; + float: right; + width: 640px; } .menu.right-column > section { - display: none; + display: none; } .menu.right-column > section.selected { - display: block; + display: block; } .menu.right-column li.no .title { - font-size: 22px; - margin-top: 10px; + font-size: 22px; + margin-top: 10px; } .menu.right-column li.no .description { - font-size: 14px; + font-size: 14px; } diff --git a/pontoon/base/static/css/style.css b/pontoon/base/static/css/style.css index cb8b60ee3..0864badb2 100755 --- a/pontoon/base/static/css/style.css +++ b/pontoon/base/static/css/style.css @@ -2,50 +2,50 @@ /* Bug 1351813 */ [data-script='Arabic'] { - font-size: 1.1em; + font-size: 1.1em; } [data-script='Arabic'] .placeable { - font-size: 0.9em; + font-size: 0.9em; } .status.fa { - font-size: 16px; - left: 20px; - position: absolute; - top: 3px; + font-size: 16px; + left: 20px; + position: absolute; + top: 3px; } .status.fa:before { - color: #5f7285; - content: ''; + color: #5f7285; + content: ''; } .translated .status.fa:before { - color: #7bc876; + color: #7bc876; } .fuzzy .status.fa:before { - color: #fed271; + color: #fed271; } .warnings .status.fa:before { - color: #ffa10f; + color: #ffa10f; } .errors .status.fa:before { - color: #f36; + color: #f36; } .unreviewed .status.fa:before { - color: #4fc4f6; - content: ''; + color: #4fc4f6; + content: ''; } /* Reset HTML5 Search Input in Webkit */ input[type='search'] { - -webkit-appearance: none; - -webkit-border-radius: 0; + -webkit-appearance: none; + -webkit-border-radius: 0; } body, @@ -53,80 +53,80 @@ select, input, textarea, button { - font-family: 'Open Sans', 'Lucida Sans', 'Lucida Grande', - 'Lucida Sans Unicode', Verdana, sans-serif; + font-family: 'Open Sans', 'Lucida Sans', 'Lucida Grande', + 'Lucida Sans Unicode', Verdana, sans-serif; } body { - background: #272a2f; - color: #ebebeb; + background: #272a2f; + color: #ebebeb; } #heading, #middle { - background: #333941; + background: #333941; } #heading, #main { - padding: 40px 0; + padding: 40px 0; } button { - font-size: 13px; - outline: none; + font-size: 13px; + outline: none; } /* Reset values for Persian (fa) to avoid colision with Font Awesome */ .language.fa { - font-family: inherit; - font-weight: inherit; - line-height: inherit; - -webkit-font-smoothing: inherit; - -moz-osx-font-smoothing: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; } h2 { - color: #888888; - font-size: 14px; - font-weight: 300; - text-transform: uppercase; - padding-bottom: 2px; + color: #888888; + font-size: 14px; + font-weight: 300; + text-transform: uppercase; + padding-bottom: 2px; } h3 { - color: #7bc876; - font-size: 26px; - font-weight: 300; - font-style: italic; - letter-spacing: -1px; + color: #7bc876; + font-size: 26px; + font-weight: 300; + font-style: italic; + letter-spacing: -1px; } h3 .stress { - color: #ebebeb; + color: #ebebeb; } h3 .small { - font-size: 18px; - letter-spacing: 0; + font-size: 18px; + letter-spacing: 0; } a:link, a:visited { - color: #aaaaaa; - font-weight: 300; - text-decoration: none; + color: #aaaaaa; + font-weight: 300; + text-decoration: none; } a:hover, a:active { - color: #7bc876; - text-decoration: none; + color: #7bc876; + text-decoration: none; } sup a:link, sup a:visited { - color: #7bc876; + color: #7bc876; } input[type='text'], @@ -135,15 +135,15 @@ input[type='password'], input[type='url'], input[type='number'], textarea { - background: #888888; - border: none; - padding: 4px 3px 3px; - width: 205px; - float: left; + background: #888888; + border: none; + padding: 4px 3px 3px; + width: 205px; + float: left; } textarea { - float: none; + float: none; } input[type='text']:focus, @@ -152,771 +152,771 @@ input[type='password']:focus, input[type='url']:focus, input[type='number']:focus, textarea:focus { - background-color: #ffffff; - outline: none; /* Remove WebKit Glow */ + background-color: #ffffff; + outline: none; /* Remove WebKit Glow */ } label { - font-weight: 300; + font-weight: 300; } table { - color: #aaaaaa; - font-size: 13px; - text-align: left; - width: 100%; + color: #aaaaaa; + font-size: 13px; + text-align: left; + width: 100%; } tbody { - font-weight: 300; + font-weight: 300; } thead tr { - border-bottom: 1px solid #4d5967; + border-bottom: 1px solid #4d5967; } th { - color: #aaaaaa; - padding: 10px; - text-transform: uppercase; + color: #aaaaaa; + padding: 10px; + text-transform: uppercase; } .table-sort th { - cursor: pointer; + cursor: pointer; } .table-sort th:hover { - color: #ebebeb; + color: #ebebeb; } .table-sort th i { - margin-left: 5px; - position: absolute; + margin-left: 5px; + position: absolute; } .table-sort th.asc i:after, .table-sort th.desc.inverted i:after { - content: ''; - display: inline-block; - margin-top: 5px; + content: ''; + display: inline-block; + margin-top: 5px; } .table-sort th.desc i:after, .table-sort th.asc.inverted i:after { - content: ''; - display: inline-block; - margin-top: -5px; + content: ''; + display: inline-block; + margin-top: -5px; } td { - padding: 15px 10px; + padding: 15px 10px; } table.striped tr:nth-child(even) { - background: #333941; + background: #333941; } tfoot tr { - border-top: 1px solid #4d5967; + border-top: 1px solid #4d5967; } tfoot td { - padding: 10px 0px; - text-align: right; - font-style: italic; + padding: 10px 0px; + text-align: right; + font-style: italic; } tfoot td a { - font-style: normal; + font-style: normal; } .count { - background: #3f4752; - border-radius: 3px; - color: #cccccc; - margin-left: 5px; - padding: 0 5px; + background: #3f4752; + border-radius: 3px; + color: #cccccc; + margin-left: 5px; + padding: 0 5px; } .button { - color: #ffffff; - cursor: pointer; - display: inline-block; - font-weight: 300; - line-height: 16px; - height: 17px; - padding: 4px 4px 3px; + color: #ffffff; + cursor: pointer; + display: inline-block; + font-weight: 300; + line-height: 16px; + height: 17px; + padding: 4px 4px 3px; } .noselect { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .select { - display: inline-block; - font-size: 14px; - margin-left: 0; - padding-left: 10px; - position: relative; + display: inline-block; + font-size: 14px; + margin-left: 0; + padding-left: 10px; + position: relative; } .select.hidden { - display: none !important; + display: none !important; } .select .menu { - background: #272a2f; - bottom: 50px; - color: #aaaaaa; - display: none; - list-style: none; - margin-left: 0; - padding: 10px 12px; - position: absolute; - width: 150px; - z-index: 20; + background: #272a2f; + bottom: 50px; + color: #aaaaaa; + display: none; + list-style: none; + margin-left: 0; + padding: 10px 12px; + position: absolute; + width: 150px; + z-index: 20; } .select .menu.permanent { - z-index: 19; /* Must be lower than for the (popup) .menu */ + z-index: 19; /* Must be lower than for the (popup) .menu */ } .select > .button.breadcrumbs, #go { - background: #3f4752; - font-size: 16px; - height: 20px; - margin: 10px 10px 10px 0; - overflow: hidden; - padding: 10px 20px 10px 40px; - text-overflow: ellipsis; - width: 200px; + background: #3f4752; + font-size: 16px; + height: 20px; + margin: 10px 10px 10px 0; + overflow: hidden; + padding: 10px 20px 10px 40px; + text-overflow: ellipsis; + width: 200px; } #settings .menu { - width: 185px; + width: 185px; } #go { - width: auto; + width: auto; } .select > .button.breadcrumbs:before, #go:before { - content: ''; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #272a2f; - position: absolute; - left: 0; - top: 10px; + content: ''; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid #272a2f; + position: absolute; + left: 0; + top: 10px; } .select > .button.breadcrumbs:after { - content: ''; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #3f4752; - position: absolute; - right: -10px; - top: 10px; - z-index: 1; + content: ''; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid #3f4752; + position: absolute; + right: -10px; + top: 10px; + z-index: 1; } .locale.select > .button.breadcrumbs { - padding-left: 20px; + padding-left: 20px; } .locale.select > .button.breadcrumbs:before { - display: none; + display: none; } .select > .button.breadcrumbs:hover { - background: #4d5967; + background: #4d5967; } .select > .button.breadcrumbs:hover:after { - border-left-color: #4d5967; + border-left-color: #4d5967; } .select.opened > .button.breadcrumbs { - background: #4d5967; + background: #4d5967; } .select.opened > .button.breadcrumbs:after { - border-left-color: #4d5967; + border-left-color: #4d5967; } #go { - color: #ffffff; - float: left; + color: #ffffff; + float: left; } #go.active, #go:hover { - background: #7bc876; - color: #272a2f; + background: #7bc876; + color: #272a2f; } .project.select, .locale.select, .part.select, .user.select { - padding-left: 0; - width: 100%; + padding-left: 0; + width: 100%; } .project.select .menu, .locale.select .menu, .part.select .menu, .user.select .menu { - bottom: auto; - display: inline-block; - position: relative; - width: 100%; + bottom: auto; + display: inline-block; + position: relative; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .project.select input[type='search'], .locale.select input[type='search'], .part.select input[type='search'] { - width: 100%; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .project.select .menu ul, .locale.select .menu ul, .part.select .menu ul { - max-height: none; + max-height: none; } .project.select .menu ul li { - position: relative; + position: relative; } .project.select .menu ul li .name, .part.select .menu ul li .name { - overflow: hidden; - padding-left: 1px; /* Insted of 0; because of overflow */ - text-overflow: ellipsis; - white-space: nowrap; - width: 219px; /* Instead of 220px; because of padding */ + overflow: hidden; + padding-left: 1px; /* Insted of 0; because of overflow */ + text-overflow: ellipsis; + white-space: nowrap; + width: 219px; /* Instead of 220px; because of padding */ } .locale.select .new ~ input[type='search'] { - padding-right: 50px; + padding-right: 50px; } .select .menu ul li { - cursor: pointer; + cursor: pointer; } .select .menu ul li span { - float: left; - text-align: left; + float: left; + text-align: left; } .select .menu ul li span a { - display: block; - height: 100%; + display: block; + height: 100%; } .select .menu ul li .chart-wrapper { - display: inline-block; - height: 17px; - width: 180px; + display: inline-block; + height: 17px; + width: 180px; } .select .menu ul li .chart-wrapper .percent { - float: right; - font-size: 12px; - margin-top: 1px; - text-align: right; + float: right; + font-size: 12px; + margin-top: 1px; + text-align: right; } .select .menu ul li .chart { - display: table; - height: 5px; - margin-top: 6px; - table-layout: fixed; - width: 140px; + display: table; + height: 5px; + margin-top: 6px; + table-layout: fixed; + width: 140px; } .select .menu ul li .chart span { - display: table-cell; - float: none; - height: 100%; + display: table-cell; + float: none; + height: 100%; } .select .menu ul li .chart span.translated { - background: #7bc876; + background: #7bc876; } .select .menu ul li .chart span.fuzzy { - background: #fed271; + background: #fed271; } .select .menu ul li .chart span.missing { - background: #5f7285; + background: #5f7285; } .select .menu ul li .latest { - font-size: 12px; - height: 15px; - margin-right: 10px; - overflow: hidden; - padding-top: 2px; - white-space: nowrap; - width: 293px; + font-size: 12px; + height: 15px; + margin-right: 10px; + overflow: hidden; + padding-top: 2px; + white-space: nowrap; + width: 293px; } .select .menu ul li .latest time { - display: inline-block; + display: inline-block; } .select .menu ul li .latest a { - color: #7bc876; - display: inline-block; + color: #7bc876; + display: inline-block; } .selector { - cursor: pointer; + cursor: pointer; } .locale .selector, .part .selector, .project .selector { - float: right; + float: right; } .menu ul { - list-style: none; - max-height: 170px; - overflow: auto; - margin: 0; - position: relative; + list-style: none; + max-height: 170px; + overflow: auto; + margin: 0; + position: relative; } .menu li { - color: #aaaaaa; - font-weight: 300; - padding: 2px 4px; + color: #aaaaaa; + font-weight: 300; + padding: 2px 4px; } .menu li.hover { - color: #ffffff; - background: #3f4752; + color: #ffffff; + background: #3f4752; } .menu li.hover a { - color: #ffffff; + color: #ffffff; } .menu li a:hover, .menu li a:active { - color: #ffffff; + color: #ffffff; } .menu li.horizontal-separator { - border-top: 1px solid #525a65; - height: 0; - margin: 5px 0; - padding: 0; + border-top: 1px solid #525a65; + height: 0; + margin: 5px 0; + padding: 0; } .menu li.no-match { - cursor: default; - display: none; - font-size: inherit; - list-style: none; - text-align: left; + cursor: default; + display: none; + font-size: inherit; + list-style: none; + text-align: left; } .menu li.no-match.hover { - background: transparent; - color: inherit; - cursor: default; - display: none; + background: transparent; + color: inherit; + cursor: default; + display: none; } .search-wrapper { - border-bottom: 1px solid #5e6475; - margin-bottom: 10px; - position: relative; + border-bottom: 1px solid #5e6475; + margin-bottom: 10px; + position: relative; } .search-wrapper .icon { - color: #aaaaaa; - font-size: 1.2em; - position: absolute; - left: 3px; - top: 5px; + color: #aaaaaa; + font-size: 1.2em; + position: absolute; + left: 3px; + top: 5px; } .search-wrapper input[type='search'] { - background: transparent; - color: #ffffff; - padding-left: 22px; + background: transparent; + color: #ffffff; + padding-left: 22px; } .search-wrapper input[type='search']::-webkit-search-decoration, .search-wrapper input[type='search']::-webkit-search-cancel-button { - display: none; + display: none; } .menu input[type='search'] { - background: transparent; - color: #ffffff; - float: none; - font-weight: 300; - padding-left: 25px; - padding-right: 0; - width: 200px; + background: transparent; + color: #ffffff; + float: none; + font-weight: 300; + padding-left: 25px; + padding-right: 0; + width: 200px; } .menu input[type='search']:focus { - background-color: transparent; + background-color: transparent; } .locale .code { - color: #7bc876; - text-align: left; + color: #7bc876; + text-align: left; } .notification { - background: rgba(51, 57, 65, 0.9); - color: #7bc876; - cursor: pointer; - font-style: italic; - left: 0; - margin: 0; - position: fixed; - top: -60px; - width: 100%; - z-index: 100; + background: rgba(51, 57, 65, 0.9); + color: #7bc876; + cursor: pointer; + font-style: italic; + left: 0; + margin: 0; + position: fixed; + top: -60px; + width: 100%; + z-index: 100; } .notification li { - font-size: 14px; - line-height: 60px; - list-style: none; - text-align: center; + font-size: 14px; + line-height: 60px; + list-style: none; + text-align: center; } .notification li.error { - color: #f36; + color: #f36; } img.rounded { - border-radius: 6px; - border: 2px solid #4d5967; + border-radius: 6px; + border: 2px solid #4d5967; } .details div { - border-top: 5px solid; - color: #aaaaaa; - display: inline-block; - margin-right: 1px; - width: 89px; + border-top: 5px solid; + color: #aaaaaa; + display: inline-block; + margin-right: 1px; + width: 89px; } .details div:last-child { - margin-right: 0; - width: 90px; + margin-right: 0; + width: 90px; } .details div.translated { - border-color: #7bc876; + border-color: #7bc876; } .details div.fuzzy { - border-color: #fed271; + border-color: #fed271; } .details div.warnings { - border-color: #ffa10f; + border-color: #ffa10f; } .details div.errors { - border-color: #f36; + border-color: #f36; } .details div.missing { - border-color: #5f7285; + border-color: #5f7285; } .details div.unreviewed { - border-color: #4fc4f6; + border-color: #4fc4f6; } .details div span { - font-size: 10px; - text-transform: uppercase; + font-size: 10px; + text-transform: uppercase; } .details div p { - color: #ffffff; - font-size: 28px; - padding: 10px 0; - position: relative; + color: #ffffff; + font-size: 28px; + padding: 10px 0; + position: relative; } #error { - background: #333941; - bottom: 0; - display: table; - height: 100%; - overflow: hidden; - position: fixed; - text-align: center; - width: 100%; - z-index: -1; + background: #333941; + bottom: 0; + display: table; + height: 100%; + overflow: hidden; + position: fixed; + text-align: center; + width: 100%; + z-index: -1; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .inner { - display: table-cell; - vertical-align: middle; + display: table-cell; + vertical-align: middle; } #addon-promotion { - background: #ff3366cc; - border-bottom: 1px solid #333941; - height: 44px; - position: fixed; - top: -44px; - transition: top 0.3s; - width: 100%; - z-index: 20; + background: #ff3366cc; + border-bottom: 1px solid #333941; + height: 44px; + position: fixed; + top: -44px; + transition: top 0.3s; + width: 100%; + z-index: 20; } body.addon-promotion-active #addon-promotion { - top: 0; + top: 0; } #addon-promotion .container { - align-items: center; - display: flex; - height: 100%; + align-items: center; + display: flex; + height: 100%; } #addon-promotion .dismiss { - background: transparent; - border: none; - color: #ebebeb; - font-size: 28px; - padding: 0; + background: transparent; + border: none; + color: #ebebeb; + font-size: 28px; + padding: 0; } #addon-promotion .dismiss:hover { - color: #272a2f; + color: #272a2f; } #addon-promotion .text { - margin-left: 20px; + margin-left: 20px; } #addon-promotion .get { - background: #272a2f; - border-radius: 3px; - color: #ebebeb; - font-weight: normal; - margin-left: auto; - padding: 7px 12px; + background: #272a2f; + border-radius: 3px; + color: #ebebeb; + font-weight: normal; + margin-left: auto; + padding: 7px 12px; } #addon-promotion .get:hover { - background: #333941; + background: #333941; } body > header { - background: #272a2f; - border-bottom: 1px solid #333941; - height: 60px; - transition: margin-top 0.3s; + background: #272a2f; + border-bottom: 1px solid #333941; + height: 60px; + transition: margin-top 0.3s; } body.addon-promotion-active > header { - margin-top: 44px; + margin-top: 44px; } header nav { - text-align: left; + text-align: left; } header nav .logo { - display: inline-block; - float: left; - margin: 14px 15px 0 0; + display: inline-block; + float: left; + margin: 14px 15px 0 0; } header nav .links { - display: inline-block; - float: left; - padding-top: 22px; + display: inline-block; + float: left; + padding-top: 22px; } header nav .right { - float: right; + float: right; } header nav .right .sign-in-header { - display: inline-block; - font-size: 14px; - float: left; + display: inline-block; + font-size: 14px; + float: left; } header nav .right .sign-in-header .button { - border: 1.5px solid #7bc176; - color: white; - border-radius: 2px; - padding: 10px 25px; - text-align: center; - margin: 10px 5px 0 0; + border: 1.5px solid #7bc176; + color: white; + border-radius: 2px; + padding: 10px 25px; + text-align: center; + margin: 10px 5px 0 0; } header nav .right .sign-in-header .button:hover { - background-color: #7bc176; + background-color: #7bc176; } header .select .menu { - bottom: auto; - top: 60px; + bottom: auto; + top: 60px; } header .right .select .menu { - right: 0; + right: 0; } #notifications { - float: left; - height: 60px; - margin-right: 3px; + float: left; + height: 60px; + margin-right: 3px; } #notifications .button { - height: 100%; - padding: 0; + height: 100%; + padding: 0; } #notifications .button .icon { - color: #4d5967; - font-size: 26px; - margin-top: 17px; + color: #4d5967; + font-size: 26px; + margin-top: 17px; } #notifications .button .badge { - background: #f36; - border: 3px solid #272a2f; - border-radius: 12px; - color: #fff; - display: none; - font-size: 12px; - font-style: normal; - font-weight: 400; - height: 14px; - line-height: 16px; - padding: 1px 6px 3px; - position: absolute; - right: -6px; - text-align: center; - top: 4px; + background: #f36; + border: 3px solid #272a2f; + border-radius: 12px; + color: #fff; + display: none; + font-size: 12px; + font-style: normal; + font-weight: 400; + height: 14px; + line-height: 16px; + padding: 1px 6px 3px; + position: absolute; + right: -6px; + text-align: center; + top: 4px; } #notifications.unread .button .badge { - display: block; + display: block; } #notifications .menu, #profile .menu { - border: 1px solid #333941; - border-top: none; + border: 1px solid #333941; + border-top: none; } #notifications .menu { - padding: 0; - width: 350px; + padding: 0; + width: 350px; } .notifications .menu ul, #profile .menu ul, #filter .menu ul { - max-height: none; + max-height: none; } #notifications .menu li.see-all { - padding: 0; - text-align: center; + padding: 0; + text-align: center; } #notifications .menu ul.notification-list { - max-height: 280px; + max-height: 280px; } .notifications .menu ul.notification-list li.notification-item { - cursor: default; - padding: 0; + cursor: default; + padding: 0; } .notifications - .menu - ul.notification-list - li.notification-item[data-unread='true'] { - background: #3f4752; + .menu + ul.notification-list + li.notification-item[data-unread='true'] { + background: #3f4752; } .notifications .menu li.notification-item span { - color: #ebebeb; - float: none; + color: #ebebeb; + float: none; } .notifications .menu li.notification-item a { - color: #f36; - display: inline; + color: #f36; + display: inline; } .notifications .menu li.notification-item .verb { - color: #ebebeb; + color: #ebebeb; } .notifications .menu li.notification-item .description ul { - list-style: inside; - padding-top: 10px; + list-style: inside; + padding-top: 10px; } .notifications .menu li.notification-item .message { - padding: 10px; + padding: 10px; } .notifications .menu li.notification-item .message.trim, .notifications .menu li.notification-item .message.trim > p { - pointer-events: none; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + pointer-events: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .notifications .menu li.notification-item .message.trim a { - color: #aaaaaa; + color: #aaaaaa; } .notifications .menu li.notification-item.hover .message.trim a { - color: #ebebeb; + color: #ebebeb; } .notifications .menu li.notification-item .message.trim p { - padding: 0; + padding: 0; } /* Users can include HTML tags in their messages */ @@ -926,594 +926,594 @@ header .right .select .menu { .notifications .menu li.notification-item .message h4, .notifications .menu li.notification-item .message h5, .notifications .menu li.notification-item .message h6 { - color: #ebebeb; - font-size: 14px; - font-style: normal; - font-weight: bold; - letter-spacing: 0; - text-transform: none; + color: #ebebeb; + font-size: 14px; + font-style: normal; + font-weight: bold; + letter-spacing: 0; + text-transform: none; } .notifications .menu li.notification-item .message h1 { - font-size: 18px; + font-size: 18px; } .notifications .menu li.notification-item .message h2 { - font-size: 16px; + font-size: 16px; } .notifications .menu li.notification-item .message p { - padding: 5px 0; + padding: 5px 0; } .notifications .menu li.notification-item .message ol { - margin-left: 1.2em; + margin-left: 1.2em; } .notifications .menu li.notification-item .message ul { - list-style: inside; + list-style: inside; } /* End message styling */ .notifications .menu li.notification-item .timeago { - color: #888888; - font-size: 11px; - font-weight: normal; - margin-top: 8px; - text-align: right; - text-transform: uppercase; + color: #888888; + font-size: 11px; + font-weight: normal; + margin-top: 8px; + text-align: right; + text-transform: uppercase; } #notifications .menu li.horizontal-separator { - border-color: #333941; - margin: 0; + border-color: #333941; + margin: 0; } .notification-list li.horizontal-separator { - border-color: #272a2f; - margin: 0; + border-color: #272a2f; + margin: 0; } .notifications .menu li.no { - background: transparent; - color: inherit; - padding: 0 4px 14px; - text-align: center; + background: transparent; + color: inherit; + padding: 0 4px 14px; + text-align: center; } .notifications .menu li.no .icon { - color: #333941; - font-size: 92px; - padding-bottom: 8px; + color: #333941; + font-size: 92px; + padding-bottom: 8px; } .notifications .menu li.no .title, #profile .menu li.details .name { - color: #ebebeb; - font-size: 16px; + color: #ebebeb; + font-size: 16px; } .notifications .menu li.no .description, #profile .menu li.details .email { - color: #aaaaaa; - font-size: 12px; + color: #aaaaaa; + font-size: 12px; } .notifications .menu li.no .description { - padding-top: 4px; + padding-top: 4px; } #notifications .menu .see-all > a, .notifications .menu li > .item-content { - display: block; - padding: 10px; + display: block; + padding: 10px; } .notifications:not(#notifications) .menu li > .item-content { - font-size: 15px; + font-size: 15px; } #profile { - padding-left: 0; + padding-left: 0; } #profile > .button { - height: 100%; - padding: 0; + height: 100%; + padding: 0; } #profile .button img, #profile .button .menu-icon { - margin: 6px 0 6px 5px; + margin: 6px 0 6px 5px; } #profile .button img { - height: 44px; - width: 44px; + height: 44px; + width: 44px; } #profile .button .menu-icon { - border: 2px solid #4d5967; - border-radius: 100%; - float: right; - font-size: 20px; - height: 20px; - padding: 12px; - text-align: center; - width: 20px; + border: 2px solid #4d5967; + border-radius: 100%; + float: right; + font-size: 20px; + height: 20px; + padding: 12px; + text-align: center; + width: 20px; } #profile .menu { - line-height: 18px; - width: 250px; + line-height: 18px; + width: 250px; } #profile .menu li { - padding-bottom: 0; - padding-top: 0; + padding-bottom: 0; + padding-top: 0; } #profile .menu li a { - display: block; - line-height: 22px; + display: block; + line-height: 22px; } #profile .menu li i.fa, #profile .menu li i.fab { - margin: 0 8px 0 -2px; + margin: 0 8px 0 -2px; } #profile .menu li.details { - padding: 10px 4px; - text-align: center; + padding: 10px 4px; + text-align: center; } .links li { - list-style: none; - display: inline-block; - margin-left: 20px; + list-style: none; + display: inline-block; + margin-left: 20px; } nav .links li { - margin: 0 15px; - font-size: 14px; - line-height: 14px; + margin: 0 15px; + font-size: 14px; + line-height: 14px; } .links li.hidden { - display: none; + display: none; } .links li a:link, .links li a:visited { - color: #ebebeb; + color: #ebebeb; } .links li a:hover, .links li a:active { - color: #7bc876; + color: #7bc876; } .submenu.tabs { - border-bottom: 1px solid #5e6475; + border-bottom: 1px solid #5e6475; } .submenu .links a:link, .submenu .links a:visited { - color: #aaaaaa; - display: inline-block; - padding: 6px; - text-transform: uppercase; + color: #aaaaaa; + display: inline-block; + padding: 6px; + text-transform: uppercase; } .submenu .links li.active a, .submenu .links a:hover, .submenu .links a:active { - color: #7bc876; + color: #7bc876; } .submenu.tabs .links { - margin-bottom: -1px; - text-align: left; - white-space: nowrap; - width: 100%; + margin-bottom: -1px; + text-align: left; + white-space: nowrap; + width: 100%; } .submenu.tabs .links li { - border-color: transparent; - border-style: solid solid none; - border-width: 1px 1px 0; - margin: 0; + border-color: transparent; + border-style: solid solid none; + border-width: 1px 1px 0; + margin: 0; } .submenu.tabs .links li.active { - border-color: #5e6475; - background: #272a2f; + border-color: #5e6475; + background: #272a2f; } .submenu.tabs .links a { - padding: 12px 20px; - width: 100%; + padding: 12px 20px; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .submenu.tabs .links a:hover { - color: #ffffff; + color: #ffffff; } .submenu.tabs .links li.active a { - background-color: transparent; - color: #ffffff; + background-color: transparent; + color: #ffffff; } .submenu.tabs .links a .fa { - margin-right: 4px; + margin-right: 4px; } noscript p { - text-transform: uppercase; + text-transform: uppercase; } #title, #title a { - color: #ebebeb; - display: inline-block; - font-size: 48px; - font-weight: bold; - letter-spacing: -1px; - line-height: 1.5em; + color: #ebebeb; + display: inline-block; + font-size: 48px; + font-weight: bold; + letter-spacing: -1px; + line-height: 1.5em; } #error #title { - line-height: 108px; + line-height: 108px; } #error #title a { - font-size: 108px; - letter-spacing: -4px; - line-height: 162px; + font-size: 108px; + letter-spacing: -4px; + line-height: 162px; } #error #box a { - color: #7bc876; - font-size: 108px; + color: #7bc876; + font-size: 108px; } #error #box p { - font-size: 32px; - line-height: 50px; - font-weight: bold; + font-size: 32px; + line-height: 50px; + font-weight: bold; } #subtitle, #subtitle a { - color: #7bc876; - font-size: 32px; - font-style: italic; - text-transform: none; + color: #7bc876; + font-size: 32px; + font-style: italic; + text-transform: none; } #action { - color: #aaaaaa; - font-size: 14px; - font-weight: 300; - height: 40px; - margin-top: 70px; + color: #aaaaaa; + font-size: 14px; + font-weight: 300; + height: 40px; + margin-top: 70px; } #action #link { - color: #7bc876; + color: #7bc876; } body > form, .container { - margin: 0 auto; - width: 980px; + margin: 0 auto; + width: 980px; } .unselectable * { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; } .check-box .fa { - margin-right: 6px; + margin-right: 6px; } .check-box .fa:before { - color: #f36; - content: ''; + color: #f36; + content: ''; } .check-box.enabled .fa:before { - color: #7bc876; - content: ''; + color: #7bc876; + content: ''; } #helpers > section ul { - list-style: none; - margin: 0; + list-style: none; + margin: 0; } #helpers > section ul li { - border-bottom: 1px solid #5e6475; - padding-bottom: 20px; - padding-top: 5px; - text-align: right; + border-bottom: 1px solid #5e6475; + padding-bottom: 20px; + padding-top: 5px; + text-align: right; } #helpers > section ul li > header { - color: #aaaaaa; - display: block; - font-size: 11px; - font-weight: 300; - padding: 1px 0 5px; - text-transform: uppercase; + color: #aaaaaa; + display: block; + font-size: 11px; + font-weight: 300; + padding: 1px 0 5px; + text-transform: uppercase; } #helpers > section ul li > header .stress { - color: #7bc876; - padding-left: 3px; - padding-right: 3px; + color: #7bc876; + padding-left: 3px; + padding-right: 3px; } #helpers > section.machinery ul li > header .stress { - padding-right: 0; + padding-right: 0; } #helpers > section.machinery ul li > header sup { - color: #7bc876; + color: #7bc876; } #helpers > section.machinery ul li > header .sources { - display: inline-block; + display: inline-block; } #helpers > section.machinery ul li > header .sources li:before { - content: '•'; - padding-right: 3px; + content: '•'; + padding-right: 3px; } #helpers - > section.machinery - ul - li - > header - .sources - li[data-source='Google Translate']:only-child:before, + > section.machinery + ul + li + > header + .sources + li[data-source='Google Translate']:only-child:before, #helpers - > section.machinery - ul - li - > header - .sources - li[data-source='Microsoft Translator']:only-child:before, + > section.machinery + ul + li + > header + .sources + li[data-source='Microsoft Translator']:only-child:before, #helpers - > section.machinery - ul - li - > header - .sources - li[data-source='Systran Translate']:only-child:before, + > section.machinery + ul + li + > header + .sources + li[data-source='Systran Translate']:only-child:before, #helpers - > section.machinery - ul - li - > header - .sources - li[data-source='Caighdean']:only-child:before { - content: ''; - padding-right: 0; + > section.machinery + ul + li + > header + .sources + li[data-source='Caighdean']:only-child:before { + content: ''; + padding-right: 0; } #helpers > section.machinery ul li > header .sources li { - border: none; - display: inline-block; - list-style-type: disc; - list-style-position: inside; - padding: 0 0 0 3px; + border: none; + display: inline-block; + list-style-type: disc; + list-style-position: inside; + padding: 0 0 0 3px; } #helpers > section ul li > p { - min-height: 22px; - text-align: left; + min-height: 22px; + text-align: left; } #helpers > section ul li > p.translation-clipboard, #helpers > section ul li > p.translation-diff { - display: none; + display: none; } #helpers > section ul li > p ins, #helpers > section ul li > p del { - border-radius: 2px; - white-space: pre-wrap; + border-radius: 2px; + white-space: pre-wrap; } #helpers > section ul li > p ins { - background: #4b6259; - color: #9cd699; + background: #4b6259; + color: #9cd699; } #helpers > section ul li > p del { - background: #674b54; - color: #fe8f8f; + background: #674b54; + color: #fe8f8f; } #helpers > section ul li > p ins mark, #helpers > section ul li > p del mark { - background: transparent; - border-color: transparent; - margin: 0; + background: transparent; + border-color: transparent; + margin: 0; } #helpers > section ul li.disabled p { - color: #aaaaaa; - font-style: italic; + color: #aaaaaa; + font-style: italic; } #helpers > section ul li p.original { - color: #aaaaaa; + color: #aaaaaa; } .tabs nav ul li { - float: left; - padding: 0; - text-align: center; - text-transform: uppercase; + float: left; + padding: 0; + text-align: center; + text-transform: uppercase; } .tabs nav ul li a { - display: inline-block; - outline: none; - padding: 10px 5px; - width: 100%; + display: inline-block; + outline: none; + padding: 10px 5px; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .tabs > section { - display: none; + display: none; } .no-results { - color: #aaaaaa; - font-style: italic; - text-align: center; + color: #aaaaaa; + font-style: italic; + text-align: center; } #standalone-sign-in .clearfix { - padding: 15px; + padding: 15px; } #standalone-sign-in label { - font-size: 24px; - font-weight: 300; + font-size: 24px; + font-weight: 300; } #standalone-sign-in input[type='text'], #standalone-sign-in input[type='password'] { - float: none; - background-color: white; - padding: 10px; - margin: 10px; + float: none; + background-color: white; + padding: 10px; + margin: 10px; } #standalone-sign-in .controls .button { - font-size: 14px; + font-size: 14px; } .controls { - margin-bottom: 20px; - position: relative; + margin-bottom: 20px; + position: relative; } .controls .button { - background: #333941; - border: none; - border-radius: 2px; - color: #aaaaaa; - font-weight: 400; - height: auto; - padding: 6px; - text-align: center; - text-transform: uppercase; + background: #333941; + border: none; + border-radius: 2px; + color: #aaaaaa; + font-weight: 400; + height: auto; + padding: 6px; + text-align: center; + text-transform: uppercase; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .controls .button.small { - width: 240px; + width: 240px; } .controls .select.opened .button { - border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; } .controls button.button:hover, .controls a.button:hover, .controls li.active a.button, .controls .button.active { - background: #7bc876; - color: #272a2f; + background: #7bc876; + color: #272a2f; } .controls .button.active { - width: 100px; + width: 100px; } .controls .button.selector:hover { - background: #333941; - color: inherit; + background: #333941; + color: inherit; } .controls > .search-wrapper { - border: none; - display: inline-block; - height: 28px; - margin: 0; - vertical-align: top; - width: 100%; + border: none; + display: inline-block; + height: 28px; + margin: 0; + vertical-align: top; + width: 100%; } .controls > .search-wrapper.small { - width: 240px; + width: 240px; } .controls > .search-wrapper.big { - width: 714px; + width: 714px; } .controls > .search-wrapper .icon { - left: 6px; - top: 6px; - z-index: 20; + left: 6px; + top: 6px; + z-index: 20; } .controls > .search-wrapper input { - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - font-size: 13px; - font-weight: 300; - height: 28px; - padding-left: 25px; - position: absolute; - width: 100%; - z-index: 10; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + font-size: 13px; + font-weight: 300; + height: 28px; + padding-left: 25px; + position: absolute; + width: 100%; + z-index: 10; } .priority .fa-star { - color: #637283; - float: left; + color: #637283; + float: left; } .priority .fa-star.active { - color: #7bc876; + color: #7bc876; } .container .info { - font-weight: 300; - line-height: 18px; + font-weight: 300; + line-height: 18px; } .container .info ul { - list-style: inherit; - margin-left: 1.8em; + list-style: inherit; + margin-left: 1.8em; } .container .info p { - padding: 12px 0; + padding: 12px 0; } .container .info a { - color: #7bc876; + color: #7bc876; } diff --git a/pontoon/base/static/css/table.css b/pontoon/base/static/css/table.css index c13f0ee90..5ff4cf0c4 100644 --- a/pontoon/base/static/css/table.css +++ b/pontoon/base/static/css/table.css @@ -1,388 +1,388 @@ table.table { - text-align: center; + text-align: center; } table.table.project-list.hidden { - display: none; + display: none; } .table td { - padding: 0 10px; - vertical-align: middle; + padding: 0 10px; + vertical-align: middle; } /* Used to designate from main style, e.g. deadline not set, no latest activity, project not ready... */ .table td .not, .table td .not-ready { - font-style: italic; + font-style: italic; } .table tbody tr:hover { - background: #333941; + background: #333941; } .table th:first-child { - text-align: left; + text-align: left; } .table th.name { - width: 220px; + width: 220px; } .table th.resource { - width: 420px; + width: 420px; } .table th.resource.with-deadline:not(.with-priority) { - width: 310px; + width: 310px; } .table th.resource.with-priority:not(.with-deadline) { - width: 330px; + width: 330px; } .table th.resource.with-deadline.with-priority { - width: 220px; + width: 220px; } .table th.tag { - width: 330px; + width: 330px; } .table th.population, .table th.deadline { - width: 90px; + width: 90px; } .table th.code, .table th.priority { - width: 70px; + width: 70px; } .table th.latest-activity, .table th.all-strings { - width: 140px; + width: 140px; } .table th.unreviewed-status { - position: relative; - width: 16px; + position: relative; + width: 16px; } .table-sort th.unreviewed-status i { - margin: -13px 0 0 13px; + margin: -13px 0 0 13px; } .table .unreviewed-status span { - position: absolute; - font-size: 18px; - margin: -15px 0 0 -5px; + position: absolute; + font-size: 18px; + margin: -15px 0 0 -5px; } .table .progress { - text-align: left; + text-align: left; } .table h4 { - overflow: hidden; - padding-left: 1px; /* Needed to avoid cutting off text due to overflow: hidden; */ - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; + padding-left: 1px; /* Needed to avoid cutting off text due to overflow: hidden; */ + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; } .table .name h4 { - width: 219px; + width: 219px; } .table .resource h4 { - width: 419px; + width: 419px; } .table .resource.with-deadline:not(.with-priority) h4 { - width: 309px; + width: 309px; } .table .resource.with-priority:not(.with-deadline) h4 { - width: 329px; + width: 329px; } .table .resource.with-deadline.with-priority h4 { - width: 219px; + width: 219px; } .table h4 a { - font-size: 15px; - line-height: 47px; - padding: 12px 0 11px; + font-size: 15px; + line-height: 47px; + padding: 12px 0 11px; } .table a { - color: #ebebeb; + color: #ebebeb; } .table a:hover { - color: #7bc876; + color: #7bc876; } /* Selector must also match heading-info */ .deadline time.approaching { - color: #ffa10f; + color: #ffa10f; } /* Selector must also match heading-info */ .deadline time.overdue { - color: #f36; + color: #f36; } .table td.code div { - width: 70px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + width: 70px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .table td.code a { - color: #7bc876; - line-height: 47px; - padding: 15px 5px 14px; + color: #7bc876; + line-height: 47px; + padding: 15px 5px 14px; } .table td.priority { - padding-left: 15px; + padding-left: 15px; } .table td.priority .fa { - margin-left: -1px; - font-size: 12px; + margin-left: -1px; + font-size: 12px; } .table .latest-activity .latest { - display: block; - position: relative; - width: 140px; + display: block; + position: relative; + width: 140px; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .table .latest-activity time { - white-space: nowrap; + white-space: nowrap; } .table .latest-activity time:hover { - color: #ebebeb; + color: #ebebeb; } .table .latest-activity .tooltip { - display: block; - background: #1c1e21; - border-radius: 10px; - bottom: 30px; - color: #ebebeb; - left: -100px; - padding: 10px; - position: absolute; - text-align: left; - width: 320px; - z-index: 20; + display: block; + background: #1c1e21; + border-radius: 10px; + bottom: 30px; + color: #ebebeb; + left: -100px; + padding: 10px; + position: absolute; + text-align: left; + width: 320px; + z-index: 20; } .table .latest-activity .tooltip:after { - content: ''; - position: absolute; - border: 10px solid; - border-color: #1c1e21 transparent transparent transparent; - bottom: -20px; - left: 160px; /* Must be (tooltip width + tooltip padding + bottom) / 2 */ - clip: rect(0 20px 10px 0); - -webkit-clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%); - clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%); + content: ''; + position: absolute; + border: 10px solid; + border-color: #1c1e21 transparent transparent transparent; + bottom: -20px; + left: 160px; /* Must be (tooltip width + tooltip padding + bottom) / 2 */ + clip: rect(0 20px 10px 0); + -webkit-clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%); + clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%); } .table .latest-activity .tooltip .quote { - color: #7bc876; - font-size: 30px; + color: #7bc876; + font-size: 30px; } .table .latest-activity .tooltip .translation { - display: block; - margin: -20px 0 0 40px; - overflow-wrap: break-word; + display: block; + margin: -20px 0 0 40px; + overflow-wrap: break-word; } .table .latest-activity .tooltip footer { - color: #888888; - font-style: italic; - height: 48px; - margin-top: 10px; - position: relative; + color: #888888; + font-style: italic; + height: 48px; + margin-top: 10px; + position: relative; } .table .latest-activity .tooltip footer .wrapper { - bottom: 0; - position: absolute; - right: 0; + bottom: 0; + position: absolute; + right: 0; } .table .latest-activity .tooltip footer .translation-details { - display: inline-block; - padding-top: 8px; - text-align: right; + display: inline-block; + padding-top: 8px; + text-align: right; } .table .latest-activity .tooltip footer .translation-action { - overflow-y: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 264px; + overflow-y: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 264px; } .table .latest-activity .tooltip footer .translation-action a { - color: #7bc876; + color: #7bc876; } .table .latest-activity .tooltip footer img { - display: inline-block; - margin-left: 8px; + display: inline-block; + margin-left: 8px; } .table .progress .chart-wrapper { - position: relative; + position: relative; } .table .progress .chart-wrapper .chart { - display: table; - font-size: 0; - height: 3px; - margin-top: 2px; - table-layout: fixed; - width: 295px; + display: table; + font-size: 0; + height: 3px; + margin-top: 2px; + table-layout: fixed; + width: 295px; } .table .progress .chart-wrapper .chart span { - display: table-cell; - height: 100%; + display: table-cell; + height: 100%; } .table .progress .chart-wrapper .percent { - position: absolute; - right: 25px; - top: -7px; + position: absolute; + right: 25px; + top: -7px; } .table .progress .chart-wrapper .unreviewed-status { - color: #3f4752; - font-size: 18px; - position: absolute; - right: 0; - top: -9px; + color: #3f4752; + font-size: 18px; + position: absolute; + right: 0; + top: -9px; } .table .progress .chart-wrapper .unreviewed-status.pending { - color: #4fc4f6; + color: #4fc4f6; } .table .progress .chart-wrapper .translated { - background: #7bc876; + background: #7bc876; } .table .progress .chart-wrapper .fuzzy { - background: #fed271; + background: #fed271; } .table .progress .chart-wrapper .warnings { - background: #ffa10f; + background: #ffa10f; } .table .progress .chart-wrapper .errors { - background: #f36; + background: #f36; } .table .progress .chart-wrapper .missing { - background: #5f7285; + background: #5f7285; } .table tr:hover .progress .chart-wrapper { - display: none; + display: none; } .table .progress .legend { - display: none; - margin-bottom: -9px; + display: none; + margin-bottom: -9px; } .table tr:hover .progress .legend { - display: block; + display: block; } .table .progress .legend ul { - font-size: 0; - margin-top: -1px; + font-size: 0; + margin-top: -1px; } .table .progress .legend li { - display: inline-block; - padding: 0; - text-align: center; - width: 51px; + display: inline-block; + padding: 0; + text-align: center; + width: 51px; } .table .progress .legend li:last-child { - width: 54px; + width: 54px; } .table .progress .legend li a { - color: #aaaaaa; - display: block; - margin-top: -5px; + color: #aaaaaa; + display: block; + margin-top: -5px; } .table .progress .legend li .title { - font-size: 10px; - font-weight: 400; - line-height: 10px; - text-transform: uppercase; + font-size: 10px; + font-weight: 400; + line-height: 10px; + text-transform: uppercase; } .table .progress .legend li.translated .title { - color: #7bc876; + color: #7bc876; } .table .progress .legend li.fuzzy .title { - color: #fed271; + color: #fed271; } .table .progress .legend li.warnings .title { - color: #ffa10f; + color: #ffa10f; } .table .progress .legend li.errors .title { - color: #f36; + color: #f36; } .table .progress .legend li.missing .title { - color: #7c8b9c; + color: #7c8b9c; } .table .progress .legend li.unreviewed .title { - color: #4fc4f6; + color: #4fc4f6; } .table .progress .legend li.all .title { - color: #aaaaaa; + color: #aaaaaa; } .table .progress .legend li .value { - font-size: 15px; - line-height: 22px; + font-size: 15px; + line-height: 22px; } .table .progress .legend li a:hover .title { - color: #ebebeb; + color: #ebebeb; } .table .progress .legend li a:hover .value { - color: #ebebeb; - font-weight: 700; + color: #ebebeb; + font-weight: 700; } diff --git a/pontoon/base/static/css/terms.css b/pontoon/base/static/css/terms.css index 84dc82945..2ed0e6d29 100644 --- a/pontoon/base/static/css/terms.css +++ b/pontoon/base/static/css/terms.css @@ -1,22 +1,22 @@ #main .container { - color: #aaaaaa; - font-size: 16px; - font-weight: 300; + color: #aaaaaa; + font-size: 16px; + font-weight: 300; } #main a:link, #main a:visited { - color: #7bc876; + color: #7bc876; } #main p, #main dd { - margin-bottom: 40px; - line-height: 1.5em; + margin-bottom: 40px; + line-height: 1.5em; } #main dt { - color: #ebebeb; - font-size: 28px; - padding-bottom: 10px; + color: #ebebeb; + font-size: 28px; + padding-bottom: 10px; } diff --git a/pontoon/base/static/js/double_list_selector.js b/pontoon/base/static/js/double_list_selector.js index 11ba44b84..080d68143 100644 --- a/pontoon/base/static/js/double_list_selector.js +++ b/pontoon/base/static/js/double_list_selector.js @@ -1,73 +1,73 @@ /* A 3-column selector to select two lists */ $(function () { - function getTarget(item) { - var list = $(item).parents('.select'); - var target = list.siblings('.select:nth-child(2)'); + function getTarget(item) { + var list = $(item).parents('.select'); + var target = list.siblings('.select:nth-child(2)'); - if (list.is('.select:nth-child(2)')) { - if ($(item).is('.left') || list.siblings().length === 1) { - target = list.siblings('.select:first-child'); - } else { - target = list.siblings('.select:last-child'); - } - } - - return target; + if (list.is('.select:nth-child(2)')) { + if ($(item).is('.left') || list.siblings().length === 1) { + target = list.siblings('.select:first-child'); + } else { + target = list.siblings('.select:last-child'); + } } - function setArrow(element, event) { - var x = event.pageX - element.offset().left; + return target; + } - if (element.outerWidth() / 2 > x) { - element.addClass('left'); - } else { - element.removeClass('left'); - } + function setArrow(element, event) { + var x = event.pageX - element.offset().left; + + if (element.outerWidth() / 2 > x) { + element.addClass('left'); + } else { + element.removeClass('left'); + } + } + + // Set translators arrow direction + $('body') + .on( + 'mouseenter', + '.double-list-selector .select:nth-child(2) li', + function (e) { + setArrow($(this), e); + }, + ) + .on( + 'mousemove', + '.double-list-selector .select:nth-child(2) li', + function (e) { + setArrow($(this), e); + }, + ); + + // Move items between lists + var mainSelector = '.double-list-selector'; + var itemSelector = mainSelector + ' .select li'; + var allSelector = mainSelector + ' .move-all'; + $('body').on('click', [itemSelector, allSelector].join(', '), function (e) { + e.preventDefault(); + + var target = getTarget(this); + var ul = target.find('ul'); + var clone = null; + + // Move selected item + if ($(this).is('li')) { + clone = $(this).remove(); + } + // Move all items in the list + else { + clone = $(this) + .parents('.select') + .find('li:visible:not(".no-match")') + .remove(); } - // Set translators arrow direction - $('body') - .on( - 'mouseenter', - '.double-list-selector .select:nth-child(2) li', - function (e) { - setArrow($(this), e); - }, - ) - .on( - 'mousemove', - '.double-list-selector .select:nth-child(2) li', - function (e) { - setArrow($(this), e); - }, - ); + ul.append(clone.removeClass('hover')); + ul.scrollTop(ul[0].scrollHeight); - // Move items between lists - var mainSelector = '.double-list-selector'; - var itemSelector = mainSelector + ' .select li'; - var allSelector = mainSelector + ' .move-all'; - $('body').on('click', [itemSelector, allSelector].join(', '), function (e) { - e.preventDefault(); - - var target = getTarget(this); - var ul = target.find('ul'); - var clone = null; - - // Move selected item - if ($(this).is('li')) { - clone = $(this).remove(); - } - // Move all items in the list - else { - clone = $(this) - .parents('.select') - .find('li:visible:not(".no-match")') - .remove(); - } - - ul.append(clone.removeClass('hover')); - ul.scrollTop(ul[0].scrollHeight); - - $('.double-list-selector .select:first-child').trigger('input').focus(); - }); + $('.double-list-selector .select:first-child').trigger('input').focus(); + }); }); diff --git a/pontoon/base/static/js/main.js b/pontoon/base/static/js/main.js index 99844b379..5e44f2142 100755 --- a/pontoon/base/static/js/main.js +++ b/pontoon/base/static/js/main.js @@ -1,466 +1,457 @@ /* Must be available immediately */ // Add case insensitive :contains-like selector to jQuery (search) $.expr[':'].containsi = function (a, i, m) { - return ( - (a.textContent || a.innerText || '') - .toUpperCase() - .indexOf(m[3].toUpperCase()) >= 0 - ); + return ( + (a.textContent || a.innerText || '') + .toUpperCase() + .indexOf(m[3].toUpperCase()) >= 0 + ); }; /* Public functions used across different files */ var Pontoon = (function (my) { - return $.extend(true, my, { - /* - * Bind NProgress (slim progress bar on top of the page) to each AJAX request - */ - NProgressBind: function () { - NProgress.configure({ showSpinner: false }); - $(document) - .bind('ajaxStart.nprogress', function () { - NProgress.start(); - }) - .bind('ajaxStop.nprogress', function () { - NProgress.done(); - }); + return $.extend(true, my, { + /* + * Bind NProgress (slim progress bar on top of the page) to each AJAX request + */ + NProgressBind: function () { + NProgress.configure({ showSpinner: false }); + $(document) + .bind('ajaxStart.nprogress', function () { + NProgress.start(); + }) + .bind('ajaxStop.nprogress', function () { + NProgress.done(); + }); + }, + + /* + * Unbind NProgress + */ + NProgressUnbind: function () { + $(document).unbind('.nprogress'); + }, + + /* + * Mark all notifications as read and update UI accordingly + */ + markAllNotificationsAsRead: function () { + this.NProgressUnbind(); + + $.ajax({ + url: '/notifications/mark-all-as-read/', + success: function () { + $('#notifications.unread .button .badge').hide(); + var unreadNotifications = $( + '.notifications .menu ul.notification-list li.notification-item[data-unread="true"]', + ); + + unreadNotifications.animate( + { backgroundColor: 'transparent' }, + 1000, + function () { + // Remove inline style and unread mark to make hover work again + unreadNotifications.removeAttr('style').removeAttr('data-unread'); + }, + ); }, + }); - /* - * Unbind NProgress - */ - NProgressUnbind: function () { - $(document).unbind('.nprogress'); + this.NProgressBind(); + }, + + /* + * Log UX action + */ + logUxAction: function (action_type, experiment, data) { + this.NProgressUnbind(); + + $.ajax({ + url: '/log-ux-action/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + action_type, + experiment, + data: JSON.stringify(data), }, + }); - /* - * Mark all notifications as read and update UI accordingly - */ - markAllNotificationsAsRead: function () { - this.NProgressUnbind(); + this.NProgressBind(); + }, - $.ajax({ - url: '/notifications/mark-all-as-read/', - success: function () { - $('#notifications.unread .button .badge').hide(); - var unreadNotifications = $( - '.notifications .menu ul.notification-list li.notification-item[data-unread="true"]', - ); - - unreadNotifications.animate( - { backgroundColor: 'transparent' }, - 1000, - function () { - // Remove inline style and unread mark to make hover work again - unreadNotifications - .removeAttr('style') - .removeAttr('data-unread'); - }, - ); - }, - }); - - this.NProgressBind(); + /* + * Close notification + */ + closeNotification: function () { + $('.notification').animate( + { + top: '-60px', }, - - /* - * Log UX action - */ - logUxAction: function (action_type, experiment, data) { - this.NProgressUnbind(); - - $.ajax({ - url: '/log-ux-action/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - action_type, - experiment, - data: JSON.stringify(data), - }, - }); - - this.NProgressBind(); + { + duration: 200, }, - - /* - * Close notification - */ - closeNotification: function () { - $('.notification').animate( - { - top: '-60px', - }, - { - duration: 200, - }, - function () { - $(this).addClass('hide').empty(); - }, - ); + function () { + $(this).addClass('hide').empty(); }, + ); + }, - /* - * Remove loader - * - * text End of operation text (e.g. Done!) - * type Notification type (e.g. error) - * duration How long should the notification remain open (default: 2000 ms) - */ - endLoader: function (text, type, duration) { - if (text) { - $('.notification') - .html('
  • ' + text + '
  • ') - .removeClass('hide') - .animate( - { - top: 0, - }, - { - duration: 200, - }, - ); - } + /* + * Remove loader + * + * text End of operation text (e.g. Done!) + * type Notification type (e.g. error) + * duration How long should the notification remain open (default: 2000 ms) + */ + endLoader: function (text, type, duration) { + if (text) { + $('.notification') + .html('
  • ' + text + '
  • ') + .removeClass('hide') + .animate( + { + top: 0, + }, + { + duration: 200, + }, + ); + } - if (Pontoon.notificationTimeout) { - clearTimeout(Pontoon.notificationTimeout); - } - Pontoon.notificationTimeout = setTimeout(function () { - Pontoon.closeNotification(); - }, duration || 2000); - }, + if (Pontoon.notificationTimeout) { + clearTimeout(Pontoon.notificationTimeout); + } + Pontoon.notificationTimeout = setTimeout(function () { + Pontoon.closeNotification(); + }, duration || 2000); + }, - /* - * Do not render HTML tags - * - * string String that has to be displayed as is instead of rendered - */ - doNotRender: function (string) { - return $('
    ').text(string).html(); - }, - }); + /* + * Do not render HTML tags + * + * string String that has to be displayed as is instead of rendered + */ + doNotRender: function (string) { + return $('
    ').text(string).html(); + }, + }); })(Pontoon || {}); /* Main code */ $(function () { - /* - * If Google Analytics is enabled, the translate frontend will send additional about Ajax calls. - * - * To send an event to GA, We pass following informations: - * event category - hardcoded 'ajax' string. - * event action - hardcoded 'request' string. - * event label - contains url that was called by $.ajax() call. - * - * GA Analytics enriches every event with additional information like e.g. browser, resolution, country etc. - */ - $(document).ajaxComplete(function (event, jqXHR, settings) { - if (typeof ga !== 'function') { - return; - } + /* + * If Google Analytics is enabled, the translate frontend will send additional about Ajax calls. + * + * To send an event to GA, We pass following informations: + * event category - hardcoded 'ajax' string. + * event action - hardcoded 'request' string. + * event label - contains url that was called by $.ajax() call. + * + * GA Analytics enriches every event with additional information like e.g. browser, resolution, country etc. + */ + $(document).ajaxComplete(function (event, jqXHR, settings) { + if (typeof ga !== 'function') { + return; + } - ga('send', 'event', 'ajax', 'request', settings.url); - }); + ga('send', 'event', 'ajax', 'request', settings.url); + }); - /* - * Display Pontoon Add-On Promotion, if: - * - * - Promotion not dismissed - * - Add-On not installed - * - Page loaded on Firefox or Chrome (add-on not available for other browsers) - */ - setTimeout(function () { - var dismissed = !$('#addon-promotion').length; - var installed = window.PontoonAddon && window.PontoonAddon.installed; - if (!dismissed && !installed) { - var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; - var isChrome = navigator.userAgent.indexOf('Chrome') !== -1; - var downloadHref = ''; - if (isFirefox) { - downloadHref = - 'https://addons.mozilla.org/firefox/addon/pontoon-tools/'; - } - if (isChrome) { - downloadHref = - 'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb'; - } - if (downloadHref) { - $('#addon-promotion').find('.get').attr('href', downloadHref); - $('body').addClass('addon-promotion-active'); - } - } - // window.PontoonAddon is made available by the Pontoon Add-On, - // but not immediatelly after the DOM is ready - }, 1000); + /* + * Display Pontoon Add-On Promotion, if: + * + * - Promotion not dismissed + * - Add-On not installed + * - Page loaded on Firefox or Chrome (add-on not available for other browsers) + */ + setTimeout(function () { + var dismissed = !$('#addon-promotion').length; + var installed = window.PontoonAddon && window.PontoonAddon.installed; + if (!dismissed && !installed) { + var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; + var isChrome = navigator.userAgent.indexOf('Chrome') !== -1; + var downloadHref = ''; + if (isFirefox) { + downloadHref = + 'https://addons.mozilla.org/firefox/addon/pontoon-tools/'; + } + if (isChrome) { + downloadHref = + 'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb'; + } + if (downloadHref) { + $('#addon-promotion').find('.get').attr('href', downloadHref); + $('body').addClass('addon-promotion-active'); + } + } + // window.PontoonAddon is made available by the Pontoon Add-On, + // but not immediatelly after the DOM is ready + }, 1000); - // Dismiss Add-On Promotion - $('#addon-promotion .dismiss').click(function () { - Pontoon.NProgressUnbind(); + // Dismiss Add-On Promotion + $('#addon-promotion .dismiss').click(function () { + Pontoon.NProgressUnbind(); - $.ajax({ - url: '/dismiss-addon-promotion/', - success: function () { - $('body').removeClass('addon-promotion-active'); - }, - }); - - Pontoon.NProgressBind(); - }); - - // Hide Add-On Promotion if Add-On installed while active - window.addEventListener('message', (event) => { - // only allow messages from authorized senders (extension content script, or Pontoon itself) - if (event.origin !== window.origin || event.source !== window) { - return; - } - let data; - switch (typeof event.data) { - case 'object': - data = event.data; - break; - case 'string': - // backward compatibility - // TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released - // and convert this switch into a condition - try { - data = JSON.parse(event.data); - } catch (_) { - return; - } - break; - } - if (data && data._type === 'PontoonAddonInfo' && data.value) { - if (data.value.installed === true) { - $('body').removeClass('addon-promotion-active'); - } - } + $.ajax({ + url: '/dismiss-addon-promotion/', + success: function () { + $('body').removeClass('addon-promotion-active'); + }, }); Pontoon.NProgressBind(); + }); - // Log display of the unread notification icon - if ($('#notifications').is('.unread')) { - Pontoon.logUxAction( - 'Render: Unread notifications icon', - 'Notifications 1.0', - { - pathname: window.location.pathname, - }, - ); + // Hide Add-On Promotion if Add-On installed while active + window.addEventListener('message', (event) => { + // only allow messages from authorized senders (extension content script, or Pontoon itself) + if (event.origin !== window.origin || event.source !== window) { + return; } - - // Log clicks on the notifications icon - $('#notifications .button').click(function () { - if ($('#notifications').is('.opened')) { - return; + let data; + switch (typeof event.data) { + case 'object': + data = event.data; + break; + case 'string': + // backward compatibility + // TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released + // and convert this switch into a condition + try { + data = JSON.parse(event.data); + } catch (_) { + return; } - - Pontoon.logUxAction('Click: Notifications icon', 'Notifications 1.0', { - pathname: window.location.pathname, - unread: $('#notifications').is('.unread'), - }); - }); - - // Display any notifications - var notifications = $('.notification li'); - if (notifications.length) { - Pontoon.endLoader(notifications.text()); + break; } - - // Close notification on click - $('body > header').on('click', '.notification', function () { - Pontoon.closeNotification(); - }); - - // Mark notifications as read when notification menu opens - $('#notifications.unread .button').click(function () { - Pontoon.markAllNotificationsAsRead(); - }); - - function getRedirectUrl() { - return window.location.pathname + window.location.search; + if (data && data._type === 'PontoonAddonInfo' && data.value) { + if (data.value.installed === true) { + $('body').removeClass('addon-promotion-active'); + } } + }); - // Sign in button action - $('#fxa-sign-in, #standalone-signin a, #sidebar-signin').on( - 'click', - function () { - var $this = $(this); - var loginUrl = $this.prop('href'), - startSign = loginUrl.match(/\?/) ? '&' : '?'; - $this.prop( - 'href', - loginUrl + startSign + 'next=' + getRedirectUrl(), - ); - }, + Pontoon.NProgressBind(); + + // Log display of the unread notification icon + if ($('#notifications').is('.unread')) { + Pontoon.logUxAction( + 'Render: Unread notifications icon', + 'Notifications 1.0', + { + pathname: window.location.pathname, + }, ); + } - // Sign out button action - $('.sign-out a, #sign-out a').on('click', function (ev) { - var $this = $(this), - $form = $this.find('form'); + // Log clicks on the notifications icon + $('#notifications .button').click(function () { + if ($('#notifications').is('.opened')) { + return; + } - ev.preventDefault(); - $form.prop('action', $this.prop('href') + '?next=' + getRedirectUrl()); - $form.submit(); + Pontoon.logUxAction('Click: Notifications icon', 'Notifications 1.0', { + pathname: window.location.pathname, + unread: $('#notifications').is('.unread'), + }); + }); + + // Display any notifications + var notifications = $('.notification li'); + if (notifications.length) { + Pontoon.endLoader(notifications.text()); + } + + // Close notification on click + $('body > header').on('click', '.notification', function () { + Pontoon.closeNotification(); + }); + + // Mark notifications as read when notification menu opens + $('#notifications.unread .button').click(function () { + Pontoon.markAllNotificationsAsRead(); + }); + + function getRedirectUrl() { + return window.location.pathname + window.location.search; + } + + // Sign in button action + $('#fxa-sign-in, #standalone-signin a, #sidebar-signin').on( + 'click', + function () { + var $this = $(this); + var loginUrl = $this.prop('href'), + startSign = loginUrl.match(/\?/) ? '&' : '?'; + $this.prop('href', loginUrl + startSign + 'next=' + getRedirectUrl()); + }, + ); + + // Sign out button action + $('.sign-out a, #sign-out a').on('click', function (ev) { + var $this = $(this), + $form = $this.find('form'); + + ev.preventDefault(); + $form.prop('action', $this.prop('href') + '?next=' + getRedirectUrl()); + $form.submit(); + }); + + // Show/hide menu on click + $('body').on('click', '.selector', function (e) { + if (!$(this).siblings('.menu').is(':visible')) { + e.stopPropagation(); + $('.menu:not(".permanent")').hide(); + $('.select').removeClass('opened'); + $(this) + .siblings('.menu') + .show() + .end() + .parents('.select') + .addClass('opened'); + $('.menu:not(".permanent"):visible input[type=search]') + .focus() + .trigger('input'); + } + }); + + // Hide menus on click outside + $('body').bind('click.main', function () { + $('.menu:not(".permanent")').hide(); + $('.select').removeClass('opened'); + $('.menu:not(".permanent") li').removeClass('hover'); + }); + + // Menu hover + $('body') + .on('mouseenter', '.menu li', function () { + // Ignore on nested menus + if ($(this).parents('li').length) { + return false; + } + + $('.menu li.hover').removeClass('hover'); + $(this).toggleClass('hover'); + }) + .on('mouseleave', '.menu li', function () { + // Ignore on nested menus + if ($(this).parents('li').length) { + return false; + } + + $('.menu li.hover').removeClass('hover'); }); - // Show/hide menu on click - $('body').on('click', '.selector', function (e) { - if (!$(this).siblings('.menu').is(':visible')) { - e.stopPropagation(); - $('.menu:not(".permanent")').hide(); - $('.select').removeClass('opened'); - $(this) - .siblings('.menu') - .show() - .end() - .parents('.select') - .addClass('opened'); - $('.menu:not(".permanent"):visible input[type=search]') - .focus() - .trigger('input'); - } + // Menu search + $('body') + .on('click', '.menu input[type=search]', function (e) { + e.stopPropagation(); + }) + .on('input.search', '.menu input[type=search]', function (e) { + // Tab + if (e.which === 9) { + return; + } + + var ul = $(this).parent().siblings('ul'), + val = $(this).val(), + // Only search a limited set if defined + limited = ul.find('li.limited').length > 0 ? '.limited' : ''; + + ul.find('li' + limited) + .show() + .end() + .find('li' + limited + ':not(":containsi(\'' + val + '\')")') + .hide(); + + if (ul.find('li:not(".no-match"):visible').length === 0) { + ul.find('.no-match').show(); + } else { + ul.find('.no-match').hide(); + } + }) + .on('keydown.search', '.menu input[type=search]', function (e) { + // Prevent form submission on Enter + if (e.which === 13) { + return false; + } }); - // Hide menus on click outside - $('body').bind('click.main', function () { - $('.menu:not(".permanent")').hide(); - $('.select').removeClass('opened'); - $('.menu:not(".permanent") li').removeClass('hover'); - }); + // General keyboard shortcuts + generalShortcutsHandler = function (e) { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); - // Menu hover - $('body') - .on('mouseenter', '.menu li', function () { - // Ignore on nested menus - if ($(this).parents('li').length) { - return false; - } + function moveMenu(type) { + var options = + type === 'up' ? ['first', 'last', -1] : ['last', 'first', 1]; + var items = menu.find('li:visible:not(.horizontal-separator, :has(li))'); + var element = null; - $('.menu li.hover').removeClass('hover'); - $(this).toggleClass('hover'); - }) - .on('mouseleave', '.menu li', function () { - // Ignore on nested menus - if ($(this).parents('li').length) { - return false; - } + if ( + hovered.length === 0 || + menu.find('li:not(:has(li)):visible:' + options[0]).is('.hover') + ) { + menu.find('li.hover').removeClass('hover'); + element = items[options[1]](); + } else { + var current = menu.find('li.hover'), + next = items.index(current) + options[2]; - $('.menu li.hover').removeClass('hover'); + current.removeClass('hover'); + element = $(items.get(next)); + } + + if (element) { + const behavior = mediaQuery.matches ? 'auto' : 'smooth'; + element.addClass('hover'); + element[0].scrollIntoView({ + behavior: behavior, + block: 'nearest', }); + } + } - // Menu search - $('body') - .on('click', '.menu input[type=search]', function (e) { - e.stopPropagation(); - }) - .on('input.search', '.menu input[type=search]', function (e) { - // Tab - if (e.which === 9) { - return; - } + var key = e.which; - var ul = $(this).parent().siblings('ul'), - val = $(this).val(), - // Only search a limited set if defined - limited = ul.find('li.limited').length > 0 ? '.limited' : ''; + if ($('.menu:not(".permanent")').is(':visible')) { + var menu = $('.menu:not(".permanent"):visible'), + hovered = menu.find('li.hover'); - ul.find('li' + limited) - .show() - .end() - .find('li' + limited + ':not(":containsi(\'' + val + '\')")') - .hide(); + // Skip for the tabs + if (menu.is('.tabs')) { + return; + } - if (ul.find('li:not(".no-match"):visible').length === 0) { - ul.find('.no-match').show(); - } else { - ul.find('.no-match').hide(); - } - }) - .on('keydown.search', '.menu input[type=search]', function (e) { - // Prevent form submission on Enter - if (e.which === 13) { - return false; - } - }); + // Up arrow + if (key === 38) { + moveMenu('up'); + return false; + } - // General keyboard shortcuts - generalShortcutsHandler = function (e) { - const mediaQuery = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ); + // Down arrow + if (key === 40) { + moveMenu('down'); + return false; + } - function moveMenu(type) { - var options = - type === 'up' ? ['first', 'last', -1] : ['last', 'first', 1]; - var items = menu.find( - 'li:visible:not(.horizontal-separator, :has(li))', - ); - var element = null; - - if ( - hovered.length === 0 || - menu.find('li:not(:has(li)):visible:' + options[0]).is('.hover') - ) { - menu.find('li.hover').removeClass('hover'); - element = items[options[1]](); - } else { - var current = menu.find('li.hover'), - next = items.index(current) + options[2]; - - current.removeClass('hover'); - element = $(items.get(next)); - } - - if (element) { - const behavior = mediaQuery.matches ? 'auto' : 'smooth'; - element.addClass('hover'); - element[0].scrollIntoView({ - behavior: behavior, - block: 'nearest', - }); - } + // Enter: confirm + if (key === 13) { + var a = hovered.find('a'); + if (a.length > 0) { + a.click(); + } else { + hovered.click(); } + return false; + } - var key = e.which; - - if ($('.menu:not(".permanent")').is(':visible')) { - var menu = $('.menu:not(".permanent"):visible'), - hovered = menu.find('li.hover'); - - // Skip for the tabs - if (menu.is('.tabs')) { - return; - } - - // Up arrow - if (key === 38) { - moveMenu('up'); - return false; - } - - // Down arrow - if (key === 40) { - moveMenu('down'); - return false; - } - - // Enter: confirm - if (key === 13) { - var a = hovered.find('a'); - if (a.length > 0) { - a.click(); - } else { - hovered.click(); - } - return false; - } - - // Escape: close - if (key === 27) { - $('body').click(); - return false; - } - } - }; - $('html').on('keydown', generalShortcutsHandler); + // Escape: close + if (key === 27) { + $('body').click(); + return false; + } + } + }; + $('html').on('keydown', generalShortcutsHandler); }); diff --git a/pontoon/base/static/js/progress-chart.js b/pontoon/base/static/js/progress-chart.js index dbd58768e..5ee99438a 100644 --- a/pontoon/base/static/js/progress-chart.js +++ b/pontoon/base/static/js/progress-chart.js @@ -2,77 +2,73 @@ * Draw progress indicator and value */ $(function () { - $('canvas.chart').each(function () { - // Get data - var stats = {}, - progress = $(this).parents('.progress'); + $('canvas.chart').each(function () { + // Get data + var stats = {}, + progress = $(this).parents('.progress'); - progress - .siblings('.legend') - .find('li') - .each(function () { - stats[$(this).attr('class')] = $(this) - .find('.value') - .data('value'); - }); + progress + .siblings('.legend') + .find('li') + .each(function () { + stats[$(this).attr('class')] = $(this).find('.value').data('value'); + }); - stats.all = progress - .siblings('.non-plottable') - .find('.all .value') - .data('value'); + stats.all = progress + .siblings('.non-plottable') + .find('.all .value') + .data('value'); - var fraction = { - translated: stats.all ? stats.translated / stats.all : 0, - fuzzy: stats.all ? stats.fuzzy / stats.all : 0, - warnings: stats.all ? stats.warnings / stats.all : 0, - errors: stats.all ? stats.errors / stats.all : 0, - missing: stats.all - ? stats.missing / stats.all - : 1 /* Draw "empty" progress if no projects enabled */, - }, - number = Math.floor( - (fraction.translated + fraction.warnings) * 100, - ); + var fraction = { + translated: stats.all ? stats.translated / stats.all : 0, + fuzzy: stats.all ? stats.fuzzy / stats.all : 0, + warnings: stats.all ? stats.warnings / stats.all : 0, + errors: stats.all ? stats.errors / stats.all : 0, + missing: stats.all + ? stats.missing / stats.all + : 1 /* Draw "empty" progress if no projects enabled */, + }, + number = Math.floor((fraction.translated + fraction.warnings) * 100); - // Update graph - var canvas = this, - context = canvas.getContext('2d'); + // Update graph + var canvas = this, + context = canvas.getContext('2d'); - // Set up canvas to be HiDPI display ready - var dpr = window.devicePixelRatio || 1; - canvas.style.width = canvas.width + 'px'; - canvas.style.height = canvas.height + 'px'; - canvas.width = canvas.width * dpr; - canvas.height = canvas.height * dpr; + // Set up canvas to be HiDPI display ready + var dpr = window.devicePixelRatio || 1; + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + canvas.width = canvas.width * dpr; + canvas.height = canvas.height * dpr; - // Clear old canvas content to avoid aliasing - context.clearRect(0, 0, canvas.width, canvas.height); - context.lineWidth = 3 * dpr; + // Clear old canvas content to avoid aliasing + context.clearRect(0, 0, canvas.width, canvas.height); + context.lineWidth = 3 * dpr; - var x = canvas.width / 2, - y = canvas.height / 2, - radius = (canvas.width - context.lineWidth) / 2, - end = -0.5; + var x = canvas.width / 2, + y = canvas.height / 2, + radius = (canvas.width - context.lineWidth) / 2, + end = -0.5; - progress - .siblings('.legend') - .find('li') - .each(function () { - var length = fraction[$(this).attr('class')] * 2, - start = end, - color = window - .getComputedStyle($(this).find('.status')[0], ':before') - .getPropertyValue('color'); + progress + .siblings('.legend') + .find('li') + .each(function () { + var length = fraction[$(this).attr('class')] * 2, + start = end, + color = window + .getComputedStyle($(this).find('.status')[0], ':before') + .getPropertyValue('color'); - end = start + length; + end = start + length; - context.beginPath(); - context.arc(x, y, radius, start * Math.PI, end * Math.PI); - context.strokeStyle = color; - context.stroke(); - }); + context.beginPath(); + context.arc(x, y, radius, start * Math.PI, end * Math.PI); + context.strokeStyle = color; + context.stroke(); + }); - // Update number - progress.find('.number').html(number).show(); - }); + // Update number + progress.find('.number').html(number).show(); + }); }); diff --git a/pontoon/base/static/js/sidebar_menu.js b/pontoon/base/static/js/sidebar_menu.js index f0a936804..f86d7b91e 100644 --- a/pontoon/base/static/js/sidebar_menu.js +++ b/pontoon/base/static/js/sidebar_menu.js @@ -1,14 +1,14 @@ /* Sidebar with left column acting as menu and right column as panel do display content */ $(function () { - $('body').on('click', '.menu.left-column > ul > li > a', function (e) { - e.preventDefault(); + $('body').on('click', '.menu.left-column > ul > li > a', function (e) { + e.preventDefault(); - $(this) - .parents('li') - .addClass('selected') - .siblings() - .removeClass('selected'); + $(this) + .parents('li') + .addClass('selected') + .siblings() + .removeClass('selected'); - $($(this).data('target')).show().siblings().hide(); - }); + $($(this).data('target')).show().siblings().hide(); + }); }); diff --git a/pontoon/base/static/js/table.js b/pontoon/base/static/js/table.js index 29be150fd..36179f921 100644 --- a/pontoon/base/static/js/table.js +++ b/pontoon/base/static/js/table.js @@ -1,279 +1,257 @@ /* Must be available immediately */ // Add case insensitive :contains-like selector to jQuery (search & filter) $.expr[':'].containsi = function (a, i, m) { - return ( - (a.textContent || a.innerText || '') - .toUpperCase() - .indexOf(m[3].toUpperCase()) >= 0 - ); + return ( + (a.textContent || a.innerText || '') + .toUpperCase() + .indexOf(m[3].toUpperCase()) >= 0 + ); }; /* Latest activity tooltip */ var date_formatter = new Intl.DateTimeFormat('en-GB', { - day: 'numeric', - month: 'long', - year: 'numeric', - }), - time_formatter = new Intl.DateTimeFormat('en-GB', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }), - timer = null, - delay = 500; + day: 'numeric', + month: 'long', + year: 'numeric', + }), + time_formatter = new Intl.DateTimeFormat('en-GB', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), + timer = null, + delay = 500; $('body') - .on('mouseenter', '.latest-activity .latest time', function () { - var $element = $(this); + .on('mouseenter', '.latest-activity .latest time', function () { + var $element = $(this); - timer = setTimeout(function () { - var translation = Pontoon.doNotRender($element.data('translation')), - avatar = $element.data('user-avatar'), - action = $element.data('action'), - name = $element.data('user-name'), - link = $element.data('user-link'), - date = date_formatter.format( - new Date($element.attr('datetime')), - ), - time = time_formatter.format( - new Date($element.attr('datetime')), - ); + timer = setTimeout(function () { + var translation = Pontoon.doNotRender($element.data('translation')), + avatar = $element.data('user-avatar'), + action = $element.data('action'), + name = $element.data('user-name'), + link = $element.data('user-link'), + date = date_formatter.format(new Date($element.attr('datetime'))), + time = time_formatter.format(new Date($element.attr('datetime'))); - $element.after( - '', - ); - }, delay); - }) - .on('mouseleave', 'td.latest-activity', function () { - $('.latest-activity .latest .tooltip').remove(); - clearTimeout(timer); - }); + $element.after( + '', + ); + }, delay); + }) + .on('mouseleave', 'td.latest-activity', function () { + $('.latest-activity .latest .tooltip').remove(); + clearTimeout(timer); + }); /* Public functions used across different files */ var Pontoon = (function (my) { - return $.extend(true, my, { - table: { - /* - * Filter table - * - * TODO: remove old search code from main.js - */ - filter: (function () { - $('body').on( - 'input.filter', - 'input.table-filter', - function (e) { - if (e.which === 9) { - return; - } + return $.extend(true, my, { + table: { + /* + * Filter table + * + * TODO: remove old search code from main.js + */ + filter: (function () { + $('body').on('input.filter', 'input.table-filter', function (e) { + if (e.which === 9) { + return; + } - // Filter input field - var field = $(this), - // Selector of the element containing a list of items to filter - list = $(this).data('list') || '.table-sort tbody', - // Selector of the list item element, relative to list - item = $(this).data('item') || 'tr', - // Selector of the list item element's child to match filter query against - filter = $(this).data('filter') || 'td:first-child'; + // Filter input field + var field = $(this), + // Selector of the element containing a list of items to filter + list = $(this).data('list') || '.table-sort tbody', + // Selector of the list item element, relative to list + item = $(this).data('item') || 'tr', + // Selector of the list item element's child to match filter query against + filter = $(this).data('filter') || 'td:first-child'; - $(list) - .find(item + '.limited') - .hide() - .end() - .find( - item + - '.limited ' + - filter + - ':containsi("' + - $(field).val() + - '")', - ) - .parents(item) - .show(); - }, - ); - })(), + $(list) + .find(item + '.limited') + .hide() + .end() + .find( + item + + '.limited ' + + filter + + ':containsi("' + + $(field).val() + + '")', + ) + .parents(item) + .show(); + }); + })(), - /* - * Sort table - */ - sort: (function () { - $('body').on('click', 'table.table-sort th', function () { - function getProgress(el) { - var legend = $(el).find('.progress .legend'), - all = legend.find('.all .value').data('value') || 0, - translated = - legend - .find('.translated .value') - .data('value') / all || 0, - fuzzy = - legend.find('.fuzzy .value').data('value') / - all || 0; + /* + * Sort table + */ + sort: (function () { + $('body').on('click', 'table.table-sort th', function () { + function getProgress(el) { + var legend = $(el).find('.progress .legend'), + all = legend.find('.all .value').data('value') || 0, + translated = + legend.find('.translated .value').data('value') / all || 0, + fuzzy = legend.find('.fuzzy .value').data('value') / all || 0; - if ($(el).find('.progress .not-ready').length) { - return 'not-ready'; - } + if ($(el).find('.progress .not-ready').length) { + return 'not-ready'; + } - return { - translated: translated, - fuzzy: fuzzy, - }; - } + return { + translated: translated, + fuzzy: fuzzy, + }; + } - function getUnreviewed(el) { - return parseInt( - $(el) - .find('.progress .legend .unreviewed .value') - .data('value') || 0, - ); - } + function getUnreviewed(el) { + return parseInt( + $(el) + .find('.progress .legend .unreviewed .value') + .data('value') || 0, + ); + } - function getTime(el) { - var date = - $(el) - .find('td:eq(' + index + ')') - .find('time') - .attr('datetime') || 0; - return new Date(date).getTime(); - } + function getTime(el) { + var date = + $(el) + .find('td:eq(' + index + ')') + .find('time') + .attr('datetime') || 0; + return new Date(date).getTime(); + } - function getPriority(el) { - return $(el).find('.priority .fa-star.active').length; - } + function getPriority(el) { + return $(el).find('.priority .fa-star.active').length; + } - function getEnabled(el) { - return $(el).find('.check.enabled').length; - } + function getEnabled(el) { + return $(el).find('.check.enabled').length; + } - function getNumber(el) { - return parseInt( - $(el).find('span').text().replace(/,/g, ''), - ); - } + function getNumber(el) { + return parseInt($(el).find('span').text().replace(/,/g, '')); + } - function getString(el) { - return $(el) - .find('td:eq(' + index + ')') - .text(); - } + function getString(el) { + return $(el) + .find('td:eq(' + index + ')') + .text(); + } - var node = $(this), - index = node.index(), - table = node.parents('.table-sort'), - list = table.find('tbody'), - items = list.find('tr'), - dir = node.hasClass('asc') ? -1 : 1, - cls = node.hasClass('asc') ? 'desc' : 'asc'; + var node = $(this), + index = node.index(), + table = node.parents('.table-sort'), + list = table.find('tbody'), + items = list.find('tr'), + dir = node.hasClass('asc') ? -1 : 1, + cls = node.hasClass('asc') ? 'desc' : 'asc'; - // Default value for rows which don't have a timestamp - if (node.is('.deadline')) { - var defaultTime = new Date(0).getTime(); - } + // Default value for rows which don't have a timestamp + if (node.is('.deadline')) { + var defaultTime = new Date(0).getTime(); + } - $(table).find('th').removeClass('asc desc'); - node.addClass(cls); + $(table).find('th').removeClass('asc desc'); + node.addClass(cls); - items.sort(function (a, b) { - // Sort by translated, then by fuzzy percentage - if (node.is('.progress')) { - var chartA = getProgress(a), - chartB = getProgress(b); + items.sort(function (a, b) { + // Sort by translated, then by fuzzy percentage + if (node.is('.progress')) { + var chartA = getProgress(a), + chartB = getProgress(b); - if (chartA === 'not-ready') { - if (chartB === 'not-ready') { - return 0; - } else { - return -1 * dir; - } - } - if (chartB === 'not-ready') { - return 1 * dir; - } + if (chartA === 'not-ready') { + if (chartB === 'not-ready') { + return 0; + } else { + return -1 * dir; + } + } + if (chartB === 'not-ready') { + return 1 * dir; + } - return ( - (chartA.translated - chartB.translated) * dir || - (chartA.fuzzy - chartB.fuzzy) * dir - ); + return ( + (chartA.translated - chartB.translated) * dir || + (chartA.fuzzy - chartB.fuzzy) * dir + ); - // Sort by unreviewed state - } else if (node.is('.unreviewed-status')) { - return (getUnreviewed(b) - getUnreviewed(a)) * dir; + // Sort by unreviewed state + } else if (node.is('.unreviewed-status')) { + return (getUnreviewed(b) - getUnreviewed(a)) * dir; - // Sort by deadline - } else if (node.is('.deadline')) { - var timeA = getTime(a), - timeB = getTime(b); + // Sort by deadline + } else if (node.is('.deadline')) { + var timeA = getTime(a), + timeB = getTime(b); - if ( - timeA === defaultTime && - timeB === defaultTime - ) { - return ( - getString(a).localeCompare(getString(b)) * - dir - ); - } else if (timeA === defaultTime) { - return 1 * dir; - } else if (timeB === defaultTime) { - return -1 * dir; - } - return (timeA - timeB) * dir; + if (timeA === defaultTime && timeB === defaultTime) { + return getString(a).localeCompare(getString(b)) * dir; + } else if (timeA === defaultTime) { + return 1 * dir; + } else if (timeB === defaultTime) { + return -1 * dir; + } + return (timeA - timeB) * dir; - // Sort by last activity - } else if (node.is('.latest-activity')) { - return (getTime(b) - getTime(a)) * dir; + // Sort by last activity + } else if (node.is('.latest-activity')) { + return (getTime(b) - getTime(a)) * dir; - // Sort by priority - } else if (node.is('.priority')) { - return (getPriority(b) - getPriority(a)) * dir; + // Sort by priority + } else if (node.is('.priority')) { + return (getPriority(b) - getPriority(a)) * dir; - // Sort by enabled state - } else if (node.is('.check')) { - return (getEnabled(a) - getEnabled(b)) * dir; + // Sort by enabled state + } else if (node.is('.check')) { + return (getEnabled(a) - getEnabled(b)) * dir; - // Sort by number of speakers - } else if (node.is('.population')) { - return (getNumber(a) - getNumber(b)) * dir; + // Sort by number of speakers + } else if (node.is('.population')) { + return (getNumber(a) - getNumber(b)) * dir; - // Sort by alphabetical order - } else { - return ( - getString(a).localeCompare(getString(b)) * dir - ); - } - }); + // Sort by alphabetical order + } else { + return getString(a).localeCompare(getString(b)) * dir; + } + }); - list.append(items); - }); - })(), - }, - }); + list.append(items); + }); + })(), + }, + }); })(Pontoon || {}); diff --git a/pontoon/base/static/js/tabs.js b/pontoon/base/static/js/tabs.js index 9153bd4c6..feedee1d2 100644 --- a/pontoon/base/static/js/tabs.js +++ b/pontoon/base/static/js/tabs.js @@ -2,109 +2,109 @@ * Manage tab content as single-page application */ $(function () { - var urlSplit = $('#server').data('url-split'), - container = $('#main .container'), - inProgress = false; + var urlSplit = $('#server').data('url-split'), + container = $('#main .container'), + inProgress = false; - // Page load + // Page load + loadTabContent(window.location.pathname + window.location.search); + + // History + window.onpopstate = function () { loadTabContent(window.location.pathname + window.location.search); + }; - // History - window.onpopstate = function () { - loadTabContent(window.location.pathname + window.location.search); - }; + // Menu + $('body').on( + 'click', + '#middle .links a, #main .contributors .links a', + function (e) { + // Keep default middle-, control- and command-click behaviour (open in new tab) + if (e.which === 2 || e.metaKey || e.ctrlKey) { + return; + } - // Menu - $('body').on( - 'click', - '#middle .links a, #main .contributors .links a', - function (e) { - // Keep default middle-, control- and command-click behaviour (open in new tab) - if (e.which === 2 || e.metaKey || e.ctrlKey) { - return; - } + // Filtered teams are only supported by the Teams tab, so we need to drop them + // when switching to other tabs and update stats in the heading section by + // reloading the page + if (new URLSearchParams(window.location.search).get('teams')) { + return; + } - // Filtered teams are only supported by the Teams tab, so we need to drop them - // when switching to other tabs and update stats in the heading section by - // reloading the page - if (new URLSearchParams(window.location.search).get('teams')) { - return; - } + e.preventDefault(); - e.preventDefault(); + var url = $(this).attr('href'); + loadTabContent(url); + window.history.pushState({}, '', url); + }, + ); - var url = $(this).attr('href'); - loadTabContent(url); - window.history.pushState({}, '', url); + function showTabMessage(text) { + var message = $('

    ', { + class: 'no-results', + html: text, + }); + + container.append(message); + } + + function updateTabCount(tab, count) { + tab.find('span').remove(); + if (count > 0) { + $('', { + class: 'count', + html: count, + }).appendTo(tab); + } + } + + function loadTabContent(path) { + if (inProgress) { + inProgress.abort(); + } + + var url = '/' + path.split('/' + urlSplit + '/')[1], + tab = $('#middle .links a[href="' + path.split('?')[0] + '"]'); + + // Update menu + $('#middle .links li').removeClass('active'); + tab.parents('li').addClass('active'); + + container.empty(); + + if (url !== '/bugs/') { + inProgress = $.ajax({ + url: '/' + urlSplit + '/ajax' + url, + success: function (data) { + container.append(data); + + if (url.startsWith('/contributors/')) { + var count = $('table > tbody > tr').length; + updateTabCount(tab, count); + } + + if (url.startsWith('/insights/')) { + Pontoon.insights.initialize(); + } + + if (url === '/') { + $('.controls input').focus(); + } }, - ); - - function showTabMessage(text) { - var message = $('

    ', { - class: 'no-results', - html: text, - }); - - container.append(message); - } - - function updateTabCount(tab, count) { - tab.find('span').remove(); - if (count > 0) { - $('', { - class: 'count', - html: count, - }).appendTo(tab); - } - } - - function loadTabContent(path) { - if (inProgress) { - inProgress.abort(); - } - - var url = '/' + path.split('/' + urlSplit + '/')[1], - tab = $('#middle .links a[href="' + path.split('?')[0] + '"]'); - - // Update menu - $('#middle .links li').removeClass('active'); - tab.parents('li').addClass('active'); - - container.empty(); - - if (url !== '/bugs/') { - inProgress = $.ajax({ - url: '/' + urlSplit + '/ajax' + url, - success: function (data) { - container.append(data); - - if (url.startsWith('/contributors/')) { - var count = $('table > tbody > tr').length; - updateTabCount(tab, count); - } - - if (url.startsWith('/insights/')) { - Pontoon.insights.initialize(); - } - - if (url === '/') { - $('.controls input').focus(); - } - }, - error: function (error) { - if (error.status === 0 && error.statusText !== 'abort') { - showTabMessage('Oops, something went wrong.'); - } - }, - }); - } else { - inProgress = Pontoon.bugzilla.getLocaleBugs( - $('#server').data('locale'), - container, - tab, - updateTabCount, - showTabMessage, - ); - } + error: function (error) { + if (error.status === 0 && error.statusText !== 'abort') { + showTabMessage('Oops, something went wrong.'); + } + }, + }); + } else { + inProgress = Pontoon.bugzilla.getLocaleBugs( + $('#server').data('locale'), + container, + tab, + updateTabCount, + showTabMessage, + ); } + } }); diff --git a/pontoon/contributors/static/css/contributor.css b/pontoon/contributors/static/css/contributor.css index c62cc6dee..e8529f966 100644 --- a/pontoon/contributors/static/css/contributor.css +++ b/pontoon/contributors/static/css/contributor.css @@ -1,79 +1,79 @@ #heading, #middle { - text-align: center; + text-align: center; } a.avatar { - display: block; - position: relative; + display: block; + position: relative; } a.avatar .desc { - bottom: 0; - color: #ffffff; - display: none; - font-size: 16px; - font-weight: bold; - height: 20px; - left: 0; - letter-spacing: 0; - margin: auto; - position: absolute; - right: 0; - top: 0; + bottom: 0; + color: #ffffff; + display: none; + font-size: 16px; + font-weight: bold; + height: 20px; + left: 0; + letter-spacing: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; } a.avatar:hover .desc { - display: block; + display: block; } a.avatar:hover .desc ~ img { - opacity: 0.3; + opacity: 0.3; } #username { - color: #ebebeb; - font-size: 48px; - letter-spacing: normal; - padding: 8px 0 20px; - text-transform: none; + color: #ebebeb; + font-size: 48px; + letter-spacing: normal; + padding: 8px 0 20px; + text-transform: none; } .info { - list-style: none; - margin: 0; - display: inline-block; - text-align: left; + list-style: none; + margin: 0; + display: inline-block; + text-align: left; } .info li { - color: #aaaaaa; - display: table; - font-size: 16px; - font-weight: 300; - overflow: hidden; - padding: 4px 0; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 980px; + color: #aaaaaa; + display: table; + font-size: 16px; + font-weight: 300; + overflow: hidden; + padding: 4px 0; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 980px; } .info a { - color: #7bc876; + color: #7bc876; } .info .fa { - margin-right: 6px; + margin-right: 6px; } #timeline-loader { - display: none; - margin: 0 auto; - height: 50px; - width: 50px; + display: none; + margin: 0 auto; + height: 50px; + width: 50px; } #timeline-loader div { - color: #7bc876; - font-size: 48px; + color: #7bc876; + font-size: 48px; } diff --git a/pontoon/contributors/static/css/contributors.css b/pontoon/contributors/static/css/contributors.css index 384a550ea..b78817eda 100644 --- a/pontoon/contributors/static/css/contributors.css +++ b/pontoon/contributors/static/css/contributors.css @@ -1,94 +1,94 @@ #heading .banner .title { - color: #7bc876; + color: #7bc876; } #heading .legend { - width: 360px; + width: 360px; } #heading .legend li, #heading .legend li.unreviewed { - padding-left: 0; + padding-left: 0; } #heading .legend li.banner { - padding: 0; + padding: 0; } body.top-contributors #heading .legend li .status.fa { - display: none; + display: none; } .controls .submenu .links { - text-align: right; + text-align: right; } .contributor { - display: inline-block; - position: relative; - width: 580px; + display: inline-block; + position: relative; + width: 580px; } .contributor img { - margin-right: 5px; + margin-right: 5px; } .contributor p { - overflow: hidden; - padding-left: 2px; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 520px; + overflow: hidden; + padding-left: 2px; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 520px; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .contributor p.name { - display: inline-block; - font-size: 1.5em; - height: 1.5em; + display: inline-block; + font-size: 1.5em; + height: 1.5em; } .contributor p.role { - color: #ebebeb; - left: 67px; - position: absolute; - top: 42px; + color: #ebebeb; + left: 67px; + position: absolute; + top: 42px; } th:last-child, .stats .details { - text-align: center; - width: 360px; + text-align: center; + width: 360px; } th:last-child { - position: relative; + position: relative; } th:last-child sup { - top: 7px; - position: absolute; + top: 7px; + position: absolute; } .stats .details div { - border: none; + border: none; } .stats .details div.approved { - color: #7bc876; + color: #7bc876; } .stats .details div.fuzzy { - color: #fed271; + color: #fed271; } .stats .details div.unreviewed { - color: #4fc4f6; + color: #4fc4f6; } .stats .details div p { - font-size: 20px; - padding: 3px 0 0; + font-size: 20px; + padding: 3px 0 0; } diff --git a/pontoon/contributors/static/css/notifications.css b/pontoon/contributors/static/css/notifications.css index c75e55b3b..2ba1d02ba 100644 --- a/pontoon/contributors/static/css/notifications.css +++ b/pontoon/contributors/static/css/notifications.css @@ -1,14 +1,14 @@ .right-column ul { - max-height: none; + max-height: none; } /* No notifications */ #main.no .left-column { - display: none; + display: none; } #main.no .right-column { - background: transparent; - float: none; - width: auto; + background: transparent; + float: none; + width: auto; } diff --git a/pontoon/contributors/static/css/profile.css b/pontoon/contributors/static/css/profile.css index e5e519e4b..a00b44164 100644 --- a/pontoon/contributors/static/css/profile.css +++ b/pontoon/contributors/static/css/profile.css @@ -1,202 +1,202 @@ #middle .container { - padding: 1em 0; + padding: 1em 0; } #middle .container p { - line-height: 18px; + line-height: 18px; } #middle .container div { - width: 244px; + width: 244px; } #middle .container div:last-child { - width: 245px; + width: 245px; } #main { - padding: 5em 0; + padding: 5em 0; } #main .container { - margin: 0 auto; - position: relative; - text-align: left; + margin: 0 auto; + position: relative; + text-align: left; } #main .container:before { - background: #aaaaaa; - content: ''; - height: calc(100% - 8em); - left: 50%; - margin-left: -1px; - position: absolute; - top: 0; - width: 2px; + background: #aaaaaa; + content: ''; + height: calc(100% - 8em); + left: 50%; + margin-left: -1px; + position: absolute; + top: 0; + width: 2px; } #main .container > div { - position: relative; - margin: 8em 0 0; + position: relative; + margin: 8em 0 0; } #main .container > div:first-child { - margin: 0; + margin: 0; } #main .container > div:nth-last-child(2) { - padding-bottom: 8em; + padding-bottom: 8em; } #main .tick { - background: #272a2f; - border: 4px solid #aaaaaa; - position: absolute; - left: 50%; - width: 16px; - height: 16px; - border-radius: 50%; - margin-left: -12px; - visibility: hidden; + background: #272a2f; + border: 4px solid #aaaaaa; + position: absolute; + left: 50%; + width: 16px; + height: 16px; + border-radius: 50%; + margin-left: -12px; + visibility: hidden; } #main .content { - border: 2px solid #aaaaaa; - border-radius: 8px; - position: relative; - padding: 1.5em; - top: -19px; - visibility: hidden; - width: 39%; + border: 2px solid #aaaaaa; + border-radius: 8px; + position: relative; + padding: 1.5em; + top: -19px; + visibility: hidden; + width: 39%; } #main .content:before { - content: ''; - position: absolute; - top: 24px; - left: 100%; - border: 6px solid transparent; - border-left-color: #aaaaaa; + content: ''; + position: absolute; + top: 24px; + left: 100%; + border: 6px solid transparent; + border-left-color: #aaaaaa; } #main .content:after { - content: ''; - display: table; - clear: both; + content: ''; + display: table; + clear: both; } #main .content h4 { } #main .content h4 > div { - display: inline-block; + display: inline-block; } #main .content h4 .day { - font-size: 36px; + font-size: 36px; } #main .content h4 .weekday { - text-transform: uppercase; + text-transform: uppercase; } #main .content h4 .month { - font-weight: 300; + font-weight: 300; } #main .content h2 { - color: #7bc876; - font-size: 20px; + color: #7bc876; + font-size: 20px; } #main .content h2 div { - font-size: 0.7em; + font-size: 0.7em; } #main .content .quote { - margin-top: 1em; + margin-top: 1em; } #main .content .quote.fa-quote-right { - text-align: right; - width: 100%; + text-align: right; + width: 100%; } #main .content p { - color: #aaaaaa; - font-size: 16px; - line-height: 1.6; - margin: -1em 0 0 2em; - text-align: start; - word-wrap: break-word; + color: #aaaaaa; + font-size: 16px; + line-height: 1.6; + margin: -1em 0 0 2em; + text-align: start; + word-wrap: break-word; } #main .content p[dir='rtl'] { - margin-left: 0; - margin-right: 2em; + margin-left: 0; + margin-right: 2em; } #main .content p[data-script='Arabic'] { - font-size: 18px; + font-size: 18px; } #main .label { - padding: 0.5em 0; - position: absolute; - width: 100%; - left: 122%; - top: 0; + padding: 0.5em 0; + position: absolute; + width: 100%; + left: 122%; + top: 0; } #main .label figure { - display: inline-block; - text-align: center; + display: inline-block; + text-align: center; } #main .label .icon { - color: #7bc876; + color: #7bc876; } #main .label figcaption { - margin: 4px 2px 0 0; + margin: 4px 2px 0 0; } #main .label a, #main .label span { - color: #ebebeb; - display: inline-block; - font-size: 1.4em; - font-weight: 400; - margin-top: 12px; - padding: 0 10px; - vertical-align: top; + color: #ebebeb; + display: inline-block; + font-size: 1.4em; + font-weight: 400; + margin-top: 12px; + padding: 0 10px; + vertical-align: top; } #main .label span { - color: #888888; + color: #888888; } #main .container > div:nth-child(even) .content, #main .container > div:nth-child(even) .label figure { - float: right; + float: right; } #main .container > div:nth-child(even) .content:before { - top: 24px; - left: auto; - right: 100%; - border-color: transparent; - border-right-color: #aaaaaa; + top: 24px; + left: auto; + right: 100%; + border-color: transparent; + border-right-color: #aaaaaa; } #main .container > div:nth-child(even) .content .read-more { - float: right; + float: right; } #main .container > div:nth-child(even) .content .label { - left: auto; - right: 122%; - text-align: right; + left: auto; + right: 122%; + text-align: right; } /* @@ -207,114 +207,114 @@ */ #main .tick.bounce-in { - visibility: visible; - -webkit-animation: bounce-1 0.6s; - animation: bounce-1 0.6s; + visibility: visible; + -webkit-animation: bounce-1 0.6s; + animation: bounce-1 0.6s; } @-webkit-keyframes bounce-1 { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - } + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + } - 60% { - opacity: 1; - -webkit-transform: scale(1.5); - } + 60% { + opacity: 1; + -webkit-transform: scale(1.5); + } - 100% { - -webkit-transform: scale(1); - } + 100% { + -webkit-transform: scale(1); + } } @keyframes bounce-1 { - 0% { - opacity: 0; - transform: scale(0.5); - } + 0% { + opacity: 0; + transform: scale(0.5); + } - 60% { - opacity: 1; - transform: scale(1.5); - } + 60% { + opacity: 1; + transform: scale(1.5); + } - 100% { - transform: scale(1); - } + 100% { + transform: scale(1); + } } #main .content.bounce-in { - visibility: visible; - -webkit-animation: bounce-2 0.6s; - animation: bounce-2 0.6s; + visibility: visible; + -webkit-animation: bounce-2 0.6s; + animation: bounce-2 0.6s; } #main .container > div:nth-child(even) .content.bounce-in { - -webkit-animation: bounce-2-inverse 0.6s; - animation: bounce-2-inverse 0.6s; + -webkit-animation: bounce-2-inverse 0.6s; + animation: bounce-2-inverse 0.6s; } @-webkit-keyframes bounce-2 { - 0% { - opacity: 0; - -webkit-transform: translateX(-100px); - } + 0% { + opacity: 0; + -webkit-transform: translateX(-100px); + } - 60% { - opacity: 1; - -webkit-transform: translateX(20px); - } + 60% { + opacity: 1; + -webkit-transform: translateX(20px); + } - 100% { - -webkit-transform: translateX(0); - } + 100% { + -webkit-transform: translateX(0); + } } @keyframes bounce-2 { - 0% { - opacity: 0; - transform: translateX(-100px); - } + 0% { + opacity: 0; + transform: translateX(-100px); + } - 60% { - opacity: 1; - transform: translateX(20px); - } + 60% { + opacity: 1; + transform: translateX(20px); + } - 100% { - transform: translateX(0); - } + 100% { + transform: translateX(0); + } } @-webkit-keyframes bounce-2-inverse { - 0% { - opacity: 0; - -webkit-transform: translateX(100px); - } + 0% { + opacity: 0; + -webkit-transform: translateX(100px); + } - 60% { - opacity: 1; - -webkit-transform: translateX(-20px); - } + 60% { + opacity: 1; + -webkit-transform: translateX(-20px); + } - 100% { - -webkit-transform: translateX(0); - } + 100% { + -webkit-transform: translateX(0); + } } @keyframes bounce-2-inverse { - 0% { - opacity: 0; - transform: translateX(100px); - } + 0% { + opacity: 0; + transform: translateX(100px); + } - 60% { - opacity: 1; - transform: translateX(-20px); - } + 60% { + opacity: 1; + transform: translateX(-20px); + } - 100% { - transform: translateX(0); - } + 100% { + transform: translateX(0); + } } diff --git a/pontoon/contributors/static/css/settings.css b/pontoon/contributors/static/css/settings.css index 4d2db758f..64bc1694f 100644 --- a/pontoon/contributors/static/css/settings.css +++ b/pontoon/contributors/static/css/settings.css @@ -1,114 +1,114 @@ #locale-settings { - margin-top: 30px; + margin-top: 30px; } #preferred-locale { - margin-top: 10px; + margin-top: 10px; } #locale-settings .label { - color: #aaa; - display: inline-block; - font-size: 16px; - font-weight: 300; - margin: 6px 10px 0 0; - text-align: right; - width: 280px; - vertical-align: top; + color: #aaa; + display: inline-block; + font-size: 16px; + font-weight: 300; + margin: 6px 10px 0 0; + text-align: right; + width: 280px; + vertical-align: top; } #locale-settings .locale-selector { - display: inline-block; + display: inline-block; } #locale-settings .locale-selector .locale.select { - width: 280px; + width: 280px; } #locale-settings .locale-selector .locale.select .button { - background: #272a2f; - color: #aaaaaa; - font-size: 16px; - font-weight: 400; - height: 36px; - margin: 0; - padding: 8px 12px; - width: 100%; + background: #272a2f; + color: #aaaaaa; + font-size: 16px; + font-weight: 400; + height: 36px; + margin: 0; + padding: 8px 12px; + width: 100%; } #locale-settings .locale-selector .locale.select .menu { - background: #272a2f; - border: 1px solid #333941; - border-top: none; - top: 36px; - left: -1px; - width: 282px; - z-index: 30; + background: #272a2f; + border: 1px solid #333941; + border-top: none; + top: 36px; + left: -1px; + width: 282px; + z-index: 30; } #main form { - margin: 0 auto; + margin: 0 auto; } #main form section { - margin: 0 auto 70px; + margin: 0 auto 70px; } #main form section h3 { - margin-bottom: 20px; + margin-bottom: 20px; } #main .controls .cancel { - float: none; - margin: 9px; + float: none; + margin: 9px; } #profile-form { - display: block; - position: relative; - text-align: left; - width: 620px; + display: block; + position: relative; + text-align: left; + width: 620px; } #profile-form .field { - text-align: left; + text-align: left; } #profile-form .field:not(:last-child) { - margin-bottom: 20px; + margin-bottom: 20px; } #profile-form .field input { - color: #ffffff; - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - float: none; - width: 290px; - padding: 4px; + color: #ffffff; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + float: none; + width: 290px; + padding: 4px; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } #profile-form button { - margin-top: 10px; + margin-top: 10px; } #profile-form .help { - color: #888888; - font-style: italic; - margin-top: 5px; + color: #888888; + font-style: italic; + margin-top: 5px; } .errorlist { - color: #f36; - list-style: none; - margin: 0; - margin-top: 5px; - text-align: left; + color: #f36; + list-style: none; + margin: 0; + margin-top: 5px; + text-align: left; } .check-list { - cursor: pointer; + cursor: pointer; } diff --git a/pontoon/contributors/static/js/contributor.js b/pontoon/contributors/static/js/contributor.js index 92de094b8..7e2b57e6c 100644 --- a/pontoon/contributors/static/js/contributor.js +++ b/pontoon/contributors/static/js/contributor.js @@ -1,79 +1,79 @@ $(function () { - function loadNextEvents(cb) { - var currentPage = $timeline.data('page'), - nextPage = parseInt(currentPage, 10) + 1, - // Determines if client should request new timeline events. - finalized = parseInt($timeline.data('finalized'), 10); + function loadNextEvents(cb) { + var currentPage = $timeline.data('page'), + nextPage = parseInt(currentPage, 10) + 1, + // Determines if client should request new timeline events. + finalized = parseInt($timeline.data('finalized'), 10); - if (finalized || $timelineLoader.is(':visible')) { - return; + if (finalized || $timelineLoader.is(':visible')) { + return; + } + + $timelineLoader.show(); + + $.get(timelineUrl, { page: nextPage }).then( + function (timelineContents) { + $('#main > .container').append(timelineContents); + $timelineLoader.hide(); + $timeline.data('page', nextPage); + cb(); + }, + function (response) { + $timeline.data('page', nextPage); + if (response.status === 404) { + $timeline.data('finalized', 1); + cb(); + } else { + Pontoon.endLoader("Couldn't load the timeline."); } + $timelineLoader.hide(); + }, + ); + } - $timelineLoader.show(); + // Show/animate timeline blocks inside viewport + function animate() { + var $blocks = $('#main > .container > div'); - $.get(timelineUrl, { page: nextPage }).then( - function (timelineContents) { - $('#main > .container').append(timelineContents); - $timelineLoader.hide(); - $timeline.data('page', nextPage); - cb(); - }, - function (response) { - $timeline.data('page', nextPage); - if (response.status === 404) { - $timeline.data('finalized', 1); - cb(); - } else { - Pontoon.endLoader("Couldn't load the timeline."); - } - $timelineLoader.hide(); - }, - ); - } + $blocks.each(function () { + var block_bottom = $(this).offset().top + $(this).outerHeight(), + window_bottom = $(window).scrollTop() + $(window).height(), + blockSelf = this; - // Show/animate timeline blocks inside viewport - function animate() { - var $blocks = $('#main > .container > div'); + // Animation of event that's displayed on the user timeline. + function showEvent() { + $(this) + .find('.tick, .content') + .css('visibility', 'visible') + .addClass(function () { + return $blocks.length > 1 ? 'bounce-in' : ''; + }); + } - $blocks.each(function () { - var block_bottom = $(this).offset().top + $(this).outerHeight(), - window_bottom = $(window).scrollTop() + $(window).height(), - blockSelf = this; - - // Animation of event that's displayed on the user timeline. - function showEvent() { - $(this) - .find('.tick, .content') - .css('visibility', 'visible') - .addClass(function () { - return $blocks.length > 1 ? 'bounce-in' : ''; - }); - } - - if (block_bottom <= window_bottom) { - if ($blocks.index($(this)) === $blocks.length - 1) { - loadNextEvents(function () { - showEvent.apply(blockSelf); - }); - } else { - showEvent.apply(blockSelf); - } - } - }); - } - - var $timelineLoader = $('#timeline-loader'), - $timeline = $('#main'), - timelineUrl = $timeline.data('url'); - - // The first page of events. - loadNextEvents(function () { - $(window).scroll(); + if (block_bottom <= window_bottom) { + if ($blocks.index($(this)) === $blocks.length - 1) { + loadNextEvents(function () { + showEvent.apply(blockSelf); + }); + } else { + showEvent.apply(blockSelf); + } + } }); + } - $(window).on('scroll', animate); + var $timelineLoader = $('#timeline-loader'), + $timeline = $('#main'), + timelineUrl = $timeline.data('url'); - if ($('.notification li').length) { - Pontoon.endLoader(); - } + // The first page of events. + loadNextEvents(function () { + $(window).scroll(); + }); + + $(window).on('scroll', animate); + + if ($('.notification li').length) { + Pontoon.endLoader(); + } }); diff --git a/pontoon/contributors/static/js/notifications.js b/pontoon/contributors/static/js/notifications.js index cd6eb80ba..9f4a563c5 100644 --- a/pontoon/contributors/static/js/notifications.js +++ b/pontoon/contributors/static/js/notifications.js @@ -1,42 +1,40 @@ $(function () { - window.history.replaceState( - {}, - document.title, - document.location.href.split('?')[0], - ); + window.history.replaceState( + {}, + document.title, + document.location.href.split('?')[0], + ); - // Filter notifications - $('.left-column a').on('click', function () { - var notifications = $(this).data('notifications'); + // Filter notifications + $('.left-column a').on('click', function () { + var notifications = $(this).data('notifications'); - // Show all notifications - if (!notifications) { - $( - '.right-column .notification-item, .right-column .horizontal-separator', - ).show(); - $('.right-column .horizontal-separator').show(); + // Show all notifications + if (!notifications) { + $( + '.right-column .notification-item, .right-column .horizontal-separator', + ).show(); + $('.right-column .horizontal-separator').show(); - // Show project notifications - } else { - $('.right-column .notification-item').each(function () { - var isProjectNotification = - $.inArray($(this).data('id'), notifications) > -1; - $(this).toggle(isProjectNotification); - $(this) - .next('.horizontal-separator') - .toggle(isProjectNotification); - }); + // Show project notifications + } else { + $('.right-column .notification-item').each(function () { + var isProjectNotification = + $.inArray($(this).data('id'), notifications) > -1; + $(this).toggle(isProjectNotification); + $(this).next('.horizontal-separator').toggle(isProjectNotification); + }); - $('.right-column .notification-item:visible:last') - .next('.horizontal-separator') - .hide(); - } - }); - - // Mark all notifications as read - if ($('.right-column li.notification-item[data-unread="true"]').length) { - setTimeout(function () { - Pontoon.markAllNotificationsAsRead(); - }, 1000); + $('.right-column .notification-item:visible:last') + .next('.horizontal-separator') + .hide(); } + }); + + // Mark all notifications as read + if ($('.right-column li.notification-item[data-unread="true"]').length) { + setTimeout(function () { + Pontoon.markAllNotificationsAsRead(); + }, 1000); + } }); diff --git a/pontoon/contributors/static/js/settings.js b/pontoon/contributors/static/js/settings.js index dcea035a1..2e61bba4b 100644 --- a/pontoon/contributors/static/js/settings.js +++ b/pontoon/contributors/static/js/settings.js @@ -1,82 +1,82 @@ $(function () { - // Toggle user profile attribute - $('.check-box').click(function () { - var self = $(this); + // Toggle user profile attribute + $('.check-box').click(function () { + var self = $(this); - $.ajax({ - url: '/api/v1/user/' + $('#server').data('username') + '/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - attribute: self.data('attribute'), - value: !self.is('.enabled'), - }, - success: function () { - self.toggleClass('enabled'); - var is_enabled = self.is('.enabled'); - var status = is_enabled ? 'enabled' : 'disabled'; + $.ajax({ + url: '/api/v1/user/' + $('#server').data('username') + '/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + attribute: self.data('attribute'), + value: !self.is('.enabled'), + }, + success: function () { + self.toggleClass('enabled'); + var is_enabled = self.is('.enabled'); + var status = is_enabled ? 'enabled' : 'disabled'; - Pontoon.endLoader(self.text() + ' ' + status + '.'); - }, - error: function (request) { - if (request.responseText === 'error') { - Pontoon.endLoader('Oops, something went wrong.', 'error'); - } else { - Pontoon.endLoader(request.responseText, 'error'); - } - }, - }); + Pontoon.endLoader(self.text() + ' ' + status + '.'); + }, + error: function (request) { + if (request.responseText === 'error') { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + } else { + Pontoon.endLoader(request.responseText, 'error'); + } + }, }); + }); - // Save custom homepage - $('#homepage .locale .menu li:not(".no-match")').click(function () { - var custom_homepage = $(this).find('.language').data('code'); + // Save custom homepage + $('#homepage .locale .menu li:not(".no-match")').click(function () { + var custom_homepage = $(this).find('.language').data('code'); - $.ajax({ - url: '/save-custom-homepage/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - custom_homepage: custom_homepage, - }, - success: function (data) { - if (data === 'ok') { - Pontoon.endLoader('Custom homepage saved.'); - } - }, - error: function (request) { - if (request.responseText === 'error') { - Pontoon.endLoader('Oops, something went wrong.', 'error'); - } else { - Pontoon.endLoader(request.responseText, 'error'); - } - }, - }); + $.ajax({ + url: '/save-custom-homepage/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + custom_homepage: custom_homepage, + }, + success: function (data) { + if (data === 'ok') { + Pontoon.endLoader('Custom homepage saved.'); + } + }, + error: function (request) { + if (request.responseText === 'error') { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + } else { + Pontoon.endLoader(request.responseText, 'error'); + } + }, }); + }); - // Save preferred source locale - $('#preferred-locale .locale .menu li:not(".no-match")').click(function () { - var preferred_source_locale = $(this).find('.language').data('code'); + // Save preferred source locale + $('#preferred-locale .locale .menu li:not(".no-match")').click(function () { + var preferred_source_locale = $(this).find('.language').data('code'); - $.ajax({ - url: '/save-preferred-source-locale/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - preferred_source_locale: preferred_source_locale, - }, - success: function (data) { - if (data === 'ok') { - Pontoon.endLoader('Preferred source locale saved.'); - } - }, - error: function (request) { - if (request.responseText === 'error') { - Pontoon.endLoader('Oops, something went wrong.', 'error'); - } else { - Pontoon.endLoader(request.responseText, 'error'); - } - }, - }); + $.ajax({ + url: '/save-preferred-source-locale/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + preferred_source_locale: preferred_source_locale, + }, + success: function (data) { + if (data === 'ok') { + Pontoon.endLoader('Preferred source locale saved.'); + } + }, + error: function (request) { + if (request.responseText === 'error') { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + } else { + Pontoon.endLoader(request.responseText, 'error'); + } + }, }); + }); }); diff --git a/pontoon/homepage/static/css/homepage.css b/pontoon/homepage/static/css/homepage.css index e32582dea..7e32dfbe9 100644 --- a/pontoon/homepage/static/css/homepage.css +++ b/pontoon/homepage/static/css/homepage.css @@ -1,409 +1,409 @@ /* General styles */ :root { - --content-width: 980px; - --header-height: 60px; + --content-width: 980px; + --header-height: 60px; } body > header { - background: transparent; - border-color: transparent; - position: fixed; - width: 100%; - z-index: 10; + background: transparent; + border-color: transparent; + position: fixed; + width: 100%; + z-index: 10; } body > header.menu-opened { - border-color: #333941; + border-color: #333941; } #main { - font-size: 16px; - font-weight: 300; - padding: 0; + font-size: 16px; + font-weight: 300; + padding: 0; } #main p { - color: #dddddd; - line-height: 1.5rem; + color: #dddddd; + line-height: 1.5rem; } #main .section { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100vh; - width: 100vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; - background-image: url(../img/background.svg); - background-attachment: fixed; - background-size: cover; + background-image: url(../img/background.svg); + background-attachment: fixed; + background-size: cover; } .section { - color: #f4f4f4; + color: #f4f4f4; } .section .container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - overflow: hidden; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + overflow: hidden; } .section .container.with-footer { - flex: 1; + flex: 1; } .section .content-wrapper { - padding-right: 60px; + padding-right: 60px; } h2 { - color: #ffffff; - font-weight: 700; - font-size: 36px; - letter-spacing: -1px; - line-height: 50px; - padding-bottom: 16px; - text-transform: none; + color: #ffffff; + font-weight: 700; + font-size: 36px; + letter-spacing: -1px; + line-height: 50px; + padding-bottom: 16px; + text-transform: none; } #main .button { - background-color: #ffffff; - color: #000000; - display: flex; - border-radius: 2px; - width: 240px; - height: 40px; - justify-content: center; - align-items: center; - text-align: center; - font-weight: 400; + background-color: #ffffff; + color: #000000; + display: flex; + border-radius: 2px; + width: 240px; + height: 40px; + justify-content: center; + align-items: center; + text-align: center; + font-weight: 400; } #main .button.primary { - background-color: #7bc876; + background-color: #7bc876; } .flex { - display: flex; + display: flex; } .flex-direction-col { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .flex-col-2 { - flex-basis: 50%; + flex-basis: 50%; } .flex-col-3 { - flex-basis: 33.3333333%; + flex-basis: 33.3333333%; } /* Side Navigation */ nav#sections { - position: fixed; - left: 17px; - top: 50%; - opacity: 1; - transform: translate(0, -50%); + position: fixed; + left: 17px; + top: 50%; + opacity: 1; + transform: translate(0, -50%); } nav#sections ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } nav#sections ul li { - display: block; - width: 14px; - height: 13px; - margin: 7px; - position: relative; + display: block; + width: 14px; + height: 13px; + margin: 7px; + position: relative; } nav#sections ul li a { - display: block; - position: relative; - z-index: 1; - width: 100%; - height: 100%; - cursor: pointer; - text-decoration: none; + display: block; + position: relative; + z-index: 1; + width: 100%; + height: 100%; + cursor: pointer; + text-decoration: none; } nav#sections ul li a.active span, nav#sections ul li:hover a.active span { - height: 12px; - width: 12px; - margin: -6px 0 0 -6px; - border-radius: 100%; - background-color: #7bc876; + height: 12px; + width: 12px; + margin: -6px 0 0 -6px; + border-radius: 100%; + background-color: #7bc876; } nav#sections ul li a span { - border-radius: 50%; - position: absolute; - z-index: 1; - height: 4px; - width: 4px; - border: 0; - background-color: #888888; - left: 50%; - top: 50%; - margin: -2px 0 0 -2px; - transition: all 0.1s ease-in-out; + border-radius: 50%; + position: absolute; + z-index: 1; + height: 4px; + width: 4px; + border: 0; + background-color: #888888; + left: 50%; + top: 50%; + margin: -2px 0 0 -2px; + transition: all 0.1s ease-in-out; } nav#sections ul li:hover a span { - width: 10px; - height: 10px; - margin: -5px 0px 0px -5px; + width: 10px; + height: 10px; + margin: -5px 0px 0px -5px; } /* Edit Homepage button */ #edit-homepage { - position: fixed; - left: 0; - right: 0; - margin: 0 auto; - width: var(--content-width); - text-align: right; + position: fixed; + left: 0; + right: 0; + margin: 0 auto; + width: var(--content-width); + text-align: right; } #edit-homepage .select { - top: calc(var(--header-height) + 10px); - transition: top 0.3s; + top: calc(var(--header-height) + 10px); + transition: top 0.3s; } body.addon-promotion-active #edit-homepage .select { - top: calc(var(--header-height) + 54px); + top: calc(var(--header-height) + 54px); } #edit-homepage .button { - background: #7bc876; - color: #000; + background: #7bc876; + color: #000; } #edit-homepage .fa { - margin-right: 7px; + margin-right: 7px; } /* Section-1 */ #section-1 .container { - flex-direction: column; - flex: 1; - align-items: start; + flex-direction: column; + flex: 1; + align-items: start; } #section-1 h1 { - font-size: 64px; - margin-bottom: 10px; + font-size: 64px; + margin-bottom: 10px; } #section-1 p { - font-size: 22px; - line-height: 36px; - margin-bottom: 60px; - width: 900px; + font-size: 22px; + line-height: 36px; + margin-bottom: 60px; + width: 900px; } #section-1 .flex { - align-items: center; + align-items: center; } #section-1 .flex span { - padding: 0 20px; + padding: 0 20px; } /* Scroll for more animation */ #section-1 .footer { - text-align: center; - width: var(--content-width); + text-align: center; + width: var(--content-width); } #section-1 .scroll { - display: block; - position: relative; - font-size: 12px; - height: 90px; - letter-spacing: 3px; - text-transform: uppercase; + display: block; + position: relative; + font-size: 12px; + height: 90px; + letter-spacing: 3px; + text-transform: uppercase; } #section-1 .scroll::after { - content: ''; - border-right: 2px solid #7bc876; - border-bottom: 2px solid #7bc876; - width: 30px; - height: 30px; - position: absolute; - margin: auto; - right: 0; - left: 0; - animation: 3s jump infinite ease; - transform: rotate(45deg); + content: ''; + border-right: 2px solid #7bc876; + border-bottom: 2px solid #7bc876; + width: 30px; + height: 30px; + position: absolute; + margin: auto; + right: 0; + left: 0; + animation: 3s jump infinite ease; + transform: rotate(45deg); } @keyframes jump { - 0%, - 100% { - top: 20px; - } - 50% { - top: 40px; - } + 0%, + 100% { + top: 20px; + } + 50% { + top: 40px; + } } /* Section-2 */ /* Section-3 */ #section-3 .flex { - justify-content: center; + justify-content: center; } #section-3 .timeline { - margin-top: 40px; - width: 2px; - height: 290px; - background-color: #7bc876; + margin-top: 40px; + width: 2px; + height: 290px; + background-color: #7bc876; } #section-3 ol { - counter-reset: item; - list-style: none; - margin-left: -28px; + counter-reset: item; + list-style: none; + margin-left: -28px; } #section-3 ol .flex { - align-items: center; + align-items: center; } #section-3 li { - color: #dddddd; - counter-increment: item; - margin: 40px 0; + color: #dddddd; + counter-increment: item; + margin: 40px 0; } #section-3 li:before { - background-color: #272a2f; - border: 2px solid #7bc876; - border-radius: 100%; - color: #ffffff; - content: counter(item); - display: inline-block; - font-size: 20px; - font-weight: 600; - height: 39px; - margin-right: 20px; - padding-top: 11px; - text-align: center; - width: 50px; + background-color: #272a2f; + border: 2px solid #7bc876; + border-radius: 100%; + color: #ffffff; + content: counter(item); + display: inline-block; + font-size: 20px; + font-weight: 600; + height: 39px; + margin-right: 20px; + padding-top: 11px; + text-align: center; + width: 50px; } #section-3 .checkmark { - background-color: #7bc876; - border-radius: 100%; - width: 54px; - height: 39px; - font-size: 24px; - margin-left: -74px; - margin-right: 20px; - text-align: center; - padding-top: 15px; + background-color: #7bc876; + border-radius: 100%; + width: 54px; + height: 39px; + font-size: 24px; + margin-left: -74px; + margin-right: 20px; + text-align: center; + padding-top: 15px; } /* Section-4 */ #section-4 .flex { - margin: 20px 0; + margin: 20px 0; } #section-4 .box { - border: 2px solid #4d5967; - border-radius: 3px; - margin-left: 20px; - padding: 10px; - padding-top: 18px; - text-align: center; - width: 186px; + border: 2px solid #4d5967; + border-radius: 3px; + margin-left: 20px; + padding: 10px; + padding-top: 18px; + text-align: center; + width: 186px; } #section-4 .box .box-image { - color: #7bc876; - font-size: 35px; - margin-bottom: 10px; + color: #7bc876; + font-size: 35px; + margin-bottom: 10px; } #section-4 .box p { - font-size: 12px; - text-transform: uppercase; + font-size: 12px; + text-transform: uppercase; } /* Section-5 */ #section-5 p { - padding-bottom: 30px; + padding-bottom: 30px; } #section-5 .image-wrapper { - text-align: right; + text-align: right; } #section-5 img { - border: 2px solid #4d5967; - border-radius: 3px; - width: 436px; + border: 2px solid #4d5967; + border-radius: 3px; + width: 436px; } /* Section-6 */ #section-6 .container { - flex-direction: column; - flex: 1; + flex-direction: column; + flex: 1; } #section-6 .content-wrapper { - flex: 1; - align-items: center; - justify-content: center; - padding: 0; + flex: 1; + align-items: center; + justify-content: center; + padding: 0; } #section-6 p { - padding-bottom: 10px; + padding-bottom: 10px; } #section-6 .button.primary { - margin-bottom: 30px; + margin-bottom: 30px; } #section-6 .footer { - height: 60px; - width: var(--content-width); - align-items: center; - font-size: 14px; + height: 60px; + width: var(--content-width); + align-items: center; + font-size: 14px; } #section-6 .footer .mozilla .logo { - width: 80px; + width: 80px; } #section-6 .footer .github { - text-align: center; + text-align: center; } #section-6 .footer .contact { - text-align: right; + text-align: right; } #section-6 .footer a { - color: #ffffff; + color: #ffffff; } #section-6 .footer a:hover { - color: #7bc876; + color: #7bc876; } diff --git a/pontoon/homepage/static/css/homepage_admin.css b/pontoon/homepage/static/css/homepage_admin.css index 435033576..e0395d660 100644 --- a/pontoon/homepage/static/css/homepage_admin.css +++ b/pontoon/homepage/static/css/homepage_admin.css @@ -1,4 +1,4 @@ form .aligned div.help { - margin-left: 0; - padding-left: 0; + margin-left: 0; + padding-left: 0; } diff --git a/pontoon/homepage/static/js/homepage.js b/pontoon/homepage/static/js/homepage.js index d7644b097..bcc2904d4 100644 --- a/pontoon/homepage/static/js/homepage.js +++ b/pontoon/homepage/static/js/homepage.js @@ -1,160 +1,158 @@ const Sections = { - init(rootSel) { - this.root = document.querySelector(rootSel); - this.nav = null; - this.sections = this.root.querySelectorAll('[id^=section-]'); - this.activeSectionIdx = 0; - this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + init(rootSel) { + this.root = document.querySelector(rootSel); + this.nav = null; + this.sections = this.root.querySelectorAll('[id^=section-]'); + this.activeSectionIdx = 0; + this.mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); - this._initKeyboard(); - this._initWheel(); - this._render(); - }, + this._initKeyboard(); + this._initWheel(); + this._render(); + }, - goToNext() { - this.goTo( - Math.min(this.activeSectionIdx + 1, this.sections.length - 1), - ); - }, + goToNext() { + this.goTo(Math.min(this.activeSectionIdx + 1, this.sections.length - 1)); + }, - goToPrev() { - this.goTo(Math.max(this.activeSectionIdx - 1, 0)); - }, + goToPrev() { + this.goTo(Math.max(this.activeSectionIdx - 1, 0)); + }, - goTo(idx) { - if (idx !== this.activeSectionIdx) { - this.activeSectionIdx = idx; - this.navigate(); + goTo(idx) { + if (idx !== this.activeSectionIdx) { + this.activeSectionIdx = idx; + this.navigate(); + } + }, + + navigate() { + requestAnimationFrame(() => { + const section = this.sections[this.activeSectionIdx]; + const behavior = this.mediaQuery.matches ? 'auto' : 'smooth'; + + section.scrollIntoView({ + behavior: behavior, + block: 'nearest', + }); + this._render(); + }); + }, + + _initKeyboard() { + document.addEventListener('keydown', (e) => { + if (e.keyCode === 38) { + this.goToPrev(); + } + + if (e.keyCode === 40) { + this.goToNext(); + } + }); + }, + + _initWheel() { + let samples = []; + let lastScroll = new Date().getTime(); + // Disable scrollbars + document.body.style.overflow = 'hidden'; + + const avg = (numbers, count) => { + let sum = 0; + for (let i = 0; i < count && i < numbers.length; i++) { + const idx = numbers.length - i - 1; + sum += numbers[idx]; + } + return Math.ceil(sum / count); + }; + + document.addEventListener( + 'wheel', + (e) => { + if (samples.length >= 50) { + samples.shift(); } - }, + samples.push(Math.abs(e.deltaY)); - navigate() { - requestAnimationFrame(() => { - const section = this.sections[this.activeSectionIdx]; - const behavior = this.mediaQuery.matches ? 'auto' : 'smooth'; - - section.scrollIntoView({ - behavior: behavior, - block: 'nearest', - }); - this._render(); - }); - }, - - _initKeyboard() { - document.addEventListener('keydown', (e) => { - if (e.keyCode === 38) { - this.goToPrev(); - } - - if (e.keyCode === 40) { - this.goToNext(); - } - }); - }, - - _initWheel() { - let samples = []; - let lastScroll = new Date().getTime(); - // Disable scrollbars - document.body.style.overflow = 'hidden'; - - const avg = (numbers, count) => { - let sum = 0; - for (let i = 0; i < count && i < numbers.length; i++) { - const idx = numbers.length - i - 1; - sum += numbers[idx]; - } - return Math.ceil(sum / count); - }; - - document.addEventListener( - 'wheel', - (e) => { - if (samples.length >= 50) { - samples.shift(); - } - samples.push(Math.abs(e.deltaY)); - - const now = new Date().getTime(); - const elapsed = now - lastScroll; - // Too fast! - if (elapsed < 550) { - return; - } - - // Higher recent sample values mean scroll now happens faster - const isAccelerating = avg(samples, 10) >= avg(samples, 50); - if (!isAccelerating) { - return; - } - - // Record the current scroll and restart measuring the next time - lastScroll = new Date().getTime(); - samples = []; - - if (e.deltaY < 0) { - this.goToPrev(); - } else if (0 < e.deltaY) { - this.goToNext(); - } - }, - { passive: true }, - ); - }, - - _renderNav() { - const nav = document.createElement('nav'); - nav.id = 'sections'; - const ul = document.createElement('ul'); - this.sections.forEach((section, idx) => { - const li = document.createElement('li'); - const a = document.createElement('a'); - const span = document.createElement('span'); - a.className = 'js-section-nav'; - a.onclick = (e) => { - e.preventDefault(); - this.goTo(idx); - }; - a.appendChild(span); - li.appendChild(a); - ul.appendChild(li); - }); - - nav.appendChild(ul); - this.root.appendChild(nav); - - return nav; - }, - - _render() { - if (!this.nav) { - this.nav = this._renderNav(); + const now = new Date().getTime(); + const elapsed = now - lastScroll; + // Too fast! + if (elapsed < 550) { + return; } - const navElements = this.nav.querySelectorAll('.js-section-nav'); - navElements.forEach((el, idx) => - el.classList.toggle('active', idx === this.activeSectionIdx), - ); - }, + // Higher recent sample values mean scroll now happens faster + const isAccelerating = avg(samples, 10) >= avg(samples, 50); + if (!isAccelerating) { + return; + } + + // Record the current scroll and restart measuring the next time + lastScroll = new Date().getTime(); + samples = []; + + if (e.deltaY < 0) { + this.goToPrev(); + } else if (0 < e.deltaY) { + this.goToNext(); + } + }, + { passive: true }, + ); + }, + + _renderNav() { + const nav = document.createElement('nav'); + nav.id = 'sections'; + const ul = document.createElement('ul'); + this.sections.forEach((section, idx) => { + const li = document.createElement('li'); + const a = document.createElement('a'); + const span = document.createElement('span'); + a.className = 'js-section-nav'; + a.onclick = (e) => { + e.preventDefault(); + this.goTo(idx); + }; + a.appendChild(span); + li.appendChild(a); + ul.appendChild(li); + }); + + nav.appendChild(ul); + this.root.appendChild(nav); + + return nav; + }, + + _render() { + if (!this.nav) { + this.nav = this._renderNav(); + } + + const navElements = this.nav.querySelectorAll('.js-section-nav'); + navElements.forEach((el, idx) => + el.classList.toggle('active', idx === this.activeSectionIdx), + ); + }, }; $(function () { - Sections.init('#main'); + Sections.init('#main'); - // Scroll from Section 1 to Section 2 - $('#section-1 .footer .scroll').on('click', function (e) { - e.preventDefault(); - Sections.goToNext(); - }); + // Scroll from Section 1 to Section 2 + $('#section-1 .footer .scroll').on('click', function (e) { + e.preventDefault(); + Sections.goToNext(); + }); - // Show/hide header border on menu open/close - $('body > header').on('click', '.selector', function () { - if (!$(this).siblings('.menu').is(':visible')) { - $('body > header').addClass('menu-opened'); - } - }); - $('body').bind('click.main', function () { - $('body > header').removeClass('menu-opened'); - }); + // Show/hide header border on menu open/close + $('body > header').on('click', '.selector', function () { + if (!$(this).siblings('.menu').is(':visible')) { + $('body > header').addClass('menu-opened'); + } + }); + $('body').bind('click.main', function () { + $('body > header').removeClass('menu-opened'); + }); }); diff --git a/pontoon/insights/static/css/insights.css b/pontoon/insights/static/css/insights.css index a35ee2d61..bb6462f71 100644 --- a/pontoon/insights/static/css/insights.css +++ b/pontoon/insights/static/css/insights.css @@ -1,288 +1,288 @@ #insights .half { - width: 470px; - float: left; + width: 470px; + float: left; } #insights .half:last-child { - float: right; + float: right; } #insights .block { - border-radius: 6px; - background: #333941; - margin-bottom: 40px; - overflow: hidden; - padding: 30px; + border-radius: 6px; + background: #333941; + margin-bottom: 40px; + overflow: hidden; + padding: 30px; } #insights h3 { - color: #ebebeb; - font-size: 20px; - font-style: normal; - font-weight: bold; - height: 25px; - letter-spacing: normal; - margin-bottom: 30px; + color: #ebebeb; + font-size: 20px; + font-style: normal; + font-weight: bold; + height: 25px; + letter-spacing: normal; + margin-bottom: 30px; } #insights .controls .period-selector { - float: right; - font-size: 0; - margin-right: 10px; + float: right; + font-size: 0; + margin-right: 10px; } #insights .controls .period-selector li { - display: inline-block; + display: inline-block; } #insights .controls .period-selector li .selector { - font-size: 12px; - text-transform: uppercase; - width: 32px; + font-size: 12px; + text-transform: uppercase; + width: 32px; } #insights .controls .period-selector li .selector.active, #insights .controls .period-selector li .selector:hover { - background: #7bc876; - color: #272a2f; + background: #7bc876; + color: #272a2f; } #insights .controls .selector { - background: #3f4752; - color: #aaa; - cursor: pointer; - width: 24px; - height: 24px; - text-align: center; - border-radius: 3px; - margin-left: 5px; - padding-top: 5px; - box-sizing: border-box; + background: #3f4752; + color: #aaa; + cursor: pointer; + width: 24px; + height: 24px; + text-align: center; + border-radius: 3px; + margin-left: 5px; + padding-top: 5px; + box-sizing: border-box; } #insights .active-users-chart, #insights #unreviewed-suggestions-lifespan-chart { - height: 160px; + height: 160px; } #insights .active-users-chart { - float: left; - margin-right: 40px; - position: relative; - text-align: center; + float: left; + margin-right: 40px; + position: relative; + text-align: center; } #insights .active-users-chart:last-child { - margin-right: 0; + margin-right: 0; } #insights .active-users-chart h4 { - font-size: 14px; - font-weight: bold; - margin: 10px auto 0; - width: 100px; + font-size: 14px; + font-weight: bold; + margin: 10px auto 0; + width: 100px; } #insights .active-users-chart .active-wrapper { - left: 0; - right: 0; - top: 15px; - position: absolute; + left: 0; + right: 0; + top: 15px; + position: absolute; } #insights .active-users-chart .active { - border-bottom: 2px solid #888888; - display: inline-block; - font-size: 40px; - font-weight: bold; - line-height: 48px; + border-bottom: 2px solid #888888; + display: inline-block; + font-size: 40px; + font-weight: bold; + line-height: 48px; } #insights .active-users-chart .total { - color: #888; - font-size: 16px; - left: 0; - right: 0; - top: 68px; - position: absolute; + color: #888; + font-size: 16px; + left: 0; + right: 0; + top: 68px; + position: absolute; } #insights figure { - margin-bottom: 40px; + margin-bottom: 40px; } #insights .suggestions-age .block { - position: relative; + position: relative; } #insights .suggestions-age-items { - width: 940px; - height: 180px; - display: inline-block; - overflow: hidden; - transition: margin 0.4s ease-in-out; + width: 940px; + height: 180px; + display: inline-block; + overflow: hidden; + transition: margin 0.4s ease-in-out; } #insights .suggestions-age-items .suggestions-age-item { - float: left; - padding-right: 60px; + float: left; + padding-right: 60px; } #insights .suggestions-age nav { - text-align: center; + text-align: center; } #insights .suggestions-age nav ul li { - cursor: pointer; - display: inline-block; - margin: 15px 15px 0; + cursor: pointer; + display: inline-block; + margin: 15px 15px 0; } #insights .suggestions-age nav ul li .icon { - background-color: #272a2f; + background-color: #272a2f; } #insights .suggestions-age nav ul li .label { - color: #4d5967; + color: #4d5967; } #insights .suggestions-age nav ul li.active .icon { - background-color: #4fc4f6; + background-color: #4fc4f6; } #insights .suggestions-age nav ul li.active .label { - color: #ffffff; + color: #ffffff; } /* Info tooltip */ #insights h3 .fa { - float: right; - font-size: 14px; + float: right; + font-size: 14px; } #insights h3 .fa.active, #insights h3 .fa:hover { - background: #272a2f; + background: #272a2f; } #insights h3 .tooltip { - background: #000000dd; - position: absolute; - display: none; - margin-top: 10px; - padding: 10px; - z-index: 1; - font-size: 14px; - font-weight: normal; - line-height: 1.5em; - border-radius: 3px; - right: 0; - max-width: 570px; + background: #000000dd; + position: absolute; + display: none; + margin-top: 10px; + padding: 10px; + z-index: 1; + font-size: 14px; + font-weight: normal; + line-height: 1.5em; + border-radius: 3px; + right: 0; + max-width: 570px; } #insights h3 .tooltip ul { - margin-top: 15px; - margin-left: 15px; + margin-top: 15px; + margin-left: 15px; } #insights h3 .tooltip li { - list-style-type: disc; + list-style-type: disc; } #insights h3 .tooltip li:not(:last-child) { - padding-bottom: 5px; + padding-bottom: 5px; } /* Active users info tooltip */ #insights h3 .tooltip li::marker { - color: #7bc876; + color: #7bc876; } /* Active users info tooltip */ #insights h3 .tooltip li.current-month::marker { - color: #4fc4f6; + color: #4fc4f6; } #insights h3 .tooltip li.twelve-month-average::marker { - color: #385465; + color: #385465; } /* Translation activity info tooltip */ #insights h3 .tooltip li.human-translations::marker { - color: #4f7256; + color: #4f7256; } #insights h3 .tooltip li.machinery-translations::marker { - color: #41554c; + color: #41554c; } #insights h3 .tooltip li.new-source-strings::marker { - color: #272a2f; + color: #272a2f; } #insights h3 .tooltip li.completion::marker { - color: #7bc876; + color: #7bc876; } /* Review activity info tooltip */ #insights h3 .tooltip li.peer-approved::marker { - color: #3e7089; + color: #3e7089; } #insights h3 .tooltip li.self-approved::marker { - color: #385465; + color: #385465; } #insights h3 .tooltip li.rejected::marker { - color: #843650; + color: #843650; } #insights h3 .tooltip li.new-suggestions::marker { - color: #272a2f; + color: #272a2f; } #insights h3 .tooltip li.unreviewed::marker { - color: #4fc4f6; + color: #4fc4f6; } /* Custom chart legend */ #insights .legend { - text-align: center; + text-align: center; } #insights .legend li { - display: inline-block; - font-size: 12px; - margin: 15px; - margin-bottom: 5px; + display: inline-block; + font-size: 12px; + margin: 15px; + margin-bottom: 5px; } #insights .legend li .icon, #insights nav li .icon { - display: inline-block; - border-radius: 50%; - margin-right: 8px; - height: 12px; - width: 12px; + display: inline-block; + border-radius: 50%; + margin-right: 8px; + height: 12px; + width: 12px; } #insights .legend li .label, #insights nav li .label { - cursor: pointer; - font-weight: bold; - vertical-align: text-top; + cursor: pointer; + font-weight: bold; + vertical-align: text-top; } #insights .legend li.disabled .label { - color: #4d5967; + color: #4d5967; } #insights .legend li.disabled .label:hover { - color: #fff; + color: #fff; } diff --git a/pontoon/insights/static/js/insights.js b/pontoon/insights/static/js/insights.js index 90e65334a..085f00d09 100644 --- a/pontoon/insights/static/js/insights.js +++ b/pontoon/insights/static/js/insights.js @@ -1,763 +1,724 @@ var Pontoon = (function (my) { - const nf = new Intl.NumberFormat('en'); - return $.extend(true, my, { - insights: { - initialize: function () { - // Show/hide info tooltips on click on the icon - $('#insights h3 .fa-info').on('click', function (e) { - e.stopPropagation(); - $(this).next('.tooltip').toggle(); - $(this).toggleClass('active'); - }); + const nf = new Intl.NumberFormat('en'); + return $.extend(true, my, { + insights: { + initialize: function () { + // Show/hide info tooltips on click on the icon + $('#insights h3 .fa-info').on('click', function (e) { + e.stopPropagation(); + $(this).next('.tooltip').toggle(); + $(this).toggleClass('active'); + }); - // Hide info tooltips on click outside - $(window).click(function () { - $('#insights .tooltip').hide(); - $('#insights h3 .fa-info').removeClass('active'); - }); + // Hide info tooltips on click outside + $(window).click(function () { + $('#insights .tooltip').hide(); + $('#insights h3 .fa-info').removeClass('active'); + }); - // Select active users period - $('#insights h3 .period-selector .selector').on( - 'click', - function () { - $( - '#insights h3 .period-selector .selector', - ).removeClass('active'); - $(this).addClass('active'); - Pontoon.insights.renderActiveUsers(); - }, - ); + // Select active users period + $('#insights h3 .period-selector .selector').on('click', function () { + $('#insights h3 .period-selector .selector').removeClass('active'); + $(this).addClass('active'); + Pontoon.insights.renderActiveUsers(); + }); - // Set up canvas to be HiDPI display ready - $('#insights canvas.chart').each(function () { - var canvas = this; + // Set up canvas to be HiDPI display ready + $('#insights canvas.chart').each(function () { + var canvas = this; - var dpr = window.devicePixelRatio || 1; - canvas.style.width = canvas.width + 'px'; - canvas.style.height = canvas.height + 'px'; - canvas.width = canvas.width * dpr; - canvas.height = canvas.height * dpr; - }); + var dpr = window.devicePixelRatio || 1; + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + canvas.width = canvas.width * dpr; + canvas.height = canvas.height * dpr; + }); - // Set up default Chart.js configuration - Chart.defaults.global.defaultFontColor = '#AAA'; - Chart.defaults.global.defaultFontFamily = 'Open Sans'; - Chart.defaults.global.defaultFontStyle = '100'; - Chart.defaults.global.datasets.bar.barPercentage = 0.7; - Chart.defaults.global.datasets.bar.categoryPercentage = 0.7; + // Set up default Chart.js configuration + Chart.defaults.global.defaultFontColor = '#AAA'; + Chart.defaults.global.defaultFontFamily = 'Open Sans'; + Chart.defaults.global.defaultFontStyle = '100'; + Chart.defaults.global.datasets.bar.barPercentage = 0.7; + Chart.defaults.global.datasets.bar.categoryPercentage = 0.7; - Pontoon.insights.renderActiveUsers(); - Pontoon.insights.renderUnreviewedSuggestionsLifespan(); - Pontoon.insights.renderTimeToReviewSuggestions(); - Pontoon.insights.renderTranslationActivity(); - Pontoon.insights.renderReviewActivity(); + Pontoon.insights.renderActiveUsers(); + Pontoon.insights.renderUnreviewedSuggestionsLifespan(); + Pontoon.insights.renderTimeToReviewSuggestions(); + Pontoon.insights.renderTranslationActivity(); + Pontoon.insights.renderReviewActivity(); + }, + renderActiveUsers: function () { + $('#insights canvas.chart').each(function () { + // Collect data + var parent = $(this).parents('.active-users-chart'); + var id = parent.attr('id'); + var period = $('.period-selector .active').data('period').toString(); + var active = $('.active-users').data(period)[id]; + var total = $('.active-users').data('total')[id]; + + // Clear old canvas content to avoid aliasing + var canvas = this; + var context = canvas.getContext('2d'); + var dpr = window.devicePixelRatio || 1; + context.clearRect(0, 0, canvas.width, canvas.height); + context.lineWidth = 3 * dpr; + + var x = canvas.width / 2; + var y = canvas.height / 2; + var radius = (canvas.width - context.lineWidth) / 2; + + var activeLength = 0; + if (total !== 0) { + activeLength = (active / total) * 2; + } + var activeStart = -0.5; + var activeEnd = activeStart + activeLength; + plot(activeStart, activeEnd, '#7BC876'); + + var inactiveLength = 2; + if (total !== 0) { + inactiveLength = ((total - active) / total) * 2; + } + var inactiveStart = activeEnd; + var inactiveEnd = inactiveStart + inactiveLength; + plot(inactiveStart, inactiveEnd, '#5F7285'); + + // Update number + parent.find('.active').html(active); + parent.find('.total').html(total); + + function plot(start, end, color) { + context.beginPath(); + context.arc(x, y, radius, start * Math.PI, end * Math.PI); + context.strokeStyle = color; + context.stroke(); + } + }); + }, + renderUnreviewedSuggestionsLifespan: function () { + var chart = $('#unreviewed-suggestions-lifespan-chart'); + if (chart.length === 0) return; + var ctx = chart[0].getContext('2d'); + + var gradient = ctx.createLinearGradient(0, 0, 0, 160); + gradient.addColorStop(0, '#4fc4f666'); + gradient.addColorStop(1, 'transparent'); + + new Chart(chart, { + type: 'line', + data: { + labels: $('#insights').data('dates'), + datasets: [ + { + label: 'Age of unreviewed suggestions', + data: chart.data('lifespans'), + backgroundColor: gradient, + borderColor: ['#4fc4f6'], + borderWidth: 2, + pointBackgroundColor: '#4fc4f6', + pointHitRadius: 10, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: '#4fc4f6', + pointHoverBorderColor: '#FFF', + }, + ], + }, + options: { + legend: { + display: false, }, - renderActiveUsers: function () { - $('#insights canvas.chart').each(function () { - // Collect data - var parent = $(this).parents('.active-users-chart'); - var id = parent.attr('id'); - var period = $('.period-selector .active') - .data('period') - .toString(); - var active = $('.active-users').data(period)[id]; - var total = $('.active-users').data('total')[id]; + tooltips: { + borderColor: '#4fc4f6', + borderWidth: 1, + caretPadding: 5, + xPadding: 10, + yPadding: 10, + displayColors: false, + callbacks: { + label: (item) => nf.format(item.value) + ' days', + }, + }, + scales: { + xAxes: [ + { + type: 'time', + time: { + displayFormats: { + month: 'MMM', + }, + tooltipFormat: 'MMMM YYYY', + }, + gridLines: { + display: false, + }, + ticks: { + source: 'data', + }, + }, + ], + yAxes: [ + { + gridLines: { + display: false, + }, + position: 'right', + ticks: { + beginAtZero: true, + maxTicksLimit: 3, + precision: 0, + callback: function (value) { + return value + ' days'; + }, + }, + }, + ], + }, + }, + }); + }, + renderTimeToReviewSuggestions: function () { + var chart = $('#time-to-review-suggestions-chart'); + if (chart.length === 0) return; + var ctx = chart[0].getContext('2d'); - // Clear old canvas content to avoid aliasing - var canvas = this; - var context = canvas.getContext('2d'); - var dpr = window.devicePixelRatio || 1; - context.clearRect(0, 0, canvas.width, canvas.height); - context.lineWidth = 3 * dpr; + var gradient = ctx.createLinearGradient(0, 0, 0, 160); + gradient.addColorStop(0, '#4fc4f666'); + gradient.addColorStop(1, 'transparent'); - var x = canvas.width / 2; - var y = canvas.height / 2; - var radius = (canvas.width - context.lineWidth) / 2; + new Chart(chart, { + type: 'bar', + data: { + labels: $('#insights').data('dates'), + datasets: [ + { + type: 'line', + label: 'Current month', + data: chart.data('time-to-review-suggestions'), + backgroundColor: gradient, + borderColor: ['#4fc4f6'], + borderWidth: 2, + pointBackgroundColor: '#4fc4f6', + pointHitRadius: 10, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: '#4fc4f6', + pointHoverBorderColor: '#FFF', + }, + { + type: 'line', + label: '12-month average', + data: chart.data('time-to-review-suggestions-12-month-avg'), + borderColor: ['#3e7089'], + borderWidth: 1, + pointBackgroundColor: '#3e7089', + pointHitRadius: 10, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: '#3e7089', + pointHoverBorderColor: '#FFF', + }, + ], + }, + options: { + legend: { + display: false, + }, + tooltips: { + mode: 'index', + intersect: false, + borderColor: '#4fc4f6', + borderWidth: 1, + caretPadding: 5, + xPadding: 10, + yPadding: 10, + callbacks: { + label(items, chart) { + const { label } = chart.datasets[items.datasetIndex]; + return `${label}: ${items.value} days`; + }, + }, + }, + scales: { + xAxes: [ + { + type: 'time', + time: { + displayFormats: { + month: 'MMM', + }, + tooltipFormat: 'MMMM YYYY', + }, + gridLines: { + display: false, + }, + offset: true, + ticks: { + source: 'data', + }, + }, + ], + yAxes: [ + { + gridLines: { + display: false, + }, + position: 'right', + ticks: { + beginAtZero: true, + maxTicksLimit: 3, + precision: 0, + callback: (value) => `${value} days`, + }, + }, + ], + }, + }, + }); + }, + renderTranslationActivity: function () { + var chart = $('#translation-activity-chart'); + if (chart.length === 0) return; + var ctx = chart[0].getContext('2d'); - var activeLength = 0; - if (total !== 0) { - activeLength = (active / total) * 2; + var gradient = ctx.createLinearGradient(0, 0, 0, 400); + gradient.addColorStop(0, '#7BC87633'); + gradient.addColorStop(1, 'transparent'); + + var humanData = chart.data('human-translations') || []; + var machineryData = chart.data('machinery-translations') || []; + var newSourcesData = chart.data('new-source-strings') || []; + + var translationActivityChart = new Chart(chart, { + type: 'bar', + data: { + labels: $('#insights').data('dates'), + datasets: [ + { + type: 'line', + label: 'Completion', + data: chart.data('completion'), + yAxisID: 'completion-y-axis', + backgroundColor: gradient, + borderColor: ['#7BC876'], + borderWidth: 2, + pointBackgroundColor: '#7BC876', + pointHitRadius: 10, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: '#7BC876', + pointHoverBorderColor: '#FFF', + }, + humanData.length > 0 && { + type: 'bar', + label: 'Human translations', + data: humanData, + yAxisID: 'strings-y-axis', + backgroundColor: '#4f7256', + hoverBackgroundColor: '#4f7256', + stack: 'translations', + order: 2, + }, + machineryData.length > 0 && { + type: 'bar', + label: 'Machinery translations', + data: machineryData, + yAxisID: 'strings-y-axis', + backgroundColor: '#41554c', + hoverBackgroundColor: '#41554c', + stack: 'translations', + order: 1, + }, + newSourcesData.length > 0 && { + type: 'bar', + label: 'New source strings', + data: newSourcesData, + yAxisID: 'strings-y-axis', + backgroundColor: '#272a2f', + hoverBackgroundColor: '#272a2f', + stack: 'source-strings', + order: 3, + hidden: true, + }, + ].filter(Boolean), // Filter out empty values + }, + options: { + legend: { + display: false, + }, + legendCallback: Pontoon.insights.customLegend(chart), + tooltips: { + mode: 'index', + intersect: false, + borderColor: '#7BC876', + borderWidth: 1, + caretPadding: 5, + xPadding: 10, + yPadding: 10, + itemSort: function (a, b) { + // Dataset order affects stacking, tooltip and + // legend, but it doesn't work intuitively, so + // we need to manually sort tooltip items. + if (a.datasetIndex === 2 && b.datasetIndex === 1) { + return 1; + } + }, + callbacks: { + label: function (items, chart) { + const human = chart.datasets[1].data[items.index]; + const machinery = chart.datasets[2].data[items.index]; + + const label = chart.datasets[items.datasetIndex].label; + const value = items.yLabel; + const base = label + ': ' + nf.format(value); + + switch (label) { + case 'Completion': + return base + '%'; + case 'Human translations': + case 'Machinery translations': { + const pct = Pontoon.insights.getPercent( + value, + human + machinery, + ); + return `${base} (${pct} of all translations)`; } - var activeStart = -0.5; - var activeEnd = activeStart + activeLength; - plot(activeStart, activeEnd, '#7BC876'); + default: + return base; + } + }, + }, + }, + scales: { + xAxes: [ + { + stacked: true, + type: 'time', + time: { + displayFormats: { + month: 'MMM', + }, + tooltipFormat: 'MMMM YYYY', + }, + gridLines: { + display: false, + }, + offset: true, + ticks: { + source: 'data', + }, + }, + ], + yAxes: [ + { + id: 'completion-y-axis', + position: 'right', + scaleLabel: { + display: true, + labelString: 'COMPLETION', + fontColor: '#FFF', + fontStyle: 100, + }, + gridLines: { + display: false, + }, + ticks: { + beginAtZero: true, + max: 100, + stepSize: 20, + callback: function (value) { + return value + ' %'; + }, + }, + }, + { + stacked: true, + id: 'strings-y-axis', + position: 'left', + scaleLabel: { + display: true, + labelString: 'STRINGS', + fontColor: '#FFF', + fontStyle: 100, + }, + gridLines: { + display: false, + }, + ticks: { + beginAtZero: true, + precision: 0, + }, + }, + ], + }, + }, + }); - var inactiveLength = 2; - if (total !== 0) { - inactiveLength = ((total - active) / total) * 2; + // Render custom legend + $('#translation-activity-chart-legend').html( + translationActivityChart.generateLegend(), + ); + Pontoon.insights.attachCustomLegendHandler( + translationActivityChart, + '#translation-activity-chart-legend .label', + ); + }, + renderReviewActivity: function () { + var chart = $('#review-activity-chart'); + if (chart.length === 0) return; + var ctx = chart[0].getContext('2d'); + + var gradient = ctx.createLinearGradient(0, 0, 0, 400); + gradient.addColorStop(0, '#4fc4f688'); + gradient.addColorStop(1, 'transparent'); + + var unreviewedData = chart.data('unreviewed') || []; + var peerApprovedData = chart.data('peer-approved') || []; + var selfApprovedData = chart.data('self-approved') || []; + var rejectedData = chart.data('rejected') || []; + var newSuggestionsData = chart.data('new-suggestions') || []; + + var reviewActivityChart = new Chart(chart, { + type: 'bar', + data: { + labels: $('#insights').data('dates'), + datasets: [ + { + type: 'line', + label: 'Unreviewed', + data: unreviewedData, + yAxisID: 'strings-y-axis', + backgroundColor: gradient, + borderColor: ['#4fc4f6'], + borderWidth: 2, + pointBackgroundColor: '#4fc4f6', + pointHitRadius: 10, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: '#4fc4f6', + pointHoverBorderColor: '#FFF', + }, + peerApprovedData.length > 0 && { + type: 'bar', + label: 'Peer-approved', + data: peerApprovedData, + yAxisID: 'strings-y-axis', + backgroundColor: '#3e7089', + hoverBackgroundColor: '#3e7089', + stack: 'review-actions', + order: 3, + }, + selfApprovedData.length > 0 && { + type: 'bar', + label: 'Self-approved', + data: selfApprovedData, + yAxisID: 'strings-y-axis', + backgroundColor: '#385465', + hoverBackgroundColor: '#385465', + stack: 'review-actions', + order: 2, + }, + rejectedData.length > 0 && { + type: 'bar', + label: 'Rejected', + data: rejectedData, + yAxisID: 'strings-y-axis', + backgroundColor: '#843650', + hoverBackgroundColor: '#843650', + stack: 'review-actions', + order: 1, + }, + newSuggestionsData.length > 0 && { + type: 'bar', + label: 'New suggestions', + data: chart.data('new-suggestions'), + yAxisID: 'strings-y-axis', + backgroundColor: '#272a2f', + hoverBackgroundColor: '#272a2f', + stack: 'new-suggestions', + order: 4, + hidden: true, + }, + ].filter(Boolean), // Filter out empty values + }, + options: { + legend: { + display: false, + }, + legendCallback: Pontoon.insights.customLegend(chart), + tooltips: { + mode: 'index', + intersect: false, + borderColor: '#4fc4f6', + borderWidth: 1, + caretPadding: 5, + xPadding: 10, + yPadding: 10, + itemSort: function (a, b) { + // Dataset order affects stacking, tooltip and + // legend, but it doesn't work intuitively, so + // we need to manually sort tooltip items. + if ( + (a.datasetIndex === 3 && b.datasetIndex === 2) || + (a.datasetIndex === 3 && b.datasetIndex === 1) || + (a.datasetIndex === 2 && b.datasetIndex === 1) + ) { + return 1; + } + }, + callbacks: { + label: function (items, chart) { + const label = chart.datasets[items.datasetIndex].label; + const value = items.yLabel; + const base = label + ': ' + nf.format(value); + + if (chart.datasets.length < 4) return base; + + const peerApproved = chart.datasets[1].data[items.index]; + const selfApproved = chart.datasets[2].data[items.index]; + const rejected = chart.datasets[3].data[items.index]; + + switch (label) { + case 'Self-approved': { + const pct = Pontoon.insights.getPercent( + value, + peerApproved + selfApproved, + ); + return `${base} (${pct} of all approvals)`; } - var inactiveStart = activeEnd; - var inactiveEnd = inactiveStart + inactiveLength; - plot(inactiveStart, inactiveEnd, '#5F7285'); - - // Update number - parent.find('.active').html(active); - parent.find('.total').html(total); - - function plot(start, end, color) { - context.beginPath(); - context.arc( - x, - y, - radius, - start * Math.PI, - end * Math.PI, - ); - context.strokeStyle = color; - context.stroke(); + case 'Peer-approved': + case 'Rejected': { + const pct = Pontoon.insights.getPercent( + value, + peerApproved + rejected, + ); + return `${base} (${pct} of peer-reviews)`; } - }); + default: + return base; + } + }, + }, }, - renderUnreviewedSuggestionsLifespan: function () { - var chart = $('#unreviewed-suggestions-lifespan-chart'); - if (chart.length === 0) return; - var ctx = chart[0].getContext('2d'); - - var gradient = ctx.createLinearGradient(0, 0, 0, 160); - gradient.addColorStop(0, '#4fc4f666'); - gradient.addColorStop(1, 'transparent'); - - new Chart(chart, { - type: 'line', - data: { - labels: $('#insights').data('dates'), - datasets: [ - { - label: 'Age of unreviewed suggestions', - data: chart.data('lifespans'), - backgroundColor: gradient, - borderColor: ['#4fc4f6'], - borderWidth: 2, - pointBackgroundColor: '#4fc4f6', - pointHitRadius: 10, - pointRadius: 4, - pointHoverRadius: 6, - pointHoverBackgroundColor: '#4fc4f6', - pointHoverBorderColor: '#FFF', - }, - ], + scales: { + xAxes: [ + { + stacked: true, + type: 'time', + time: { + displayFormats: { + month: 'MMM', }, - options: { - legend: { - display: false, - }, - tooltips: { - borderColor: '#4fc4f6', - borderWidth: 1, - caretPadding: 5, - xPadding: 10, - yPadding: 10, - displayColors: false, - callbacks: { - label: (item) => - nf.format(item.value) + ' days', - }, - }, - scales: { - xAxes: [ - { - type: 'time', - time: { - displayFormats: { - month: 'MMM', - }, - tooltipFormat: 'MMMM YYYY', - }, - gridLines: { - display: false, - }, - ticks: { - source: 'data', - }, - }, - ], - yAxes: [ - { - gridLines: { - display: false, - }, - position: 'right', - ticks: { - beginAtZero: true, - maxTicksLimit: 3, - precision: 0, - callback: function (value) { - return value + ' days'; - }, - }, - }, - ], - }, - }, - }); + tooltipFormat: 'MMMM YYYY', + }, + gridLines: { + display: false, + }, + offset: true, + ticks: { + source: 'data', + }, + }, + ], + yAxes: [ + { + stacked: true, + id: 'strings-y-axis', + position: 'left', + scaleLabel: { + display: true, + labelString: 'STRINGS', + fontColor: '#FFF', + fontStyle: 100, + }, + gridLines: { + display: false, + }, + ticks: { + beginAtZero: true, + precision: 0, + }, + }, + ], }, - renderTimeToReviewSuggestions: function () { - var chart = $('#time-to-review-suggestions-chart'); - if (chart.length === 0) return; - var ctx = chart[0].getContext('2d'); + }, + }); - var gradient = ctx.createLinearGradient(0, 0, 0, 160); - gradient.addColorStop(0, '#4fc4f666'); - gradient.addColorStop(1, 'transparent'); + // Render custom legend + $('#review-activity-chart-legend').html( + reviewActivityChart.generateLegend(), + ); + Pontoon.insights.attachCustomLegendHandler( + reviewActivityChart, + '#review-activity-chart-legend .label', + ); + }, - new Chart(chart, { - type: 'bar', - data: { - labels: $('#insights').data('dates'), - datasets: [ - { - type: 'line', - label: 'Current month', - data: chart.data('time-to-review-suggestions'), - backgroundColor: gradient, - borderColor: ['#4fc4f6'], - borderWidth: 2, - pointBackgroundColor: '#4fc4f6', - pointHitRadius: 10, - pointRadius: 4, - pointHoverRadius: 6, - pointHoverBackgroundColor: '#4fc4f6', - pointHoverBorderColor: '#FFF', - }, - { - type: 'line', - label: '12-month average', - data: chart.data( - 'time-to-review-suggestions-12-month-avg', - ), - borderColor: ['#3e7089'], - borderWidth: 1, - pointBackgroundColor: '#3e7089', - pointHitRadius: 10, - pointRadius: 4, - pointHoverRadius: 6, - pointHoverBackgroundColor: '#3e7089', - pointHoverBorderColor: '#FFF', - }, - ], - }, - options: { - legend: { - display: false, - }, - tooltips: { - mode: 'index', - intersect: false, - borderColor: '#4fc4f6', - borderWidth: 1, - caretPadding: 5, - xPadding: 10, - yPadding: 10, - callbacks: { - label(items, chart) { - const { label } = - chart.datasets[items.datasetIndex]; - return `${label}: ${items.value} days`; - }, - }, - }, - scales: { - xAxes: [ - { - type: 'time', - time: { - displayFormats: { - month: 'MMM', - }, - tooltipFormat: 'MMMM YYYY', - }, - gridLines: { - display: false, - }, - offset: true, - ticks: { - source: 'data', - }, - }, - ], - yAxes: [ - { - gridLines: { - display: false, - }, - position: 'right', - ticks: { - beginAtZero: true, - maxTicksLimit: 3, - precision: 0, - callback: (value) => `${value} days`, - }, - }, - ], - }, - }, - }); - }, - renderTranslationActivity: function () { - var chart = $('#translation-activity-chart'); - if (chart.length === 0) return; - var ctx = chart[0].getContext('2d'); + getPercent: function (value, total) { + const pf = new Intl.NumberFormat('en', { + style: 'percent', + maximumFractionDigits: 2, + }); + const n = value / total; + return pf.format(isFinite(n) ? n : 0); + }, - var gradient = ctx.createLinearGradient(0, 0, 0, 400); - gradient.addColorStop(0, '#7BC87633'); - gradient.addColorStop(1, 'transparent'); + // Legend configuration doesn't allow for enough flexibility, + // so we build our own legend + // eslint-disable-next-line no-unused-vars + customLegend: function (chart) { + return function (chart) { + function renderLabels(chart) { + return chart.data.datasets + .map(function (dataset) { + var disabled = dataset.hidden ? 'disabled' : ''; + var color = dataset.borderColor || dataset.backgroundColor; - var humanData = chart.data('human-translations') || []; - var machineryData = chart.data('machinery-translations') || []; - var newSourcesData = chart.data('new-source-strings') || []; - - var translationActivityChart = new Chart(chart, { - type: 'bar', - data: { - labels: $('#insights').data('dates'), - datasets: [ - { - type: 'line', - label: 'Completion', - data: chart.data('completion'), - yAxisID: 'completion-y-axis', - backgroundColor: gradient, - borderColor: ['#7BC876'], - borderWidth: 2, - pointBackgroundColor: '#7BC876', - pointHitRadius: 10, - pointRadius: 4, - pointHoverRadius: 6, - pointHoverBackgroundColor: '#7BC876', - pointHoverBorderColor: '#FFF', - }, - humanData.length > 0 && { - type: 'bar', - label: 'Human translations', - data: humanData, - yAxisID: 'strings-y-axis', - backgroundColor: '#4f7256', - hoverBackgroundColor: '#4f7256', - stack: 'translations', - order: 2, - }, - machineryData.length > 0 && { - type: 'bar', - label: 'Machinery translations', - data: machineryData, - yAxisID: 'strings-y-axis', - backgroundColor: '#41554c', - hoverBackgroundColor: '#41554c', - stack: 'translations', - order: 1, - }, - newSourcesData.length > 0 && { - type: 'bar', - label: 'New source strings', - data: newSourcesData, - yAxisID: 'strings-y-axis', - backgroundColor: '#272a2f', - hoverBackgroundColor: '#272a2f', - stack: 'source-strings', - order: 3, - hidden: true, - }, - ].filter(Boolean), // Filter out empty values - }, - options: { - legend: { - display: false, - }, - legendCallback: Pontoon.insights.customLegend(chart), - tooltips: { - mode: 'index', - intersect: false, - borderColor: '#7BC876', - borderWidth: 1, - caretPadding: 5, - xPadding: 10, - yPadding: 10, - itemSort: function (a, b) { - // Dataset order affects stacking, tooltip and - // legend, but it doesn't work intuitively, so - // we need to manually sort tooltip items. - if ( - a.datasetIndex === 2 && - b.datasetIndex === 1 - ) { - return 1; - } - }, - callbacks: { - label: function (items, chart) { - const human = - chart.datasets[1].data[items.index]; - const machinery = - chart.datasets[2].data[items.index]; - - const label = - chart.datasets[items.datasetIndex] - .label; - const value = items.yLabel; - const base = - label + ': ' + nf.format(value); - - switch (label) { - case 'Completion': - return base + '%'; - case 'Human translations': - case 'Machinery translations': { - const pct = - Pontoon.insights.getPercent( - value, - human + machinery, - ); - return `${base} (${pct} of all translations)`; - } - default: - return base; - } - }, - }, - }, - scales: { - xAxes: [ - { - stacked: true, - type: 'time', - time: { - displayFormats: { - month: 'MMM', - }, - tooltipFormat: 'MMMM YYYY', - }, - gridLines: { - display: false, - }, - offset: true, - ticks: { - source: 'data', - }, - }, - ], - yAxes: [ - { - id: 'completion-y-axis', - position: 'right', - scaleLabel: { - display: true, - labelString: 'COMPLETION', - fontColor: '#FFF', - fontStyle: 100, - }, - gridLines: { - display: false, - }, - ticks: { - beginAtZero: true, - max: 100, - stepSize: 20, - callback: function (value) { - return value + ' %'; - }, - }, - }, - { - stacked: true, - id: 'strings-y-axis', - position: 'left', - scaleLabel: { - display: true, - labelString: 'STRINGS', - fontColor: '#FFF', - fontStyle: 100, - }, - gridLines: { - display: false, - }, - ticks: { - beginAtZero: true, - precision: 0, - }, - }, - ], - }, - }, - }); - - // Render custom legend - $('#translation-activity-chart-legend').html( - translationActivityChart.generateLegend(), + return ( + '

  • ' + + '' + + '' + + dataset.label + + '' + + '
  • ' ); - Pontoon.insights.attachCustomLegendHandler( - translationActivityChart, - '#translation-activity-chart-legend .label', - ); - }, - renderReviewActivity: function () { - var chart = $('#review-activity-chart'); - if (chart.length === 0) return; - var ctx = chart[0].getContext('2d'); + }) + .join(''); + } - var gradient = ctx.createLinearGradient(0, 0, 0, 400); - gradient.addColorStop(0, '#4fc4f688'); - gradient.addColorStop(1, 'transparent'); + return '
      ' + renderLabels(chart) + '
    '; + }; + }, + // Custom legend item event handler + attachCustomLegendHandler: function (chart, selector) { + $('body').on('click', selector, function () { + var li = $(this).parent(); + var index = li.index(); - var unreviewedData = chart.data('unreviewed') || []; - var peerApprovedData = chart.data('peer-approved') || []; - var selfApprovedData = chart.data('self-approved') || []; - var rejectedData = chart.data('rejected') || []; - var newSuggestionsData = chart.data('new-suggestions') || []; + var meta = chart.getDatasetMeta(index); + var dataset = chart.data.datasets[index]; - var reviewActivityChart = new Chart(chart, { - type: 'bar', - data: { - labels: $('#insights').data('dates'), - datasets: [ - { - type: 'line', - label: 'Unreviewed', - data: unreviewedData, - yAxisID: 'strings-y-axis', - backgroundColor: gradient, - borderColor: ['#4fc4f6'], - borderWidth: 2, - pointBackgroundColor: '#4fc4f6', - pointHitRadius: 10, - pointRadius: 4, - pointHoverRadius: 6, - pointHoverBackgroundColor: '#4fc4f6', - pointHoverBorderColor: '#FFF', - }, - peerApprovedData.length > 0 && { - type: 'bar', - label: 'Peer-approved', - data: peerApprovedData, - yAxisID: 'strings-y-axis', - backgroundColor: '#3e7089', - hoverBackgroundColor: '#3e7089', - stack: 'review-actions', - order: 3, - }, - selfApprovedData.length > 0 && { - type: 'bar', - label: 'Self-approved', - data: selfApprovedData, - yAxisID: 'strings-y-axis', - backgroundColor: '#385465', - hoverBackgroundColor: '#385465', - stack: 'review-actions', - order: 2, - }, - rejectedData.length > 0 && { - type: 'bar', - label: 'Rejected', - data: rejectedData, - yAxisID: 'strings-y-axis', - backgroundColor: '#843650', - hoverBackgroundColor: '#843650', - stack: 'review-actions', - order: 1, - }, - newSuggestionsData.length > 0 && { - type: 'bar', - label: 'New suggestions', - data: chart.data('new-suggestions'), - yAxisID: 'strings-y-axis', - backgroundColor: '#272a2f', - hoverBackgroundColor: '#272a2f', - stack: 'new-suggestions', - order: 4, - hidden: true, - }, - ].filter(Boolean), // Filter out empty values - }, - options: { - legend: { - display: false, - }, - legendCallback: Pontoon.insights.customLegend(chart), - tooltips: { - mode: 'index', - intersect: false, - borderColor: '#4fc4f6', - borderWidth: 1, - caretPadding: 5, - xPadding: 10, - yPadding: 10, - itemSort: function (a, b) { - // Dataset order affects stacking, tooltip and - // legend, but it doesn't work intuitively, so - // we need to manually sort tooltip items. - if ( - (a.datasetIndex === 3 && - b.datasetIndex === 2) || - (a.datasetIndex === 3 && - b.datasetIndex === 1) || - (a.datasetIndex === 2 && - b.datasetIndex === 1) - ) { - return 1; - } - }, - callbacks: { - label: function (items, chart) { - const label = - chart.datasets[items.datasetIndex] - .label; - const value = items.yLabel; - const base = - label + ': ' + nf.format(value); + meta.hidden = meta.hidden === null ? !dataset.hidden : null; + chart.update(); - if (chart.datasets.length < 4) return base; - - const peerApproved = - chart.datasets[1].data[items.index]; - const selfApproved = - chart.datasets[2].data[items.index]; - const rejected = - chart.datasets[3].data[items.index]; - - switch (label) { - case 'Self-approved': { - const pct = - Pontoon.insights.getPercent( - value, - peerApproved + selfApproved, - ); - return `${base} (${pct} of all approvals)`; - } - case 'Peer-approved': - case 'Rejected': { - const pct = - Pontoon.insights.getPercent( - value, - peerApproved + rejected, - ); - return `${base} (${pct} of peer-reviews)`; - } - default: - return base; - } - }, - }, - }, - scales: { - xAxes: [ - { - stacked: true, - type: 'time', - time: { - displayFormats: { - month: 'MMM', - }, - tooltipFormat: 'MMMM YYYY', - }, - gridLines: { - display: false, - }, - offset: true, - ticks: { - source: 'data', - }, - }, - ], - yAxes: [ - { - stacked: true, - id: 'strings-y-axis', - position: 'left', - scaleLabel: { - display: true, - labelString: 'STRINGS', - fontColor: '#FFF', - fontStyle: 100, - }, - gridLines: { - display: false, - }, - ticks: { - beginAtZero: true, - precision: 0, - }, - }, - ], - }, - }, - }); - - // Render custom legend - $('#review-activity-chart-legend').html( - reviewActivityChart.generateLegend(), - ); - Pontoon.insights.attachCustomLegendHandler( - reviewActivityChart, - '#review-activity-chart-legend .label', - ); - }, - - getPercent: function (value, total) { - const pf = new Intl.NumberFormat('en', { - style: 'percent', - maximumFractionDigits: 2, - }); - const n = value / total; - return pf.format(isFinite(n) ? n : 0); - }, - - // Legend configuration doesn't allow for enough flexibility, - // so we build our own legend - // eslint-disable-next-line no-unused-vars - customLegend: function (chart) { - return function (chart) { - function renderLabels(chart) { - return chart.data.datasets - .map(function (dataset) { - var disabled = dataset.hidden ? 'disabled' : ''; - var color = - dataset.borderColor || - dataset.backgroundColor; - - return ( - '
  • ' + - '' + - '' + - dataset.label + - '' + - '
  • ' - ); - }) - .join(''); - } - - return '
      ' + renderLabels(chart) + '
    '; - }; - }, - // Custom legend item event handler - attachCustomLegendHandler: function (chart, selector) { - $('body').on('click', selector, function () { - var li = $(this).parent(); - var index = li.index(); - - var meta = chart.getDatasetMeta(index); - var dataset = chart.data.datasets[index]; - - meta.hidden = meta.hidden === null ? !dataset.hidden : null; - chart.update(); - - li.toggleClass('disabled'); - }); - }, - }, - }); + li.toggleClass('disabled'); + }); + }, + }, + }); })(Pontoon || {}); /* Main code */ $('body').on('click', '#insights .suggestions-age nav li', function () { - var items = $('.suggestions-age nav li').removeClass('active'); - $(this).addClass('active'); - var index = items.index(this); - var itemWidth = $('.suggestions-age-item').first().outerWidth(); + var items = $('.suggestions-age nav li').removeClass('active'); + $(this).addClass('active'); + var index = items.index(this); + var itemWidth = $('.suggestions-age-item').first().outerWidth(); - // Show the selected graph view - $('.suggestions-age-items').css('marginLeft', -index * itemWidth); + // Show the selected graph view + $('.suggestions-age-items').css('marginLeft', -index * itemWidth); }); diff --git a/pontoon/machinery/static/css/machinery.css b/pontoon/machinery/static/css/machinery.css index 130c92728..a85b15f7d 100644 --- a/pontoon/machinery/static/css/machinery.css +++ b/pontoon/machinery/static/css/machinery.css @@ -1,32 +1,32 @@ .controls { - margin-bottom: 40px; + margin-bottom: 40px; } .locale-selector { - float: right; - width: auto; + float: right; + width: auto; } .controls > .search-wrapper .fa-spin, .controls > .search-wrapper.loading .fa-search { - display: none; + display: none; } .controls > .search-wrapper.loading .fa-spin { - display: block; + display: block; } .clipboard-success { - float: left; - color: #7bc876; + float: left; + color: #7bc876; } #helpers .machinery li { - padding-left: 5px; - padding-right: 5px; + padding-left: 5px; + padding-right: 5px; } #helpers .machinery li:hover { - background: #333941; - cursor: pointer; + background: #333941; + cursor: pointer; } diff --git a/pontoon/machinery/static/js/machinery.js b/pontoon/machinery/static/js/machinery.js index d011f71be..d5f7f882f 100644 --- a/pontoon/machinery/static/js/machinery.js +++ b/pontoon/machinery/static/js/machinery.js @@ -1,570 +1,540 @@ $(function () { - var self = Pontoon; + var self = Pontoon; - // Trigger search with Enter - $('#search input') - .unbind('keydown.pontoon') - .bind('keydown.pontoon', function (e) { - var value = $(this).val(); + // Trigger search with Enter + $('#search input') + .unbind('keydown.pontoon') + .bind('keydown.pontoon', function (e) { + var value = $(this).val(); - if (e.which === 13 && value.length > 0) { - self.locale = $('.locale .selector .language').data(); - getMachinery(value); - return false; - } - }); - - // Handle "Copy to clipboard" of search results on main Machinery page - var clipboard = new Clipboard('.machinery .machinery li'); - - clipboard.on('success', function (event) { - var successMessage = $( - 'Copied!', - ), - $trigger = $(event.trigger); - - $('.clipboard-success').remove(); - $trigger.find('header').prepend(successMessage); - setTimeout(function () { - successMessage.fadeOut(500, function () { - successMessage.remove(); - }); - }, 1000); + if (e.which === 13 && value.length > 0) { + self.locale = $('.locale .selector .language').data(); + getMachinery(value); + return false; + } }); - /* - * Get suggestions from machine translation and translation memory - * - * original Original string - */ - function getMachinery(original) { - var ul = $('#helpers > .machinery').children('ul').empty(), - tab = $('#search').addClass('loading'), // .loading class used on the /machinery page - requests = 0, - preferred = 0, - remaining = 0, - sourcesMap = {}; + // Handle "Copy to clipboard" of search results on main Machinery page + var clipboard = new Clipboard('.machinery .machinery li'); - self.NProgressUnbind(); + clipboard.on('success', function (event) { + var successMessage = $('Copied!'), + $trigger = $(event.trigger); - function append(data) { - var sources = sourcesMap[data.original + data.translation], - occurrencesTitle = 'Number of translation occurrences', - originalText = data.original, - translationText = data.translation; + $('.clipboard-success').remove(); + $trigger.find('header').prepend(successMessage); + setTimeout(function () { + successMessage.fadeOut(500, function () { + successMessage.remove(); + }); + }, 1000); + }); - if (sources) { - sources.append( - '
  • ' + - '' + - data.source + - '' + - (data.count - ? '' + - data.count + - '' - : '') + - '
  • ', - ); + /* + * Get suggestions from machine translation and translation memory + * + * original Original string + */ + function getMachinery(original) { + var ul = $('#helpers > .machinery').children('ul').empty(), + tab = $('#search').addClass('loading'), // .loading class used on the /machinery page + requests = 0, + preferred = 0, + remaining = 0, + sourcesMap = {}; - if (data.quality && sources.siblings('.stress').length === 0) { - sources.prepend( - '' + data.quality + '', - ); - } - } else { - var originalTextForDiff = originalText; - originalText = originalText - ? diff(original, originalTextForDiff) - : ''; + self.NProgressUnbind(); - var li = $( - '
  • ' + - '
    ' + - (data.quality - ? '' + data.quality + '' - : '') + - '' + - '
    ' + - '

    ' + - originalText + - '

    ' + - '

    ' + - markPlaceables(translationText) + - '

    ' + - '

    ' + - self.doNotRender(translationText) + - '

    ' + - '
  • ', - ); - ul.append(li); - sourcesMap[data.original + data.translation] = - li.find('.sources'); - if (data.source === 'Translation memory') { - preferred++; - } else { - remaining++; - } - } + function append(data) { + var sources = sourcesMap[data.original + data.translation], + occurrencesTitle = 'Number of translation occurrences', + originalText = data.original, + translationText = data.translation; - // Sort by quality - var listitems = ul.children('li'), - sourceMap = { - 'Translation memory': 1, - Mozilla: 2, - Microsoft: 3, - 'Systran Translate': 4, - 'Google Translate': 5, - 'Microsoft Translator': 6, - }; - - function getTranslationSource(el) { - var sources = $(el).find('.translation-source span'); - - if (sources.length > 1) { - return Math.min.apply( - Math, - $.map(sources, function (elem) { - return sourceMap[$(elem).text()]; - }), - ); - } else { - return sourceMap[sources.text()]; - } - } - - listitems.sort(function (a, b) { - var stressA = $(a).find('.stress'), - stressB = $(b).find('.stress'), - valA = stressA.length - ? parseInt(stressA.html().split('%')[0]) - : 0, - valB = stressB.length - ? parseInt(stressB.html().split('%')[0]) - : 0, - sourceA = getTranslationSource(a), - sourceB = getTranslationSource(b); - - return valA < valB - ? 1 - : valA > valB - ? -1 - : sourceA > sourceB - ? 1 - : sourceA < sourceB - ? -1 - : 0; - }); - - ul.html(listitems); - - // Sort sources inside results. - ul.find('.sources').each(function () { - var $sourcesList = $(this), - sources = $sourcesList.children('li'), - sortedItems = sources.sort(function (a, b) { - var sourceA = sourceMap[$(a).find('span').text()], - sourceB = sourceMap[$(b).find('span').text()]; - return sourceA > sourceB - ? 1 - : sourceA < sourceB - ? -1 - : 0; - }); - - $sourcesList.children('li').remove(); - - sortedItems.each(function () { - $sourcesList.append(this); - }); - }); - } - - function error(error) { - if (error.status === 0 && error.statusText !== 'abort') { - // Allows requesting Machinery again - editor.machinery = null; - if (ul.children('li').length === 0) { - noConnectionError(ul); - } - } - } - - function complete(jqXHR, status) { - if (status !== 'abort') { - requests--; - tab.find('.count') - .find('.preferred') - .html(preferred) - .toggle(preferred > 0) - .end() - .find('.plus') - .html('+') - .toggle(preferred > 0 && remaining > 0) - .end() - .find('.remaining') - .html(remaining) - .toggle(remaining > 0) - .end() - .toggle(preferred > 0 || remaining > 0); - - // All requests complete - if (requests === 0) { - // Stop the loader - $('#search').removeClass('loading'); - - // No match - if (ul.children('li').length === 0) { - tab.find('.count').hide(); - ul.append( - '
  • ' + - '

    No translations available.

    ' + - '
  • ', - ); - } - } - } - } - - // Translation memory - requests++; - - if (self.XHRtranslationMemory) { - self.XHRtranslationMemory.abort(); - } - - self.XHRtranslationMemory = $.ajax({ - url: '/translation-memory/', - data: { - text: original, - locale: self.locale.code, - }, - }) - .success(function (data) { - if (data) { - $.each(data, function () { - append({ - original: this.source, - quality: Math.round(this.quality) + '%', - url: '/', - title: 'Pontoon Homepage', - source: 'Translation memory', - translation: this.target, - count: this.count, - }); - }); - } - }) - .error(error) - .complete(complete); - - // Google Translate - if ( - $('#server').data('is-google-translate-supported') && - self.locale.google_translate_code - ) { - requests++; - - if (self.XHRgoogleTranslate) { - self.XHRgoogleTranslate.abort(); - } - - self.XHRgoogleTranslate = $.ajax({ - url: '/google-translate/', - data: { - text: original, - locale: self.locale.google_translate_code, - }, - }) - .success(function (data) { - if (data.translation) { - append({ - url: 'https://translate.google.com/', - title: 'Visit Google Translate', - source: 'Google Translate', - original: original, - translation: data.translation, - }); - } - }) - .error(error) - .complete(complete); - } - - // Microsoft Translator - if ( - $('#server').data('is-microsoft-translator-supported') && - self.locale.ms_translator_code - ) { - requests++; - - if (self.XHRmicrosoftTranslator) { - self.XHRmicrosoftTranslator.abort(); - } - - self.XHRmicrosoftTranslator = $.ajax({ - url: '/microsoft-translator/', - data: { - text: original, - locale: self.locale.ms_translator_code, - }, - }) - .success(function (data) { - if (data.translation) { - append({ - url: 'https://www.bing.com/translator', - title: 'Visit Bing Translator', - source: 'Microsoft Translator', - original: original, - translation: data.translation, - }); - } - }) - .error(error) - .complete(complete); - } - - // Systran Translate - if ( - $('#server').data('is-systran-translate-supported') && - self.locale.systran_translate_code - ) { - requests++; - - if (self.XHRsystranTranslate) { - self.XHRsystranTranslate.abort(); - } - - self.XHRsystranTranslate = $.ajax({ - url: '/systran-translate/', - data: { - text: original, - locale: self.locale.systran_translate_code, - }, - }) - .success(function (data) { - if (data.translation) { - append({ - url: 'https://translate.systran.net/translationTools', - title: 'Visit Systran Translate', - source: 'Systran Translate', - original: original, - translation: data.translation, - }); - } - }) - .error(error) - .complete(complete); - } - - // Microsoft Terminology - if (self.locale.ms_terminology_code.length) { - requests++; - - if (self.XHRmicrosoftTerminology) { - self.XHRmicrosoftTerminology.abort(); - } - - self.XHRmicrosoftTerminology = $.ajax({ - url: '/microsoft-terminology/', - data: { - text: original, - locale: self.locale.ms_terminology_code, - }, - }) - .success(function (data) { - if (data.translations) { - $.each(data.translations, function () { - append({ - original: this.source, - quality: Math.round(this.quality) + '%', - url: - 'https://www.microsoft.com/Language/en-US/Search.aspx?sString=' + - this.source + - '&langID=' + - self.locale.ms_terminology_code, - title: - 'Visit Microsoft Terminology Service API.\n' + - '© 2018 Microsoft Corporation. All rights reserved.', - source: 'Microsoft', - translation: this.target, - }); - }); - } - }) - .error(error) - .complete(complete); - } - - self.NProgressBind(); - } - - /* - * Get markup for a placeable instance. - */ - function getPlaceableMarkup(title, replacement) { - return ( - '' + - replacement + - '' + '' + + data.source + + '' + + (data.count + ? '' + data.count + '' + : '') + + '', ); - } - /* - * Mark single instance of a placeable in string - */ - function markPlaceable(string, regex, title, replacement) { - replacement = replacement || '$&'; - return string.replace(regex, getPlaceableMarkup(title, replacement)); - } - - /* - * Markup placeables - */ - function markPlaceables(string, whiteSpaces) { - whiteSpaces = whiteSpaces !== false; - - string = self.doNotRender(string); - - /* Special spaces */ - // Pontoon.doNotRender() replaces \u00A0 with   - string = markPlaceable(string, / /gi, 'Non-breaking space'); - string = markPlaceable( - string, - /[\u202F]/gi, - 'Narrow non-breaking space', - ); - string = markPlaceable(string, /[\u2009]/gi, 'Thin space'); - - /* Multiple spaces */ - string = string.replace(/ +/gi, function (match) { - var title = 'Multiple spaces'; - var replacement = ''; - - for (var i = 0; i < match.length; i++) { - replacement += ' · '; - } - return getPlaceableMarkup(title, replacement); - }); - - if (whiteSpaces) { - string = markWhiteSpaces(string); + if (data.quality && sources.siblings('.stress').length === 0) { + sources.prepend('' + data.quality + ''); } + } else { + var originalTextForDiff = originalText; + originalText = originalText ? diff(original, originalTextForDiff) : ''; - return string; - } - - /* - * Mark leading/trailing spaces in multiline strings (that contain newlines inside). - * Should be applied to a fully concatenated string, doesn't handle substrings well. - */ - function markWhiteSpaces(string) { - /* 'm' modifier makes regex applicable to every separate line in string, not the string as the whole. */ - - /* Leading space */ - string = string.replace( - /^(<(ins|del)>)*( )/gim, - '$1' + getPlaceableMarkup('Leading space', ' '), + var li = $( + '
  • ' + + '
    ' + + (data.quality + ? '' + data.quality + '' + : '') + + '' + + '
    ' + + '

    ' + + originalText + + '

    ' + + '

    ' + + markPlaceables(translationText) + + '

    ' + + '

    ' + + self.doNotRender(translationText) + + '

    ' + + '
  • ', ); + ul.append(li); + sourcesMap[data.original + data.translation] = li.find('.sources'); + if (data.source === 'Translation memory') { + preferred++; + } else { + remaining++; + } + } - /* Trailing space */ - string = string.replace( - /( )(<\/(ins|del)>)*$/gim, - getPlaceableMarkup('Trailing space', ' ') + '$2', - ); + // Sort by quality + var listitems = ul.children('li'), + sourceMap = { + 'Translation memory': 1, + Mozilla: 2, + Microsoft: 3, + 'Systran Translate': 4, + 'Google Translate': 5, + 'Microsoft Translator': 6, + }; - /* Newline */ - string = markPlaceable(string, /\n/gi, 'Newline character', '¶$&'); + function getTranslationSource(el) { + var sources = $(el).find('.translation-source span'); - /* Tab */ - string = markPlaceable(string, /\t/gi, 'Tab character', '→'); - return string; - } + if (sources.length > 1) { + return Math.min.apply( + Math, + $.map(sources, function (elem) { + return sourceMap[$(elem).text()]; + }), + ); + } else { + return sourceMap[sources.text()]; + } + } - /* - * Mark diff between the string and the reference string - */ - function diff(reference, string) { - var diff_obj = new diff_match_patch(); - var diff = diff_obj.diff_main(reference, string); - var output = ''; + listitems.sort(function (a, b) { + var stressA = $(a).find('.stress'), + stressB = $(b).find('.stress'), + valA = stressA.length ? parseInt(stressA.html().split('%')[0]) : 0, + valB = stressB.length ? parseInt(stressB.html().split('%')[0]) : 0, + sourceA = getTranslationSource(a), + sourceB = getTranslationSource(b); - diff_obj.diff_cleanupSemantic(diff); - diff_obj.diff_cleanupEfficiency(diff); + return valA < valB + ? 1 + : valA > valB + ? -1 + : sourceA > sourceB + ? 1 + : sourceA < sourceB + ? -1 + : 0; + }); - $.each(diff, function () { - var type = this[0]; - var slice = this[1]; + ul.html(listitems); - switch (type) { - case DIFF_INSERT: - output += '' + markPlaceables(slice, false) + ''; - break; + // Sort sources inside results. + ul.find('.sources').each(function () { + var $sourcesList = $(this), + sources = $sourcesList.children('li'), + sortedItems = sources.sort(function (a, b) { + var sourceA = sourceMap[$(a).find('span').text()], + sourceB = sourceMap[$(b).find('span').text()]; + return sourceA > sourceB ? 1 : sourceA < sourceB ? -1 : 0; + }); - case DIFF_DELETE: - output += '' + markPlaceables(slice, false) + ''; - break; + $sourcesList.children('li').remove(); - case DIFF_EQUAL: - output += markPlaceables(slice, false); - break; - } + sortedItems.each(function () { + $sourcesList.append(this); }); - - /* Marking of leading/trailing spaces has to be the last step to avoid false positives. */ - return markWhiteSpaces(output); + }); } - /* - * Show no connection error in helpers - * - * list List to append no connection error to - */ - function noConnectionError(list) { - list.append( - '
  • ' + - '

    Content not available while offline.

    ' + - '

    Check your connection and try again.

    ' + + function error(error) { + if (error.status === 0 && error.statusText !== 'abort') { + // Allows requesting Machinery again + editor.machinery = null; + if (ul.children('li').length === 0) { + noConnectionError(ul); + } + } + } + + function complete(jqXHR, status) { + if (status !== 'abort') { + requests--; + tab + .find('.count') + .find('.preferred') + .html(preferred) + .toggle(preferred > 0) + .end() + .find('.plus') + .html('+') + .toggle(preferred > 0 && remaining > 0) + .end() + .find('.remaining') + .html(remaining) + .toggle(remaining > 0) + .end() + .toggle(preferred > 0 || remaining > 0); + + // All requests complete + if (requests === 0) { + // Stop the loader + $('#search').removeClass('loading'); + + // No match + if (ul.children('li').length === 0) { + tab.find('.count').hide(); + ul.append( + '
  • ' + + '

    No translations available.

    ' + '
  • ', - ); + ); + } + } + } } + + // Translation memory + requests++; + + if (self.XHRtranslationMemory) { + self.XHRtranslationMemory.abort(); + } + + self.XHRtranslationMemory = $.ajax({ + url: '/translation-memory/', + data: { + text: original, + locale: self.locale.code, + }, + }) + .success(function (data) { + if (data) { + $.each(data, function () { + append({ + original: this.source, + quality: Math.round(this.quality) + '%', + url: '/', + title: 'Pontoon Homepage', + source: 'Translation memory', + translation: this.target, + count: this.count, + }); + }); + } + }) + .error(error) + .complete(complete); + + // Google Translate + if ( + $('#server').data('is-google-translate-supported') && + self.locale.google_translate_code + ) { + requests++; + + if (self.XHRgoogleTranslate) { + self.XHRgoogleTranslate.abort(); + } + + self.XHRgoogleTranslate = $.ajax({ + url: '/google-translate/', + data: { + text: original, + locale: self.locale.google_translate_code, + }, + }) + .success(function (data) { + if (data.translation) { + append({ + url: 'https://translate.google.com/', + title: 'Visit Google Translate', + source: 'Google Translate', + original: original, + translation: data.translation, + }); + } + }) + .error(error) + .complete(complete); + } + + // Microsoft Translator + if ( + $('#server').data('is-microsoft-translator-supported') && + self.locale.ms_translator_code + ) { + requests++; + + if (self.XHRmicrosoftTranslator) { + self.XHRmicrosoftTranslator.abort(); + } + + self.XHRmicrosoftTranslator = $.ajax({ + url: '/microsoft-translator/', + data: { + text: original, + locale: self.locale.ms_translator_code, + }, + }) + .success(function (data) { + if (data.translation) { + append({ + url: 'https://www.bing.com/translator', + title: 'Visit Bing Translator', + source: 'Microsoft Translator', + original: original, + translation: data.translation, + }); + } + }) + .error(error) + .complete(complete); + } + + // Systran Translate + if ( + $('#server').data('is-systran-translate-supported') && + self.locale.systran_translate_code + ) { + requests++; + + if (self.XHRsystranTranslate) { + self.XHRsystranTranslate.abort(); + } + + self.XHRsystranTranslate = $.ajax({ + url: '/systran-translate/', + data: { + text: original, + locale: self.locale.systran_translate_code, + }, + }) + .success(function (data) { + if (data.translation) { + append({ + url: 'https://translate.systran.net/translationTools', + title: 'Visit Systran Translate', + source: 'Systran Translate', + original: original, + translation: data.translation, + }); + } + }) + .error(error) + .complete(complete); + } + + // Microsoft Terminology + if (self.locale.ms_terminology_code.length) { + requests++; + + if (self.XHRmicrosoftTerminology) { + self.XHRmicrosoftTerminology.abort(); + } + + self.XHRmicrosoftTerminology = $.ajax({ + url: '/microsoft-terminology/', + data: { + text: original, + locale: self.locale.ms_terminology_code, + }, + }) + .success(function (data) { + if (data.translations) { + $.each(data.translations, function () { + append({ + original: this.source, + quality: Math.round(this.quality) + '%', + url: + 'https://www.microsoft.com/Language/en-US/Search.aspx?sString=' + + this.source + + '&langID=' + + self.locale.ms_terminology_code, + title: + 'Visit Microsoft Terminology Service API.\n' + + '© 2018 Microsoft Corporation. All rights reserved.', + source: 'Microsoft', + translation: this.target, + }); + }); + } + }) + .error(error) + .complete(complete); + } + + self.NProgressBind(); + } + + /* + * Get markup for a placeable instance. + */ + function getPlaceableMarkup(title, replacement) { + return ( + '' + replacement + '' + ); + } + + /* + * Mark single instance of a placeable in string + */ + function markPlaceable(string, regex, title, replacement) { + replacement = replacement || '$&'; + return string.replace(regex, getPlaceableMarkup(title, replacement)); + } + + /* + * Markup placeables + */ + function markPlaceables(string, whiteSpaces) { + whiteSpaces = whiteSpaces !== false; + + string = self.doNotRender(string); + + /* Special spaces */ + // Pontoon.doNotRender() replaces \u00A0 with   + string = markPlaceable(string, / /gi, 'Non-breaking space'); + string = markPlaceable(string, /[\u202F]/gi, 'Narrow non-breaking space'); + string = markPlaceable(string, /[\u2009]/gi, 'Thin space'); + + /* Multiple spaces */ + string = string.replace(/ +/gi, function (match) { + var title = 'Multiple spaces'; + var replacement = ''; + + for (var i = 0; i < match.length; i++) { + replacement += ' · '; + } + return getPlaceableMarkup(title, replacement); + }); + + if (whiteSpaces) { + string = markWhiteSpaces(string); + } + + return string; + } + + /* + * Mark leading/trailing spaces in multiline strings (that contain newlines inside). + * Should be applied to a fully concatenated string, doesn't handle substrings well. + */ + function markWhiteSpaces(string) { + /* 'm' modifier makes regex applicable to every separate line in string, not the string as the whole. */ + + /* Leading space */ + string = string.replace( + /^(<(ins|del)>)*( )/gim, + '$1' + getPlaceableMarkup('Leading space', ' '), + ); + + /* Trailing space */ + string = string.replace( + /( )(<\/(ins|del)>)*$/gim, + getPlaceableMarkup('Trailing space', ' ') + '$2', + ); + + /* Newline */ + string = markPlaceable(string, /\n/gi, 'Newline character', '¶$&'); + + /* Tab */ + string = markPlaceable(string, /\t/gi, 'Tab character', '→'); + return string; + } + + /* + * Mark diff between the string and the reference string + */ + function diff(reference, string) { + var diff_obj = new diff_match_patch(); + var diff = diff_obj.diff_main(reference, string); + var output = ''; + + diff_obj.diff_cleanupSemantic(diff); + diff_obj.diff_cleanupEfficiency(diff); + + $.each(diff, function () { + var type = this[0]; + var slice = this[1]; + + switch (type) { + case DIFF_INSERT: + output += '' + markPlaceables(slice, false) + ''; + break; + + case DIFF_DELETE: + output += '' + markPlaceables(slice, false) + ''; + break; + + case DIFF_EQUAL: + output += markPlaceables(slice, false); + break; + } + }); + + /* Marking of leading/trailing spaces has to be the last step to avoid false positives. */ + return markWhiteSpaces(output); + } + + /* + * Show no connection error in helpers + * + * list List to append no connection error to + */ + function noConnectionError(list) { + list.append( + '
  • ' + + '

    Content not available while offline.

    ' + + '

    Check your connection and try again.

    ' + + '
  • ', + ); + } }); diff --git a/pontoon/projects/static/css/manual_notifications.css b/pontoon/projects/static/css/manual_notifications.css index 6b1c5ae6f..ce621cc7e 100644 --- a/pontoon/projects/static/css/manual_notifications.css +++ b/pontoon/projects/static/css/manual_notifications.css @@ -1,85 +1,85 @@ .manual-notifications .right-column #compose { - padding: 20px; + padding: 20px; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .manual-notifications #compose h3 { - color: #ebebeb; - font-size: 22px; - letter-spacing: 0; + color: #ebebeb; + font-size: 22px; + letter-spacing: 0; } .manual-notifications #compose h3 .stress { - color: #7bc876; + color: #7bc876; } .manual-notifications #compose .toolbar { - padding: 10px 0 0; + padding: 10px 0 0; } .manual-notifications #compose .controls { - margin: 0; - text-align: right; + margin: 0; + text-align: right; } .manual-notifications #compose .errors { - float: right; - text-align: right; + float: right; + text-align: right; } .manual-notifications #compose .errors p { - color: #f36; - text-transform: uppercase; - visibility: hidden; + color: #f36; + text-transform: uppercase; + visibility: hidden; } .manual-notifications #compose .locale-selector { - margin: 20px 0 40px; + margin: 20px 0 40px; } .manual-notifications #compose .locale-selector .locale.select .menu { - width: 285px; /* must be same as .shortcuts */ + width: 285px; /* must be same as .shortcuts */ } .manual-notifications #compose .locale-selector .shortcuts { - float: left; - font-size: 14px; - width: 285px; /* must be same as .menu */ + float: left; + font-size: 14px; + width: 285px; /* must be same as .menu */ } .manual-notifications #compose .locale-selector .shortcuts .complete { - float: left; + float: left; } .manual-notifications #compose .locale-selector .shortcuts .incomplete { - float: right; + float: right; } .manual-notifications #compose .message-wrapper .subtitle { - color: #aaa; - float: left; - text-transform: uppercase; + color: #aaa; + float: left; + text-transform: uppercase; } .manual-notifications #compose textarea { - background: #272a2f; - color: #ebebeb; - font-size: 14px; - font-weight: 300; - height: 150px; - padding: 10px; - width: 100%; + background: #272a2f; + color: #ebebeb; + font-size: 14px; + font-weight: 300; + height: 150px; + padding: 10px; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .manual-notifications #sent li.no { - padding-top: 14px; + padding-top: 14px; } .manual-notifications #sent li.no .icon { - color: #272a2f; + color: #272a2f; } diff --git a/pontoon/projects/static/js/manual_notifications.js b/pontoon/projects/static/js/manual_notifications.js index 91fb97fd4..dc22c9e13 100644 --- a/pontoon/projects/static/js/manual_notifications.js +++ b/pontoon/projects/static/js/manual_notifications.js @@ -1,71 +1,67 @@ $(function () { - var container = $('#main .container'); + var container = $('#main .container'); - function isValidForm($form, locales, message) { - $form.find('.errors p').css('visibility', 'hidden'); + function isValidForm($form, locales, message) { + $form.find('.errors p').css('visibility', 'hidden'); - if (!locales) { - $form - .find('.locale-selector .errors p') - .css('visibility', 'visible'); - } - - if (!message) { - $form - .find('.message-wrapper .errors p') - .css('visibility', 'visible'); - } - - return locales && message; + if (!locales) { + $form.find('.locale-selector .errors p').css('visibility', 'visible'); } - // Send notification - container.on('click', '#send-notification .send', function (e) { - e.preventDefault(); - var $form = $('#send-notification'); + if (!message) { + $form.find('.message-wrapper .errors p').css('visibility', 'visible'); + } - // Validate form - var locales = $form.find('[name=selected_locales]').val(), - message = $form.find('[name=message]').val(); + return locales && message; + } - if (!isValidForm($form, locales, message)) { - return; + // Send notification + container.on('click', '#send-notification .send', function (e) { + e.preventDefault(); + var $form = $('#send-notification'); + + // Validate form + var locales = $form.find('[name=selected_locales]').val(), + message = $form.find('[name=message]').val(); + + if (!isValidForm($form, locales, message)) { + return; + } + + // Submit form + $.ajax({ + url: $form.prop('action'), + type: $form.prop('method'), + data: $form.serialize(), + success: function (data) { + if (data.selected_locales || data.message) { + isValidForm($form, !data.selected_locales, !data.message); + return false; } - // Submit form - $.ajax({ - url: $form.prop('action'), - type: $form.prop('method'), - data: $form.serialize(), - success: function (data) { - if (data.selected_locales || data.message) { - isValidForm($form, !data.selected_locales, !data.message); - return false; - } - - Pontoon.endLoader('Notification sent.'); - container.empty().append(data); - }, - error: function () { - Pontoon.endLoader('Oops, something went wrong.', 'error'); - }, - }); + Pontoon.endLoader('Notification sent.'); + container.empty().append(data); + }, + error: function () { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + }, }); + }); - // Recipient shortcuts - container.on('click', '.locale-selector .shortcuts a', function (e) { - e.preventDefault(); + // Recipient shortcuts + container.on('click', '.locale-selector .shortcuts a', function (e) { + e.preventDefault(); - var locales = $(this).data('ids').reverse(), - $localeSelector = $(this).parents('.locale-selector'); + var locales = $(this).data('ids').reverse(), + $localeSelector = $(this).parents('.locale-selector'); - $localeSelector.find('.selected .move-all').click(); + $localeSelector.find('.selected .move-all').click(); - $(locales).each(function (i, id) { - $localeSelector - .find('.locale.select:first') - .find('[data-id=' + id + ']') - .click(); - }); + $(locales).each(function (i, id) { + $localeSelector + .find('.locale.select:first') + .find('[data-id=' + id + ']') + .click(); }); + }); }); diff --git a/pontoon/static/js/errors/index.js b/pontoon/static/js/errors/index.js index 95b38ba7c..d3a1cfa2b 100644 --- a/pontoon/static/js/errors/index.js +++ b/pontoon/static/js/errors/index.js @@ -1,8 +1,8 @@ export class NotImplementedError extends Error { - constructor(...args) { - super(...args); - Error.captureStackTrace(this, NotImplementedError); - } + constructor(...args) { + super(...args); + Error.captureStackTrace(this, NotImplementedError); + } } NotImplementedError.prototype = Error.prototype; diff --git a/pontoon/sync/README.md b/pontoon/sync/README.md index cbb372868..52d5575ea 100644 --- a/pontoon/sync/README.md +++ b/pontoon/sync/README.md @@ -9,7 +9,6 @@ directly, as well as to write its own changes back. This document describes that sync process in detail. - ## Triggering a Sync Pontoon is assumed to run a sync once an hour, although this is configurable. @@ -18,7 +17,6 @@ disabled within the admin interface and schedules a sync task for each one. Sync tasks are executed in parallel, using [Celery](http://www.celeryproject.org/) to manage the worker queue. - ## Syncing a Project Syncing an individual project is split into two tasks. The first one is syncing @@ -46,7 +44,6 @@ The second step is syncing translations: changes, no commit is made. - Clean up leftover information in the database. - ## Comparing Entities The heart of the syncing process is comparing an entity stored in Pontoon's @@ -70,7 +67,6 @@ The actual comparison logic goes something like this: ![](./sync-process-diagram.png) - ## Executing Changes Entity comparison produces a Changeset, which is used to make the necessary @@ -80,24 +76,24 @@ Changesets can perform 4 different operations on an entity: **Update Pontoon from VCS** -  Add a translation from VCS to Pontoon if necessary. Existing translations - that match the VCS translation are re-used, and all non-matching translations - are marked as unapproved. + Add a translation from VCS to Pontoon if necessary. Existing translations +that match the VCS translation are re-used, and all non-matching translations +are marked as unapproved. **Update VCS from Pontoon** -  Add a translation from Pontoon to VCS, overwriting the existing translation - if it exists. + Add a translation from Pontoon to VCS, overwriting the existing translation +if it exists. **Create New Entity in Pontoon** -  Create a new entity in the Pontoon database, including the VCS translation if - it is present. + Create a new entity in the Pontoon database, including the VCS translation if +it is present. **Obsolete Pontoon Entity** -  Mark an entity in the database as obsolete, due to it not existing in VCS. - The entity will no longer appear on the website. + Mark an entity in the database as obsolete, due to it not existing in VCS. +The entity will no longer appear on the website. When possible, Changesets perform database operations in bulk in order to speed up the syncing process. diff --git a/pontoon/sync/static/css/sync_logs.css b/pontoon/sync/static/css/sync_logs.css index d8307624e..2ee0c6393 100644 --- a/pontoon/sync/static/css/sync_logs.css +++ b/pontoon/sync/static/css/sync_logs.css @@ -1,141 +1,141 @@ /** @listing */ .log-list { - text-align: left; - width: 100%; + text-align: left; + width: 100%; } .log-list .log-list-column { - font-weight: bold; - padding: 0.4em; - text-transform: uppercase; + font-weight: bold; + padding: 0.4em; + text-transform: uppercase; } .sync-log .start-time, .sync-log .start-date, .sync-log .duration { - padding: 0.4em; - font-size: 14px; + padding: 0.4em; + font-size: 14px; } .sync-log .start-time a { - color: #7bc876; + color: #7bc876; } /** @details */ td { - padding: 5px 10px; + padding: 5px 10px; } .sync-details { - display: flex; - flex-direction: row; - justify-content: center; - list-style-type: none; - margin: 0; - padding: 1em; - text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + list-style-type: none; + margin: 0; + padding: 1em; + text-align: center; } .sync-details .detail-item { - flex: 1; - margin: 0 1px; + flex: 1; + margin: 0 1px; } .sync-details .detail-label { - border: none; - color: #aaaaaa; - font-size: 12px; - margin: 0.5em 0; - text-transform: uppercase; + border: none; + color: #aaaaaa; + font-size: 12px; + margin: 0.5em 0; + text-transform: uppercase; } .sync-details .detail-value { - border: none; - font-size: 16px; + border: none; + font-size: 16px; } .command-details { - background: #333941; + background: #333941; } .command-details .detail-item { - border-top: 4px solid; - flex: 0 0 325px; + border-top: 4px solid; + flex: 0 0 325px; } .command-details .start-time { - border-color: #7bc876; + border-color: #7bc876; } .command-details .end-time { - border-color: #4fc4f6; + border-color: #4fc4f6; } .command-details .duration { - border-color: #fed271; + border-color: #fed271; } .project-details .start-time .detail-label, .repository-details .start-time .detail-label { - color: #7bc876; + color: #7bc876; } .project-details .end-time .detail-label, .repository-details .end-time .detail-label { - color: #4fc4f6; + color: #4fc4f6; } .project-details .duration .detail-label, .repository-details .duration .detail-label { - color: #fed271; + color: #fed271; } .project { - border: 1px solid #5c6172; - border-radius: 5px; - margin-bottom: 1em; - padding: 1em; + border: 1px solid #5c6172; + border-radius: 5px; + margin-bottom: 1em; + padding: 1em; } .project .project-name { - color: #aaaaaa; - font-size: 36px; - text-align: left; - text-transform: none; + color: #aaaaaa; + font-size: 36px; + text-align: left; + text-transform: none; } .repository-logs { - list-style-type: none; - width: 100%; + list-style-type: none; + width: 100%; } .repository-logs .repository-logs-column { - color: #aaaaaa; - padding: 1em; - text-align: left; - text-transform: uppercase; + color: #aaaaaa; + padding: 1em; + text-align: left; + text-transform: uppercase; } .repository .repository-url { - vertical-align: middle; + vertical-align: middle; } .repository:nth-child(even) { - background: #333941; + background: #333941; } /** @pagination */ .pagination { - font-size: 14px; - padding: 3em 0; - text-align: center; - text-transform: uppercase; + font-size: 14px; + padding: 3em 0; + text-align: center; + text-transform: uppercase; } .pagination .previous { - float: left; + float: left; } .pagination .next { - float: right; + float: right; } diff --git a/pontoon/teams/static/css/info.css b/pontoon/teams/static/css/info.css index 8a4236ad7..4638480e7 100644 --- a/pontoon/teams/static/css/info.css +++ b/pontoon/teams/static/css/info.css @@ -1,38 +1,38 @@ #info-wrapper .edit-info .fa { - padding-right: 5px; + padding-right: 5px; } #info-wrapper .read-write-info textarea { - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - color: #ffffff; - font-weight: 300; - margin-bottom: 10px; - padding: 5px; - width: 100%; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + color: #ffffff; + font-weight: 300; + margin-bottom: 10px; + padding: 5px; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } #info-wrapper .read-write-info .toolbar .subtitle { - color: #aaa; - float: left; - text-transform: uppercase; + color: #aaa; + float: left; + text-transform: uppercase; } #info-wrapper .controls { - text-align: right; + text-align: right; } #info-wrapper .controls .cancel { - display: none; - color: #7bc876; - margin: 9px; - text-transform: uppercase; + display: none; + color: #7bc876; + margin: 9px; + text-transform: uppercase; } #info-wrapper .controls .save { - display: none; + display: none; } diff --git a/pontoon/teams/static/css/multiple_team_selector.css b/pontoon/teams/static/css/multiple_team_selector.css index 9ee2fd1f7..21f0822a4 100644 --- a/pontoon/teams/static/css/multiple_team_selector.css +++ b/pontoon/teams/static/css/multiple_team_selector.css @@ -2,70 +2,70 @@ * All styles are related to forms visible for the contributor. */ form.user-locales-settings { - width: 620px; + width: 620px; } form.user-locales-settings div { - text-align: right; + text-align: right; } /** * CSS classes related to the select locale widget. */ form .locale.select.selected { - float: right; + float: right; } form .locale.select .menu { - background: transparent; - border-bottom: 1px solid #5e6475; - margin: 2px 0 -4px -1px; - overflow: auto; - padding: 10px 0; - width: 295px; + background: transparent; + border-bottom: 1px solid #5e6475; + margin: 2px 0 -4px -1px; + overflow: auto; + padding: 10px 0; + width: 295px; } form > div { - margin: 20px 0; + margin: 20px 0; } form .locale.select .menu ul { - height: 170px; - margin-bottom: 0; + height: 170px; + margin-bottom: 0; } form .locale.select .menu ul li span.code { - float: right; - width: auto; + float: right; + width: auto; } form .locale.select { - float: left; - width: auto; + float: left; + width: auto; } form .locale li { - cursor: pointer; + cursor: pointer; } form .locale .sortable li { - cursor: grab; - cursor: -webkit-grab; + cursor: grab; + cursor: -webkit-grab; } .select { - text-align: left; + text-align: left; } label { - display: block; - padding-bottom: 3px; - text-align: left; + display: block; + padding-bottom: 3px; + text-align: left; } form a:link, form a:visited { - color: #7bc876; - float: right; - text-transform: uppercase; + color: #7bc876; + float: right; + text-transform: uppercase; } diff --git a/pontoon/teams/static/css/request.css b/pontoon/teams/static/css/request.css index fa69efa10..13802912e 100644 --- a/pontoon/teams/static/css/request.css +++ b/pontoon/teams/static/css/request.css @@ -1,185 +1,185 @@ .request-toggle { - float: right; + float: right; } .request-toggle:before { - margin-right: 2px; + margin-right: 2px; } .request-toggle.back:after { - margin-left: 2px; + margin-left: 2px; } /* Bug 1468997 */ .request-team { - display: none; + display: none; } .request-team:before { - content: 'Request new team'; + content: 'Request new team'; } .request-team.back:after { - content: 'Back to enabled teams'; + content: 'Back to enabled teams'; } .request-projects:before { - content: 'Request more projects'; + content: 'Request more projects'; } .request-projects.back:after { - content: 'Back to enabled projects'; + content: 'Back to enabled projects'; } .request-teams:before { - content: 'Request new language'; + content: 'Request new language'; } .request-teams.back:after { - content: 'Back to enabled languages'; + content: 'Back to enabled languages'; } .request-toggle:after, .request-toggle.back:before { - content: ''; + content: ''; } #request-team-form { - margin: 0 auto; - display: none; + margin: 0 auto; + display: none; } #request-item-note { - margin: 25px 0 5px; - display: none; + margin: 25px 0 5px; + display: none; } #request-item-note p { - font-style: italic; - color: #aaaaaa; - text-align: center; - display: block; + font-style: italic; + color: #aaaaaa; + text-align: center; + display: block; } #request-item { - display: none; - background: #7bc876; - border: none; - border-radius: 3px; - margin-top: 15px; - padding: 10px; - text-transform: uppercase; - width: 100%; + display: none; + background: #7bc876; + border: none; + border-radius: 3px; + margin-top: 15px; + padding: 10px; + text-transform: uppercase; + width: 100%; } #request-item.confirmed { - background: #fed271; + background: #fed271; } #team-form { - display: flex; - position: relative; - text-align: left; - margin: 0 auto; - justify-content: space-between; + display: flex; + position: relative; + text-align: left; + margin: 0 auto; + justify-content: space-between; } #team-form .field { - display: inline-block; - margin-top: 10px; - text-align: left; + display: inline-block; + margin-top: 10px; + text-align: left; } #team-form .field input { - color: #ffffff; - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - float: none; - width: 480px; - padding: 4px; - box-shadow: none; + color: #ffffff; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + float: none; + width: 480px; + padding: 4px; + box-shadow: none; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } #team-form .field label { - display: block; - padding-bottom: 3px; - text-align: left; + display: block; + padding-bottom: 3px; + text-align: left; } .item-list tbody tr:not(.limited) { - display: none; + display: none; } .items.request .item-list .all-strings { - display: table-cell; + display: table-cell; } .items.request .item-list th.check { - text-align: right; - width: auto; + text-align: right; + width: auto; } .item-list .check, .item-list .radio, .item-list .all-strings { - display: none; + display: none; } .items.request .item-list .check, .items.request .item-list .radio { - cursor: pointer; - display: block; + cursor: pointer; + display: block; } .item-list td.check, .item-list td.radio { - color: #3f4752; - float: right; - font-size: 16px; - height: 47px; - text-align: right; - width: 100%; + color: #3f4752; + float: right; + font-size: 16px; + height: 47px; + text-align: right; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .item-list td.radio { - color: #4d5967; + color: #4d5967; } .item-list td.check:before { - content: ''; - display: block; - margin-top: 16px; + content: ''; + display: block; + margin-top: 16px; } .item-list td.check:hover:before, .item-list td.check.enabled:before { - content: ''; - color: #7bc876; + content: ''; + color: #7bc876; } .item-list td.check.enabled:before { - content: ''; + content: ''; } .item-list td.radio:before { - display: block; - margin-top: 16px; + display: block; + margin-top: 16px; } .item-list td.radio:hover:before, .item-list td.radio.enabled:before { - color: #7bc876; + color: #7bc876; } .items.request .item-list .latest-activity, .items.request .item-list .progress, .items.request .item-list .unreviewed-status { - display: none; + display: none; } diff --git a/pontoon/teams/static/css/team.css b/pontoon/teams/static/css/team.css index 01a37fe02..03a87cd8b 100644 --- a/pontoon/teams/static/css/team.css +++ b/pontoon/teams/static/css/team.css @@ -1,221 +1,221 @@ .buglist { - display: none; - table-layout: fixed; + display: none; + table-layout: fixed; } .buglist .id { - width: 80px; + width: 80px; } .buglist .summary { - width: 430px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + width: 430px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .buglist .last-changed { - text-align: left; - width: 130px; + text-align: left; + width: 130px; } .buglist .assigned-to { - width: 260px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: left; + width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; } .buglist tbody .id a { - color: #7bc876; + color: #7bc876; } .buglist th { - cursor: pointer; + cursor: pointer; } .buglist th:hover { - color: #ebebeb; + color: #ebebeb; } .buglist th i { - margin-left: 5px; - position: absolute; + margin-left: 5px; + position: absolute; } .buglist th.asc i:after { - content: ''; - display: inline-block; - margin-top: 5px; + content: ''; + display: inline-block; + margin-top: 5px; } .buglist th.desc i:after { - content: ''; - display: inline-block; - margin-top: -5px; + content: ''; + display: inline-block; + margin-top: -5px; } .controls.no-projects { - text-align: right; + text-align: right; } .controls.no-projects > .request-toggle.button { - float: none; + float: none; } .controls.no-projects > .search-wrapper { - display: none; + display: none; } #permissions-form .selector-wrapper { - white-space: nowrap; + white-space: nowrap; } #permissions-form h3 { - padding-bottom: 30px; + padding-bottom: 30px; } #permissions-form h3 .remove-project { - bottom: 25px; - font-size: 13px; - font-style: normal; - letter-spacing: 0; - position: absolute; - right: 0; + bottom: 25px; + font-size: 13px; + font-style: normal; + letter-spacing: 0; + position: absolute; + right: 0; } #permissions-form h3 .remove-project:hover { - background: #f36; + background: #f36; } #permissions-form h3 .remove-project .fa { - float: left; - font-size: 13px; - margin-top: 2px; - padding-right: 5px; + float: left; + font-size: 13px; + margin-top: 2px; + padding-right: 5px; } #permissions-form .permissions-groups { - margin-bottom: 80px; + margin-bottom: 80px; } #permissions-form .user.select { - display: inline-block; - vertical-align: top; - width: 300px; + display: inline-block; + vertical-align: top; + width: 300px; } #permissions-form .user.select.translators { - text-align: center; + text-align: center; } #permissions-form .user.select.managers { - text-align: right; + text-align: right; } #permissions-form .user.select.translators, #permissions-form .user.select.managers { - margin-left: 40px; + margin-left: 40px; } #permissions-form .user.select label { - display: block; + display: block; } #permissions-form .user.select label, #permissions-form .user.select label a { - color: #aaaaaa; - font-size: 13px; - font-weight: bold; - text-align: center; - text-transform: uppercase; + color: #aaaaaa; + font-size: 13px; + font-weight: bold; + text-align: center; + text-transform: uppercase; } #permissions-form .user.select label a { - display: inline-block; + display: inline-block; } #permissions-form .user.select.translators label:hover, #permissions-form .user.select.managers label:hover, #permissions-form .user.select label a.active, #permissions-form .user.select label a:hover { - color: #ebebeb; + color: #ebebeb; } #permissions-form .user.select .menu { - border-bottom: 1px solid #5e6475; - overflow: auto; - padding: 10px 0; + border-bottom: 1px solid #5e6475; + overflow: auto; + padding: 10px 0; } #permissions-form .user.select .menu input[type='search'] { - width: 100%; + width: 100%; } #permissions-form .user.select .menu ul { - height: 168px; - margin-bottom: 0; - white-space: normal; + height: 168px; + margin-bottom: 0; + white-space: normal; } #permissions-form .user.select .menu ul li { - cursor: pointer; - line-height: 17px; - position: relative; - width: 100%; + cursor: pointer; + line-height: 17px; + position: relative; + width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } #permissions-form .user.select.available .menu ul li:not(.contributor) { - display: none; + display: none; } #permissions-form .user.select .intro { - color: #aaa; - font-size: 13px; - font-style: italic; - font-weight: 300; - margin: 10px 0; - text-align: center; - white-space: normal; + color: #aaa; + font-size: 13px; + font-style: italic; + font-weight: 300; + margin: 10px 0; + text-align: center; + white-space: normal; } #permissions-form .button.save { - float: right; + float: right; } #permissions-form #project-selector { - float: right; + float: right; } #permissions-form #project-selector .button { - margin-right: 10px; - padding: 6px 12px; + margin-right: 10px; + padding: 6px 12px; } #permissions-form #project-selector .button .icon { - float: right; - margin-left: 5px; + float: right; + margin-left: 5px; } #permissions-form #project-selector .menu { - background: #333941; - bottom: 28px; - width: 287px; + background: #333941; + bottom: 28px; + width: 287px; } #permissions-form #project-selector .menu .search-wrapper input { - width: 100%; + width: 100%; } #permissions-form #project-selector .menu li { - color: #cccccc; - cursor: pointer; + color: #cccccc; + cursor: pointer; } #project-selector .menu li:not(.limited) { - display: none; + display: none; } diff --git a/pontoon/teams/static/css/team_selector.css b/pontoon/teams/static/css/team_selector.css index 173987401..860945a08 100644 --- a/pontoon/teams/static/css/team_selector.css +++ b/pontoon/teams/static/css/team_selector.css @@ -1,53 +1,53 @@ .locale-selector .locale.select .button.breadcrumbs { - background: #333941; - border-radius: 2px; - box-sizing: border-box; - font-size: 14px; - height: 28px; - margin: 0; - padding: 6px 12px; - text-align: left; - text-transform: uppercase; - width: 240px; + background: #333941; + border-radius: 2px; + box-sizing: border-box; + font-size: 14px; + height: 28px; + margin: 0; + padding: 6px 12px; + text-align: left; + text-transform: uppercase; + width: 240px; } .locale-selector .locale.select .button:before, .locale-selector .locale.select .button:after { - display: none; + display: none; } .locale-selector .locale.select.opened .button { - border-radius: 2px 2px 0 0; + border-radius: 2px 2px 0 0; } .locale-selector .locale.select .button .code { - text-transform: none; + text-transform: none; } .locale-selector .locale.select .menu { - background: #333941; - top: 28px; - display: none; - position: absolute; - right: 0; - width: 240px; + background: #333941; + top: 28px; + display: none; + position: absolute; + right: 0; + width: 240px; } .locale-selector .locale.select .menu ul { - max-height: 214px; + max-height: 214px; } .locale-selector .locale.select .menu li { - color: #cccccc; - cursor: pointer; - text-align: left; + color: #cccccc; + cursor: pointer; + text-align: left; } .locale-selector .locale.select .code { - float: right; + float: right; } .locale-selector .locale.select .menu ul li span.code { - float: right; - width: auto; + float: right; + width: auto; } diff --git a/pontoon/teams/static/js/bugzilla.js b/pontoon/teams/static/js/bugzilla.js index d0f37a497..141d95830 100755 --- a/pontoon/teams/static/js/bugzilla.js +++ b/pontoon/teams/static/js/bugzilla.js @@ -1,177 +1,164 @@ var Pontoon = (function (my) { - return $.extend(true, my, { - bugzilla: { - /* - * Retrieve bugs for the given locale and update bug count and tab content - * using the provided elements and callbacks. - * - * Heavily inspired by the similar functionality available in Elmo. - * - * Source: https://github.com/mozilla/elmo/blob/master/apps/bugsy/static/bugsy/js/bugcount.js - * Authors: Pike, peterbe, adngdb - */ - getLocaleBugs: function ( - locale, - container, - tab, - countCallback, - errorCallback, - ) { - return $.ajax({ - url: 'https://bugzilla.mozilla.org/rest/bug', - data: { - 'field0-0-0': 'component', - 'type0-0-0': 'regexp', - 'value0-0-0': '^' + locale + ' / ', - 'field0-0-1': 'cf_locale', - 'type0-0-1': 'regexp', - 'value0-0-1': '^' + locale + ' / ', - resolution: '---', - include_fields: - 'id,summary,last_change_time,assigned_to', - }, - success: function (data) { - if (data.bugs.length) { - data.bugs.sort(function (l, r) { - return l.last_change_time < r.last_change_time - ? 1 - : -1; - }); + return $.extend(true, my, { + bugzilla: { + /* + * Retrieve bugs for the given locale and update bug count and tab content + * using the provided elements and callbacks. + * + * Heavily inspired by the similar functionality available in Elmo. + * + * Source: https://github.com/mozilla/elmo/blob/master/apps/bugsy/static/bugsy/js/bugcount.js + * Authors: Pike, peterbe, adngdb + */ + getLocaleBugs: function ( + locale, + container, + tab, + countCallback, + errorCallback, + ) { + return $.ajax({ + url: 'https://bugzilla.mozilla.org/rest/bug', + data: { + 'field0-0-0': 'component', + 'type0-0-0': 'regexp', + 'value0-0-0': '^' + locale + ' / ', + 'field0-0-1': 'cf_locale', + 'type0-0-1': 'regexp', + 'value0-0-1': '^' + locale + ' / ', + resolution: '---', + include_fields: 'id,summary,last_change_time,assigned_to', + }, + success: function (data) { + if (data.bugs.length) { + data.bugs.sort(function (l, r) { + return l.last_change_time < r.last_change_time ? 1 : -1; + }); - var tbody = $(''), - formatter = new Intl.DateTimeFormat('en-GB', { - day: 'numeric', - month: 'long', - year: 'numeric', - }); - - $.each(data.bugs, function (i, bug) { - // Prevent malicious bug summary from executin JS code - var summary = Pontoon.doNotRender(bug.summary); - - var tr = $('', { - title: summary, - }); - - $('', { - class: 'id', - html: - '' + - bug.id + - '', - }).appendTo(tr); - - $('', { - class: 'summary', - html: summary, - }).appendTo(tr); - - $('', { - class: 'last-changed', - datetime: bug.last_change_time, - html: formatter.format( - new Date(bug.last_change_time), - ), - }).appendTo(tr); - - $('', { - class: 'assigned-to', - html: bug.assigned_to, - }).appendTo(tr); - - tbody.append(tr); - }); - - var table = $('', { - class: 'buglist striped', - html: - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '', - }).append(tbody); - - container.append(table.show()); - - var count = data.bugs.length; - countCallback(tab, count); - } else { - errorCallback('Zarro Boogs Found.'); - } - }, - error: function (error) { - if ( - error.status === 0 && - error.statusText !== 'abort' - ) { - errorCallback( - 'Oops, something went wrong. We were unable to load the bugs. Please try again later.', - ); - } - }, + var tbody = $(''), + formatter = new Intl.DateTimeFormat('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric', }); - }, - /* - * Sort Bug Table - */ - sort: (function () { - $('body').on('click', 'table.buglist th', function () { - function getString(el) { - return $(el) - .find('td:eq(' + index + ')') - .text(); - } + $.each(data.bugs, function (i, bug) { + // Prevent malicious bug summary from executin JS code + var summary = Pontoon.doNotRender(bug.summary); - function getNumber(el) { - return parseInt( - $(el).find('.id').text().replace(/,/g, ''), - ); - } - - function getTime(el) { - var date = - $(el).find('.last-changed').attr('datetime') || 0; - return new Date(date).getTime(); - } - - var node = $(this), - index = node.index(), - table = node.parents('.buglist'), - list = table.find('tbody'), - items = list.find('tr'), - dir = node.hasClass('desc') ? -1 : 1, - cls = node.hasClass('desc') ? 'asc' : 'desc'; - - $(table).find('th').removeClass('asc desc'); - node.addClass(cls); - - items.sort(function (a, b) { - // Sort by bugzilla ID - if (node.is('.id')) { - return (getNumber(a) - getNumber(b)) * dir; - - // Sort by last changed - } else if (node.is('.last-changed')) { - return (getTime(b) - getTime(a)) * dir; - - // Sort by alphabetical order - } else { - return ( - getString(a).localeCompare(getString(b)) * dir - ); - } - }); - - list.append(items); + var tr = $('', { + title: summary, }); - })(), - }, - }); + + $(' + + + + + + + , + }} + > + + + + + + + + , + mod1: , + }} + > + + + + + + + + , + mod1: , + }} + > + + + + + + + + , + mod1: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + + + + + , + mod1: , + mod2: , + }} + > + + + + +
    IDSummaryLast ChangedAssigned To
    ', { + class: 'id', + html: + '' + + bug.id + + '', + }).appendTo(tr); + + $('', { + class: 'summary', + html: summary, + }).appendTo(tr); + + $('', { + class: 'last-changed', + datetime: bug.last_change_time, + html: formatter.format(new Date(bug.last_change_time)), + }).appendTo(tr); + + $('', { + class: 'assigned-to', + html: bug.assigned_to, + }).appendTo(tr); + + tbody.append(tr); + }); + + var table = $('', { + class: 'buglist striped', + html: + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + }).append(tbody); + + container.append(table.show()); + + var count = data.bugs.length; + countCallback(tab, count); + } else { + errorCallback('Zarro Boogs Found.'); + } + }, + error: function (error) { + if (error.status === 0 && error.statusText !== 'abort') { + errorCallback( + 'Oops, something went wrong. We were unable to load the bugs. Please try again later.', + ); + } + }, + }); + }, + + /* + * Sort Bug Table + */ + sort: (function () { + $('body').on('click', 'table.buglist th', function () { + function getString(el) { + return $(el) + .find('td:eq(' + index + ')') + .text(); + } + + function getNumber(el) { + return parseInt($(el).find('.id').text().replace(/,/g, '')); + } + + function getTime(el) { + var date = $(el).find('.last-changed').attr('datetime') || 0; + return new Date(date).getTime(); + } + + var node = $(this), + index = node.index(), + table = node.parents('.buglist'), + list = table.find('tbody'), + items = list.find('tr'), + dir = node.hasClass('desc') ? -1 : 1, + cls = node.hasClass('desc') ? 'asc' : 'desc'; + + $(table).find('th').removeClass('asc desc'); + node.addClass(cls); + + items.sort(function (a, b) { + // Sort by bugzilla ID + if (node.is('.id')) { + return (getNumber(a) - getNumber(b)) * dir; + + // Sort by last changed + } else if (node.is('.last-changed')) { + return (getTime(b) - getTime(a)) * dir; + + // Sort by alphabetical order + } else { + return getString(a).localeCompare(getString(b)) * dir; + } + }); + + list.append(items); + }); + })(), + }, + }); })(Pontoon || {}); diff --git a/pontoon/teams/static/js/info.js b/pontoon/teams/static/js/info.js index fe9c2a501..fa9aa77ba 100644 --- a/pontoon/teams/static/js/info.js +++ b/pontoon/teams/static/js/info.js @@ -1,55 +1,55 @@ $(function () { - var container = $('#main .container'); + var container = $('#main .container'); - function toggleWidgets() { + function toggleWidgets() { + container + .find('.controls > *') + .toggle() + .end() + .find('.read-only-info') + .toggle() + .end() + .find('.read-write-info') + .toggleClass('hidden'); + } + + container.on('click', '#info-wrapper .edit-info', function (e) { + e.preventDefault(); + var content = container.find('.info').html(); + var textArea = container + .find('.read-write-info textarea') + .val($.trim(content)); + toggleWidgets(); + textArea.focus(); + }); + + container.on('click', '#info-wrapper .cancel', function (e) { + e.preventDefault(); + toggleWidgets(); + }); + + container.on('click', '#info-wrapper .save', function (e) { + e.preventDefault(); + var textArea = container.find('.read-write-info textarea'); + $.ajax({ + url: textArea.parent().data('url'), + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + team_info: textArea.val(), + }, + success: function (data) { container - .find('.controls > *') - .toggle() - .end() - .find('.read-only-info') - .toggle() - .end() - .find('.read-write-info') - .toggleClass('hidden'); - } - - container.on('click', '#info-wrapper .edit-info', function (e) { - e.preventDefault(); - var content = container.find('.info').html(); - var textArea = container - .find('.read-write-info textarea') - .val($.trim(content)); + .find('.info') + .html(data) + .toggle(data !== ''); + container.find('.no-results').toggle(data === ''); toggleWidgets(); - textArea.focus(); - }); - - container.on('click', '#info-wrapper .cancel', function (e) { - e.preventDefault(); - toggleWidgets(); - }); - - container.on('click', '#info-wrapper .save', function (e) { - e.preventDefault(); - var textArea = container.find('.read-write-info textarea'); - $.ajax({ - url: textArea.parent().data('url'), - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - team_info: textArea.val(), - }, - success: function (data) { - container - .find('.info') - .html(data) - .toggle(data !== ''); - container.find('.no-results').toggle(data === ''); - toggleWidgets(); - Pontoon.endLoader('Team info saved.'); - }, - error: function (request) { - Pontoon.endLoader(request.responseText, 'error'); - }, - }); + Pontoon.endLoader('Team info saved.'); + }, + error: function (request) { + Pontoon.endLoader(request.responseText, 'error'); + }, }); + }); }); diff --git a/pontoon/teams/static/js/multiple_team_selector.js b/pontoon/teams/static/js/multiple_team_selector.js index f7591c6c7..19bd2f01e 100644 --- a/pontoon/teams/static/js/multiple_team_selector.js +++ b/pontoon/teams/static/js/multiple_team_selector.js @@ -1,58 +1,58 @@ // Contains behaviours of widgets that are shared between admin and end-user interface. $(function () { - /** - * Function keeps track of inputs that contain information about the order of selected locales. - */ - function updateSelectedLocales() { - var $selectedList = $('.multiple-team-selector .locale.selected'), - $selectedLocalesField = $selectedList.find('input[type=hidden]'), - selectedLocales = $selectedList - .find('li[data-id]') - .map(function () { - return $(this).data('id'); - }) - .get(); + /** + * Function keeps track of inputs that contain information about the order of selected locales. + */ + function updateSelectedLocales() { + var $selectedList = $('.multiple-team-selector .locale.selected'), + $selectedLocalesField = $selectedList.find('input[type=hidden]'), + selectedLocales = $selectedList + .find('li[data-id]') + .map(function () { + return $(this).data('id'); + }) + .get(); - $selectedLocalesField.val(selectedLocales.join()); - } + $selectedLocalesField.val(selectedLocales.join()); + } - // Choose locales - $('body').on( - 'click', - '.multiple-team-selector .locale.select li', - function () { - var ls = $(this).parents('.locale.select'), - target = ls.siblings('.locale.select').find('ul'), - item = $(this).remove(); + // Choose locales + $('body').on( + 'click', + '.multiple-team-selector .locale.select li', + function () { + var ls = $(this).parents('.locale.select'), + target = ls.siblings('.locale.select').find('ul'), + item = $(this).remove(); - target.append(item); - target.scrollTop(target[0].scrollHeight); - updateSelectedLocales(); - }, - ); + target.append(item); + target.scrollTop(target[0].scrollHeight); + updateSelectedLocales(); + }, + ); - // Choose/remove all locales - $('body').on('click', '.multiple-team-selector .move-all', function (e) { - e.preventDefault(); - var ls = $(this).parents('.locale.select'), - target = ls.siblings('.locale.select').find('ul'), - items = ls.find('li:visible:not(".no-match")').remove(); + // Choose/remove all locales + $('body').on('click', '.multiple-team-selector .move-all', function (e) { + e.preventDefault(); + var ls = $(this).parents('.locale.select'), + target = ls.siblings('.locale.select').find('ul'), + items = ls.find('li:visible:not(".no-match")').remove(); - target.append(items); - target.scrollTop(target[0].scrollHeight); - updateSelectedLocales(); + target.append(items); + target.scrollTop(target[0].scrollHeight); + updateSelectedLocales(); + }); + + if ($.ui && $.ui.sortable) { + $('.multiple-team-selector .locale.select .sortable').sortable({ + axis: 'y', + containment: 'parent', + update: updateSelectedLocales, + tolerance: 'pointer', }); + } - if ($.ui && $.ui.sortable) { - $('.multiple-team-selector .locale.select .sortable').sortable({ - axis: 'y', - containment: 'parent', - update: updateSelectedLocales, - tolerance: 'pointer', - }); - } - - $('body').on('submit', '.form.user-locales-settings', function () { - updateSelectedLocales(); - }); + $('body').on('submit', '.form.user-locales-settings', function () { + updateSelectedLocales(); + }); }); diff --git a/pontoon/teams/static/js/permissions.js b/pontoon/teams/static/js/permissions.js index 3c26eced3..314872136 100644 --- a/pontoon/teams/static/js/permissions.js +++ b/pontoon/teams/static/js/permissions.js @@ -1,163 +1,155 @@ $(function () { - var container = $('#main .container'); + var container = $('#main .container'); - function inputHidden(name, value, cssClass) { - return $( - '', - ); + function inputHidden(name, value, cssClass) { + return $( + '', + ); + } + + container.on('click', '#permissions-form .save', function (e) { + e.preventDefault(); + var $form = $('#permissions-form'); + + // Remove stale permissions items (bug 1416890) + $('input.permissions-form-item').remove(); + + // Before submitting the form, update translators and managers + $.each(['translators', 'managers'], function (i, value) { + var data = $form.find('.user.' + value + ' li'); + data.each(function () { + var itemId = $(this).data('id'); + + if ($(this).parents('.general').length > 0) { + $form.append( + inputHidden('general-' + value, itemId, 'permissions-form-item'), + ); + } else { + // We have to retrieve an index of parent project locale form + var localeProjectIndex = $(this) + .parents('.project-locale') + .data('index'); + $form.append( + inputHidden( + 'project-locale-' + localeProjectIndex + '-translators', + itemId, + 'permissions-form-item', + ), + ); + } + }); + }); + + $.ajax({ + url: $('#permissions-form').prop('action'), + type: $('#permissions-form').prop('method'), + data: $('#permissions-form').serialize(), + success: function () { + Pontoon.endLoader('Permissions saved.'); + }, + error: function () { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + }, + }); + }); + + // Switch available users + container.on('click', '.user.available label a', function (e) { + e.preventDefault(); + + $(this).addClass('active').siblings('a').removeClass('active'); + + var available = $(this).parents('.user.available'); + available.find('li').show(); + + if ($(this).is('.contributors')) { + available.find('li:not(".contributor")').hide(); } - container.on('click', '#permissions-form .save', function (e) { - e.preventDefault(); - var $form = $('#permissions-form'); + available.find('.search-wrapper input').trigger('input').focus(); + }); - // Remove stale permissions items (bug 1416890) - $('input.permissions-form-item').remove(); + // While in contributors tab, search contributors only + // Has to be attached to body, like the input.search event in main.js + $('body').on( + 'input.search', + '.user.available .menu input[type=search]', + function () { + var available = $(this).parents('.user.available'); - // Before submitting the form, update translators and managers - $.each(['translators', 'managers'], function (i, value) { - var data = $form.find('.user.' + value + ' li'); - data.each(function () { - var itemId = $(this).data('id'); + if (available.find('label a.contributors').is('.active')) { + available.find('li:not(".contributor")').hide(); + } + }, + ); - if ($(this).parents('.general').length > 0) { - $form.append( - inputHidden( - 'general-' + value, - itemId, - 'permissions-form-item', - ), - ); - } else { - // We have to retrieve an index of parent project locale form - var localeProjectIndex = $(this) - .parents('.project-locale') - .data('index'); - $form.append( - inputHidden( - 'project-locale-' + - localeProjectIndex + - '-translators', - itemId, - 'permissions-form-item', - ), - ); - } - }); - }); + // Focus project selector search field + container.on('click', '#project-selector .selector', function () { + $('#project-selector .search-wrapper input').focus(); + }); - $.ajax({ - url: $('#permissions-form').prop('action'), - type: $('#permissions-form').prop('method'), - data: $('#permissions-form').serialize(), - success: function () { - Pontoon.endLoader('Permissions saved.'); - }, - error: function () { - Pontoon.endLoader('Oops, something went wrong.', 'error'); - }, - }); - }); + // Add project + container.on('click', '#project-selector .menu li', function () { + var slug = $(this).data('slug'), + $permsForm = $(".project-locale[data-slug='" + slug + "']"); - // Switch available users - container.on('click', '.user.available label a', function (e) { - e.preventDefault(); + $('.project-locale:last').after($permsForm.removeClass('hidden')); - $(this).addClass('active').siblings('a').removeClass('active'); - - var available = $(this).parents('.user.available'); - available.find('li').show(); - - if ($(this).is('.contributors')) { - available.find('li:not(".contributor")').hide(); - } - - available.find('.search-wrapper input').trigger('input').focus(); - }); - - // While in contributors tab, search contributors only - // Has to be attached to body, like the input.search event in main.js - $('body').on( - 'input.search', - '.user.available .menu input[type=search]', - function () { - var available = $(this).parents('.user.available'); - - if (available.find('label a.contributors').is('.active')) { - available.find('li:not(".contributor")').hide(); - } - }, + $permsForm.append( + inputHidden( + 'project-locale-' + + $permsForm.data('index') + + '-has_custom_translators', + 1, + ), ); - // Focus project selector search field - container.on('click', '#project-selector .selector', function () { - $('#project-selector .search-wrapper input').focus(); - }); - - // Add project - container.on('click', '#project-selector .menu li', function () { - var slug = $(this).data('slug'), - $permsForm = $(".project-locale[data-slug='" + slug + "']"); - - $('.project-locale:last').after($permsForm.removeClass('hidden')); - - $permsForm.append( - inputHidden( - 'project-locale-' + - $permsForm.data('index') + - '-has_custom_translators', - 1, - ), - ); - - // Update menu (must be above Copying Translators) - $(this).addClass('hidden').removeClass('limited').removeAttr('style'); - if ($('#project-selector .menu li:not(".hidden")').length === 0) { - $('#project-selector').addClass('hidden'); - } - - // Copy Translators from the General section - // Reverse selector order to keep presentation order (prepend) - $( - $('.permissions-groups.general .translators li').get().reverse(), - ).each(function () { - $permsForm - .find( - '.user.available li[data-id="' + $(this).data('id') + '"]', - ) - .click(); - }); - - // Scroll to the right project locale - $('html, body').animate( - { - scrollTop: $permsForm.offset().top, - }, - 500, - ); - }); - - // Remove project - container.on('click', '.remove-project', function (e) { - var $permsForm = $(this).parents('.project-locale'); - e.preventDefault(); - - $('#project-selector').removeClass('hidden'); - $("#project-selector li[data-slug='" + $permsForm.data('slug') + "']") - .removeClass('hidden') - .addClass('limited'); - - $permsForm.find('input[name$=has_custom_translators]').remove(); - - $permsForm.addClass('hidden'); - $permsForm.find('.select.translators li').each(function () { - $permsForm.find('.select.available ul').append($(this).remove()); - }); + // Update menu (must be above Copying Translators) + $(this).addClass('hidden').removeClass('limited').removeAttr('style'); + if ($('#project-selector .menu li:not(".hidden")').length === 0) { + $('#project-selector').addClass('hidden'); + } + + // Copy Translators from the General section + // Reverse selector order to keep presentation order (prepend) + $($('.permissions-groups.general .translators li').get().reverse()).each( + function () { + $permsForm + .find('.user.available li[data-id="' + $(this).data('id') + '"]') + .click(); + }, + ); + + // Scroll to the right project locale + $('html, body').animate( + { + scrollTop: $permsForm.offset().top, + }, + 500, + ); + }); + + // Remove project + container.on('click', '.remove-project', function (e) { + var $permsForm = $(this).parents('.project-locale'); + e.preventDefault(); + + $('#project-selector').removeClass('hidden'); + $("#project-selector li[data-slug='" + $permsForm.data('slug') + "']") + .removeClass('hidden') + .addClass('limited'); + + $permsForm.find('input[name$=has_custom_translators]').remove(); + + $permsForm.addClass('hidden'); + $permsForm.find('.select.translators li').each(function () { + $permsForm.find('.select.available ul').append($(this).remove()); }); + }); }); diff --git a/pontoon/teams/static/js/request.js b/pontoon/teams/static/js/request.js index 067ff7fc2..212c99390 100644 --- a/pontoon/teams/static/js/request.js +++ b/pontoon/teams/static/js/request.js @@ -1,270 +1,243 @@ var Pontoon = (function (my) { - return $.extend(true, my, { - requestItem: { - /* - * Toggle available projects/teams and request div - * - * show Show enabled projects/teams? - */ - toggleItem: function (show, type) { - // Toggle - $('.controls .request-toggle') - .toggleClass('back', !show) - .find('span') - .toggleClass('fa-chevron-right', show) - .toggleClass('fa-chevron-left', !show); + return $.extend(true, my, { + requestItem: { + /* + * Toggle available projects/teams and request div + * + * show Show enabled projects/teams? + */ + toggleItem: function (show, type) { + // Toggle + $('.controls .request-toggle') + .toggleClass('back', !show) + .find('span') + .toggleClass('fa-chevron-right', show) + .toggleClass('fa-chevron-left', !show); - if (type === 'locale-projects') { - var localeProjects = $('#server').data('locale-projects'); + if (type === 'locale-projects') { + var localeProjects = $('#server').data('locale-projects'); - // Hide all projects - $('.items') - .toggleClass('request', !show) - .find('tbody tr') - .toggleClass('limited', !show) - .toggle(!show); + // Hide all projects + $('.items') + .toggleClass('request', !show) + .find('tbody tr') + .toggleClass('limited', !show) + .toggle(!show); - // Show requested projects - $(localeProjects).each(function () { - $('.items') - .find('td[data-slug="' + this + '"]') - .parent() - .toggleClass('limited', show) - .toggle(show); - }); + // Show requested projects + $(localeProjects).each(function () { + $('.items') + .find('td[data-slug="' + this + '"]') + .parent() + .toggleClass('limited', show) + .toggle(show); + }); - // Toggle table & search box, show no results message based on project visibility - var noProject = $('.project-list tr.limited').length === 0; - $('.project-list').toggleClass('hidden', noProject); - $('menu.controls').toggleClass('no-projects', noProject); - $('.no-results').toggle(); + // Toggle table & search box, show no results message based on project visibility + var noProject = $('.project-list tr.limited').length === 0; + $('.project-list').toggleClass('hidden', noProject); + $('menu.controls').toggleClass('no-projects', noProject); + $('.no-results').toggle(); - Pontoon.requestItem.toggleButton(!show, 'locale-projects'); - } else if (type === 'team') { - // Hide all teams and the search bar - $('.team-list').toggle(show); - $('.search-wrapper').toggle(show); + Pontoon.requestItem.toggleButton(!show, 'locale-projects'); + } else if (type === 'team') { + // Hide all teams and the search bar + $('.team-list').toggle(show); + $('.search-wrapper').toggle(show); - // Show team form - $('#request-team-form').toggle(!show); - Pontoon.requestItem.toggleButton(!show, 'team'); - } + // Show team form + $('#request-team-form').toggle(!show); + Pontoon.requestItem.toggleButton(!show, 'team'); + } - $('.controls input[type=search]:visible').trigger('input'); - }, + $('.controls input[type=search]:visible').trigger('input'); + }, - toggleButton: function (condition, type) { - condition = condition || true; - var show = condition; + toggleButton: function (condition, type) { + condition = condition || true; + var show = condition; - if (type === 'locale-projects') { - show = - condition && $('.items td.enabled:visible').length > 0; - } else if (type === 'team') { - show = - condition && - $.trim($('#request-team-form #id_name').val()) !== '' && - $.trim($('#request-team-form #id_code').val()) !== ''; - } + if (type === 'locale-projects') { + show = condition && $('.items td.enabled:visible').length > 0; + } else if (type === 'team') { + show = + condition && + $.trim($('#request-team-form #id_name').val()) !== '' && + $.trim($('#request-team-form #id_code').val()) !== ''; + } - $('#request-item-note').toggle(show); - $('#request-item').toggle(show); - }, + $('#request-item-note').toggle(show); + $('#request-item').toggle(show); + }, - requestProjects: function (locale, projects, type) { - $.ajax({ - url: '/' + locale + '/request/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - projects: projects, - }, - success: function () { - Pontoon.endLoader( - 'New ' + type + ' request sent.', - '', - 5000, - ); - }, - error: function () { - Pontoon.endLoader( - 'Oops, something went wrong.', - 'error', - ); - }, - complete: function () { - $('.items td.check').removeClass('enabled'); - $('.items td.radio.enabled').toggleClass( - 'far fa fa-circle fa-dot-circle enabled', - ); - Pontoon.requestItem.toggleItem(true, 'locale-projects'); - window.scrollTo(0, 0); - }, - }); - }, + requestProjects: function (locale, projects, type) { + $.ajax({ + url: '/' + locale + '/request/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + projects: projects, + }, + success: function () { + Pontoon.endLoader('New ' + type + ' request sent.', '', 5000); + }, + error: function () { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + }, + complete: function () { + $('.items td.check').removeClass('enabled'); + $('.items td.radio.enabled').toggleClass( + 'far fa fa-circle fa-dot-circle enabled', + ); + Pontoon.requestItem.toggleItem(true, 'locale-projects'); + window.scrollTo(0, 0); + }, + }); + }, - requestTeam: function (name, code) { - $.ajax({ - url: '/teams/request/', - type: 'POST', - data: { - csrfmiddlewaretoken: $('body').data('csrf'), - name: name, - code: code, - }, - success: function () { - Pontoon.endLoader('New team request sent.', '', 5000); - }, - error: function (res) { - if (res.status === 409) { - Pontoon.endLoader(res.responseText, 'error'); - } else { - Pontoon.endLoader( - 'Oops, something went wrong.', - 'error', - ); - } - }, - complete: function () { - $('#request-team-form #id_name').val(''); - $('#request-team-form #id_code').val(''); - Pontoon.requestItem.toggleButton(true, 'team'); - window.scrollTo(0, 0); - }, - }); - }, - }, - }); + requestTeam: function (name, code) { + $.ajax({ + url: '/teams/request/', + type: 'POST', + data: { + csrfmiddlewaretoken: $('body').data('csrf'), + name: name, + code: code, + }, + success: function () { + Pontoon.endLoader('New team request sent.', '', 5000); + }, + error: function (res) { + if (res.status === 409) { + Pontoon.endLoader(res.responseText, 'error'); + } else { + Pontoon.endLoader('Oops, something went wrong.', 'error'); + } + }, + complete: function () { + $('#request-team-form #id_name').val(''); + $('#request-team-form #id_code').val(''); + Pontoon.requestItem.toggleButton(true, 'team'); + window.scrollTo(0, 0); + }, + }); + }, + }, + }); })(Pontoon || {}); $(function () { - var container = $('#main .container'); - var type = $('#server').data('locale-projects') - ? 'locale-projects' - : 'team'; + var container = $('#main .container'); + var type = $('#server').data('locale-projects') ? 'locale-projects' : 'team'; - // Switch between available projects/teams and projects/team to request - container.on('click', '.controls .request-toggle', function (e) { + // Switch between available projects/teams and projects/team to request + container.on('click', '.controls .request-toggle', function (e) { + e.stopPropagation(); + e.preventDefault(); + + Pontoon.requestItem.toggleItem($(this).is('.back'), type); + }); + + // Select projects + container.on('click', '.items td.check', function (e) { + if ($('.controls .request-toggle').is('.back:visible')) { + e.stopPropagation(); + + $(this).toggleClass('enabled'); + Pontoon.requestItem.toggleButton(true, (type = 'locale-projects')); + } + }); + + // Radio button hover behavior + container.on( + { + mouseenter: function () { + $(this).toggleClass('fa-circle fa-dot-circle'); + }, + mouseleave: function () { + $(this).toggleClass('fa-circle fa-dot-circle'); + }, + }, + '.items td.radio:not(.enabled)', + ); + + // Select team + container.on('click', '.items td.radio', function (e) { + if ($('.controls .request-toggle').is('.back:visible')) { + e.stopPropagation(); + + $(this) + .add('.items td.radio.enabled') + .toggleClass('fa far fa-circle fa-dot-circle enabled'); + + if ($(this).hasClass('enabled')) { + $(this).toggleClass('fa-circle fa-dot-circle'); + } + + Pontoon.requestItem.toggleButton(true, (type = 'locale-projects')); + } + }); + + // Prevent openning project page from the request panel + var menu = container.find('.project .menu'); + menu.find('a').click(function (e) { + if (menu.find('.search-wrapper > a').is('.back:visible')) { + e.preventDefault(); + } + }); + + // Enter team details + container.on( + 'change keyup click', + '#request-team-form input[type=text]', + function (e) { + if ($('.controls .request-toggle').is('.back:visible')) { e.stopPropagation(); - e.preventDefault(); + Pontoon.requestItem.toggleButton(true, (type = 'team')); + } + }, + ); - Pontoon.requestItem.toggleItem($(this).is('.back'), type); - }); + // Request projects/team + container.on('click', '#request-item', function (e) { + e.preventDefault(); + e.stopPropagation(); - // Select projects - container.on('click', '.items td.check', function (e) { - if ($('.controls .request-toggle').is('.back:visible')) { - e.stopPropagation(); + var locale = ''; - $(this).toggleClass('enabled'); - Pontoon.requestItem.toggleButton(true, (type = 'locale-projects')); - } - }); + if ($(this).is('.confirmed')) { + // Requesting from team page + if (type === 'locale-projects' && $('body').hasClass('locale')) { + var projects = $('.items td.check.enabled') + .map(function (val, element) { + return $(element).siblings('.name').data('slug'); + }) + .get(); + locale = $('#server').data('locale') || Pontoon.getSelectedLocale(); - // Radio button hover behavior - container.on( - { - mouseenter: function () { - $(this).toggleClass('fa-circle fa-dot-circle'); - }, - mouseleave: function () { - $(this).toggleClass('fa-circle fa-dot-circle'); - }, - }, - '.items td.radio:not(.enabled)', - ); + Pontoon.requestItem.requestProjects(locale, projects, 'projects'); - // Select team - container.on('click', '.items td.radio', function (e) { - if ($('.controls .request-toggle').is('.back:visible')) { - e.stopPropagation(); + $(this).removeClass('confirmed').html('Request new projects'); + } - $(this) - .add('.items td.radio.enabled') - .toggleClass('fa far fa-circle fa-dot-circle enabled'); + // Requesting from project page + else if (type === 'locale-projects' && $('body').hasClass('project')) { + var project = $('#server').data('project'); + locale = $('.items td.radio.enabled').siblings('.name').data('slug'); - if ($(this).hasClass('enabled')) { - $(this).toggleClass('fa-circle fa-dot-circle'); - } + Pontoon.requestItem.requestProjects(locale, [project], 'language'); - Pontoon.requestItem.toggleButton(true, (type = 'locale-projects')); - } - }); + $(this).removeClass('confirmed').html('Request new language'); + } else if (type === 'team') { + locale = $.trim($('#request-team-form #id_name').val()); + var code = $.trim($('#request-team-form #id_code').val()); - // Prevent openning project page from the request panel - var menu = container.find('.project .menu'); - menu.find('a').click(function (e) { - if (menu.find('.search-wrapper > a').is('.back:visible')) { - e.preventDefault(); - } - }); + Pontoon.requestItem.requestTeam(locale, code); - // Enter team details - container.on( - 'change keyup click', - '#request-team-form input[type=text]', - function (e) { - if ($('.controls .request-toggle').is('.back:visible')) { - e.stopPropagation(); - Pontoon.requestItem.toggleButton(true, (type = 'team')); - } - }, - ); - - // Request projects/team - container.on('click', '#request-item', function (e) { - e.preventDefault(); - e.stopPropagation(); - - var locale = ''; - - if ($(this).is('.confirmed')) { - // Requesting from team page - if (type === 'locale-projects' && $('body').hasClass('locale')) { - var projects = $('.items td.check.enabled') - .map(function (val, element) { - return $(element).siblings('.name').data('slug'); - }) - .get(); - locale = - $('#server').data('locale') || Pontoon.getSelectedLocale(); - - Pontoon.requestItem.requestProjects( - locale, - projects, - 'projects', - ); - - $(this).removeClass('confirmed').html('Request new projects'); - } - - // Requesting from project page - else if ( - type === 'locale-projects' && - $('body').hasClass('project') - ) { - var project = $('#server').data('project'); - locale = $('.items td.radio.enabled') - .siblings('.name') - .data('slug'); - - Pontoon.requestItem.requestProjects( - locale, - [project], - 'language', - ); - - $(this).removeClass('confirmed').html('Request new language'); - } else if (type === 'team') { - locale = $.trim($('#request-team-form #id_name').val()); - var code = $.trim($('#request-team-form #id_code').val()); - - Pontoon.requestItem.requestTeam(locale, code); - - $(this).removeClass('confirmed').html('Request new team'); - } - } else { - $(this).addClass('confirmed').html('Are you sure?'); - } - }); + $(this).removeClass('confirmed').html('Request new team'); + } + } else { + $(this).addClass('confirmed').html('Are you sure?'); + } + }); }); diff --git a/pontoon/teams/static/js/team_selector.js b/pontoon/teams/static/js/team_selector.js index 4b5c05a3b..e78a7df27 100644 --- a/pontoon/teams/static/js/team_selector.js +++ b/pontoon/teams/static/js/team_selector.js @@ -1,8 +1,8 @@ $(function () { - $('.locale-selector .locale .menu li:not(".no-match")').click(function () { - $(this) - .parents('.locale-selector') - .find('.locale .selector') - .html($(this).html()); - }); + $('.locale-selector .locale .menu li:not(".no-match")').click(function () { + $(this) + .parents('.locale-selector') + .find('.locale .selector') + .html($(this).html()); + }); }); diff --git a/tag-admin/jest.config.js b/tag-admin/jest.config.js index 49d3c252a..10149fe23 100644 --- a/tag-admin/jest.config.js +++ b/tag-admin/jest.config.js @@ -2,14 +2,14 @@ /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - testEnvironment: 'jsdom', - testURL: 'https://nowhere.com/at/all', - moduleNameMapper: { - '\\.(css|less)$': 'identity-obj-proxy', - }, - setupFiles: ['./src/setupTests.js'], - transform: { - '\\.[jt]sx?$': ['babel-jest', { configFile: '../babel.config.json' }], - }, - collectCoverage: true, + testEnvironment: 'jsdom', + testURL: 'https://nowhere.com/at/all', + moduleNameMapper: { + '\\.(css|less)$': 'identity-obj-proxy', + }, + setupFiles: ['./src/setupTests.js'], + transform: { + '\\.[jt]sx?$': ['babel-jest', { configFile: '../babel.config.json' }], + }, + collectCoverage: true, }; diff --git a/tag-admin/rollup.config.js b/tag-admin/rollup.config.js index d1629aeee..e5282f627 100644 --- a/tag-admin/rollup.config.js +++ b/tag-admin/rollup.config.js @@ -9,26 +9,26 @@ import css from 'rollup-plugin-css-only'; /** @type {import('rollup').RollupOptions} */ const config = { - input: 'src/index.js', - output: { file: 'dist/tag_admin.js' }, + input: 'src/index.js', + output: { file: 'dist/tag_admin.js' }, - treeshake: 'recommended', + treeshake: 'recommended', - plugins: [ - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify( - process.env.BUILD ?? 'development', - ), - }), - resolve(), - babel({ - babelHelpers: 'runtime', - configFile: path.resolve('../babel.config.json'), - }), - commonjs(), - css({ output: 'tag_admin.css' }), - ], + plugins: [ + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify( + process.env.BUILD ?? 'development', + ), + }), + resolve(), + babel({ + babelHelpers: 'runtime', + configFile: path.resolve('../babel.config.json'), + }), + commonjs(), + css({ output: 'tag_admin.css' }), + ], }; export default config; diff --git a/tag-admin/src/button.js b/tag-admin/src/button.js index d42bb162a..96f6ac98c 100644 --- a/tag-admin/src/button.js +++ b/tag-admin/src/button.js @@ -3,20 +3,20 @@ import React, { useState } from 'react'; import { TagResourceManager } from './manager.js'; export function TagResourcesButton(props) { - const [open, setOpen] = useState(false); - const message = open - ? 'Hide the resource manager for this tag' - : 'Manage resources for this tag'; + const [open, setOpen] = useState(false); + const message = open + ? 'Hide the resource manager for this tag' + : 'Manage resources for this tag'; - const toggle = (ev) => { - ev.preventDefault(); - setOpen((open) => !open); - }; + const toggle = (ev) => { + ev.preventDefault(); + setOpen((open) => !open); + }; - return ( -
    - - {open ? : null} -
    - ); + return ( +
    + + {open ? : null} +
    + ); } diff --git a/tag-admin/src/button.test.js b/tag-admin/src/button.test.js index e930a9717..d383a3ac2 100644 --- a/tag-admin/src/button.test.js +++ b/tag-admin/src/button.test.js @@ -4,33 +4,33 @@ import React from 'react'; import { TagResourcesButton } from './button.js'; test('TagResourcesButton renders only button initially', () => { - const button = shallow( - , - ); - expect(button.html()).toBe( - '
    ', - ); + const button = shallow( + , + ); + expect(button.html()).toBe( + '
    ', + ); }); test('TagResourcesButton shows TagResourceManager when clicked', () => { - const button = shallow( - , - ); + const button = shallow( + , + ); - const preventDefault = jest.fn(); - button.find('button').simulate('click', { preventDefault }); - expect(button.html()).toMatch('
    '); - expect(preventDefault).toHaveBeenCalled(); + const preventDefault = jest.fn(); + button.find('button').simulate('click', { preventDefault }); + expect(button.html()).toMatch('
    '); + expect(preventDefault).toHaveBeenCalled(); }); test('TagResourcesButton renders only Button when clicked twice', () => { - const button = shallow( - , - ); + const button = shallow( + , + ); - button.find('button').simulate('click', { preventDefault: () => {} }); - expect(button.html()).toMatch('Hide the resource manager for this tag'); + button.find('button').simulate('click', { preventDefault: () => {} }); + expect(button.html()).toMatch('Hide the resource manager for this tag'); - button.find('button').simulate('click', { preventDefault: () => {} }); - expect(button.html()).toMatch('Manage resources for this tag'); + button.find('button').simulate('click', { preventDefault: () => {} }); + expect(button.html()).toMatch('Manage resources for this tag'); }); diff --git a/tag-admin/src/index.js b/tag-admin/src/index.js index 5729c2923..3919e9433 100644 --- a/tag-admin/src/index.js +++ b/tag-admin/src/index.js @@ -4,11 +4,11 @@ import ReactDOM from 'react-dom'; import { TagResourcesButton } from './button.js'; document.addEventListener('DOMContentLoaded', () => { - document.querySelectorAll('.js-tag-resources').forEach((node) => { - const { api, project, tag } = node.dataset; - ReactDOM.render( - , - node, - ); - }); + document.querySelectorAll('.js-tag-resources').forEach((node) => { + const { api, project, tag } = node.dataset; + ReactDOM.render( + , + node, + ); + }); }); diff --git a/tag-admin/src/manager.js b/tag-admin/src/manager.js index 096ca056b..eb15f7a89 100644 --- a/tag-admin/src/manager.js +++ b/tag-admin/src/manager.js @@ -8,39 +8,39 @@ import { ErrorList } from './widgets/error-list.js'; import './tag-resources.css'; export function TagResourceManager({ api }) { - const [data, setData] = useState([]); - const [errors, setErrors] = useState({}); - const [type, setType] = useState('assoc'); - const [search, setSearch] = useState(''); + const [data, setData] = useState([]); + const [errors, setErrors] = useState({}); + const [type, setType] = useState('assoc'); + const [search, setSearch] = useState(''); - const handleChange = useCallback( - async (params) => { - const response = await post(api, params); - const json = await response.json(); - if (response.status === 200) { - setData(json.data || []); - setErrors({}); - } else { - setErrors(json.errors || {}); - } - }, - [api], - ); + const handleChange = useCallback( + async (params) => { + const response = await post(api, params); + const json = await response.json(); + if (response.status === 200) { + setData(json.data || []); + setErrors({}); + } else { + setErrors(json.errors || {}); + } + }, + [api], + ); - useEffect(() => { - handleChange({ search, type }); - }, [search, type]); + useEffect(() => { + handleChange({ search, type }); + }, [search, type]); - const message = type === 'assoc' ? 'Unlink resources' : 'Link resources'; - return ( -
    - - - handleChange({ data, search, type })} - submitMessage={message} - /> -
    - ); + const message = type === 'assoc' ? 'Unlink resources' : 'Link resources'; + return ( +
    + + + handleChange({ data, search, type })} + submitMessage={message} + /> +
    + ); } diff --git a/tag-admin/src/manager.test.js b/tag-admin/src/manager.test.js index 36321b73a..c6082e94a 100644 --- a/tag-admin/src/manager.test.js +++ b/tag-admin/src/manager.test.js @@ -8,79 +8,79 @@ import { TagResourceManager } from './manager.js'; const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); test('TagResourceManager search', async () => { - document.querySelector = jest.fn(() => ({ value: '73' })); - window.fetch = jest.fn(async () => ({ - status: 200, - json: async () => ({ data: [] }), - })); + document.querySelector = jest.fn(() => ({ value: '73' })); + window.fetch = jest.fn(async () => ({ + status: 200, + json: async () => ({ data: [] }), + })); - const manager = mount(); + const manager = mount(); - // Enter 'FOO' in the search input - manager - .find('input.search-tag-resources') - .simulate('change', { target: { value: 'FOO' } }); - await act(async () => { - await flushPromises(); - }); + // Enter 'FOO' in the search input + manager + .find('input.search-tag-resources') + .simulate('change', { target: { value: 'FOO' } }); + await act(async () => { + await flushPromises(); + }); - const { calls } = window.fetch.mock; - expect(calls).toHaveLength(2); + const { calls } = window.fetch.mock; + expect(calls).toHaveLength(2); - expect(Array.from(calls[0][1].body.entries())).toMatchObject([ - ['search', ''], - ['type', 'assoc'], - ['csrfmiddlewaretoken', '73'], - ]); + expect(Array.from(calls[0][1].body.entries())).toMatchObject([ + ['search', ''], + ['type', 'assoc'], + ['csrfmiddlewaretoken', '73'], + ]); - expect(Array.from(calls[1][1].body.entries())).toMatchObject([ - ['search', 'FOO'], - ['type', 'assoc'], - ['csrfmiddlewaretoken', '73'], - ]); + expect(Array.from(calls[1][1].body.entries())).toMatchObject([ + ['search', 'FOO'], + ['type', 'assoc'], + ['csrfmiddlewaretoken', '73'], + ]); }); test('TagResourceManager checkboxing', async () => { - document.querySelector = jest.fn(() => ({ value: '73' })); - window.fetch = jest.fn(async () => ({ - status: 200, - json: async () => ({ data: [['foo'], ['bar']] }), - })); + document.querySelector = jest.fn(() => ({ value: '73' })); + window.fetch = jest.fn(async () => ({ + status: 200, + json: async () => ({ data: [['foo'], ['bar']] }), + })); - const manager = mount(); + const manager = mount(); - await act(async () => { - await flushPromises(); - }); - manager.update(); + await act(async () => { + await flushPromises(); + }); + manager.update(); - // Check the 'foo' checkbox - manager - .find('input[name="foo"]') - .simulate('change', { target: { name: 'foo', checked: true } }); + // Check the 'foo' checkbox + manager + .find('input[name="foo"]') + .simulate('change', { target: { name: 'foo', checked: true } }); - // Click on 'Unlink resources' - manager - .find('button.tag-resources-associate') - .simulate('click', { preventDefault: () => {} }); + // Click on 'Unlink resources' + manager + .find('button.tag-resources-associate') + .simulate('click', { preventDefault: () => {} }); - await act(async () => { - await flushPromises(); - }); + await act(async () => { + await flushPromises(); + }); - const { calls } = window.fetch.mock; - expect(calls).toHaveLength(2); + const { calls } = window.fetch.mock; + expect(calls).toHaveLength(2); - expect(Array.from(calls[0][1].body.entries())).toMatchObject([ - ['search', ''], - ['type', 'assoc'], - ['csrfmiddlewaretoken', '73'], - ]); + expect(Array.from(calls[0][1].body.entries())).toMatchObject([ + ['search', ''], + ['type', 'assoc'], + ['csrfmiddlewaretoken', '73'], + ]); - expect(Array.from(calls[1][1].body.entries())).toMatchObject([ - ['data', 'foo'], - ['search', ''], - ['type', 'assoc'], - ['csrfmiddlewaretoken', '73'], - ]); + expect(Array.from(calls[1][1].body.entries())).toMatchObject([ + ['data', 'foo'], + ['search', ''], + ['type', 'assoc'], + ['csrfmiddlewaretoken', '73'], + ]); }); diff --git a/tag-admin/src/search.js b/tag-admin/src/search.js index ba7d3a084..f49e1684a 100644 --- a/tag-admin/src/search.js +++ b/tag-admin/src/search.js @@ -1,44 +1,44 @@ import React from 'react'; export const TagResourceSearch = ({ onSearch, onType }) => ( +
    -
    -
    - onSearch(ev.target.value)} - placeholder='Search for resources' - /> -
    -
    -
    -
    - -
    -
    +
    + onSearch(ev.target.value)} + placeholder='Search for resources' + /> +
    +
    +
    + +
    +
    +
    ); diff --git a/tag-admin/src/search.test.js b/tag-admin/src/search.test.js index d5ddbd023..116de25f8 100644 --- a/tag-admin/src/search.test.js +++ b/tag-admin/src/search.test.js @@ -4,32 +4,30 @@ import React from 'react'; import { TagResourceSearch } from './search.js'; test('TagResourceSearch renders search input', () => { - const search = mount(); - const input = search.find('input.search-tag-resources'); + const search = mount(); + const input = search.find('input.search-tag-resources'); - expect(input).toHaveLength(1); - expect(input.html()).toMatch('placeholder="Search for resources"'); + expect(input).toHaveLength(1); + expect(input.html()).toMatch('placeholder="Search for resources"'); }); test('TagResourceSearch renders select', () => { - const search = mount(); - const options = search.find('select.search-tag-resource-type option'); + const search = mount(); + const options = search.find('select.search-tag-resource-type option'); - expect(options).toHaveLength(2); - expect(options.at(0).html()).toMatch('"assoc"'); - expect(options.at(1).html()).toMatch('"nonassoc"'); + expect(options).toHaveLength(2); + expect(options.at(0).html()).toMatch('"assoc"'); + expect(options.at(1).html()).toMatch('"nonassoc"'); }); test('TagResourceSearch onChange', async () => { - const search = jest.fn(); - const type = jest.fn(); - const wrapper = mount( - , - ); + const search = jest.fn(); + const type = jest.fn(); + const wrapper = mount(); - wrapper.find('input').simulate('change', { target: { value: 'FOO' } }); - wrapper.find('select').simulate('change', { target: { value: 'BAR' } }); + wrapper.find('input').simulate('change', { target: { value: 'FOO' } }); + wrapper.find('select').simulate('change', { target: { value: 'BAR' } }); - expect(search.mock.calls).toEqual([['FOO']]); - expect(type.mock.calls).toEqual([['BAR']]); + expect(search.mock.calls).toEqual([['FOO']]); + expect(type.mock.calls).toEqual([['BAR']]); }); diff --git a/tag-admin/src/tag-resources.css b/tag-admin/src/tag-resources.css index d6e3c1031..b5b3ff036 100644 --- a/tag-admin/src/tag-resources.css +++ b/tag-admin/src/tag-resources.css @@ -1,49 +1,49 @@ .tag-resources { - text-align: left; + text-align: left; } .tag-resources button { - margin: 0.2em auto 1em auto; - display: block; + margin: 0.2em auto 1em auto; + display: block; } .tag-resources .rt-table { - color: white; + color: white; } .tag-resource-widget { - background: #999; - margin-bottom: 2em; - padding-bottom: 0.3em; + background: #999; + margin-bottom: 2em; + padding-bottom: 0.3em; } .tag-resources .rt-table input { - margin: auto; - display: block; + margin: auto; + display: block; } .tag-resources .ReactTable .pagination-bottom .-pageInfo { - color: #333; + color: #333; } .tag-resources .rt-th { - text-align: left; + text-align: left; } button.tag-resources-associate { - margin-top: 0.5em; + margin-top: 0.5em; } .tag-resources select.search-tag-resource-type { - width: 90%; - display: block; - margin: 1em auto; - float: none; + width: 90%; + display: block; + margin: 1em auto; + float: none; } .tag-resources input.search-tag-resources { - width: 90%; - display: block; - margin: 1em auto; - float: none; + width: 90%; + display: block; + margin: 1em auto; + float: none; } diff --git a/tag-admin/src/utils/http-post.js b/tag-admin/src/utils/http-post.js index 57ef93e4d..766dd7ed3 100644 --- a/tag-admin/src/utils/http-post.js +++ b/tag-admin/src/utils/http-post.js @@ -1,45 +1,45 @@ function asFormData(data) { - /** - * Mangle a data object to FormData - * - * If any of the object values are arrayish, it will append k=v[n] - * for each item n of v - */ - const formData = new FormData(); - if (data) { - for (const [k, v] of Object.entries(data)) { - if (Array.isArray(v)) { - for (const item of v) formData.append(k, item); - } else { - formData.append(k, v); - } - } + /** + * Mangle a data object to FormData + * + * If any of the object values are arrayish, it will append k=v[n] + * for each item n of v + */ + const formData = new FormData(); + if (data) { + for (const [k, v] of Object.entries(data)) { + if (Array.isArray(v)) { + for (const item of v) formData.append(k, item); + } else { + formData.append(k, v); + } } - return formData; + } + return formData; } function getLocation() { - let hostname = window.location.hostname; - const protocol = window.location.protocol; - const port = window.location.port; - if (port) { - hostname += ':' + port; - } - const origin = '//' + hostname; - const domain = protocol + origin; - return { origin, domain, port }; + let hostname = window.location.hostname; + const protocol = window.location.protocol; + const port = window.location.port; + if (port) { + hostname += ':' + port; + } + const origin = '//' + hostname; + const domain = protocol + origin; + return { origin, domain, port }; } function parseURL(url, port) { - const parser = document.createElement('a'); - parser.href = url; - let parsedURL = parser.protocol + '//' + parser.hostname; - if (parser.port !== '') { - parsedURL += ':' + parser.port; - } else if (!/^(\/\/|http:|https:).*/.test(url) && port) { - parsedURL += ':' + port; - } - return parsedURL; + const parser = document.createElement('a'); + parser.href = url; + let parsedURL = parser.protocol + '//' + parser.hostname; + if (parser.port !== '') { + parsedURL += ':' + parser.port; + } else if (!/^(\/\/|http:|https:).*/.test(url) && port) { + parsedURL += ':' + port; + } + return parsedURL; } /** @@ -49,13 +49,13 @@ function parseURL(url, port) { * regared as same-origin */ export function isSameOrigin(url) { - const { origin, domain, port } = getLocation(); - const parsedURL = parseURL(url, port); - return ( - parsedURL === domain || - parsedURL === origin || - !/^(\/\/|http:|https:).*/.test(parsedURL) - ); + const { origin, domain, port } = getLocation(); + const parsedURL = parseURL(url, port); + return ( + parsedURL === domain || + parsedURL === origin || + !/^(\/\/|http:|https:).*/.test(parsedURL) + ); } /** @@ -66,24 +66,22 @@ export function isSameOrigin(url) { * to prevent leaking of csrf data */ export function post(url, data) { - // this is a bit sketchy but the only afaict way due to session_csrf - const csrf = document.querySelector( - 'input[name=csrfmiddlewaretoken]', - ).value; + // this is a bit sketchy but the only afaict way due to session_csrf + const csrf = document.querySelector('input[name=csrfmiddlewaretoken]').value; - const init = { - body: asFormData(data), - headers: new Headers({ - Accept: 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - 'X-CSRFToken': csrf, - }), - method: 'POST', - }; - if (isSameOrigin(url)) { - init.body.append('csrfmiddlewaretoken', csrf); - init.credentials = 'same-origin'; - } + const init = { + body: asFormData(data), + headers: new Headers({ + Accept: 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': csrf, + }), + method: 'POST', + }; + if (isSameOrigin(url)) { + init.body.append('csrfmiddlewaretoken', csrf); + init.credentials = 'same-origin'; + } - return window.fetch(url, init); + return window.fetch(url, init); } diff --git a/tag-admin/src/utils/http-post.test.js b/tag-admin/src/utils/http-post.test.js index b03c0d1d9..fd8cec964 100644 --- a/tag-admin/src/utils/http-post.test.js +++ b/tag-admin/src/utils/http-post.test.js @@ -1,67 +1,67 @@ import { isSameOrigin, post } from './http-post.js'; test('isSameOrigin', () => { - // default url is set to https://nowhere.com/at/all - expect(isSameOrigin('https://somwhere.else/all/together')).toBe(false); - expect(isSameOrigin('https://somwhere.else/')).toBe(false); - expect(isSameOrigin('http://somwhere.else/')).toBe(false); - expect(isSameOrigin('//somwhere.else/')).toBe(false); - expect(isSameOrigin('http://nowhere.com/')).toBe(false); - expect(isSameOrigin('http://nowhere.com/like/this')).toBe(false); - expect(isSameOrigin('https://nowhere.org/and/this')).toBe(false); - expect(isSameOrigin('https://nowhere.org')).toBe(false); + // default url is set to https://nowhere.com/at/all + expect(isSameOrigin('https://somwhere.else/all/together')).toBe(false); + expect(isSameOrigin('https://somwhere.else/')).toBe(false); + expect(isSameOrigin('http://somwhere.else/')).toBe(false); + expect(isSameOrigin('//somwhere.else/')).toBe(false); + expect(isSameOrigin('http://nowhere.com/')).toBe(false); + expect(isSameOrigin('http://nowhere.com/like/this')).toBe(false); + expect(isSameOrigin('https://nowhere.org/and/this')).toBe(false); + expect(isSameOrigin('https://nowhere.org')).toBe(false); - expect(isSameOrigin('https://nowhere.com/but/this')).toBe(true); - expect(isSameOrigin('https://nowhere.com/and/this')).toBe(true); - expect(isSameOrigin('https://nowhere.com/')).toBe(true); - expect(isSameOrigin('https://nowhere.com')).toBe(true); - expect(isSameOrigin('//nowhere.com/or/this')).toBe(true); - expect(isSameOrigin('/else/this')).toBe(true); - expect(isSameOrigin('even/this')).toBe(true); + expect(isSameOrigin('https://nowhere.com/but/this')).toBe(true); + expect(isSameOrigin('https://nowhere.com/and/this')).toBe(true); + expect(isSameOrigin('https://nowhere.com/')).toBe(true); + expect(isSameOrigin('https://nowhere.com')).toBe(true); + expect(isSameOrigin('//nowhere.com/or/this')).toBe(true); + expect(isSameOrigin('/else/this')).toBe(true); + expect(isSameOrigin('even/this')).toBe(true); - Object.defineProperty(window, 'location', { - writable: true, - value: { - href: 'https://nowhere.com:2323/some/where', - protocol: 'https:', - hostname: 'nowhere.com', - host: 'nowhere.com:2323', - port: '2323', - }, - }); + Object.defineProperty(window, 'location', { + writable: true, + value: { + href: 'https://nowhere.com:2323/some/where', + protocol: 'https:', + hostname: 'nowhere.com', + host: 'nowhere.com:2323', + port: '2323', + }, + }); - expect(isSameOrigin('https://nowhere.com/but/this')).toBe(false); - expect(isSameOrigin('https://nowhere.com/and/this')).toBe(false); - expect(isSameOrigin('https://nowhere.com/')).toBe(false); - expect(isSameOrigin('https://nowhere.com')).toBe(false); - expect(isSameOrigin('//nowhere.com/or/this')).toBe(false); - expect(isSameOrigin('https://nowhere.com:2323')).toBe(true); - expect(isSameOrigin('//nowhere.com:2323/or/this')).toBe(true); - expect(isSameOrigin('/still/this')).toBe(true); - expect(isSameOrigin('and/even/this')).toBe(true); + expect(isSameOrigin('https://nowhere.com/but/this')).toBe(false); + expect(isSameOrigin('https://nowhere.com/and/this')).toBe(false); + expect(isSameOrigin('https://nowhere.com/')).toBe(false); + expect(isSameOrigin('https://nowhere.com')).toBe(false); + expect(isSameOrigin('//nowhere.com/or/this')).toBe(false); + expect(isSameOrigin('https://nowhere.com:2323')).toBe(true); + expect(isSameOrigin('//nowhere.com:2323/or/this')).toBe(true); + expect(isSameOrigin('/still/this')).toBe(true); + expect(isSameOrigin('and/even/this')).toBe(true); }); test('http post', () => { - window.fetch = jest.fn(() => 73); - document.querySelector = jest.fn(() => ({ value: '37' })); - expect(post('foo', { bar: 11 })).toBe(73); + window.fetch = jest.fn(() => 73); + document.querySelector = jest.fn(() => ({ value: '37' })); + expect(post('foo', { bar: 11 })).toBe(73); - const { calls } = window.fetch.mock; - expect(calls).toMatchObject([ - ['foo', { credentials: 'same-origin', method: 'POST' }], - ]); - expect(document.querySelector.mock.calls).toEqual([ - ['input[name=csrfmiddlewaretoken]'], - ]); + const { calls } = window.fetch.mock; + expect(calls).toMatchObject([ + ['foo', { credentials: 'same-origin', method: 'POST' }], + ]); + expect(document.querySelector.mock.calls).toEqual([ + ['input[name=csrfmiddlewaretoken]'], + ]); - const { body, headers } = calls[0][1]; - expect(Array.from(body.entries())).toMatchObject([ - ['bar', '11'], - ['csrfmiddlewaretoken', '37'], - ]); - expect(Array.from(headers.entries())).toMatchObject([ - ['accept', 'application/json'], - ['x-csrftoken', '37'], - ['x-requested-with', 'XMLHttpRequest'], - ]); + const { body, headers } = calls[0][1]; + expect(Array.from(body.entries())).toMatchObject([ + ['bar', '11'], + ['csrfmiddlewaretoken', '37'], + ]); + expect(Array.from(headers.entries())).toMatchObject([ + ['accept', 'application/json'], + ['x-csrftoken', '37'], + ['x-requested-with', 'XMLHttpRequest'], + ]); }); diff --git a/tag-admin/src/widgets/checkbox-table.js b/tag-admin/src/widgets/checkbox-table.js index 8ec26f1c2..ffd8d4000 100644 --- a/tag-admin/src/widgets/checkbox-table.js +++ b/tag-admin/src/widgets/checkbox-table.js @@ -7,103 +7,98 @@ import { Checkbox } from './checkbox.js'; // Returns a copy of the `checked` set with only resource paths that are in `visible` const prune = (checked, visible) => - new Set([...checked].filter((v) => visible.includes(v))); + new Set([...checked].filter((v) => visible.includes(v))); export function CheckboxTable({ data, onSubmit, submitMessage }) { - const visible = useRef([]); - const [checked, setChecked] = useState(new Set()); - const clearChecked = () => setChecked(new Set()); - const pruneChecked = () => - setChecked((checked) => prune(checked, visible.current)); + const visible = useRef([]); + const [checked, setChecked] = useState(new Set()); + const clearChecked = () => setChecked(new Set()); + const pruneChecked = () => + setChecked((checked) => prune(checked, visible.current)); - useEffect(() => { - visible.current.length = 0; - clearChecked(); - }, [data]); + useEffect(() => { + visible.current.length = 0; + clearChecked(); + }, [data]); - const selectAll = useCallback(() => { - setChecked((checked) => { - if (checked.size > 0) return new Set(); - else return new Set([...visible.current.filter(Boolean)]); - }); - }, []); + const selectAll = useCallback(() => { + setChecked((checked) => { + if (checked.size > 0) return new Set(); + else return new Set([...visible.current.filter(Boolean)]); + }); + }, []); - const selectOne = useCallback(({ target }) => { - setChecked((checked) => { - const next = new Set(checked); - if (target.checked) next.add(target.name); - else next.delete(target.name); - return next; - }); - }, []); + const selectOne = useCallback(({ target }) => { + setChecked((checked) => { + const next = new Set(checked); + if (target.checked) next.add(target.name); + else next.delete(target.name); + return next; + }); + }, []); - const Header = () => { - const pruned = prune(checked, visible.current); - // some rows can be empty strings if there are more visible rows than resources - const some = pruned.size > 0; - const all = - some && pruned.size === visible.current.filter(Boolean).length; - - return ( - - ); - }; - - const Cell = (item) => { - const name = item.original[0]; - visible.current.length = item.pageSize; - visible.current[item.viewIndex] = name; - - return ( - - ); - }; - - const columns = [ - { Header, Cell, sortable: false, width: 45 }, - { - Header: 'Resource', - id: 'type', - Cell: (item) => {item.original[0]}, - }, - ]; - - const handleSubmit = async (evt) => { - // after emitting handleSubmit to parent with list of currently - // checked, clears the checkboxes - evt.preventDefault(); - await onSubmit({ data: [...checked] }); - clearChecked(); - }; + const Header = () => { + const pruned = prune(checked, visible.current); + // some rows can be empty strings if there are more visible rows than resources + const some = pruned.size > 0; + const all = some && pruned.size === visible.current.filter(Boolean).length; return ( -
    - { - visible.current.length = 0; - clearChecked(); - }} - onPageSizeChange={(pageSize) => { - visible.current.length = pageSize; - pruneChecked(); - }} - onSortedChange={pruneChecked} - columns={columns} - /> - -
    + ); + }; + + const Cell = (item) => { + const name = item.original[0]; + visible.current.length = item.pageSize; + visible.current[item.viewIndex] = name; + + return ( + + ); + }; + + const columns = [ + { Header, Cell, sortable: false, width: 45 }, + { + Header: 'Resource', + id: 'type', + Cell: (item) => {item.original[0]}, + }, + ]; + + const handleSubmit = async (evt) => { + // after emitting handleSubmit to parent with list of currently + // checked, clears the checkboxes + evt.preventDefault(); + await onSubmit({ data: [...checked] }); + clearChecked(); + }; + + return ( +
    + { + visible.current.length = 0; + clearChecked(); + }} + onPageSizeChange={(pageSize) => { + visible.current.length = pageSize; + pruneChecked(); + }} + onSortedChange={pruneChecked} + columns={columns} + /> + +
    + ); } diff --git a/tag-admin/src/widgets/checkbox-table.test.js b/tag-admin/src/widgets/checkbox-table.test.js index db9c0e333..0db7510e1 100644 --- a/tag-admin/src/widgets/checkbox-table.test.js +++ b/tag-admin/src/widgets/checkbox-table.test.js @@ -5,200 +5,194 @@ import { act } from 'react-dom/test-utils'; import { CheckboxTable } from './checkbox-table.js'; test('CheckboxTable render', () => { - const table = shallow(); - expect(table.text()).toBe(''); + const table = shallow(); + expect(table.text()).toBe(''); }); test('CheckboxTable render checkboxes', () => { - const table = mount(); - expect(table.find('input[type="checkbox"]')).toHaveLength(1); + const table = mount(); + expect(table.find('input[type="checkbox"]')).toHaveLength(1); - table.setProps({ data: [['foo'], ['bar']] }); - table.update(); - expect(table.find('input[type="checkbox"]')).toHaveLength(3); + table.setProps({ data: [['foo'], ['bar']] }); + table.update(); + expect(table.find('input[type="checkbox"]')).toHaveLength(3); - expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false); - expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false); }); test('CheckboxTable select checkboxes', () => { - const table = mount(); - const inputs = table.find('input[type="checkbox"]'); - expect(inputs).toHaveLength(3); - let checkbox = inputs.at(0).getDOMNode(); + const table = mount(); + const inputs = table.find('input[type="checkbox"]'); + expect(inputs).toHaveLength(3); + let checkbox = inputs.at(0).getDOMNode(); - // Start with no selections - expect(checkbox.checked).toBe(false); - expect(checkbox.indeterminate).toBe(false); + // Start with no selections + expect(checkbox.checked).toBe(false); + expect(checkbox.indeterminate).toBe(false); - // Select 'foo' - table - .find('input[name="foo"]') - .simulate('change', { target: { name: 'foo', checked: true } }); - checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); - expect(checkbox.checked).toBe(false); - expect(checkbox.indeterminate).toBe(true); + // Select 'foo' + table + .find('input[name="foo"]') + .simulate('change', { target: { name: 'foo', checked: true } }); + checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); + expect(checkbox.checked).toBe(false); + expect(checkbox.indeterminate).toBe(true); - // Select also 'bar' - table - .find('input[name="bar"]') - .simulate('change', { target: { name: 'bar', checked: true } }); - checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); - expect(checkbox.checked).toBe(true); - expect(checkbox.indeterminate).toBe(false); + // Select also 'bar' + table + .find('input[name="bar"]') + .simulate('change', { target: { name: 'bar', checked: true } }); + checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); + expect(checkbox.checked).toBe(true); + expect(checkbox.indeterminate).toBe(false); - // Unselect all - inputs.at(0).simulate('change', {}); - expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false); - expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false); + // Unselect all + inputs.at(0).simulate('change', {}); + expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(false); - // Select all - inputs.at(0).simulate('change', {}); - expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(true); - expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(true); + // Select all + inputs.at(0).simulate('change', {}); + expect(table.find('input[name="foo"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="bar"]').getDOMNode().checked).toBe(true); - // Unselect 'foo' - table - .find('input[name="foo"]') - .simulate('change', { target: { name: 'foo', checked: false } }); - checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); - expect(checkbox.checked).toBe(false); - expect(checkbox.indeterminate).toBe(true); + // Unselect 'foo' + table + .find('input[name="foo"]') + .simulate('change', { target: { name: 'foo', checked: false } }); + checkbox = table.find('input[type="checkbox"]').at(0).getDOMNode(); + expect(checkbox.checked).toBe(false); + expect(checkbox.indeterminate).toBe(true); }); test('CheckboxTable sort change', () => { - // Tests what happens when the table sort is changed. - // The expectation is that any items in state.checked are - // removed if they are no longer visible + // Tests what happens when the table sort is changed. + // The expectation is that any items in state.checked are + // removed if they are no longer visible - const table = mount( - , - ); + const table = mount( + , + ); - // Select '2' and '3' - table - .find('input[name="2"]') - .simulate('change', { target: { name: '2', checked: true } }); - table - .find('input[name="3"]') - .simulate('change', { target: { name: '3', checked: true } }); + // Select '2' and '3' + table + .find('input[name="2"]') + .simulate('change', { target: { name: '2', checked: true } }); + table + .find('input[name="3"]') + .simulate('change', { target: { name: '3', checked: true } }); - // Click twice to use descending sort, which sets checkboxes 7..3 visible - const th = table.find('.rt-th.-cursor-pointer'); - th.simulate('click', {}); - th.simulate('click', {}); + // Click twice to use descending sort, which sets checkboxes 7..3 visible + const th = table.find('.rt-th.-cursor-pointer'); + th.simulate('click', {}); + th.simulate('click', {}); - expect(table.find('input[name="2"]')).toHaveLength(0); - expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="2"]')).toHaveLength(0); + expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); - // Switch back to ascending sort - th.simulate('click', {}); + // Switch back to ascending sort + th.simulate('click', {}); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); - expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); }); test('CheckboxTable page change', () => { - const table = mount( - , - ); + const table = mount( + , + ); - expect(table.find('input[name="2"]')).toHaveLength(1); - expect(table.find('input[name="6"]')).toHaveLength(0); + expect(table.find('input[name="2"]')).toHaveLength(1); + expect(table.find('input[name="6"]')).toHaveLength(0); - // Click 'Next' button - const buttons = table.find('button.-btn'); - expect(buttons).toHaveLength(2); - buttons.at(1).simulate('click', {}); + // Click 'Next' button + const buttons = table.find('button.-btn'); + expect(buttons).toHaveLength(2); + buttons.at(1).simulate('click', {}); - expect(table.find('input[name="2"]')).toHaveLength(0); - expect(table.find('input[name="6"]')).toHaveLength(1); + expect(table.find('input[name="2"]')).toHaveLength(0); + expect(table.find('input[name="6"]')).toHaveLength(1); }); test('CheckboxTable resize', () => { - const table = mount( - , - ); + const table = mount( + , + ); - // Resize to 10 rows - const select = table.find('.-pageSizeOptions select'); - select.simulate('change', { target: { value: '10' } }); + // Resize to 10 rows + const select = table.find('.-pageSizeOptions select'); + select.simulate('change', { target: { value: '10' } }); - // Select '2' and '6' - table - .find('input[name="2"]') - .simulate('change', { target: { name: '2', checked: true } }); - table - .find('input[name="6"]') - .simulate('change', { target: { name: '6', checked: true } }); + // Select '2' and '6' + table + .find('input[name="2"]') + .simulate('change', { target: { name: '2', checked: true } }); + table + .find('input[name="6"]') + .simulate('change', { target: { name: '6', checked: true } }); - // Resize to 5 rows - select.simulate('change', { target: { value: '5' } }); + // Resize to 5 rows + select.simulate('change', { target: { value: '5' } }); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); - expect(table.find('input[name="6"]')).toHaveLength(0); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="6"]')).toHaveLength(0); - // Resize to 10 rows - select.simulate('change', { target: { value: '10' } }); + // Resize to 10 rows + select.simulate('change', { target: { value: '10' } }); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); - expect(table.find('input[name="6"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="6"]').getDOMNode().checked).toBe(false); }); test('CheckboxTable submit', async () => { - const spy = jest.fn(); - const table = mount( - , - ); + const spy = jest.fn(); + const table = mount( + , + ); - // Select '2' and '3' - table - .find('input[name="2"]') - .simulate('change', { target: { name: '2', checked: true } }); - table - .find('input[name="3"]') - .simulate('change', { target: { name: '3', checked: true } }); + // Select '2' and '3' + table + .find('input[name="2"]') + .simulate('change', { target: { name: '2', checked: true } }); + table + .find('input[name="3"]') + .simulate('change', { target: { name: '3', checked: true } }); - // Submit changes - table - .find('button.tag-resources-associate') - .simulate('click', { preventDefault: () => {} }); - await act(() => new Promise((resolve) => setTimeout(resolve, 0))); - table.update(); + // Submit changes + table + .find('button.tag-resources-associate') + .simulate('click', { preventDefault: () => {} }); + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + table.update(); - expect(spy.mock.calls).toEqual([[{ data: ['2', '3'] }]]); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); - expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false); + expect(spy.mock.calls).toEqual([[{ data: ['2', '3'] }]]); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false); }); test('CheckboxTable props change', () => { - const data = [['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]; - const table = mount(); + const data = [['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]; + const table = mount(); - // Select '2' and '3' - table - .find('input[name="2"]') - .simulate('change', { target: { name: '2', checked: true } }); - table - .find('input[name="3"]') - .simulate('change', { target: { name: '3', checked: true } }); + // Select '2' and '3' + table + .find('input[name="2"]') + .simulate('change', { target: { name: '2', checked: true } }); + table + .find('input[name="3"]') + .simulate('change', { target: { name: '3', checked: true } }); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); - expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(true); + expect(table.find('input[name="3"]').getDOMNode().checked).toBe(true); - // Re-setting props clears checkboxes - table.setProps({ data: [...data] }); - table.update(); + // Re-setting props clears checkboxes + table.setProps({ data: [...data] }); + table.update(); - expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); - expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="2"]').getDOMNode().checked).toBe(false); + expect(table.find('input[name="3"]').getDOMNode().checked).toBe(false); }); diff --git a/tag-admin/src/widgets/checkbox.js b/tag-admin/src/widgets/checkbox.js index 980988ac7..faaa5c568 100644 --- a/tag-admin/src/widgets/checkbox.js +++ b/tag-admin/src/widgets/checkbox.js @@ -2,11 +2,11 @@ import React, { useEffect, useRef } from 'react'; /** A checkbox which you can set `indeterminate` on */ export function Checkbox({ indeterminate, ...props }) { - const ref = useRef(); + const ref = useRef(); - useEffect(() => { - if (ref.current) ref.current.indeterminate = !!indeterminate; - }, [indeterminate]); + useEffect(() => { + if (ref.current) ref.current.indeterminate = !!indeterminate; + }, [indeterminate]); - return ; + return ; } diff --git a/tag-admin/src/widgets/checkbox.test.js b/tag-admin/src/widgets/checkbox.test.js index 2ce757bd5..c03683b11 100644 --- a/tag-admin/src/widgets/checkbox.test.js +++ b/tag-admin/src/widgets/checkbox.test.js @@ -4,19 +4,19 @@ import React from 'react'; import { Checkbox } from './checkbox.js'; test('Checkbox render', () => { - const checkbox = mount(); - expect(checkbox.find('input[type="checkbox"]#x')).toHaveLength(1); + const checkbox = mount(); + expect(checkbox.find('input[type="checkbox"]#x')).toHaveLength(1); }); test('Checkbox indeterminate', () => { - const checkbox = mount(); - expect(checkbox.getDOMNode().indeterminate).toBe(true); + const checkbox = mount(); + expect(checkbox.getDOMNode().indeterminate).toBe(true); }); test('Checkbox switch', () => { - const checkbox = mount(); - expect(checkbox.getDOMNode().indeterminate).toBe(false); + const checkbox = mount(); + expect(checkbox.getDOMNode().indeterminate).toBe(false); - checkbox.setProps({ indeterminate: 1 }); - expect(checkbox.getDOMNode().indeterminate).toBe(true); + checkbox.setProps({ indeterminate: 1 }); + expect(checkbox.getDOMNode().indeterminate).toBe(true); }); diff --git a/tag-admin/src/widgets/error-list.css b/tag-admin/src/widgets/error-list.css index efdbdfa08..1c611da26 100644 --- a/tag-admin/src/widgets/error-list.css +++ b/tag-admin/src/widgets/error-list.css @@ -1,10 +1,10 @@ ul.errors { - padding: 0.3em; - margin: 0; - list-style: none; - background: #ccc; + padding: 0.3em; + margin: 0; + list-style: none; + background: #ccc; } ul.errors li.error { - color: #f36; + color: #f36; } diff --git a/tag-admin/src/widgets/error-list.js b/tag-admin/src/widgets/error-list.js index 468183c11..f18bcae98 100644 --- a/tag-admin/src/widgets/error-list.js +++ b/tag-admin/src/widgets/error-list.js @@ -3,14 +3,14 @@ import React from 'react'; import './error-list.css'; export function ErrorList({ errors }) { - const entries = Object.entries(errors); - return entries.length === 0 ? null : ( -
      - {entries.map(([name, error], index) => ( -
    • - {name}: {error} -
    • - ))} -
    - ); + const entries = Object.entries(errors); + return entries.length === 0 ? null : ( +
      + {entries.map(([name, error], index) => ( +
    • + {name}: {error} +
    • + ))} +
    + ); } diff --git a/tag-admin/src/widgets/error-list.test.js b/tag-admin/src/widgets/error-list.test.js index b10b7f481..c33ea29af 100644 --- a/tag-admin/src/widgets/error-list.test.js +++ b/tag-admin/src/widgets/error-list.test.js @@ -4,14 +4,14 @@ import React from 'react'; import { ErrorList } from './error-list.js'; test('ErrorList render', () => { - const errors = mount(); - expect(errors.text()).toBe(''); - expect(errors.find('ul').length).toBe(0); + const errors = mount(); + expect(errors.text()).toBe(''); + expect(errors.find('ul').length).toBe(0); - errors.setProps({ errors: { foo: 'Did a foo', bar: 'Bars happen' } }); - expect(errors.find('ul.errors')).toHaveLength(1); - expect(errors.find('li.error')).toHaveLength(2); - expect(errors.find('li.error').at(1).html()).toBe( - '
  • bar: Bars happen
  • ', - ); + errors.setProps({ errors: { foo: 'Did a foo', bar: 'Bars happen' } }); + expect(errors.find('ul.errors')).toHaveLength(1); + expect(errors.find('li.error')).toHaveLength(2); + expect(errors.find('li.error').at(1).html()).toBe( + '
  • bar: Bars happen
  • ', + ); }); diff --git a/translate/.eslintrc.js b/translate/.eslintrc.js index 779a755ea..ab8d3ecb3 100644 --- a/translate/.eslintrc.js +++ b/translate/.eslintrc.js @@ -1,22 +1,23 @@ -/* global module */ +/* eslint-env node */ + module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - env: { - browser: true, - jest: true, - }, - rules: { - 'prefer-const': 0, - 'no-var': 0, - '@typescript-eslint/ban-ts-comment': 0, - '@typescript-eslint/ban-types': 0, - '@typescript-eslint/explicit-module-boundary-types': 0, - '@typescript-eslint/no-empty-function': 0, - '@typescript-eslint/no-explicit-any': 0, - '@typescript-eslint/no-inferrable-types': 0, - '@typescript-eslint/prefer-as-const': 0, - }, + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + env: { + browser: true, + jest: true, + }, + rules: { + 'prefer-const': 0, + 'no-var': 0, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/ban-types': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-inferrable-types': 0, + '@typescript-eslint/prefer-as-const': 0, + }, }; diff --git a/translate/README.md b/translate/README.md index 5ecf3a67b..963cd99e1 100644 --- a/translate/README.md +++ b/translate/README.md @@ -45,7 +45,6 @@
    IDSummaryLast ChangedAssigned To
    - ## Code architecture ### Where code goes @@ -74,6 +73,7 @@ Of course, more can be added if needed. For example, modules with a high number To import code from further away than the parent directory, use paths starting with `~` to refer to the root of the `src/` directory. For example: + ```js import { SearchBox } from '~/modules/search'; ``` @@ -111,7 +111,6 @@ As far as we know, it is not possible to make that work in Chrome or Edge. This If you can't turn on websockets, you will see errors in the console (that's not very impacting) and you'll have to reload your Django server regularly, because polling requests don't close, and after so many web page reloads, the Django process won't be able to accept new requests. - ## Dependencies We manage our JavaScript dependencies with `npm`. @@ -134,7 +133,6 @@ You might want to remove the `translate/node_modules` folder after you've run th (and the `package.json` and `package-lock.json` files have been updated) and before rebuilding the image, to reduce the size of the docker context. - ## Type checking Our code uses TypeScript for type-checking the production code. Tests are not type-checked in general, which allows for smaller test fixtures. Visit the [TypeScript documentation](https://www.typescriptlang.org/docs) to learn more about TypeScript. @@ -143,7 +141,6 @@ To check for TypeScript errors locally, run: $ make types - ## Testing Tests are run using [`jest`](https://facebook.github.io/jest/). We use [`enzyme`](http://airbnb.io/enzyme/docs/api/) for mounting React components and [`sinon`](http://sinonjs.org/) for mocking. @@ -158,7 +155,7 @@ Tests are put in files called `fileToTest.test.js` in the same directory as the ```javascript describe('', () => { - // test suite here + // test suite here }); ``` @@ -166,13 +163,12 @@ Individual tests follow `mocha`'s descriptive syntax. Try to be as explicit as p ```javascript it('does something specific', () => { - // unit test here + // unit test here }); ``` We use `jest`'s [`expect`](https://facebook.github.io/jest/docs/en/expect.html) assertion tool. - ## Localization The user interface is localized using [Fluent](https://projectfluent.org/) and the library `fluent-react`. Fluent allows to move the complexity of handling translated content from the developer to the translator. Thus, when using it, you should care only about the English version, and trust that the localizers will know what to do with your string in their language. @@ -185,13 +181,15 @@ That would give: ```js class Editor extends React.Component { - render() { - return
    - - - -
    ; - } + render() { + return ( +
    + + + +
    + ); + } } ``` @@ -206,7 +204,7 @@ Those files use the FTL format. In its simplest form, a string in such a file (c ### Semantic identifiers -Fluent uses the concept of a *social contract* between developer and localizers. This contract is established by the selection of a unique identifier, called `l10n-id`, which carries a promise of being used in a particular place to carry a particular meaning. +Fluent uses the concept of a _social contract_ between developer and localizers. This contract is established by the selection of a unique identifier, called `l10n-id`, which carries a promise of being used in a particular place to carry a particular meaning. You should consider the `l10n-id` as a variable name. If the meaning of the content changes, then you should also change the ID. This will notify localizers that the content is different from before and that a new translation is needed. However, if you make minor changes (fix a typo, make a change that keeps the same meaning) you should instead keep the same ID. @@ -220,11 +218,10 @@ In Fluent, the developer is not to be bothered with inner logic and complexity t In order to easily verify that a string is effectively localized, you can turn on pseudo-localization. To do that, add `pseudolocalization=accented` or `pseudolocalization=bidi` to the URL, then refresh the page. -Pseudo-localization turns every supported string into a different version of itself. We support two modes: "accented" (transforms "Accented English" into "Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ") and "bidi" (transforms "Reversed English" into "‮ᴚǝʌǝɹsǝp Ǝuƃʅısɥ‬"). Because only strings that are actually localized (they exist in our reference en-US FTL file and they are properly set in a `` component) get that transformation, it is easy to spot which strings are *not* properly localized in the interface. +Pseudo-localization turns every supported string into a different version of itself. We support two modes: "accented" (transforms "Accented English" into "Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ") and "bidi" (transforms "Reversed English" into "‮ᴚǝʌǝɹsǝp Ǝuƃʅısɥ‬"). Because only strings that are actually localized (they exist in our reference en-US FTL file and they are properly set in a `` component) get that transformation, it is easy to spot which strings are _not_ properly localized in the interface. You can read [more about pseudo-localization on Wikipedia](https://en.wikipedia.org/wiki/Pseudolocalization). - ## Development resources ### Integration between Django and React diff --git a/translate/jest.config.js b/translate/jest.config.js index 288d5b38d..b66a0b506 100644 --- a/translate/jest.config.js +++ b/translate/jest.config.js @@ -2,39 +2,39 @@ /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - verbose: true, - roots: ['/src'], - setupFilesAfterEnv: ['/src/setupTests.ts'], - collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], - coveragePathIgnorePatterns: ['/node_modules/'], - testMatch: [ - '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', - '/src/**/*.{spec,test}.{js,jsx,ts,tsx}', - ], - testEnvironment: 'jsdom', - preset: 'ts-jest', - globals: { - 'ts-jest': { - isolatedModules: true, - }, + verbose: true, + roots: ['/src'], + setupFilesAfterEnv: ['/src/setupTests.ts'], + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], + coveragePathIgnorePatterns: ['/node_modules/'], + testMatch: [ + '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', + '/src/**/*.{spec,test}.{js,jsx,ts,tsx}', + ], + testEnvironment: 'jsdom', + preset: 'ts-jest', + globals: { + 'ts-jest': { + isolatedModules: true, }, - transform: { - '\\.jsx?$': ['babel-jest', { configFile: '../babel.config.json' }], - '\\.tsx?$': 'ts-jest', - }, - transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', - ], - resetMocks: true, - moduleNameMapper: { - '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': - 'identity-obj-proxy', - '\\.svg$': '/__mocks__/svg.js', - '~(.*)$': '/src/$1', - }, - watchPlugins: [ - 'jest-watch-typeahead/filename', - 'jest-watch-typeahead/testname', - ], - testTimeout: 10000, // optional + }, + transform: { + '\\.jsx?$': ['babel-jest', { configFile: '../babel.config.json' }], + '\\.tsx?$': 'ts-jest', + }, + transformIgnorePatterns: [ + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', + ], + resetMocks: true, + moduleNameMapper: { + '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': + 'identity-obj-proxy', + '\\.svg$': '/__mocks__/svg.js', + '~(.*)$': '/src/$1', + }, + watchPlugins: [ + 'jest-watch-typeahead/filename', + 'jest-watch-typeahead/testname', + ], + testTimeout: 10000, // optional }; diff --git a/translate/package.json b/translate/package.json index dd314848d..28f6934b9 100644 --- a/translate/package.json +++ b/translate/package.json @@ -47,9 +47,6 @@ "start": "rollup -c --watch", "build": "rollup -c", "build:prod": "rollup -c --environment BUILD:production", - "lint": "eslint 'src/**/*.{js,ts,tsx}'", - "prettier": "prettier --write src/", - "check-prettier": "prettier --check src/", "test": "jest", "types": "tsc --noEmit" } diff --git a/translate/rollup.config.js b/translate/rollup.config.js index 605a835e4..790c139b9 100644 --- a/translate/rollup.config.js +++ b/translate/rollup.config.js @@ -9,38 +9,38 @@ import css from 'rollup-plugin-css-only'; /** @type {import('rollup').RollupOptions} */ const config = { - input: 'src/index.tsx', - output: { file: 'dist/translate.js' }, + input: 'src/index.tsx', + output: { file: 'dist/translate.js' }, - treeshake: 'recommended', + treeshake: 'recommended', - plugins: [ - json(), - typescript(), - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify( - process.env.BUILD ?? 'development', - ), - }), - resolve(), - commonjs(), - css({ output: 'translate.css' }), - ], + plugins: [ + json(), + typescript(), + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify( + process.env.BUILD ?? 'development', + ), + }), + resolve(), + commonjs(), + css({ output: 'translate.css' }), + ], - onwarn(warning, warn) { - // https://github.com/reduxjs/redux-toolkit/issues/1466 - if (warning.id?.includes('@reduxjs/toolkit')) { - switch (warning.code) { - case 'SOURCEMAP_ERROR': - return; - case 'THIS_IS_UNDEFINED': - if (warning.frame?.includes('this && this')) return; - } - } + onwarn(warning, warn) { + // https://github.com/reduxjs/redux-toolkit/issues/1466 + if (warning.id?.includes('@reduxjs/toolkit')) { + switch (warning.code) { + case 'SOURCEMAP_ERROR': + return; + case 'THIS_IS_UNDEFINED': + if (warning.frame?.includes('this && this')) return; + } + } - warn(warning); - }, + warn(warning); + }, }; export default config; diff --git a/translate/src/App.css b/translate/src/App.css index 1cd0e9141..90ad5af46 100644 --- a/translate/src/App.css +++ b/translate/src/App.css @@ -1,64 +1,64 @@ #app { - display: flex; - flex-direction: column; - flex-flow: column; - height: 100%; + display: flex; + flex-direction: column; + flex-flow: column; + height: 100%; } #app > header { - background: #272a2f; - border-bottom: 1px solid #333941; - box-sizing: border-box; - height: 60px; - min-width: 700px; - position: relative; + background: #272a2f; + border-bottom: 1px solid #333941; + box-sizing: border-box; + height: 60px; + min-width: 700px; + position: relative; } #app > .main-content { - display: flex; - flex: 1; - justify-content: space-between; - overflow: auto; + display: flex; + flex: 1; + justify-content: space-between; + overflow: auto; } #app > .main-content > .panel-content, #app > .main-content > .panel-list { - height: 100%; - position: relative; + height: 100%; + position: relative; } #app > .main-content > .panel-list { - width: 25%; + width: 25%; } #app > .main-content > .panel-content { - border-left: 1px solid #5e6475; - box-sizing: border-box; - width: 75%; + border-left: 1px solid #5e6475; + box-sizing: border-box; + width: 75%; } /* NProgress: A nanoscopic progress bar. */ #nprogress { - pointer-events: none; + pointer-events: none; } #nprogress .bar { - background: #7bc876; - position: fixed; - z-index: 1031; - top: 0; - left: 0; - width: 100%; - height: 2px; + background: #7bc876; + position: fixed; + z-index: 1031; + top: 0; + left: 0; + width: 100%; + height: 2px; } #nprogress .peg { - display: block; - position: absolute; - right: 0px; - width: 100px; - height: 100%; - box-shadow: 0 0 10px #7bc876, 0 0 5px #7bc876; - opacity: 1; - transform: rotate(3deg) translate(0px, -4px); + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #7bc876, 0 0 5px #7bc876; + opacity: 1; + transform: rotate(3deg) translate(0px, -4px); } diff --git a/translate/src/App.test.js b/translate/src/App.test.js index a07dcfa31..589170bd9 100644 --- a/translate/src/App.test.js +++ b/translate/src/App.test.js @@ -8,13 +8,13 @@ // import store from '~/store'; describe('', () => { - it('renders without crashing', () => { - // Commented out because there's a network call that I can't figure out - // how to mock yet. - // store.dispatch = sinon.fake(); - // - // const div = document.createElement('div'); - // ReactDOM.render(shallow(), div); - // ReactDOM.unmountComponentAtNode(div); - }); + it('renders without crashing', () => { + // Commented out because there's a network call that I can't figure out + // how to mock yet. + // store.dispatch = sinon.fake(); + // + // const div = document.createElement('div'); + // ReactDOM.render(shallow(), div); + // ReactDOM.unmountComponentAtNode(div); + }); }); diff --git a/translate/src/App.tsx b/translate/src/App.tsx index 14b384c48..18c40061e 100644 --- a/translate/src/App.tsx +++ b/translate/src/App.tsx @@ -34,119 +34,114 @@ import type { Stats } from '~/core/stats'; import { AppDispatch, RootState } from '~/store'; type Props = { - batchactions: BatchActionsState; - l10n: L10nState; - locale: LocaleState; - notification: notification.NotificationState; - parameters: NavigationParams; - project: ProjectState; - stats: Stats; + batchactions: BatchActionsState; + l10n: L10nState; + locale: LocaleState; + notification: notification.NotificationState; + parameters: NavigationParams; + project: ProjectState; + stats: Stats; }; type InternalProps = Props & { - dispatch: AppDispatch; + dispatch: AppDispatch; }; /** * Main entry point to the application. Will render the structure of the page. */ class App extends React.Component { - componentDidMount() { - const { parameters } = this.props; + componentDidMount() { + const { parameters } = this.props; - this.props.dispatch(locale.actions.get(parameters.locale)); - this.props.dispatch(project.actions.get(parameters.project)); - this.props.dispatch(user.actions.getUsers()); + this.props.dispatch(locale.actions.get(parameters.locale)); + this.props.dispatch(project.actions.get(parameters.project)); + this.props.dispatch(user.actions.getUsers()); - // Load resources, unless we're in the All Projects view - if (parameters.project !== 'all-projects') { - this.props.dispatch( - resource.actions.get(parameters.locale, parameters.project), - ); - } + // Load resources, unless we're in the All Projects view + if (parameters.project !== 'all-projects') { + this.props.dispatch( + resource.actions.get(parameters.locale, parameters.project), + ); } + } - componentDidUpdate(prevProps: InternalProps) { - // If there's a notification in the DOM, passed by django, show it. - // Note that we only show it once, and only when the UI has already - // been rendered, to make sure users do see it. - if ( - !this.props.l10n.fetching && - !this.props.locale.fetching && - (prevProps.l10n.fetching || prevProps.locale.fetching) - ) { - let notifications = []; - const rootElt = document.getElementById('root'); - if (rootElt) { - notifications = JSON.parse(rootElt.dataset.notifications); - } + componentDidUpdate(prevProps: InternalProps) { + // If there's a notification in the DOM, passed by django, show it. + // Note that we only show it once, and only when the UI has already + // been rendered, to make sure users do see it. + if ( + !this.props.l10n.fetching && + !this.props.locale.fetching && + (prevProps.l10n.fetching || prevProps.locale.fetching) + ) { + let notifications = []; + const rootElt = document.getElementById('root'); + if (rootElt) { + notifications = JSON.parse(rootElt.dataset.notifications); + } - if (notifications.length) { - // Our notification system only supports showing one notification - // for the moment, so we only add the first notification here. - const notif = notifications[0]; - this.props.dispatch( - notification.actions.addRaw(notif.content, notif.type), - ); - } - } - } - - render() { - const state = this.props; - - if (state.l10n.fetching || state.locale.fetching) { - return ; - } - - return ( -
    - -
    - - - - - -
    -
    -
    - - -
    -
    - {state.batchactions.entities.length === 0 ? ( - - ) : ( - - )} -
    -
    - - -
    + if (notifications.length) { + // Our notification system only supports showing one notification + // for the moment, so we only add the first notification here. + const notif = notifications[0]; + this.props.dispatch( + notification.actions.addRaw(notif.content, notif.type), ); + } } + } + + render() { + const state = this.props; + + if (state.l10n.fetching || state.locale.fetching) { + return ; + } + + return ( +
    + +
    + + + + + +
    +
    +
    + + +
    +
    + {state.batchactions.entities.length === 0 ? ( + + ) : ( + + )} +
    +
    + + +
    + ); + } } const mapStateToProps = (state: RootState): Props => { - return { - batchactions: state[batchactions.NAME], - l10n: state[l10n.NAME], - locale: state[locale.NAME], - notification: state[notification.NAME], - parameters: navigation.selectors.getNavigationParams(state), - project: state[project.NAME], - stats: state[stats.NAME], - }; + return { + batchactions: state[batchactions.NAME], + l10n: state[l10n.NAME], + locale: state[locale.NAME], + notification: state[notification.NAME], + parameters: navigation.selectors.getNavigationParams(state), + project: state[project.NAME], + stats: state[stats.NAME], + }; }; export default connect(mapStateToProps)(App); diff --git a/translate/src/core/api/base.ts b/translate/src/core/api/base.ts index 61c2f13c3..1abf575f1 100644 --- a/translate/src/core/api/base.ts +++ b/translate/src/core/api/base.ts @@ -1,112 +1,108 @@ export default class APIBase { - abortController: AbortController; - signal: AbortSignal; + abortController: AbortController; + signal: AbortSignal; - constructor() { - // Create a controller to abort fetch requests. - this.abortController = new AbortController(); - this.signal = this.abortController.signal; + constructor() { + // Create a controller to abort fetch requests. + this.abortController = new AbortController(); + this.signal = this.abortController.signal; + } + + abort() { + // Abort the previously started requests. + this.abortController.abort(); + + // Now create a new controller for the next round of requests. + this.abortController = new AbortController(); + this.signal = this.abortController.signal; + } + + getCSRFToken(): string { + let csrfToken = ''; + const rootElt = document.getElementById('root'); + if (rootElt) { + csrfToken = rootElt.dataset.csrfToken; } + return csrfToken; + } - abort() { - // Abort the previously started requests. - this.abortController.abort(); + getFullURL(url: string): URL { + return new URL(url, window.location.origin); + } - // Now create a new controller for the next round of requests. - this.abortController = new AbortController(); - this.signal = this.abortController.signal; - } + toCamelCase: (s: string) => string = (s: string) => { + return s.replace(/([-_][a-z])/gi, ($1) => { + return $1.toUpperCase().replace('-', '').replace('_', ''); + }); + }; - getCSRFToken(): string { - let csrfToken = ''; - const rootElt = document.getElementById('root'); - if (rootElt) { - csrfToken = rootElt.dataset.csrfToken; - } - return csrfToken; - } + isObject: (obj: any) => boolean = function (obj: any) { + return ( + obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function' + ); + }; - getFullURL(url: string): URL { - return new URL(url, window.location.origin); - } + async fetch( + url: string, + method: string, + payload: URLSearchParams | FormData | null, + headers: Headers, + ): Promise { + const fullUrl = this.getFullURL(url); - toCamelCase: (s: string) => string = (s: string) => { - return s.replace(/([-_][a-z])/gi, ($1) => { - return $1.toUpperCase().replace('-', '').replace('_', ''); - }); + const requestParams = { + method: method, + credentials: 'same-origin' as RequestCredentials, + headers: headers, + // This signal is used to cancel requests with the `abort()` method. + signal: this.signal, + body: payload, }; - isObject: (obj: any) => boolean = function (obj: any) { - return ( - obj === Object(obj) && - !Array.isArray(obj) && - typeof obj !== 'function' - ); - }; - - async fetch( - url: string, - method: string, - payload: URLSearchParams | FormData | null, - headers: Headers, - ): Promise { - const fullUrl = this.getFullURL(url); - - const requestParams = { - method: method, - credentials: 'same-origin' as RequestCredentials, - headers: headers, - // This signal is used to cancel requests with the `abort()` method. - signal: this.signal, - body: payload, - }; - - if (payload !== null && method === 'GET') { - requestParams.body = null; - fullUrl.search = payload.toString(); - } - - let response; - try { - response = await fetch(fullUrl.toString(), requestParams); - } catch (e) { - // Swallow Abort errors because we trigger them ourselves. - if (e.name === 'AbortError') { - return {}; - } - throw e; - } - - try { - return await response.json(); - } catch (e) { - // Catch non-JSON responses - /* eslint-disable no-console */ - console.error('The response content is not JSON-compatible'); - console.error(`URL: ${url} - Method: ${method}`); - console.error(e); - - return {}; - } + if (payload !== null && method === 'GET') { + requestParams.body = null; + fullUrl.search = payload.toString(); } - keysToCamelCase(results: any): any { - if (this.isObject(results)) { - const newObj: any = {}; - - Object.keys(results).forEach((key) => { - newObj[this.toCamelCase(key)] = this.keysToCamelCase( - results[key], - ); - }); - - return newObj; - } else if (Array.isArray(results)) { - return results.map((i) => { - return this.keysToCamelCase(i); - }); - } - - return results; + let response; + try { + response = await fetch(fullUrl.toString(), requestParams); + } catch (e) { + // Swallow Abort errors because we trigger them ourselves. + if (e.name === 'AbortError') { + return {}; + } + throw e; } + + try { + return await response.json(); + } catch (e) { + // Catch non-JSON responses + /* eslint-disable no-console */ + console.error('The response content is not JSON-compatible'); + console.error(`URL: ${url} - Method: ${method}`); + console.error(e); + + return {}; + } + } + + keysToCamelCase(results: any): any { + if (this.isObject(results)) { + const newObj: any = {}; + + Object.keys(results).forEach((key) => { + newObj[this.toCamelCase(key)] = this.keysToCamelCase(results[key]); + }); + + return newObj; + } else if (Array.isArray(results)) { + return results.map((i) => { + return this.keysToCamelCase(i); + }); + } + + return results; + } } diff --git a/translate/src/core/api/comment.ts b/translate/src/core/api/comment.ts index 5ef0c1e27..00eeed582 100644 --- a/translate/src/core/api/comment.ts +++ b/translate/src/core/api/comment.ts @@ -1,45 +1,45 @@ import APIBase from './base'; export default class CommentAPI extends APIBase { - add( - entity: number, - locale: string, - comment: string, - translation: number, - ): Promise { - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('locale', locale); - payload.append('comment', comment); - if (translation) { - payload.append('translation', translation.toString()); - } - - const headers = new Headers(); - const csrfToken = this.getCSRFToken(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); - - return this.fetch('/add-comment/', 'POST', payload, headers); + add( + entity: number, + locale: string, + comment: string, + translation: number, + ): Promise { + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('locale', locale); + payload.append('comment', comment); + if (translation) { + payload.append('translation', translation.toString()); } - _updateComment(url: string, commentId: number): Promise { - const payload = new URLSearchParams(); - payload.append('comment_id', commentId.toString()); + const headers = new Headers(); + const csrfToken = this.getCSRFToken(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); - const headers = new Headers(); - const csrfToken = this.getCSRFToken(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); + return this.fetch('/add-comment/', 'POST', payload, headers); + } - return this.fetch(url, 'POST', payload, headers); - } + _updateComment(url: string, commentId: number): Promise { + const payload = new URLSearchParams(); + payload.append('comment_id', commentId.toString()); - pinComment(commentId: number): Promise { - return this._updateComment('/pin-comment/', commentId); - } + const headers = new Headers(); + const csrfToken = this.getCSRFToken(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); - unpinComment(commentId: number): Promise { - return this._updateComment('/unpin-comment/', commentId); - } + return this.fetch(url, 'POST', payload, headers); + } + + pinComment(commentId: number): Promise { + return this._updateComment('/pin-comment/', commentId); + } + + unpinComment(commentId: number): Promise { + return this._updateComment('/unpin-comment/', commentId); + } } diff --git a/translate/src/core/api/entity.ts b/translate/src/core/api/entity.ts index ef44a92d8..b34b0626b 100644 --- a/translate/src/core/api/entity.ts +++ b/translate/src/core/api/entity.ts @@ -3,215 +3,210 @@ import APIBase from './base'; import type { OtherLocaleTranslations } from './types'; export default class EntityAPI extends APIBase { - async batchEdit( - action: string, - locale: string, - entities: Array, - find: string | undefined, - replace: string | undefined, - ): Promise { - const payload = new FormData(); + async batchEdit( + action: string, + locale: string, + entities: Array, + find: string | undefined, + replace: string | undefined, + ): Promise { + const payload = new FormData(); - const csrfToken = this.getCSRFToken(); - payload.append('csrfmiddlewaretoken', csrfToken); + const csrfToken = this.getCSRFToken(); + payload.append('csrfmiddlewaretoken', csrfToken); - payload.append('action', action); - payload.append('locale', locale); - payload.append('entities', entities.join(',')); + payload.append('action', action); + payload.append('locale', locale); + payload.append('entities', entities.join(',')); - if (find) { - payload.append('find', find); - } - - if (replace) { - payload.append('replace', replace); - } - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - return await this.fetch( - '/batch-edit-translations/', - 'POST', - payload, - headers, - ); + if (find) { + payload.append('find', find); } - /** - * Return a list of entities for a project and locale. - * - * Pass in a `resource` to restrict the list to a specific path. - * If the `exclude` array has values, those entities will be excluded from - * the query. Use this to query for the next set of entities. - */ - async getEntities( - locale: string, - project: string, - resource: string, - entityIds: Array | null | undefined, - exclude: Array, - entity?: string | null | undefined, - search?: string | null | undefined, - status?: string | null | undefined, - extra?: string | null | undefined, - tag?: string | null | undefined, - author?: string | null | undefined, - time?: string | null | undefined, - pkOnly?: boolean | null | undefined, - ): Promise> { - const payload = new FormData(); - payload.append('locale', locale); - payload.append('project', project); - - if (resource !== 'all-resources') { - payload.append('paths[]', resource); - } - - if (entityIds && entityIds.length) { - payload.append('entity_ids', entityIds.join(',')); - } - - if (exclude.length) { - payload.append('exclude_entities', exclude.join(',')); - } - - if (entity) { - payload.append('entity', entity); - } - - if (search) { - payload.append('search', search); - } - - if (status) { - payload.append('status', status); - } - - if (extra) { - payload.append('extra', extra); - } - - if (tag) { - payload.append('tag', tag); - } - - if (author) { - payload.append('author', author); - } - - if (time) { - payload.append('time', time); - } - - if (pkOnly) { - payload.append('pk_only', 'true'); - } - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - return await this.fetch('/get-entities/', 'POST', payload, headers); + if (replace) { + payload.append('replace', replace); } - async getHistory( - entity: number, - locale: string, - pluralForm: number = -1, - ): Promise { - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('locale', locale); - payload.append('plural_form', pluralForm.toString()); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + return await this.fetch( + '/batch-edit-translations/', + 'POST', + payload, + headers, + ); + } - const results = await this.fetch( - '/get-history/', - 'GET', - payload, - headers, - ); + /** + * Return a list of entities for a project and locale. + * + * Pass in a `resource` to restrict the list to a specific path. + * If the `exclude` array has values, those entities will be excluded from + * the query. Use this to query for the next set of entities. + */ + async getEntities( + locale: string, + project: string, + resource: string, + entityIds: Array | null | undefined, + exclude: Array, + entity?: string | null | undefined, + search?: string | null | undefined, + status?: string | null | undefined, + extra?: string | null | undefined, + tag?: string | null | undefined, + author?: string | null | undefined, + time?: string | null | undefined, + pkOnly?: boolean | null | undefined, + ): Promise> { + const payload = new FormData(); + payload.append('locale', locale); + payload.append('project', project); - return this.keysToCamelCase(results); + if (resource !== 'all-resources') { + payload.append('paths[]', resource); } - async getSiblingEntities(entity: number, locale: string): Promise { - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('locale', locale); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - const results = await this.fetch( - '/get-sibling-entities/', - 'GET', - payload, - headers, - ); - - return this.keysToCamelCase(results); + if (entityIds && entityIds.length) { + payload.append('entity_ids', entityIds.join(',')); } - async getOtherLocales( - entity: number, - locale: string, - ): Promise { - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('locale', locale); - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - const results = await this.fetch( - '/other-locales/', - 'GET', - payload, - headers, - ); - - if (results.status === false) { - return []; - } - - return results as OtherLocaleTranslations; + if (exclude.length) { + payload.append('exclude_entities', exclude.join(',')); } - async getTeamComments(entity: number, locale: string): Promise { - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('locale', locale); - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - const results = await this.fetch( - '/get-team-comments/', - 'GET', - payload, - headers, - ); - - return this.keysToCamelCase(results); + if (entity) { + payload.append('entity', entity); } - async getTerms(sourceString: string, locale: string): Promise { - const payload = new URLSearchParams(); - payload.append('source_string', sourceString); - payload.append('locale', locale); - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - - const results = await this.fetch( - '/terminology/get-terms/', - 'GET', - payload, - headers, - ); - - return this.keysToCamelCase(results); + if (search) { + payload.append('search', search); } + + if (status) { + payload.append('status', status); + } + + if (extra) { + payload.append('extra', extra); + } + + if (tag) { + payload.append('tag', tag); + } + + if (author) { + payload.append('author', author); + } + + if (time) { + payload.append('time', time); + } + + if (pkOnly) { + payload.append('pk_only', 'true'); + } + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + return await this.fetch('/get-entities/', 'POST', payload, headers); + } + + async getHistory( + entity: number, + locale: string, + pluralForm: number = -1, + ): Promise { + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('locale', locale); + payload.append('plural_form', pluralForm.toString()); + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + const results = await this.fetch('/get-history/', 'GET', payload, headers); + + return this.keysToCamelCase(results); + } + + async getSiblingEntities(entity: number, locale: string): Promise { + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('locale', locale); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + const results = await this.fetch( + '/get-sibling-entities/', + 'GET', + payload, + headers, + ); + + return this.keysToCamelCase(results); + } + + async getOtherLocales( + entity: number, + locale: string, + ): Promise { + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('locale', locale); + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + const results = await this.fetch( + '/other-locales/', + 'GET', + payload, + headers, + ); + + if (results.status === false) { + return []; + } + + return results as OtherLocaleTranslations; + } + + async getTeamComments(entity: number, locale: string): Promise { + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('locale', locale); + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + const results = await this.fetch( + '/get-team-comments/', + 'GET', + payload, + headers, + ); + + return this.keysToCamelCase(results); + } + + async getTerms(sourceString: string, locale: string): Promise { + const payload = new URLSearchParams(); + payload.append('source_string', sourceString); + payload.append('locale', locale); + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + + const results = await this.fetch( + '/terminology/get-terms/', + 'GET', + payload, + headers, + ); + + return this.keysToCamelCase(results); + } } diff --git a/translate/src/core/api/filter.ts b/translate/src/core/api/filter.ts index 31f176819..85faed28b 100644 --- a/translate/src/core/api/filter.ts +++ b/translate/src/core/api/filter.ts @@ -1,18 +1,18 @@ import APIBase from './base'; export default class FilterAPI extends APIBase { - /** - * Return data needed for filtering strings. - */ - async get(locale: string, project: string, resource: string): Promise { - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + /** + * Return data needed for filtering strings. + */ + async get(locale: string, project: string, resource: string): Promise { + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch( - `/${locale}/${project}/${resource}/authors-and-time-range/`, - 'GET', - null, - headers, - ); - } + return await this.fetch( + `/${locale}/${project}/${resource}/authors-and-time-range/`, + 'GET', + null, + headers, + ); + } } diff --git a/translate/src/core/api/index.ts b/translate/src/core/api/index.ts index de872ca6b..38ca403f8 100644 --- a/translate/src/core/api/index.ts +++ b/translate/src/core/api/index.ts @@ -11,30 +11,30 @@ import UserAPI from './user'; import UxActionAPI from './uxaction'; export type { - Entities, - Entity, - EntityTranslation, - TranslationComment, - TeamComment, - TermType, - UsersList, - MachineryTranslation, - OtherLocaleTranslations, - OtherLocaleTranslation, - SourceType, - EntitySiblings, + Entities, + Entity, + EntityTranslation, + TranslationComment, + TeamComment, + TermType, + UsersList, + MachineryTranslation, + OtherLocaleTranslations, + OtherLocaleTranslation, + SourceType, + EntitySiblings, } from './types'; export default { - comment: new CommentAPI() as CommentAPI, - entity: new EntityAPI() as EntityAPI, - filter: new FilterAPI() as FilterAPI, - l10n: new L10nAPI() as L10nAPI, - locale: new LocaleAPI() as LocaleAPI, - machinery: new MachineryAPI() as MachineryAPI, - project: new ProjectAPI() as ProjectAPI, - resource: new ResourceAPI() as ResourceAPI, - translation: new TranslationAPI() as TranslationAPI, - user: new UserAPI() as UserAPI, - uxaction: new UxActionAPI() as UxActionAPI, + comment: new CommentAPI() as CommentAPI, + entity: new EntityAPI() as EntityAPI, + filter: new FilterAPI() as FilterAPI, + l10n: new L10nAPI() as L10nAPI, + locale: new LocaleAPI() as LocaleAPI, + machinery: new MachineryAPI() as MachineryAPI, + project: new ProjectAPI() as ProjectAPI, + resource: new ResourceAPI() as ResourceAPI, + translation: new TranslationAPI() as TranslationAPI, + user: new UserAPI() as UserAPI, + uxaction: new UxActionAPI() as UxActionAPI, }; diff --git a/translate/src/core/api/l10n.ts b/translate/src/core/api/l10n.ts index 15d8b331e..167281059 100644 --- a/translate/src/core/api/l10n.ts +++ b/translate/src/core/api/l10n.ts @@ -1,9 +1,9 @@ import APIBase from './base'; export default class L10nAPI extends APIBase { - async get(locale: string): Promise { - const url = this.getFullURL(`/static/locale/${locale}/translate.ftl`); - const response = await fetch(url.toString()); - return await response.text(); - } + async get(locale: string): Promise { + const url = this.getFullURL(`/static/locale/${locale}/translate.ftl`); + const response = await fetch(url.toString()); + return await response.text(); + } } diff --git a/translate/src/core/api/locale.ts b/translate/src/core/api/locale.ts index 65a1099d9..2f112a4f7 100644 --- a/translate/src/core/api/locale.ts +++ b/translate/src/core/api/locale.ts @@ -1,8 +1,8 @@ import APIBase from './base'; export default class LocaleAPI extends APIBase { - async get(code: string): Promise { - const query = `{ + async get(code: string): Promise { + const query = `{ locale(code: "${code}") { code name @@ -26,12 +26,12 @@ export default class LocaleAPI extends APIBase { } }`; - const payload = new URLSearchParams(); - payload.append('query', query); + const payload = new URLSearchParams(); + payload.append('query', query); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch('/graphql', 'GET', payload, headers); - } + return await this.fetch('/graphql', 'GET', payload, headers); + } } diff --git a/translate/src/core/api/machinery.ts b/translate/src/core/api/machinery.ts index 12b1d94c2..2707dfb6d 100644 --- a/translate/src/core/api/machinery.ts +++ b/translate/src/core/api/machinery.ts @@ -6,224 +6,220 @@ import type { MachineryTranslation } from './types'; type Translations = Array; type ConcordanceTranslations = { - results: Array; - hasMore: boolean; + results: Array; + hasMore: boolean; }; export default class MachineryAPI extends APIBase { - private _get(url: string, params: Record): Promise { - const payload = new URLSearchParams(params); - const headers = new Headers({ 'X-Requested-With': 'XMLHttpRequest' }); - return this.fetch(url, 'GET', payload, headers); + private _get(url: string, params: Record): Promise { + const payload = new URLSearchParams(params); + const headers = new Headers({ 'X-Requested-With': 'XMLHttpRequest' }); + return this.fetch(url, 'GET', payload, headers); + } + + /** + * Return results from Concordance search. + */ + async getConcordanceResults( + source: string, + locale: Locale, + page?: number, + ): Promise { + const url = '/concordance-search/'; + const params = { + text: source, + locale: locale.code, + page: (page || 1).toString(), + }; + + const { results, has_next } = (await this._get(url, params)) as { + results: Array<{ + source: string; + target: string; + project_names: string[]; + }>; + has_next: boolean; + }; + + if (!Array.isArray(results)) { + return { results: [], hasMore: false }; } - /** - * Return results from Concordance search. - */ - async getConcordanceResults( - source: string, - locale: Locale, - page?: number, - ): Promise { - const url = '/concordance-search/'; - const params = { - text: source, - locale: locale.code, - page: (page || 1).toString(), - }; + return { + results: results.map((item) => ({ + sources: ['concordance-search'], + original: item.source, + translation: item.target, + projectNames: item.project_names, + })), + hasMore: has_next, + }; + } - const { results, has_next } = (await this._get(url, params)) as { - results: Array<{ - source: string; - target: string; - project_names: string[]; - }>; - has_next: boolean; - }; + /** + * Return translations from Pontoon's memory. + */ + async getTranslationMemory( + source: string, + locale: Locale, + pk: number | null | undefined, + ): Promise { + const url = '/translation-memory/'; + let params = { + text: source, + locale: locale.code, + }; - if (!Array.isArray(results)) { - return { results: [], hasMore: false }; - } - - return { - results: results.map((item) => ({ - sources: ['concordance-search'], - original: item.source, - translation: item.target, - projectNames: item.project_names, - })), - hasMore: has_next, - }; + if (pk) { + params[pk] = pk; } - /** - * Return translations from Pontoon's memory. - */ - async getTranslationMemory( - source: string, - locale: Locale, - pk: number | null | undefined, - ): Promise { - const url = '/translation-memory/'; - let params = { - text: source, - locale: locale.code, - }; + const results = (await this._get(url, params)) as Array<{ + count: number; + source: string; + target: string; + quality: number; + }>; - if (pk) { - params[pk] = pk; - } - - const results = (await this._get(url, params)) as Array<{ - count: number; - source: string; - target: string; - quality: number; - }>; - - if (!Array.isArray(results)) { - return []; - } - - return results.map((item) => ({ - sources: ['translation-memory'], - itemCount: item.count, - original: item.source, - translation: item.target, - quality: Math.round(item.quality), - })); + if (!Array.isArray(results)) { + return []; } - /** - * Return translation by Google Translate. - */ - async getGoogleTranslation( - source: string, - locale: Locale, - ): Promise { - const url = '/google-translate/'; - const params = { - text: source, - locale: locale.googleTranslateCode, - }; + return results.map((item) => ({ + sources: ['translation-memory'], + itemCount: item.count, + original: item.source, + translation: item.target, + quality: Math.round(item.quality), + })); + } - const { translation } = (await this._get(url, params)) as { - translation: string; - }; + /** + * Return translation by Google Translate. + */ + async getGoogleTranslation( + source: string, + locale: Locale, + ): Promise { + const url = '/google-translate/'; + const params = { + text: source, + locale: locale.googleTranslateCode, + }; - if (!translation) { - return []; - } + const { translation } = (await this._get(url, params)) as { + translation: string; + }; - return [ - { sources: ['google-translate'], original: source, translation }, - ]; + if (!translation) { + return []; } - /** - * Return translation by Microsoft Translator. - */ - async getMicrosoftTranslation( - source: string, - locale: Locale, - ): Promise { - const url = '/microsoft-translator/'; - const params = { - text: source, - locale: locale.msTranslatorCode, - }; + return [{ sources: ['google-translate'], original: source, translation }]; + } - const { translation } = (await this._get(url, params)) as { - translation: string; - }; + /** + * Return translation by Microsoft Translator. + */ + async getMicrosoftTranslation( + source: string, + locale: Locale, + ): Promise { + const url = '/microsoft-translator/'; + const params = { + text: source, + locale: locale.msTranslatorCode, + }; - if (!translation) { - return []; - } + const { translation } = (await this._get(url, params)) as { + translation: string; + }; - return [ - { - sources: ['microsoft-translator'], - original: source, - translation, - }, - ]; + if (!translation) { + return []; } - /** - * Return translations by SYSTRAN. - */ - async getSystranTranslation( - source: string, - locale: Locale, - ): Promise { - const url = '/systran-translate/'; - const params = { - text: source, - locale: locale.systranTranslateCode, - }; + return [ + { + sources: ['microsoft-translator'], + original: source, + translation, + }, + ]; + } - const { translation } = (await this._get(url, params)) as { - translation: string; - }; + /** + * Return translations by SYSTRAN. + */ + async getSystranTranslation( + source: string, + locale: Locale, + ): Promise { + const url = '/systran-translate/'; + const params = { + text: source, + locale: locale.systranTranslateCode, + }; - if (!translation) { - return []; - } + const { translation } = (await this._get(url, params)) as { + translation: string; + }; - return [ - { sources: ['systran-translate'], original: source, translation }, - ]; + if (!translation) { + return []; } - /** - * Return translations from Microsoft Terminology. - */ - async getMicrosoftTerminology( - source: string, - locale: Locale, - ): Promise { - const url = '/microsoft-terminology/'; - const params = { - text: source, - locale: locale.msTerminologyCode, - }; + return [{ sources: ['systran-translate'], original: source, translation }]; + } - const { translations } = (await this._get(url, params)) as { - translations: Array<{ source: string; target: string }>; - }; + /** + * Return translations from Microsoft Terminology. + */ + async getMicrosoftTerminology( + source: string, + locale: Locale, + ): Promise { + const url = '/microsoft-terminology/'; + const params = { + text: source, + locale: locale.msTerminologyCode, + }; - if (!translations) { - return []; - } + const { translations } = (await this._get(url, params)) as { + translations: Array<{ source: string; target: string }>; + }; - return translations.map((item) => ({ - sources: ['microsoft-terminology'], - original: item.source, - translation: item.target, - })); + if (!translations) { + return []; } - /** - * Return translation by Caighdean Machine Translation. - * - * Works only for the `ga-IE` locale. - */ - async getCaighdeanTranslation(pk: number): Promise { - const url = '/caighdean/'; - const params = { - id: pk, - }; + return translations.map((item) => ({ + sources: ['microsoft-terminology'], + original: item.source, + translation: item.target, + })); + } - const { original, translation } = (await this._get(url, params)) as { - original: string; - translation: string; - }; + /** + * Return translation by Caighdean Machine Translation. + * + * Works only for the `ga-IE` locale. + */ + async getCaighdeanTranslation(pk: number): Promise { + const url = '/caighdean/'; + const params = { + id: pk, + }; - if (!translation) { - return []; - } + const { original, translation } = (await this._get(url, params)) as { + original: string; + translation: string; + }; - return [{ sources: ['caighdean'], original, translation }]; + if (!translation) { + return []; } + + return [{ sources: ['caighdean'], original, translation }]; + } } diff --git a/translate/src/core/api/project.ts b/translate/src/core/api/project.ts index b7b02d8ed..0f61b0532 100644 --- a/translate/src/core/api/project.ts +++ b/translate/src/core/api/project.ts @@ -1,8 +1,8 @@ import APIBase from './base'; export default class ProjectAPI extends APIBase { - async get(slug: string): Promise { - const query = `{ + async get(slug: string): Promise { + const query = `{ project(slug: "${slug}") { slug name @@ -15,12 +15,12 @@ export default class ProjectAPI extends APIBase { } }`; - const payload = new URLSearchParams(); - payload.append('query', query); + const payload = new URLSearchParams(); + payload.append('query', query); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch('/graphql', 'GET', payload, headers); - } + return await this.fetch('/graphql', 'GET', payload, headers); + } } diff --git a/translate/src/core/api/resource.ts b/translate/src/core/api/resource.ts index ff9c0e40c..d8933f5cb 100644 --- a/translate/src/core/api/resource.ts +++ b/translate/src/core/api/resource.ts @@ -1,12 +1,12 @@ import APIBase from './base'; export default class ResourceAPI extends APIBase { - async getAll(locale: string, project: string): Promise { - const url = `/${locale}/${project}/parts/`; + async getAll(locale: string, project: string): Promise { + const url = `/${locale}/${project}/parts/`; - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch(url, 'GET', null, headers); - } + return await this.fetch(url, 'GET', null, headers); + } } diff --git a/translate/src/core/api/translation.ts b/translate/src/core/api/translation.ts index 32ff53d6e..b6ea4f4dc 100644 --- a/translate/src/core/api/translation.ts +++ b/translate/src/core/api/translation.ts @@ -2,125 +2,115 @@ import APIBase from './base'; import type { SourceType } from './types'; export default class TranslationAPI extends APIBase { - /** - * Create a new translation. - * - * If a similar translation already exists, update it with the new data. - * Otherwise, create it. - */ - create( - entity: number, - translation: string, - locale: string, - pluralForm: number, - original: string, - forceSuggestions: boolean, - resource: string, - ignoreWarnings: boolean | null | undefined, - machinerySources: Array, - ): Promise { - const csrfToken = this.getCSRFToken(); + /** + * Create a new translation. + * + * If a similar translation already exists, update it with the new data. + * Otherwise, create it. + */ + create( + entity: number, + translation: string, + locale: string, + pluralForm: number, + original: string, + forceSuggestions: boolean, + resource: string, + ignoreWarnings: boolean | null | undefined, + machinerySources: Array, + ): Promise { + const csrfToken = this.getCSRFToken(); - const payload = new URLSearchParams(); - payload.append('entity', entity.toString()); - payload.append('translation', translation); - payload.append('locale', locale); - payload.append('plural_form', pluralForm.toString()); - payload.append('original', original); - payload.append('force_suggestions', forceSuggestions.toString()); - payload.append('machinery_sources', machinerySources.toString()); + const payload = new URLSearchParams(); + payload.append('entity', entity.toString()); + payload.append('translation', translation); + payload.append('locale', locale); + payload.append('plural_form', pluralForm.toString()); + payload.append('original', original); + payload.append('force_suggestions', forceSuggestions.toString()); + payload.append('machinery_sources', machinerySources.toString()); - if (resource !== 'all-resources') { - payload.append('paths[]', resource); - } - - if (ignoreWarnings) { - payload.append('ignore_warnings', ignoreWarnings.toString()); - } - - payload.append('csrfmiddlewaretoken', csrfToken); - - const headers = new Headers(); - headers.append( - 'Content-Type', - 'application/x-www-form-urlencoded; charset=UTF-8', - ); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); - - return this.fetch('/translations/create/', 'POST', payload, headers); + if (resource !== 'all-resources') { + payload.append('paths[]', resource); } - _changeStatus( - url: string, - id: number, - resource: string, - ignoreWarnings: boolean | null | undefined, - ): Promise { - const csrfToken = this.getCSRFToken(); - - const payload = new URLSearchParams(); - payload.append('translation', id.toString()); - - if (resource !== 'all-resources') { - payload.append('paths[]', resource); - } - - if (ignoreWarnings) { - payload.append('ignore_warnings', ignoreWarnings.toString()); - } - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); - - return this.fetch(url, 'POST', payload, headers); + if (ignoreWarnings) { + payload.append('ignore_warnings', ignoreWarnings.toString()); } - approve( - id: number, - resource: string, - ignoreWarnings: boolean | null | undefined, - ): Promise { - return this._changeStatus( - '/translations/approve/', - id, - resource, - ignoreWarnings, - ); + payload.append('csrfmiddlewaretoken', csrfToken); + + const headers = new Headers(); + headers.append( + 'Content-Type', + 'application/x-www-form-urlencoded; charset=UTF-8', + ); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); + + return this.fetch('/translations/create/', 'POST', payload, headers); + } + + _changeStatus( + url: string, + id: number, + resource: string, + ignoreWarnings: boolean | null | undefined, + ): Promise { + const csrfToken = this.getCSRFToken(); + + const payload = new URLSearchParams(); + payload.append('translation', id.toString()); + + if (resource !== 'all-resources') { + payload.append('paths[]', resource); } - unapprove(id: number, resource: string): Promise { - return this._changeStatus( - '/translations/unapprove/', - id, - resource, - false, - ); + if (ignoreWarnings) { + payload.append('ignore_warnings', ignoreWarnings.toString()); } - reject(id: number, resource: string): Promise { - return this._changeStatus('/translations/reject/', id, resource, false); - } + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); - unreject(id: number, resource: string): Promise { - return this._changeStatus( - '/translations/unreject/', - id, - resource, - false, - ); - } + return this.fetch(url, 'POST', payload, headers); + } - delete(id: number): Promise { - const payload = new URLSearchParams(); - payload.append('translation', id.toString()); + approve( + id: number, + resource: string, + ignoreWarnings: boolean | null | undefined, + ): Promise { + return this._changeStatus( + '/translations/approve/', + id, + resource, + ignoreWarnings, + ); + } - const headers = new Headers(); - const csrfToken = this.getCSRFToken(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); + unapprove(id: number, resource: string): Promise { + return this._changeStatus('/translations/unapprove/', id, resource, false); + } - return this.fetch('/translations/delete/', 'POST', payload, headers); - } + reject(id: number, resource: string): Promise { + return this._changeStatus('/translations/reject/', id, resource, false); + } + + unreject(id: number, resource: string): Promise { + return this._changeStatus('/translations/unreject/', id, resource, false); + } + + delete(id: number): Promise { + const payload = new URLSearchParams(); + payload.append('translation', id.toString()); + + const headers = new Headers(); + const csrfToken = this.getCSRFToken(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); + + return this.fetch('/translations/delete/', 'POST', payload, headers); + } } diff --git a/translate/src/core/api/types.ts b/translate/src/core/api/types.ts index 86fb4e9dc..e3c857ab8 100644 --- a/translate/src/core/api/types.ts +++ b/translate/src/core/api/types.ts @@ -2,27 +2,27 @@ * Accepted Translation of an Entity, cannot exist outside of the Entity type. */ export type EntityTranslation = { - readonly pk: number; - readonly string: string | null | undefined; - readonly approved: boolean; - readonly fuzzy: boolean; - readonly rejected: boolean; - readonly errors: Array; - readonly warnings: Array; + readonly pk: number; + readonly string: string | null | undefined; + readonly approved: boolean; + readonly fuzzy: boolean; + readonly rejected: boolean; + readonly errors: Array; + readonly warnings: Array; }; /** * Comments pertaining to a translation. */ export type TranslationComment = { - readonly author: string; - readonly username: string; - readonly userGravatarUrlSmall: string; - readonly createdAt: string; - readonly dateIso: string; - readonly content: string; - readonly pinned: boolean; - readonly id: number; + readonly author: string; + readonly username: string; + readonly userGravatarUrlSmall: string; + readonly createdAt: string; + readonly dateIso: string; + readonly content: string; + readonly pinned: boolean; + readonly id: number; }; /** @@ -34,21 +34,21 @@ export type TeamComment = TranslationComment; * All users for use in mentions suggestions within comments */ export type UsersList = { - gravatar: string; - name: string; - url: string; + gravatar: string; + name: string; + url: string; }; /** * Term entry with translation. */ export type TermType = { - readonly text: string; - readonly partOfSpeech: string; - readonly definition: string; - readonly usage: string; - readonly translation: string; - readonly entityId: number; + readonly text: string; + readonly partOfSpeech: string; + readonly definition: string; + readonly usage: string; + readonly translation: string; + readonly entityId: number; }; /** @@ -56,30 +56,30 @@ export type TermType = { * and its currently accepted translations. */ export type Entity = { - readonly pk: number; - readonly original: string; - readonly original_plural: string; - readonly machinery_original: string; - readonly comment: string; - readonly group_comment: string; - readonly resource_comment: string; - readonly key: string; - readonly context: string; - readonly format: string; - readonly path: string; - readonly project: Record; - readonly source: Array> | Record; - readonly translation: Array; - readonly readonly: boolean; - readonly isSibling: boolean; + readonly pk: number; + readonly original: string; + readonly original_plural: string; + readonly machinery_original: string; + readonly comment: string; + readonly group_comment: string; + readonly resource_comment: string; + readonly key: string; + readonly context: string; + readonly format: string; + readonly path: string; + readonly project: Record; + readonly source: Array> | Record; + readonly translation: Array; + readonly readonly: boolean; + readonly isSibling: boolean; }; /** * Lists of preceding and succeeding entities */ export type EntitySiblings = { - readonly preceding: Array; - readonly succeeding: Array; + readonly preceding: Array; + readonly succeeding: Array; }; /** @@ -97,15 +97,15 @@ export type OtherLocaleTranslations = Array; * Translation of an entity in a locale other than the currently selected locale. */ export type OtherLocaleTranslation = { - readonly locale: { - readonly code: string; - readonly name: string; - readonly pk: number; - readonly direction: string; - readonly script: string; - }; - readonly translation: string; - readonly is_preferred: boolean | null | undefined; + readonly locale: { + readonly code: string; + readonly name: string; + readonly pk: number; + readonly direction: string; + readonly script: string; + }; + readonly translation: string; + readonly is_preferred: boolean | null | undefined; }; /* @@ -113,18 +113,18 @@ export type OtherLocaleTranslation = { * Translation Memory... ). */ export type SourceType = - | 'concordance-search' - | 'translation-memory' - | 'google-translate' - | 'microsoft-translator' - | 'systran-translate' - | 'microsoft-terminology' - | 'caighdean'; + | 'concordance-search' + | 'translation-memory' + | 'google-translate' + | 'microsoft-translator' + | 'systran-translate' + | 'microsoft-terminology' + | 'caighdean'; export type MachineryTranslation = { - sources: Array; - itemCount?: number; - original: string; - translation: string; - quality?: number; - projectNames?: Array; + sources: Array; + itemCount?: number; + original: string; + translation: string; + quality?: number; + projectNames?: Array; }; diff --git a/translate/src/core/api/user.ts b/translate/src/core/api/user.ts index bb552d27a..58b897a50 100644 --- a/translate/src/core/api/user.ts +++ b/translate/src/core/api/user.ts @@ -1,113 +1,108 @@ import APIBase from './base'; const SETTINGS_NAMES_MAP = { - runQualityChecks: 'quality_checks', - forceSuggestions: 'force_suggestions', + runQualityChecks: 'quality_checks', + forceSuggestions: 'force_suggestions', }; export default class UserAPI extends APIBase { - /** - * Return data about the current user from the server. - */ - async get(): Promise { - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + /** + * Return data about the current user from the server. + */ + async get(): Promise { + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch('/user-data/', 'GET', null, headers); - } + return await this.fetch('/user-data/', 'GET', null, headers); + } - /** - * Get all users from server. - */ - async getUsers(): Promise { - const headers = new Headers(); - headers.append('X-Requested-With', `XMLHttpRequest`); + /** + * Get all users from server. + */ + async getUsers(): Promise { + const headers = new Headers(); + headers.append('X-Requested-With', `XMLHttpRequest`); - return await this.fetch('get-users', 'GET', null, headers); - } + return await this.fetch('get-users', 'GET', null, headers); + } - /** - * Mark all notifications of the current user as read. - */ - async markAllNotificationsAsRead(): Promise { - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + /** + * Mark all notifications of the current user as read. + */ + async markAllNotificationsAsRead(): Promise { + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return await this.fetch( - '/notifications/mark-all-as-read/', - 'GET', - null, - headers, - ); - } + return await this.fetch( + '/notifications/mark-all-as-read/', + 'GET', + null, + headers, + ); + } - /** - * Sign out the current user. - */ - async signOut(url: string): Promise { - const csrfToken = this.getCSRFToken(); + /** + * Sign out the current user. + */ + async signOut(url: string): Promise { + const csrfToken = this.getCSRFToken(); - const payload = new URLSearchParams(); - payload.append('csrfmiddlewaretoken', csrfToken); + const payload = new URLSearchParams(); + payload.append('csrfmiddlewaretoken', csrfToken); - const headers = new Headers(); + const headers = new Headers(); - return await this.fetch(url, 'POST', payload, headers); - } + return await this.fetch(url, 'POST', payload, headers); + } - async updateSetting( - username: string, - setting: string, - value: boolean, - ): Promise { - const csrfToken = this.getCSRFToken(); + async updateSetting( + username: string, + setting: string, + value: boolean, + ): Promise { + const csrfToken = this.getCSRFToken(); - const payload = new URLSearchParams(); - payload.append('attribute', SETTINGS_NAMES_MAP[setting]); - payload.append('value', value.toString()); - payload.append('csrfmiddlewaretoken', csrfToken); + const payload = new URLSearchParams(); + payload.append('attribute', SETTINGS_NAMES_MAP[setting]); + payload.append('value', value.toString()); + payload.append('csrfmiddlewaretoken', csrfToken); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); - return (await this.fetch( - `/api/v1/user/${username}/`, - 'POST', - payload, - headers, - )) as string; - } + return (await this.fetch( + `/api/v1/user/${username}/`, + 'POST', + payload, + headers, + )) as string; + } - /** - * Update Interactive Tour status to a given step. - */ - async updateTourStatus(step: number): Promise { - const csrfToken = this.getCSRFToken(); + /** + * Update Interactive Tour status to a given step. + */ + async updateTourStatus(step: number): Promise { + const csrfToken = this.getCSRFToken(); - const payload = new URLSearchParams(); - payload.append('tour_status', step.toString()); - payload.append('csrfmiddlewaretoken', csrfToken); + const payload = new URLSearchParams(); + payload.append('tour_status', step.toString()); + payload.append('csrfmiddlewaretoken', csrfToken); - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); - return await this.fetch( - '/update-tour-status/', - 'POST', - payload, - headers, - ); - } + return await this.fetch('/update-tour-status/', 'POST', payload, headers); + } - /** - * Dismiss Add-On Promotion. - */ - dismissAddonPromotion(): Promise { - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); + /** + * Dismiss Add-On Promotion. + */ + dismissAddonPromotion(): Promise { + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); - return this.fetch('/dismiss-addon-promotion/', 'GET', null, headers); - } + return this.fetch('/dismiss-addon-promotion/', 'GET', null, headers); + } } diff --git a/translate/src/core/api/uxaction.ts b/translate/src/core/api/uxaction.ts index 5a056decc..9986232a9 100644 --- a/translate/src/core/api/uxaction.ts +++ b/translate/src/core/api/uxaction.ts @@ -1,36 +1,36 @@ import APIBase from './base'; export default class UXActionAPI extends APIBase { - /** - * Log UX action. - */ - async log( - action_type: string, - experiment: string | null | undefined, - data: any | null | undefined, - ): Promise { - const csrfToken = this.getCSRFToken(); + /** + * Log UX action. + */ + async log( + action_type: string, + experiment: string | null | undefined, + data: any | null | undefined, + ): Promise { + const csrfToken = this.getCSRFToken(); - const payload = new URLSearchParams(); - payload.append('csrfmiddlewaretoken', csrfToken); - payload.append('action_type', action_type); + const payload = new URLSearchParams(); + payload.append('csrfmiddlewaretoken', csrfToken); + payload.append('action_type', action_type); - if (experiment) { - payload.append('experiment', experiment); - } - - if (data) { - payload.append('data', JSON.stringify(data)); - } - - const headers = new Headers(); - headers.append('X-Requested-With', 'XMLHttpRequest'); - headers.append('X-CSRFToken', csrfToken); - - try { - await this.fetch('/log-ux-action/', 'POST', payload, headers); - } catch (_) { - /* Ignore errors during UX action logging */ - } + if (experiment) { + payload.append('experiment', experiment); } + + if (data) { + payload.append('data', JSON.stringify(data)); + } + + const headers = new Headers(); + headers.append('X-Requested-With', 'XMLHttpRequest'); + headers.append('X-CSRFToken', csrfToken); + + try { + await this.fetch('/log-ux-action/', 'POST', payload, headers); + } catch (_) { + /* Ignore errors during UX action logging */ + } + } } diff --git a/translate/src/core/comments/actions.ts b/translate/src/core/comments/actions.ts index 63f0179c3..38c6baaa8 100644 --- a/translate/src/core/comments/actions.ts +++ b/translate/src/core/comments/actions.ts @@ -8,28 +8,28 @@ import * as teamcomments from '~/modules/teamcomments'; import type { AppDispatch } from '~/store'; export function addComment( - entity: number, - locale: string, - pluralForm: number, - translation: number | null | undefined, - comment: string, + entity: number, + locale: string, + pluralForm: number, + translation: number | null | undefined, + comment: string, ) { - return async (dispatch: AppDispatch) => { - NProgress.start(); + return async (dispatch: AppDispatch) => { + NProgress.start(); - await api.comment.add(entity, locale, comment, translation); + await api.comment.add(entity, locale, comment, translation); - dispatch(notification.actions.add(notification.messages.COMMENT_ADDED)); - if (translation) { - dispatch(history.actions.get(entity, locale, pluralForm)); - } else { - dispatch(teamcomments.actions.get(entity, locale)); - } + dispatch(notification.actions.add(notification.messages.COMMENT_ADDED)); + if (translation) { + dispatch(history.actions.get(entity, locale, pluralForm)); + } else { + dispatch(teamcomments.actions.get(entity, locale)); + } - NProgress.done(); - }; + NProgress.done(); + }; } export default { - addComment, + addComment, }; diff --git a/translate/src/core/comments/components/AddComment.css b/translate/src/core/comments/components/AddComment.css index 0f344f27b..aa3bde511 100644 --- a/translate/src/core/comments/components/AddComment.css +++ b/translate/src/core/comments/components/AddComment.css @@ -1,78 +1,78 @@ .comments-list .add-comment div.container { - flex-flow: row nowrap; - align-items: flex-end; - padding-bottom: 6px; + flex-flow: row nowrap; + align-items: flex-end; + padding-bottom: 6px; } .comments-list .add-comment .comment-editor { - background-color: #333941; - border: none; - border-radius: 4px; - color: #ffffff; - display: flex; - flex-grow: 2; - font-size: 11px; - max-height: 144px; - margin: auto; - overflow: auto; - padding: 7px; - word-break: break-word; + background-color: #333941; + border: none; + border-radius: 4px; + color: #ffffff; + display: flex; + flex-grow: 2; + font-size: 11px; + max-height: 144px; + margin: auto; + overflow: auto; + padding: 7px; + word-break: break-word; } .comments-list .add-comment .comment-editor .mention-element { - color: #7bc876; + color: #7bc876; } .comments-mention-list { - top: -9999px; - left: -9999px; - position: absolute; - z-index: 1; - padding: 10px 12px; - background-color: #272a2f; - border: 1px solid #333941; + top: -9999px; + left: -9999px; + position: absolute; + z-index: 1; + padding: 10px 12px; + background-color: #272a2f; + border: 1px solid #333941; } .comments-mention-list .mention { - padding: 4px 4px 0; - cursor: pointer; + padding: 4px 4px 0; + cursor: pointer; } .comments-mention-list .mention:hover, .comments-mention-list .active-mention { - background: #3f4752; + background: #3f4752; } .comments-mention-list .mention .user-avatar { - display: inline-block; - vertical-align: middle; + display: inline-block; + vertical-align: middle; } .comments-mention-list .mention .user-avatar img { - border-radius: 2px; - border: 1px solid #5e6475; - margin-right: 8px; + border-radius: 2px; + border: 1px solid #5e6475; + margin-right: 8px; } .comments-mention-list .mention .name { - font-weight: 300; + font-weight: 300; } .comments-list .add-comment .submit-button { - background-color: #4d5967; - border: solid 1px #4d5967; - border-radius: 4px; - color: #ccc; - display: flex; - align-items: center; - flex-shrink: 0; - height: 36px; - width: 36px; - margin-left: 6px; - padding-left: 10px; + background-color: #4d5967; + border: solid 1px #4d5967; + border-radius: 4px; + color: #ccc; + display: flex; + align-items: center; + flex-shrink: 0; + height: 36px; + width: 36px; + margin-left: 6px; + padding-left: 10px; } .comments-list .add-comment .submit-button:hover { - color: #272a2f; - background: #7bc876; + color: #272a2f; + background: #7bc876; } diff --git a/translate/src/core/comments/components/AddComment.test.js b/translate/src/core/comments/components/AddComment.test.js index bf5ae8992..4cba6fb8a 100644 --- a/translate/src/core/comments/components/AddComment.test.js +++ b/translate/src/core/comments/components/AddComment.test.js @@ -5,32 +5,32 @@ import sinon from 'sinon'; import AddComment from './AddComment'; const USER = { - user: { - user: 'RSwanson', - username: 'Ron_Swanson', - imageURL: '', - users: [ - { - name: 'April Ludwig', - url: 'aprilL@parksdept.com', - display: 'April', - }, - ], - }, + user: { + user: 'RSwanson', + username: 'Ron_Swanson', + imageURL: '', + users: [ + { + name: 'April Ludwig', + url: 'aprilL@parksdept.com', + display: 'April', + }, + ], + }, }; describe('', () => { - it('calls submitComment function', () => { - const submitCommentFn = sinon.spy(); - const wrapper = shallow( - , - ); + it('calls submitComment function', () => { + const submitCommentFn = sinon.spy(); + const wrapper = shallow( + , + ); - const event = { - preventDefault: sinon.spy(), - }; + const event = { + preventDefault: sinon.spy(), + }; - wrapper.find('button').simulate('onClick', event); - expect(submitCommentFn.calledOnce).toBeTruthy; - }); + wrapper.find('button').simulate('onClick', event); + expect(submitCommentFn.calledOnce).toBeTruthy; + }); }); diff --git a/translate/src/core/comments/components/AddComment.tsx b/translate/src/core/comments/components/AddComment.tsx index 82a0bab24..096456e36 100644 --- a/translate/src/core/comments/components/AddComment.tsx +++ b/translate/src/core/comments/components/AddComment.tsx @@ -2,14 +2,14 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { Localized } from '@fluent/react'; import { - BaseEditor, - Editor, - Element as SlateElement, - Transforms, - Range, - createEditor, - Text, - Node, + BaseEditor, + Editor, + Element as SlateElement, + Transforms, + Range, + createEditor, + Text, + Node, } from 'slate'; import type { Descendant } from 'slate'; import { Slate, Editable, ReactEditor, withReact } from 'slate-react'; @@ -23,565 +23,535 @@ import type { NavigationParams } from '~/core/navigation'; import type { UserState } from '~/core/user'; type Props = { - parameters: NavigationParams | null | undefined; - translation?: number | null | undefined; - user: UserState; - contactPerson?: string; - addComment: (arg0: string, arg1: number | null | undefined) => void; - resetContactPerson?: () => void; + parameters: NavigationParams | null | undefined; + translation?: number | null | undefined; + user: UserState; + contactPerson?: string; + addComment: (arg0: string, arg1: number | null | undefined) => void; + resetContactPerson?: () => void; }; type Paragraph = { - type: 'paragraph'; - children: Descendant[]; + type: 'paragraph'; + children: Descendant[]; }; type Mention = { - type: 'mention'; - character: string; - url: string; - children: Text[]; + type: 'mention'; + character: string; + url: string; + children: Text[]; }; declare module 'slate' { - interface CustomTypes { - Editor: BaseEditor & ReactEditor; - Element: Paragraph | Mention; - } + interface CustomTypes { + Editor: BaseEditor & ReactEditor; + Element: Paragraph | Mention; + } } export default function AddComment(props: Props): React.ReactElement<'div'> { - const { - parameters, - translation, - user, - contactPerson, - addComment, - resetContactPerson, - } = props; + const { + parameters, + translation, + user, + contactPerson, + addComment, + resetContactPerson, + } = props; - const mentionList: any = React.useRef(); - const [target, setTarget] = React.useState(); - const [index, setIndex] = React.useState(0); - const [search, setSearch] = React.useState(''); - const [scrollPosition, setScrollPosition] = React.useState(0); - const editor = React.useMemo( - () => withMentions(withReact(createEditor())), - [], + const mentionList: any = React.useRef(); + const [target, setTarget] = React.useState(); + const [index, setIndex] = React.useState(0); + const [search, setSearch] = React.useState(''); + const [scrollPosition, setScrollPosition] = React.useState(0); + const editor = React.useMemo( + () => withMentions(withReact(createEditor())), + [], + ); + const initialValue = [ + { type: 'paragraph', children: [{ text: '' }] } as Paragraph, + ]; + const [value, setValue] = React.useState(initialValue); + const users = user.users; + const placeFocus = React.useCallback(() => { + ReactEditor.focus(editor); + Transforms.select(editor, Editor.end(editor, [])); + }, [editor]); + + // Insert project manager as mention when 'Request context / Report issue' button used + // and then clear the value from state + React.useEffect(() => { + // check to see if contact person is already mentioned + const [isMentioned] = Editor.nodes(editor, { + at: [], + match: (n) => + SlateElement.isElement(n) && + n.type == 'mention' && + n.character === contactPerson, + }); + + if (contactPerson) { + if (!isMentioned) { + insertMention(editor, contactPerson, users); + } + + if (resetContactPerson) { + resetContactPerson(); + placeFocus(); + } + } + }, [editor, contactPerson, users, resetContactPerson, placeFocus]); + + // Set focus on Editor + React.useEffect(() => { + if (!parameters || parameters.project !== 'terminology') { + placeFocus(); + } + }, [parameters, placeFocus]); + + const userNames = users.map((user) => user.name); + const suggestedUsers = userNames + .filter((c) => c.toLowerCase().includes(search.toLowerCase())) + .slice(0, 5); + + // Set position of mentions suggestions + React.useLayoutEffect(() => { + if (!target || suggestedUsers.length <= 0) { + return; + } + // get suggestions element and gain access to its measurements + const el = mentionList.current; + const domRange = ReactEditor.toDOMRange(editor, target); + const rect = domRange.getBoundingClientRect(); + + // get team comments element, gain access to its measurements, and verify + // if it is active + const teamCommentsEl = document.querySelector('.top'); + const teamCommentsRect = !teamCommentsEl + ? null + : teamCommentsEl.getBoundingClientRect(); + const teamCommentsActive = !teamCommentsEl + ? false + : teamCommentsEl.contains(document.activeElement); + + // get translation comments element, gain access to its measurements, and verify + // if it is active + const translateCommentsEl = document.querySelector('.history'); + const translateCommentsRect = !translateCommentsEl + ? null + : translateCommentsEl.getBoundingClientRect(); + const translateCommentsActive = !translateCommentsEl + ? false + : translateCommentsEl.contains(document.activeElement); + + // get editor menu element and find its height to determine when comment editor goes above + // the editor menu in order to hide suggestions element + const editorMenu = document.querySelector('.editor-menu'); + const editorMenuHeight = !editorMenu ? 0 : editorMenu.clientHeight; + + // get tab index element and find its height to use when determining if suggestions + // element overflows the team comments container + const tabIndex = document.querySelector('.react-tabs__tab-list'); + const tabIndexHeight = !tabIndex ? 0 : tabIndex.clientHeight; + + // get comment editor element and find measurements of values needed to adjust + // the suggestions element to the correct position + const commentEditor = document.querySelector( + '.comments-list .add-comment .comment-editor', ); - const initialValue = [ - { type: 'paragraph', children: [{ text: '' }] } as Paragraph, - ]; - const [value, setValue] = React.useState(initialValue); - const users = user.users; - const placeFocus = React.useCallback(() => { - ReactEditor.focus(editor); - Transforms.select(editor, Editor.end(editor, [])); - }, [editor]); + const commentEditorLineHeight = + parseInt(window.getComputedStyle(commentEditor).lineHeight) || 0; + const commentEditorTopPadding = + parseInt(window.getComputedStyle(commentEditor).paddingTop) || 0; + const commentEditorBottomPadding = + parseInt(window.getComputedStyle(commentEditor).paddingBottom) || 0; + const commentEditorSpan = document.querySelector( + '.comments-list .add-comment .comment-editor p span', + ); + const commentEditorSpanHeight = !(commentEditorSpan instanceof HTMLElement) + ? 0 + : commentEditorSpan.offsetHeight; - // Insert project manager as mention when 'Request context / Report issue' button used - // and then clear the value from state - React.useEffect(() => { - // check to see if contact person is already mentioned - const [isMentioned] = Editor.nodes(editor, { - at: [], - match: (n) => - SlateElement.isElement(n) && - n.type == 'mention' && - n.character === contactPerson, - }); + // add value of comment editor bottom padding and span height to properly position suggestions element + const setTopAdjustment = + commentEditorBottomPadding + commentEditorSpanHeight; - if (contactPerson) { - if (!isMentioned) { - insertMention(editor, contactPerson, users); - } + // add value of comment editor top padding and difference between line height and span height + // of the top half of the comment editor to correctly size the height of the suggestions + const suggestionsHeightAdjustment = + commentEditorTopPadding + + (commentEditorLineHeight - commentEditorSpanHeight) / 2; - if (resetContactPerson) { - resetContactPerson(); - placeFocus(); - } - } - }, [editor, contactPerson, users, resetContactPerson, placeFocus]); + let setTop = rect.top + window.pageYOffset + setTopAdjustment; + let setLeft = rect.left + window.pageXOffset; - // Set focus on Editor - React.useEffect(() => { - if (!parameters || parameters.project !== 'terminology') { - placeFocus(); - } - }, [parameters, placeFocus]); + // If suggestions overflow the window or teams container height then adjust the + // position so they display above the comment + const suggestionsHeight = el.clientHeight + suggestionsHeightAdjustment; + const teamCommentsOverflow = !teamCommentsRect + ? false + : setTop + el.clientHeight - tabIndexHeight > teamCommentsRect.height; - const userNames = users.map((user) => user.name); - const suggestedUsers = userNames - .filter((c) => c.toLowerCase().includes(search.toLowerCase())) - .slice(0, 5); + if ( + (teamCommentsActive && teamCommentsOverflow) || + setTop + suggestionsHeight > window.innerHeight + ) { + setTop = rect.top + window.pageYOffset - suggestionsHeight; + } - // Set position of mentions suggestions - React.useLayoutEffect(() => { - if (!target || suggestedUsers.length <= 0) { - return; - } - // get suggestions element and gain access to its measurements - const el = mentionList.current; - const domRange = ReactEditor.toDOMRange(editor, target); - const rect = domRange.getBoundingClientRect(); + // If suggestions in team comments scroll below or suggestions in translation + // comments scroll above the next section or overflow the window then hide the suggestions + if ( + (teamCommentsRect && + teamCommentsActive && + setTop + suggestionsHeight - editorMenuHeight > + teamCommentsRect.height) || + (translateCommentsRect && + translateCommentsActive && + rect.top < translateCommentsRect.top) || + (translateCommentsRect && + translateCommentsActive && + setTop + suggestionsHeight > window.innerHeight) + ) { + el.style.display = 'none'; + } - // get team comments element, gain access to its measurements, and verify - // if it is active - const teamCommentsEl = document.querySelector('.top'); - const teamCommentsRect = !teamCommentsEl - ? null - : teamCommentsEl.getBoundingClientRect(); - const teamCommentsActive = !teamCommentsEl - ? false - : teamCommentsEl.contains(document.activeElement); + // If suggestions overflow the window width in team comments or the right side of the + // translations comments then adjust the position so they display to the left of the mention + const suggestionsWidth = el.clientWidth; + const translateCommentsOverflow = !translateCommentsRect + ? false + : setLeft + suggestionsWidth > translateCommentsRect.right; - // get translation comments element, gain access to its measurements, and verify - // if it is active - const translateCommentsEl = document.querySelector('.history'); - const translateCommentsRect = !translateCommentsEl - ? null - : translateCommentsEl.getBoundingClientRect(); - const translateCommentsActive = !translateCommentsEl - ? false - : translateCommentsEl.contains(document.activeElement); + if ( + setLeft + suggestionsWidth > window.innerWidth || + (translateCommentsActive && translateCommentsOverflow) + ) { + setLeft = rect.right - suggestionsWidth; + } - // get editor menu element and find its height to determine when comment editor goes above - // the editor menu in order to hide suggestions element - const editorMenu = document.querySelector('.editor-menu'); - const editorMenuHeight = !editorMenu ? 0 : editorMenu.clientHeight; + el.style.top = `${setTop}px`; + el.style.left = `${setLeft}px`; + }, [suggestedUsers.length, editor, index, search, target, scrollPosition]); - // get tab index element and find its height to use when determining if suggestions - // element overflows the team comments container - const tabIndex = document.querySelector('.react-tabs__tab-list'); - const tabIndexHeight = !tabIndex ? 0 : tabIndex.clientHeight; - - // get comment editor element and find measurements of values needed to adjust - // the suggestions element to the correct position - const commentEditor = document.querySelector( - '.comments-list .add-comment .comment-editor', - ); - const commentEditorLineHeight = - parseInt(window.getComputedStyle(commentEditor).lineHeight) || 0; - const commentEditorTopPadding = - parseInt(window.getComputedStyle(commentEditor).paddingTop) || 0; - const commentEditorBottomPadding = - parseInt(window.getComputedStyle(commentEditor).paddingBottom) || 0; - const commentEditorSpan = document.querySelector( - '.comments-list .add-comment .comment-editor p span', - ); - const commentEditorSpanHeight = !( - commentEditorSpan instanceof HTMLElement - ) - ? 0 - : commentEditorSpan.offsetHeight; - - // add value of comment editor bottom padding and span height to properly position suggestions element - const setTopAdjustment = - commentEditorBottomPadding + commentEditorSpanHeight; - - // add value of comment editor top padding and difference between line height and span height - // of the top half of the comment editor to correctly size the height of the suggestions - const suggestionsHeightAdjustment = - commentEditorTopPadding + - (commentEditorLineHeight - commentEditorSpanHeight) / 2; - - let setTop = rect.top + window.pageYOffset + setTopAdjustment; - let setLeft = rect.left + window.pageXOffset; - - // If suggestions overflow the window or teams container height then adjust the - // position so they display above the comment - const suggestionsHeight = el.clientHeight + suggestionsHeightAdjustment; - const teamCommentsOverflow = !teamCommentsRect - ? false - : setTop + el.clientHeight - tabIndexHeight > - teamCommentsRect.height; - - if ( - (teamCommentsActive && teamCommentsOverflow) || - setTop + suggestionsHeight > window.innerHeight - ) { - setTop = rect.top + window.pageYOffset - suggestionsHeight; - } - - // If suggestions in team comments scroll below or suggestions in translation - // comments scroll above the next section or overflow the window then hide the suggestions - if ( - (teamCommentsRect && - teamCommentsActive && - setTop + suggestionsHeight - editorMenuHeight > - teamCommentsRect.height) || - (translateCommentsRect && - translateCommentsActive && - rect.top < translateCommentsRect.top) || - (translateCommentsRect && - translateCommentsActive && - setTop + suggestionsHeight > window.innerHeight) - ) { - el.style.display = 'none'; - } - - // If suggestions overflow the window width in team comments or the right side of the - // translations comments then adjust the position so they display to the left of the mention - const suggestionsWidth = el.clientWidth; - const translateCommentsOverflow = !translateCommentsRect - ? false - : setLeft + suggestionsWidth > translateCommentsRect.right; - - if ( - setLeft + suggestionsWidth > window.innerWidth || - (translateCommentsActive && translateCommentsOverflow) - ) { - setLeft = rect.right - suggestionsWidth; - } - - el.style.top = `${setTop}px`; - el.style.left = `${setLeft}px`; - }, [suggestedUsers.length, editor, index, search, target, scrollPosition]); - - // Set scroll position values for Translation and Team Comment containers ~ - // This allows for the mention suggestions to stay properly positioned - // when the container scrolls. - React.useEffect(() => { - const handleScroll = (e: Event) => { - const element: HTMLElement = e.currentTarget as any; - setScrollPosition(element.scrollTop); - }; - - const historyScroll = document.querySelector('#history-list'); - const teamsScroll = document.querySelector('#react-tabs-3'); - - if (!historyScroll && !teamsScroll) { - return; - } - - if (historyScroll) { - historyScroll.addEventListener('scroll', handleScroll); - } - if (teamsScroll) { - teamsScroll.addEventListener('scroll', handleScroll); - } - - return () => { - if (historyScroll) { - historyScroll.removeEventListener('scroll', handleScroll); - } - if (teamsScroll) { - teamsScroll.removeEventListener('scroll', handleScroll); - } - }; - }, []); - - const handleMentionsKeyDown = (event: React.KeyboardEvent) => { - if (!target) { - return; - } - switch (event.key) { - case 'ArrowDown': { - event.preventDefault(); - const prevIndex = - index >= suggestedUsers.length - 1 ? 0 : index + 1; - setIndex(prevIndex); - break; - } - case 'ArrowUp': { - event.preventDefault(); - const nextIndex = - index <= 0 ? suggestedUsers.length - 1 : index - 1; - setIndex(nextIndex); - break; - } - case 'Tab': - case 'Enter': - event.preventDefault(); - Transforms.select(editor, target); - insertMention(editor, suggestedUsers[index], users); - setTarget(null); - placeFocus(); - break; - case 'Escape': - event.preventDefault(); - setTarget(null); - break; - default: - return; - } + // Set scroll position values for Translation and Team Comment containers ~ + // This allows for the mention suggestions to stay properly positioned + // when the container scrolls. + React.useEffect(() => { + const handleScroll = (e: Event) => { + const element: HTMLElement = e.currentTarget as any; + setScrollPosition(element.scrollTop); }; - const handleEditorKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && !event.shiftKey) { - event.preventDefault(); - submitComment(); - } - if (event.key === 'Enter' && event.shiftKey) { - event.preventDefault(); - /* - * This allows for the new lines to render while adding comments. - * To avoid an issue with the cursor placement and an error when - * navigating with arrows that occurs in Firefox '\n' can't be - * the last character so the BOM was added - */ - editor.insertText('\n\uFEFF'); - return; - } - }; + const historyScroll = document.querySelector('#history-list'); + const teamsScroll = document.querySelector('#react-tabs-3'); - const handleMentionsMouseDown = ( - event: React.MouseEvent, - ) => { + if (!historyScroll && !teamsScroll) { + return; + } + + if (historyScroll) { + historyScroll.addEventListener('scroll', handleScroll); + } + if (teamsScroll) { + teamsScroll.addEventListener('scroll', handleScroll); + } + + return () => { + if (historyScroll) { + historyScroll.removeEventListener('scroll', handleScroll); + } + if (teamsScroll) { + teamsScroll.removeEventListener('scroll', handleScroll); + } + }; + }, []); + + const handleMentionsKeyDown = (event: React.KeyboardEvent) => { + if (!target) { + return; + } + switch (event.key) { + case 'ArrowDown': { event.preventDefault(); - if (target !== null) { - const suggestedUserIndex = suggestedUsers.indexOf( - event.currentTarget.innerText, - ); - Transforms.select(editor, target); - insertMention(editor, suggestedUsers[suggestedUserIndex], users); - return setTarget(null); - } - }; - - const getUserGravatar = React.useCallback( - (name: string) => { - const user = users.find((user) => user.name === name); - if (!user) { - return; - } - return user.gravatar; - }, - [users], - ); - - const handleEditorOnChange = (value: Node[]) => { - setValue(value); - const { selection } = editor; - - if (selection && Range.isCollapsed(selection)) { - const [start] = Range.edges(selection); - const wordBefore = Editor.before(editor, start, { unit: 'word' }); - const before = wordBefore && Editor.before(editor, wordBefore); - const beforeRange = before && Editor.range(editor, before, start); - const beforeText = - beforeRange && Editor.string(editor, beforeRange); - // Unicode property escapes allow for matching non-ASCII characters - const beforeMatch = - beforeText && beforeText.match(/^@((\p{L}|\p{N}|\p{P})+)$/u); - const after = Editor.after(editor, start); - const afterRange = Editor.range(editor, start, after); - const afterText = Editor.string(editor, afterRange); - const afterMatch = afterText.match(/^(\s|$)/); - - if (beforeMatch && afterMatch) { - setTarget(beforeRange); - setSearch(beforeMatch[1]); - setIndex(0); - return; - } - } - + const prevIndex = index >= suggestedUsers.length - 1 ? 0 : index + 1; + setIndex(prevIndex); + break; + } + case 'ArrowUp': { + event.preventDefault(); + const nextIndex = index <= 0 ? suggestedUsers.length - 1 : index - 1; + setIndex(nextIndex); + break; + } + case 'Tab': + case 'Enter': + event.preventDefault(); + Transforms.select(editor, target); + insertMention(editor, suggestedUsers[index], users); setTarget(null); - }; - - const Portal = ({ children }) => { - if (!document.body) { - return null; - } - return ReactDOM.createPortal(children, document.body); - }; - - const serialize = (node: Descendant): string => { - if (Text.isText(node)) { - return escapeHtml(node.text); - } - - if (!node.type || !node.children) { - return; - } - - const children = node.children.map((n) => serialize(n)).join(''); - - switch (node.type) { - case 'paragraph': - return `

    ${children.trim()}

    `; - case 'mention': - if (node.url) { - return `${children}`; - } - break; - default: - return children; - } - }; - - const submitComment = () => { - if (Node.string(editor).trim() === '') { - return; - } - - const comment = value.map((node) => serialize(node)).join(''); - - addComment(comment, translation); - - Transforms.select(editor, { - anchor: { path: [0, 0], offset: 0 }, - focus: { path: [0, 0], offset: 0 }, - }); - setValue(initialValue); - }; - - const setStyleForHover = (event: React.MouseEvent) => { + placeFocus(); + break; + case 'Escape': event.preventDefault(); - event.currentTarget.children[index].className = 'mention'; - }; + setTarget(null); + break; + default: + return; + } + }; - const removeStyleForHover = (event: React.MouseEvent) => { - event.preventDefault(); - event.currentTarget.children[index].className = - 'mention active-mention'; - }; + const handleEditorKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + submitComment(); + } + if (event.key === 'Enter' && event.shiftKey) { + event.preventDefault(); + /* + * This allows for the new lines to render while adding comments. + * To avoid an issue with the cursor placement and an error when + * navigating with arrows that occurs in Firefox '\n' can't be + * the last character so the BOM was added + */ + editor.insertText('\n\uFEFF'); + return; + } + }; - return ( -
    - ) => { + event.preventDefault(); + if (target !== null) { + const suggestedUserIndex = suggestedUsers.indexOf( + event.currentTarget.innerText, + ); + Transforms.select(editor, target); + insertMention(editor, suggestedUsers[suggestedUserIndex], users); + return setTarget(null); + } + }; + + const getUserGravatar = React.useCallback( + (name: string) => { + const user = users.find((user) => user.name === name); + if (!user) { + return; + } + return user.gravatar; + }, + [users], + ); + + const handleEditorOnChange = (value: Node[]) => { + setValue(value); + const { selection } = editor; + + if (selection && Range.isCollapsed(selection)) { + const [start] = Range.edges(selection); + const wordBefore = Editor.before(editor, start, { unit: 'word' }); + const before = wordBefore && Editor.before(editor, wordBefore); + const beforeRange = before && Editor.range(editor, before, start); + const beforeText = beforeRange && Editor.string(editor, beforeRange); + // Unicode property escapes allow for matching non-ASCII characters + const beforeMatch = + beforeText && beforeText.match(/^@((\p{L}|\p{N}|\p{P})+)$/u); + const after = Editor.after(editor, start); + const afterRange = Editor.range(editor, start, after); + const afterText = Editor.string(editor, afterRange); + const afterMatch = afterText.match(/^(\s|$)/); + + if (beforeMatch && afterMatch) { + setTarget(beforeRange); + setSearch(beforeMatch[1]); + setIndex(0); + return; + } + } + + setTarget(null); + }; + + const Portal = ({ children }) => { + if (!document.body) { + return null; + } + return ReactDOM.createPortal(children, document.body); + }; + + const serialize = (node: Descendant): string => { + if (Text.isText(node)) { + return escapeHtml(node.text); + } + + if (!node.type || !node.children) { + return; + } + + const children = node.children.map((n) => serialize(n)).join(''); + + switch (node.type) { + case 'paragraph': + return `

    ${children.trim()}

    `; + case 'mention': + if (node.url) { + return `${children}`; + } + break; + default: + return children; + } + }; + + const submitComment = () => { + if (Node.string(editor).trim() === '') { + return; + } + + const comment = value.map((node) => serialize(node)).join(''); + + addComment(comment, translation); + + Transforms.select(editor, { + anchor: { path: [0, 0], offset: 0 }, + focus: { path: [0, 0], offset: 0 }, + }); + setValue(initialValue); + }; + + const setStyleForHover = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.children[index].className = 'mention'; + }; + + const removeStyleForHover = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.children[index].className = 'mention active-mention'; + }; + + return ( +
    + +
    + + + -
    - + + {target && suggestedUsers.length > 0 && ( + +
    + {suggestedUsers.map((suggestedUser, i) => ( +
    - + User Avatar + - {target && suggestedUsers.length > 0 && ( - -
    - {suggestedUsers.map((suggestedUser, i) => ( -
    - - - User Avatar - - - - {suggestedUser} - -
    - ))} -
    -
    - )} - - }} - > - - -
    -
    - ); + {suggestedUser} +
    + ))} +
    + + )} + + }} + > + + +
    +
    + ); } const withMentions = (editor: Editor) => { - const { isInline, isVoid } = editor; + const { isInline, isVoid } = editor; - editor.isInline = (element) => { - return element.type === 'mention' ? true : isInline(element); - }; + editor.isInline = (element) => { + return element.type === 'mention' ? true : isInline(element); + }; - editor.isVoid = (element) => { - return element.type === 'mention' ? true : isVoid(element); - }; + editor.isVoid = (element) => { + return element.type === 'mention' ? true : isVoid(element); + }; - return editor; + return editor; }; const renderElement = (props) => { - return ; + return ; }; const Element = (props) => { - const { attributes, children, element } = props; - switch (element.type) { - case 'mention': - return ; - default: - return

    {children}

    ; - } + const { attributes, children, element } = props; + switch (element.type) { + case 'mention': + return ; + default: + return

    {children}

    ; + } }; const insertMention = (editor, character, users) => { - const selectedUser = users.find((user) => user.name === character); - if (!selectedUser) { - return; - } - const userUrl = selectedUser.url; - const name = selectedUser.name; - const mention = { - type: 'mention', - character, - url: userUrl, - children: [{ text: name }], - }; - Transforms.insertNodes(editor, mention); - Transforms.move(editor); - Transforms.insertText(editor, ' '); + const selectedUser = users.find((user) => user.name === character); + if (!selectedUser) { + return; + } + const userUrl = selectedUser.url; + const name = selectedUser.name; + const mention = { + type: 'mention', + character, + url: userUrl, + children: [{ text: name }], + }; + Transforms.insertNodes(editor, mention); + Transforms.move(editor); + Transforms.insertText(editor, ' '); }; const MentionElement = ({ attributes, children, element }) => { - return ( - - @{element.character} - {children} - - ); + return ( + + @{element.character} + {children} + + ); }; diff --git a/translate/src/core/comments/components/Comment.css b/translate/src/core/comments/components/Comment.css index 3df8a2641..6cded23cb 100644 --- a/translate/src/core/comments/components/Comment.css +++ b/translate/src/core/comments/components/Comment.css @@ -1,81 +1,81 @@ .comments-list ul .comment { - padding-bottom: 10px; + padding-bottom: 10px; } .comments-list .comment a { - color: #7bc876; - font-weight: 400; + color: #7bc876; + font-weight: 400; } .comments-list - .comment - .container - .content - a[href^='/contributors/']:not(.comment-author) { - background-color: #3f4752; - padding: 0 2px; - border-radius: 2px; + .comment + .container + .content + a[href^='/contributors/']:not(.comment-author) { + background-color: #3f4752; + padding: 0 2px; + border-radius: 2px; } .comments-list .comment-author { - margin-right: 4px; + margin-right: 4px; } .comments-list .comment .content { - background-color: #4d5967; - border: solid 1px #4d5967; - border-radius: 4px; - display: flex; - font-size: 11px; - padding: 6px; - word-break: break-word; - white-space: pre-wrap; - position: relative; + background-color: #4d5967; + border: solid 1px #4d5967; + border-radius: 4px; + display: flex; + font-size: 11px; + padding: 6px; + word-break: break-word; + white-space: pre-wrap; + position: relative; } .comments-list .comment .content p, .comments-list .comment .content span { - color: #cccccc; + color: #cccccc; } .comments-list .comment .content p:first-child { - display: inline; + display: inline; } .comments-list .comment .info { - color: #aaaaaa; - font-size: 11px; - font-weight: 300; - margin-top: -2px; - padding-left: 8px; + color: #aaaaaa; + font-size: 11px; + font-weight: 300; + margin-top: -2px; + padding-left: 8px; } .comments-list .comment .info .pin-button:before { - content: '•'; - padding: 0 3px 0 3px; + content: '•'; + padding: 0 3px 0 3px; } .comments-list .comment .info .pin-button { - background-color: transparent; - border: none; - color: #aaaaaa; - font-size: 11px; - font-weight: 300; - padding: 0; + background-color: transparent; + border: none; + color: #aaaaaa; + font-size: 11px; + font-weight: 300; + padding: 0; } .comments-list .comment .info .pin-button:hover { - color: #7bc876; + color: #7bc876; } .comments-list .comment .comment-pin { - color: #7bc876; - font-size: 9px; - position: absolute; - right: 4px; - top: -10px; + color: #7bc876; + font-size: 9px; + position: absolute; + right: 4px; + top: -10px; } .comments-list .comment .comment-pin .fa { - padding-right: 2px; + padding-right: 2px; } diff --git a/translate/src/core/comments/components/Comment.test.js b/translate/src/core/comments/components/Comment.test.js index 41553fafb..bc63d4e9f 100644 --- a/translate/src/core/comments/components/Comment.test.js +++ b/translate/src/core/comments/components/Comment.test.js @@ -5,64 +5,64 @@ import sinon from 'sinon'; import Comment from './Comment'; describe('', () => { - const DEFAULT_COMMENT = { - author: '', - username: '', - userGravatarUrlSmall: '', - createdAt: '', - dateIso: '', - content: - "What I hear when I'm being yelled at is people caring loudly at me.", - translation: 0, - id: 1, + const DEFAULT_COMMENT = { + author: '', + username: '', + userGravatarUrlSmall: '', + createdAt: '', + dateIso: '', + content: + "What I hear when I'm being yelled at is people caring loudly at me.", + translation: 0, + id: 1, + }; + + const DEFAULT_USER = { + username: 'Leslie_Knope', + }; + + const DEFAULT_ISTRANSLATOR = { + isTranslator: true, + }; + + it('renders the correct text', () => { + const deleteMock = sinon.stub(); + const wrapper = shallow( + , + ); + + // Comments are hidden in a Linkify component. + const content = wrapper.find('Linkify').find('span').text(); + expect(content).toEqual( + "What I hear when I'm being yelled at is people caring loudly at me.", + ); + }); + + it('renders a link for the author', () => { + const deleteMock = sinon.stub(); + const comments = { + ...DEFAULT_COMMENT, + ...{ username: 'Leslie_Knope', author: 'LKnope' }, }; + const wrapper = shallow( + , + ); - const DEFAULT_USER = { - username: 'Leslie_Knope', - }; - - const DEFAULT_ISTRANSLATOR = { - isTranslator: true, - }; - - it('renders the correct text', () => { - const deleteMock = sinon.stub(); - const wrapper = shallow( - , - ); - - // Comments are hidden in a Linkify component. - const content = wrapper.find('Linkify').find('span').text(); - expect(content).toEqual( - "What I hear when I'm being yelled at is people caring loudly at me.", - ); - }); - - it('renders a link for the author', () => { - const deleteMock = sinon.stub(); - const comments = { - ...DEFAULT_COMMENT, - ...{ username: 'Leslie_Knope', author: 'LKnope' }, - }; - const wrapper = shallow( - , - ); - - const link = wrapper.find('a'); - expect(link).toHaveLength(1); - expect(link.at(0).props().children).toEqual('LKnope'); - expect(link.at(0).props().href).toEqual('/contributors/Leslie_Knope'); - }); + const link = wrapper.find('a'); + expect(link).toHaveLength(1); + expect(link.at(0).props().children).toEqual('LKnope'); + expect(link.at(0).props().href).toEqual('/contributors/Leslie_Knope'); + }); }); diff --git a/translate/src/core/comments/components/Comment.tsx b/translate/src/core/comments/components/Comment.tsx index 7ec0cc7f7..8fcdb4d44 100644 --- a/translate/src/core/comments/components/Comment.tsx +++ b/translate/src/core/comments/components/Comment.tsx @@ -11,107 +11,105 @@ import { UserAvatar } from '~/core/user'; import type { TranslationComment } from '~/core/api'; type Props = { - comment: TranslationComment; - canPin?: boolean; - togglePinnedStatus?: (status: boolean, id: number) => void; + comment: TranslationComment; + canPin?: boolean; + togglePinnedStatus?: (status: boolean, id: number) => void; }; export default function Comment(props: Props): null | React.ReactElement<'li'> { - const { comment, canPin, togglePinnedStatus } = props; + const { comment, canPin, togglePinnedStatus } = props; - if (!comment) { - return null; + if (!comment) { + return null; + } + + const handlePinAndUnpin = () => { + if (!togglePinnedStatus) { + return; } + togglePinnedStatus(!comment.pinned, comment.id); + }; - const handlePinAndUnpin = () => { - if (!togglePinnedStatus) { - return; - } - togglePinnedStatus(!comment.pinned, comment.id); - }; - - return ( -
  • - -
    -
    -
    - - e.stopPropagation() - } - > - {comment.author} - - - {/* We can safely use parse with comment.content as it is - * sanitized when coming from the DB. See: - * - pontoon.base.forms.AddCommentForm(} - * - pontoon.base.forms.HtmlField() - */} - {parse(comment.content)} - - {!comment.pinned ? null : ( -
    -
    - - PINNED - -
    - )} -
    -
    -
    - - {canPin ? ( - comment.pinned ? ( - // Unpin Button - - - - ) : ( - // Pin Button - - - - ) - ) : null} -
    -
    -
  • - ); + return ( +
  • + +
    +
    +
    + e.stopPropagation()} + > + {comment.author} + + + {/* We can safely use parse with comment.content as it is + * sanitized when coming from the DB. See: + * - pontoon.base.forms.AddCommentForm(} + * - pontoon.base.forms.HtmlField() + */} + {parse(comment.content)} + + {!comment.pinned ? null : ( +
    +
    + + PINNED + +
    + )} +
    +
    +
    + + {canPin ? ( + comment.pinned ? ( + // Unpin Button + + + + ) : ( + // Pin Button + + + + ) + ) : null} +
    +
    +
  • + ); } diff --git a/translate/src/core/comments/components/CommentsList.css b/translate/src/core/comments/components/CommentsList.css index ada55aa37..6e9fcdd8a 100644 --- a/translate/src/core/comments/components/CommentsList.css +++ b/translate/src/core/comments/components/CommentsList.css @@ -1,46 +1,46 @@ .comments-list { - line-height: 22px; - padding: 8px 0; + line-height: 22px; + padding: 8px 0; } .comments-list .pinned-comments > * { - padding: 0 8px; + padding: 0 8px; } .comments-list .pinned-comments .title { - color: #aaa; - font-size: 11px; - font-weight: 100; - margin-bottom: 6px; + color: #aaa; + font-size: 11px; + font-weight: 100; + margin-bottom: 6px; } .comments-list .pinned-comments ul { - border-bottom: 1px solid #5e6475; - margin-bottom: 8px; + border-bottom: 1px solid #5e6475; + margin-bottom: 8px; } .comments-list .all-comments { - padding: 0 8px; + padding: 0 8px; } .comments-list .comment { - display: flex; - margin-left: 58px; + display: flex; + margin-left: 58px; } .comments-list .comment .user-avatar { - padding-right: 8px; + padding-right: 8px; } .comments-list .comment .user-avatar img { - border-radius: 4px; - border: 2px solid #5e6475; - width: 32px; - height: 32px; + border-radius: 4px; + border: 2px solid #5e6475; + width: 32px; + height: 32px; } .comments-list .comment .container { - display: flex; - flex-flow: column wrap; - width: 100%; + display: flex; + flex-flow: column wrap; + width: 100%; } diff --git a/translate/src/core/comments/components/CommentsList.test.js b/translate/src/core/comments/components/CommentsList.test.js index 3cba82eed..69ba1974c 100644 --- a/translate/src/core/comments/components/CommentsList.test.js +++ b/translate/src/core/comments/components/CommentsList.test.js @@ -4,26 +4,26 @@ import { shallow } from 'enzyme'; import CommentsList from './CommentsList'; describe('', () => { - const DEFAULT_USER = 'AnnPerkins'; + const DEFAULT_USER = 'AnnPerkins'; - const DEFAULT_TRANSLATION = { - user: '', - username: '', - gravatarURLSmall: '', - }; + const DEFAULT_TRANSLATION = { + user: '', + username: '', + gravatarURLSmall: '', + }; - it('shows the correct number of comments', () => { - const comments = [{ id: 1 }, { id: 2 }, { id: 3 }]; + it('shows the correct number of comments', () => { + const comments = [{ id: 1 }, { id: 2 }, { id: 3 }]; - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - expect(wrapper.find('ul > *')).toHaveLength(3); - }); + expect(wrapper.find('ul > *')).toHaveLength(3); + }); }); diff --git a/translate/src/core/comments/components/CommentsList.tsx b/translate/src/core/comments/components/CommentsList.tsx index 01a778858..c3e8f2725 100644 --- a/translate/src/core/comments/components/CommentsList.tsx +++ b/translate/src/core/comments/components/CommentsList.tsx @@ -11,86 +11,80 @@ import type { UserState } from '~/core/user'; import type { HistoryTranslation } from '~/modules/history'; type Props = { - comments: Array; - parameters?: NavigationParams; - translation?: HistoryTranslation; - user: UserState; - contactPerson?: string; - canComment: boolean; - canPin?: boolean; - addComment: (arg0: string, arg1: number | null | undefined) => void; - togglePinnedStatus?: (arg0: boolean, arg1: number) => void; - resetContactPerson?: () => void; + comments: Array; + parameters?: NavigationParams; + translation?: HistoryTranslation; + user: UserState; + contactPerson?: string; + canComment: boolean; + canPin?: boolean; + addComment: (arg0: string, arg1: number | null | undefined) => void; + togglePinnedStatus?: (arg0: boolean, arg1: number) => void; + resetContactPerson?: () => void; }; export default function CommentsList(props: Props): React.ReactElement<'div'> { - const { - comments, - parameters, - translation, - user, - canComment, - canPin, - addComment, - togglePinnedStatus, - contactPerson, - resetContactPerson, - } = props; + const { + comments, + parameters, + translation, + user, + canComment, + canPin, + addComment, + togglePinnedStatus, + contactPerson, + resetContactPerson, + } = props; - const translationId = translation ? translation.pk : null; - - // rendering comment - const renderComment = (comment) => { - return ( - - ); - }; - - const pinnedComments = comments.filter((comment) => comment.pinned); - const unpinnedComments = comments.filter((comment) => !comment.pinned); - const hideAllComments = - !canComment && unpinnedComments.length === 0 && pinnedComments.length; + const translationId = translation ? translation.pk : null; + // rendering comment + const renderComment = (comment) => { return ( -
    - {pinnedComments.length ? ( -
    - -

    PINNED COMMENTS

    -
    - -
      - {pinnedComments.map((comment) => - renderComment(comment), - )} -
    - {!hideAllComments ? ( - -

    ALL COMMENTS

    -
    - ) : null} -
    - ) : null} -
    -
      - {unpinnedComments.map((comment) => renderComment(comment))} -
    - {!canComment ? null : ( - - )} -
    -
    + ); + }; + + const pinnedComments = comments.filter((comment) => comment.pinned); + const unpinnedComments = comments.filter((comment) => !comment.pinned); + const hideAllComments = + !canComment && unpinnedComments.length === 0 && pinnedComments.length; + + return ( +
    + {pinnedComments.length ? ( +
    + +

    PINNED COMMENTS

    +
    + +
      {pinnedComments.map((comment) => renderComment(comment))}
    + {!hideAllComments ? ( + +

    ALL COMMENTS

    +
    + ) : null} +
    + ) : null} +
    +
      {unpinnedComments.map((comment) => renderComment(comment))}
    + {!canComment ? null : ( + + )} +
    +
    + ); } diff --git a/translate/src/core/diff/components/TranslationDiff.css b/translate/src/core/diff/components/TranslationDiff.css index 0cca7752b..19a3e8604 100644 --- a/translate/src/core/diff/components/TranslationDiff.css +++ b/translate/src/core/diff/components/TranslationDiff.css @@ -1,22 +1,22 @@ .translation p ins, .translation p del { - border-radius: 2px; - white-space: pre-wrap; + border-radius: 2px; + white-space: pre-wrap; } .translation p ins { - background: #4b6259; - color: #9cd699; + background: #4b6259; + color: #9cd699; } .translation p del { - background: #674b54; - color: #fe8f8f; + background: #674b54; + color: #fe8f8f; } .translation p ins mark, .translation p del mark { - background: transparent; - border-color: transparent; - margin: 0; + background: transparent; + border-color: transparent; + margin: 0; } diff --git a/translate/src/core/diff/components/TranslationDiff.test.js b/translate/src/core/diff/components/TranslationDiff.test.js index 84e3bf03d..a8c5cef5f 100644 --- a/translate/src/core/diff/components/TranslationDiff.test.js +++ b/translate/src/core/diff/components/TranslationDiff.test.js @@ -4,23 +4,23 @@ import { shallow } from 'enzyme'; import TranslationDiff from './TranslationDiff'; describe('', () => { - it('returns the correct diff for provided strings', () => { - const wrapper = shallow( - , - ); + it('returns the correct diff for provided strings', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('ins')).toHaveLength(1); - expect(wrapper.find('del')).toHaveLength(1); - expect(wrapper.at(1).text()).toEqual('cdef'); - }); + expect(wrapper.find('ins')).toHaveLength(1); + expect(wrapper.find('del')).toHaveLength(1); + expect(wrapper.at(1).text()).toEqual('cdef'); + }); - it('returns the same string if provided strings are equal', () => { - const wrapper = shallow( - , - ); + it('returns the same string if provided strings are equal', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('ins')).toHaveLength(0); - expect(wrapper.find('del')).toHaveLength(0); - expect(wrapper.text()).toEqual('abcdef'); - }); + expect(wrapper.find('ins')).toHaveLength(0); + expect(wrapper.find('del')).toHaveLength(0); + expect(wrapper.text()).toEqual('abcdef'); + }); }); diff --git a/translate/src/core/diff/components/TranslationDiff.ts b/translate/src/core/diff/components/TranslationDiff.ts index 565c2d491..d1850b5b9 100644 --- a/translate/src/core/diff/components/TranslationDiff.ts +++ b/translate/src/core/diff/components/TranslationDiff.ts @@ -5,8 +5,8 @@ import './TranslationDiff.css'; import { getDiff } from '../withDiff'; type Props = { - base: string; - target: string; + base: string; + target: string; }; /** @@ -17,8 +17,8 @@ type Props = { * Removed slices are wrapped in . */ export default class TranslationDiff extends React.Component { - render(): React.ReactNode { - const { base, target } = this.props; - return getDiff(base, target); - } + render(): React.ReactNode { + const { base, target } = this.props; + return getDiff(base, target); + } } diff --git a/translate/src/core/diff/withDiff.tsx b/translate/src/core/diff/withDiff.tsx index 68c3aeacf..2445eed7e 100644 --- a/translate/src/core/diff/withDiff.tsx +++ b/translate/src/core/diff/withDiff.tsx @@ -6,40 +6,40 @@ import './components/TranslationDiff.css'; const dmp = new diff_match_patch(); export function getDiff(base: string, target: string): React.ReactNode { - const diff = dmp.diff_main(base, target); + const diff = dmp.diff_main(base, target); - dmp.diff_cleanupSemantic(diff); - dmp.diff_cleanupEfficiency(diff); + dmp.diff_cleanupSemantic(diff); + dmp.diff_cleanupEfficiency(diff); - return diff.map((item, index) => { - let type = item[0]; - let slice = item[1]; + return diff.map((item, index) => { + let type = item[0]; + let slice = item[1]; - switch (type) { - case DIFF_INSERT: - return {slice}; + switch (type) { + case DIFF_INSERT: + return {slice}; - case DIFF_DELETE: - return {slice}; + case DIFF_DELETE: + return {slice}; - default: - return slice; - } - }); + default: + return slice; + } + }); } type Props = { - diffTarget: string; + diffTarget: string; }; export default function withDiff>( - WrappedComponent: React.ComponentType, + WrappedComponent: React.ComponentType, ): React.ComponentType { - return React.memo(function WithDiff(props: Config & Props) { - return ( - - {getDiff(props.diffTarget, props.children)} - - ); - }); + return React.memo(function WithDiff(props: Config & Props) { + return ( + + {getDiff(props.diffTarget, props.children)} + + ); + }); } diff --git a/translate/src/core/editor/actions.ts b/translate/src/core/editor/actions.ts index 84865744b..42ea09bc8 100644 --- a/translate/src/core/editor/actions.ts +++ b/translate/src/core/editor/actions.ts @@ -15,29 +15,29 @@ import type { Entry } from '@fluent/syntax'; import { AppThunk } from '~/store'; export const END_UPDATE_TRANSLATION: 'editor/END_UPDATE_TRANSLATION' = - 'editor/END_UPDATE_TRANSLATION'; + 'editor/END_UPDATE_TRANSLATION'; export const RESET_EDITOR: 'editor/RESET_EDITOR' = 'editor/RESET_EDITOR'; export const RESET_FAILED_CHECKS: 'editor/RESET_FAILED_CHECKS' = - 'editor/RESET_FAILED_CHECKS'; + 'editor/RESET_FAILED_CHECKS'; export const RESET_HELPER_ELEMENT_INDEX: 'editor/RESET_HELPER_ELEMENT_INDEX' = - 'editor/RESET_HELPER_ELEMENT_INDEX'; + 'editor/RESET_HELPER_ELEMENT_INDEX'; export const RESET_SELECTION: 'editor/RESET_SELECTION' = - 'editor/RESET_SELECTION'; + 'editor/RESET_SELECTION'; export const SELECT_HELPER_ELEMENT_INDEX: 'editor/SELECT_HELPER_ELEMENT_INDEX' = - 'editor/SELECT_HELPER_ELEMENT_INDEX'; + 'editor/SELECT_HELPER_ELEMENT_INDEX'; export const SELECT_HELPER_TAB_INDEX: 'editor/SELECT_HELPER_TAB_INDEX' = - 'editor/SELECT_HELPER_TAB_INDEX'; + 'editor/SELECT_HELPER_TAB_INDEX'; export const SET_INITIAL_TRANSLATION: 'editor/SET_INITIAL_TRANSLATION' = - 'editor/SET_INITIAL_TRANSLATION'; + 'editor/SET_INITIAL_TRANSLATION'; export const START_UPDATE_TRANSLATION: 'editor/START_UPDATE_TRANSLATION' = - 'editor/START_UPDATE_TRANSLATION'; + 'editor/START_UPDATE_TRANSLATION'; export const UPDATE: 'editor/UPDATE' = 'editor/UPDATE'; export const UPDATE_FAILED_CHECKS: 'editor/UPDATE_FAILED_CHECKS' = - 'editor/UPDATE_FAILED_CHECKS'; + 'editor/UPDATE_FAILED_CHECKS'; export const UPDATE_SELECTION: 'editor/UPDATE_SELECTION' = - 'editor/UPDATE_SELECTION'; + 'editor/UPDATE_SELECTION'; export const UPDATE_MACHINERY_SOURCES: 'editor/UPDATE_MACHINERY_SOURCES' = - 'editor/UPDATE_MACHINERY_SOURCES'; + 'editor/UPDATE_MACHINERY_SOURCES'; export type Translation = string | Entry; @@ -45,19 +45,19 @@ export type Translation = string | Entry; * Update the current translation of the selected entity. */ export type UpdateAction = { - readonly type: typeof UPDATE; - readonly translation: Translation; - readonly changeSource: string; + readonly type: typeof UPDATE; + readonly translation: Translation; + readonly changeSource: string; }; export function update( - translation: Translation, - changeSource?: string, + translation: Translation, + changeSource?: string, ): UpdateAction { - return { - type: UPDATE, - translation, - changeSource: changeSource || 'internal', - }; + return { + type: UPDATE, + translation, + changeSource: changeSource || 'internal', + }; } /** @@ -65,80 +65,80 @@ export function update( * active editor. */ export type UpdateSelectionAction = { - readonly type: typeof UPDATE_SELECTION; - readonly content: string; - readonly changeSource: string; + readonly type: typeof UPDATE_SELECTION; + readonly content: string; + readonly changeSource: string; }; export function updateSelection( - content: string, - changeSource?: string, + content: string, + changeSource?: string, ): UpdateSelectionAction { - return { - type: UPDATE_SELECTION, - content, - changeSource: changeSource || 'internal', - }; + return { + type: UPDATE_SELECTION, + content, + changeSource: changeSource || 'internal', + }; } /** * Update machinerySources and machineryTranslation when copied from machinery tab. */ export type UpdateMachinerySourcesAction = { - readonly type: typeof UPDATE_MACHINERY_SOURCES; - readonly machinerySources: Array; - readonly machineryTranslation: string; + readonly type: typeof UPDATE_MACHINERY_SOURCES; + readonly machinerySources: Array; + readonly machineryTranslation: string; }; export function updateMachinerySources( - machinerySources: Array, - machineryTranslation: string, + machinerySources: Array, + machineryTranslation: string, ): UpdateMachinerySourcesAction { - return { - type: UPDATE_MACHINERY_SOURCES, - machinerySources, - machineryTranslation, - }; + return { + type: UPDATE_MACHINERY_SOURCES, + machinerySources, + machineryTranslation, + }; } /** * Reset selected helper element index to its initial value. */ export type ResetHelperElementIndexAction = { - readonly type: typeof RESET_HELPER_ELEMENT_INDEX; + readonly type: typeof RESET_HELPER_ELEMENT_INDEX; }; export function resetHelperElementIndex(): ResetHelperElementIndexAction { - return { - type: RESET_HELPER_ELEMENT_INDEX, - }; + return { + type: RESET_HELPER_ELEMENT_INDEX, + }; } /** * Set selected helper element index to a specific value. */ export type SelectHelperElementIndexAction = { - readonly type: typeof SELECT_HELPER_ELEMENT_INDEX; - readonly index: number; + readonly type: typeof SELECT_HELPER_ELEMENT_INDEX; + readonly index: number; }; function selectHelperElementIndex( - index: number, + index: number, ): SelectHelperElementIndexAction { - return { - type: SELECT_HELPER_ELEMENT_INDEX, - index, - }; + return { + type: SELECT_HELPER_ELEMENT_INDEX, + index, + }; } /** * Set selected helper tab index to a specific value. */ export type SelectHelperTabIndexAction = { - readonly type: typeof SELECT_HELPER_TAB_INDEX; - readonly index: number; + readonly type: typeof SELECT_HELPER_TAB_INDEX; + readonly index: number; }; function selectHelperTabIndex(index: number): SelectHelperTabIndexAction { - return { - type: SELECT_HELPER_TAB_INDEX, - index, - }; + return { + type: SELECT_HELPER_TAB_INDEX, + index, + }; } /** @@ -146,202 +146,198 @@ function selectHelperTabIndex(index: number): SelectHelperTabIndexAction { * active editor. */ export type InitialTranslationAction = { - readonly type: typeof SET_INITIAL_TRANSLATION; - readonly translation: Translation; + readonly type: typeof SET_INITIAL_TRANSLATION; + readonly translation: Translation; }; export function setInitialTranslation( - translation: Translation, + translation: Translation, ): InitialTranslationAction { - return { - type: SET_INITIAL_TRANSLATION, - translation, - }; + return { + type: SET_INITIAL_TRANSLATION, + translation, + }; } /** * Update failed checks in the active editor. */ export type FailedChecks = { - readonly clErrors: Array; - readonly pErrors: Array; - readonly clWarnings: Array; - readonly pndbWarnings: Array; - readonly ttWarnings: Array; + readonly clErrors: Array; + readonly pErrors: Array; + readonly clWarnings: Array; + readonly pndbWarnings: Array; + readonly ttWarnings: Array; }; export type UpdateFailedChecksAction = { - readonly type: typeof UPDATE_FAILED_CHECKS; - readonly failedChecks: FailedChecks; - readonly source: '' | 'stored' | 'submitted' | number; + readonly type: typeof UPDATE_FAILED_CHECKS; + readonly failedChecks: FailedChecks; + readonly source: '' | 'stored' | 'submitted' | number; }; export function updateFailedChecks( - failedChecks: FailedChecks, - source: '' | 'stored' | 'submitted' | number, + failedChecks: FailedChecks, + source: '' | 'stored' | 'submitted' | number, ): UpdateFailedChecksAction { - return { - type: UPDATE_FAILED_CHECKS, - failedChecks, - source, - }; + return { + type: UPDATE_FAILED_CHECKS, + failedChecks, + source, + }; } /** * Reset selected content to default value. */ export type ResetSelectionAction = { - readonly type: typeof RESET_SELECTION; + readonly type: typeof RESET_SELECTION; }; export function resetSelection(): ResetSelectionAction { - return { - type: RESET_SELECTION, - }; + return { + type: RESET_SELECTION, + }; } /** * Reset the whole editor's data to its initial value. */ export type ResetEditorAction = { - readonly type: typeof RESET_EDITOR; + readonly type: typeof RESET_EDITOR; }; export function reset(): ResetEditorAction { - return { - type: RESET_EDITOR, - }; + return { + type: RESET_EDITOR, + }; } /** * Reset failed checks to default value. */ export type ResetFailedChecksAction = { - readonly type: typeof RESET_FAILED_CHECKS; + readonly type: typeof RESET_FAILED_CHECKS; }; export function resetFailedChecks(): ResetFailedChecksAction { - return { - type: RESET_FAILED_CHECKS, - }; + return { + type: RESET_FAILED_CHECKS, + }; } export type StartUpdateTranslationAction = { - readonly type: typeof START_UPDATE_TRANSLATION; + readonly type: typeof START_UPDATE_TRANSLATION; }; function startUpdateTranslation(): StartUpdateTranslationAction { - return { - type: START_UPDATE_TRANSLATION, - }; + return { + type: START_UPDATE_TRANSLATION, + }; } export type EndUpdateTranslationAction = { - readonly type: typeof END_UPDATE_TRANSLATION; + readonly type: typeof END_UPDATE_TRANSLATION; }; function endUpdateTranslation(): EndUpdateTranslationAction { - return { - type: END_UPDATE_TRANSLATION, - }; + return { + type: END_UPDATE_TRANSLATION, + }; } /** * Save the current translation. */ export function sendTranslation( - entity: Entity, - translation: string, - locale: Locale, - pluralForm: number, - forceSuggestions: boolean, - nextEntity: Entity | null | undefined, - router: Record, - resource: string, - ignoreWarnings: boolean | null | undefined, - machinerySources: Array, + entity: Entity, + translation: string, + locale: Locale, + pluralForm: number, + forceSuggestions: boolean, + nextEntity: Entity | null | undefined, + router: Record, + resource: string, + ignoreWarnings: boolean | null | undefined, + machinerySources: Array, ): AppThunk { - return async (dispatch) => { - NProgress.start(); - dispatch(startUpdateTranslation()); + return async (dispatch) => { + NProgress.start(); + dispatch(startUpdateTranslation()); - const content = await api.translation.create( - entity.pk, - translation, - locale.code, - pluralForm, - entity.original, - forceSuggestions, - resource, - ignoreWarnings, - machinerySources, + const content = await api.translation.create( + entity.pk, + translation, + locale.code, + pluralForm, + entity.original, + forceSuggestions, + resource, + ignoreWarnings, + machinerySources, + ); + + if (content.failedChecks) { + dispatch(updateFailedChecks(content.failedChecks, 'submitted')); + } else if (content.same) { + // The translation that was provided is the same as an existing + // translation for that entity. + dispatch( + notification.actions.add(notification.messages.SAME_TRANSLATION), + ); + } else if (content.status) { + // Notify the user of the change that happened. + dispatch( + notification.actions.add(notification.messages.TRANSLATION_SAVED), + ); + + // Ignore existing unsavedchanges because they are saved now. + dispatch(unsavedchanges.actions.ignore()); + + dispatch( + entitiesActions.updateEntityTranslation( + entity.pk, + pluralForm, + content.translation, + ), + ); + + // Update stats in the filter panel and resource menu if possible. + if (content.stats) { + dispatch(statsActions.update(content.stats)); + dispatch( + resourceActions.update( + entity.path, + content.stats.approved, + content.stats.warnings, + ), ); + } - if (content.failedChecks) { - dispatch(updateFailedChecks(content.failedChecks, 'submitted')); - } else if (content.same) { - // The translation that was provided is the same as an existing - // translation for that entity. - dispatch( - notification.actions.add( - notification.messages.SAME_TRANSLATION, - ), - ); - } else if (content.status) { - // Notify the user of the change that happened. - dispatch( - notification.actions.add( - notification.messages.TRANSLATION_SAVED, - ), - ); + if (nextEntity) { + // The change did work, we want to move on to the next Entity or pluralForm. + pluralActions.moveToNextTranslation( + dispatch, + router, + entity.pk, + nextEntity.pk, + pluralForm, + locale, + ); + dispatch(reset()); + } + } - // Ignore existing unsavedchanges because they are saved now. - dispatch(unsavedchanges.actions.ignore()); - - dispatch( - entitiesActions.updateEntityTranslation( - entity.pk, - pluralForm, - content.translation, - ), - ); - - // Update stats in the filter panel and resource menu if possible. - if (content.stats) { - dispatch(statsActions.update(content.stats)); - dispatch( - resourceActions.update( - entity.path, - content.stats.approved, - content.stats.warnings, - ), - ); - } - - if (nextEntity) { - // The change did work, we want to move on to the next Entity or pluralForm. - pluralActions.moveToNextTranslation( - dispatch, - router, - entity.pk, - nextEntity.pk, - pluralForm, - locale, - ); - dispatch(reset()); - } - } - - dispatch(endUpdateTranslation()); - NProgress.done(); - }; + dispatch(endUpdateTranslation()); + NProgress.done(); + }; } export default { - endUpdateTranslation, - reset, - resetFailedChecks, - resetHelperElementIndex, - resetSelection, - sendTranslation, - selectHelperElementIndex, - selectHelperTabIndex, - setInitialTranslation, - startUpdateTranslation, - update, - updateFailedChecks, - updateSelection, - updateMachinerySources, + endUpdateTranslation, + reset, + resetFailedChecks, + resetHelperElementIndex, + resetSelection, + sendTranslation, + selectHelperElementIndex, + selectHelperTabIndex, + setInitialTranslation, + startUpdateTranslation, + update, + updateFailedChecks, + updateSelection, + updateMachinerySources, }; diff --git a/translate/src/core/editor/components/EditorMainAction.test.js b/translate/src/core/editor/components/EditorMainAction.test.js index 136b79eda..0c13ea39e 100644 --- a/translate/src/core/editor/components/EditorMainAction.test.js +++ b/translate/src/core/editor/components/EditorMainAction.test.js @@ -4,129 +4,127 @@ import * as user from '~/core/user'; import * as history from '~/modules/history'; import { - createDefaultUser, - createReduxStore, - mountComponentWithStore, + createDefaultUser, + createReduxStore, + mountComponentWithStore, } from '~/test/store'; import * as editor from '..'; import EditorMainAction from './EditorMainAction'; function createComponent(sendTranslationMock) { - const store = createReduxStore(); - createDefaultUser(store); + const store = createReduxStore(); + createDefaultUser(store); - const comp = mountComponentWithStore(EditorMainAction, store, { - sendTranslation: sendTranslationMock, - }); + const comp = mountComponentWithStore(EditorMainAction, store, { + sendTranslation: sendTranslationMock, + }); - return [comp, store]; + return [comp, store]; } describe('', () => { - it('renders the Approve button when an identical translation exists', () => { - const updateStatusMock = sinon.spy(); - sinon.stub(history.actions, 'updateStatus').returns(updateStatusMock); - sinon.stub(user.selectors, 'isTranslator').returns(true); - sinon - .stub(editor.selectors, 'sameExistingTranslation') - .returns({ pk: 1 }); + it('renders the Approve button when an identical translation exists', () => { + const updateStatusMock = sinon.spy(); + sinon.stub(history.actions, 'updateStatus').returns(updateStatusMock); + sinon.stub(user.selectors, 'isTranslator').returns(true); + sinon.stub(editor.selectors, 'sameExistingTranslation').returns({ pk: 1 }); - const [wrapper] = createComponent(); + const [wrapper] = createComponent(); - expect(wrapper.find('.action-approve')).toHaveLength(1); - expect(wrapper.find('.action-suggest')).toHaveLength(0); - expect(wrapper.find('.action-save')).toHaveLength(0); + expect(wrapper.find('.action-approve')).toHaveLength(1); + expect(wrapper.find('.action-suggest')).toHaveLength(0); + expect(wrapper.find('.action-save')).toHaveLength(0); - wrapper.find('.action-approve').simulate('click'); - expect(updateStatusMock.calledOnce).toBeTruthy(); + wrapper.find('.action-approve').simulate('click'); + expect(updateStatusMock.calledOnce).toBeTruthy(); - user.selectors.isTranslator.restore(); - editor.selectors.sameExistingTranslation.restore(); - history.actions.updateStatus.restore(); - }); + user.selectors.isTranslator.restore(); + editor.selectors.sameExistingTranslation.restore(); + history.actions.updateStatus.restore(); + }); - it('renders the Suggest button when force suggestion is on', () => { - sinon.stub(user.selectors, 'isTranslator').returns(true); + it('renders the Suggest button when force suggestion is on', () => { + sinon.stub(user.selectors, 'isTranslator').returns(true); - const sendTranslationMock = sinon.spy(); - const [wrapper, store] = createComponent(sendTranslationMock); + const sendTranslationMock = sinon.spy(); + const [wrapper, store] = createComponent(sendTranslationMock); - createDefaultUser(store, { settings: { force_suggestions: true } }); - wrapper.update(); + createDefaultUser(store, { settings: { force_suggestions: true } }); + wrapper.update(); - expect(wrapper.find('.action-suggest')).toHaveLength(1); - expect(wrapper.find('.action-approve')).toHaveLength(0); - expect(wrapper.find('.action-save')).toHaveLength(0); + expect(wrapper.find('.action-suggest')).toHaveLength(1); + expect(wrapper.find('.action-approve')).toHaveLength(0); + expect(wrapper.find('.action-save')).toHaveLength(0); - wrapper.find('.action-suggest').simulate('click'); - expect(sendTranslationMock.calledOnce).toBeTruthy(); + wrapper.find('.action-suggest').simulate('click'); + expect(sendTranslationMock.calledOnce).toBeTruthy(); - user.selectors.isTranslator.restore(); - }); + user.selectors.isTranslator.restore(); + }); - it('renders the Suggest button when user does not have permission', () => { - sinon.stub(user.selectors, 'isTranslator').returns(false); + it('renders the Suggest button when user does not have permission', () => { + sinon.stub(user.selectors, 'isTranslator').returns(false); - const [wrapper] = createComponent(); + const [wrapper] = createComponent(); - expect(wrapper.find('.action-suggest')).toHaveLength(1); - expect(wrapper.find('.action-save')).toHaveLength(0); - expect(wrapper.find('.action-approve')).toHaveLength(0); + expect(wrapper.find('.action-suggest')).toHaveLength(1); + expect(wrapper.find('.action-save')).toHaveLength(0); + expect(wrapper.find('.action-approve')).toHaveLength(0); - user.selectors.isTranslator.restore(); - }); + user.selectors.isTranslator.restore(); + }); - it('shows a spinner and a disabled Suggesting button when running request', () => { - sinon.stub(user.selectors, 'isTranslator').returns(true); + it('shows a spinner and a disabled Suggesting button when running request', () => { + sinon.stub(user.selectors, 'isTranslator').returns(true); - const sendTranslationMock = sinon.spy(); - const [wrapper, store] = createComponent(sendTranslationMock); + const sendTranslationMock = sinon.spy(); + const [wrapper, store] = createComponent(sendTranslationMock); - createDefaultUser(store, { settings: { force_suggestions: true } }); - store.dispatch(editor.actions.startUpdateTranslation()); - wrapper.update(); + createDefaultUser(store, { settings: { force_suggestions: true } }); + store.dispatch(editor.actions.startUpdateTranslation()); + wrapper.update(); - expect(wrapper.find('.action-suggest')).toHaveLength(1); - expect(wrapper.find('.action-suggest .fa-spin')).toHaveLength(1); + expect(wrapper.find('.action-suggest')).toHaveLength(1); + expect(wrapper.find('.action-suggest .fa-spin')).toHaveLength(1); - wrapper.find('.action-suggest').simulate('click'); - expect(sendTranslationMock.calledOnce).toBeFalsy(); + wrapper.find('.action-suggest').simulate('click'); + expect(sendTranslationMock.calledOnce).toBeFalsy(); - user.selectors.isTranslator.restore(); - }); + user.selectors.isTranslator.restore(); + }); - it('renders the Save button when force suggestion is off and translation is not the same', () => { - sinon.stub(user.selectors, 'isTranslator').returns(true); + it('renders the Save button when force suggestion is off and translation is not the same', () => { + sinon.stub(user.selectors, 'isTranslator').returns(true); - const sendTranslationMock = sinon.spy(); - const [wrapper] = createComponent(sendTranslationMock); + const sendTranslationMock = sinon.spy(); + const [wrapper] = createComponent(sendTranslationMock); - expect(wrapper.find('.action-save')).toHaveLength(1); - expect(wrapper.find('.action-suggest')).toHaveLength(0); - expect(wrapper.find('.action-approve')).toHaveLength(0); + expect(wrapper.find('.action-save')).toHaveLength(1); + expect(wrapper.find('.action-suggest')).toHaveLength(0); + expect(wrapper.find('.action-approve')).toHaveLength(0); - wrapper.find('.action-save').simulate('click'); - expect(sendTranslationMock.calledOnce).toBeTruthy(); + wrapper.find('.action-save').simulate('click'); + expect(sendTranslationMock.calledOnce).toBeTruthy(); - user.selectors.isTranslator.restore(); - }); + user.selectors.isTranslator.restore(); + }); - it('shows a spinner and a disabled Saving button when running request', () => { - sinon.stub(user.selectors, 'isTranslator').returns(true); + it('shows a spinner and a disabled Saving button when running request', () => { + sinon.stub(user.selectors, 'isTranslator').returns(true); - const sendTranslationMock = sinon.spy(); - const [wrapper, store] = createComponent(sendTranslationMock); + const sendTranslationMock = sinon.spy(); + const [wrapper, store] = createComponent(sendTranslationMock); - store.dispatch(editor.actions.startUpdateTranslation()); - wrapper.update(); + store.dispatch(editor.actions.startUpdateTranslation()); + wrapper.update(); - expect(wrapper.find('.action-save')).toHaveLength(1); - expect(wrapper.find('.action-save .fa-spin')).toHaveLength(1); + expect(wrapper.find('.action-save')).toHaveLength(1); + expect(wrapper.find('.action-save .fa-spin')).toHaveLength(1); - wrapper.find('.action-save').simulate('click'); - expect(sendTranslationMock.calledOnce).toBeFalsy(); + wrapper.find('.action-save').simulate('click'); + expect(sendTranslationMock.calledOnce).toBeFalsy(); - user.selectors.isTranslator.restore(); - }); + user.selectors.isTranslator.restore(); + }); }); diff --git a/translate/src/core/editor/components/EditorMainAction.tsx b/translate/src/core/editor/components/EditorMainAction.tsx index 2cb97c380..15919d0ee 100644 --- a/translate/src/core/editor/components/EditorMainAction.tsx +++ b/translate/src/core/editor/components/EditorMainAction.tsx @@ -7,7 +7,7 @@ import * as user from '~/core/user'; import * as editor from '..'; type Props = { - sendTranslation: (ignoreWarnings?: boolean) => void; + sendTranslation: (ignoreWarnings?: boolean) => void; }; /** @@ -22,107 +22,103 @@ type Props = { * Otherwise, it renders "Save". */ export default function EditorMainAction( - props: Props, + props: Props, ): React.ReactElement { - const isRunningRequest = useAppSelector( - (state) => state.editor.isRunningRequest, - ); - const forceSuggestions = useAppSelector( - (state) => state.user.settings.forceSuggestions, - ); - const isTranslator = useAppSelector((state) => - user.selectors.isTranslator(state), - ); - const sameExistingTranslation = useAppSelector((state) => - editor.selectors.sameExistingTranslation(state), - ); + const isRunningRequest = useAppSelector( + (state) => state.editor.isRunningRequest, + ); + const forceSuggestions = useAppSelector( + (state) => state.user.settings.forceSuggestions, + ); + const isTranslator = useAppSelector((state) => + user.selectors.isTranslator(state), + ); + const sameExistingTranslation = useAppSelector((state) => + editor.selectors.sameExistingTranslation(state), + ); - const updateTranslationStatus = editor.useUpdateTranslationStatus(); + const updateTranslationStatus = editor.useUpdateTranslationStatus(); - function approveTranslation() { - if (sameExistingTranslation) { - updateTranslationStatus(sameExistingTranslation.pk, 'approve'); - } + function approveTranslation() { + if (sameExistingTranslation) { + updateTranslationStatus(sameExistingTranslation.pk, 'approve'); } + } - let btn: { - id: string; - className: string; - action: (event: React.SyntheticEvent) => void; - title: string; - label: string; - glyph: React.ReactElement<'i'> | null | undefined; + let btn: { + id: string; + className: string; + action: (event: React.SyntheticEvent) => void; + title: string; + label: string; + glyph: React.ReactElement<'i'> | null | undefined; + }; + + if ( + isTranslator && + sameExistingTranslation && + !sameExistingTranslation.approved + ) { + // Approve button, will approve the translation. + btn = { + id: 'editor-EditorMenu--button-approve', + className: 'action-approve', + action: approveTranslation, + title: 'Approve Translation (Enter)', + label: 'APPROVE', + glyph: null, }; - if ( - isTranslator && - sameExistingTranslation && - !sameExistingTranslation.approved - ) { - // Approve button, will approve the translation. - btn = { - id: 'editor-EditorMenu--button-approve', - className: 'action-approve', - action: approveTranslation, - title: 'Approve Translation (Enter)', - label: 'APPROVE', - glyph: null, - }; - - if (isRunningRequest) { - btn.id = 'editor-EditorMenu--button-approving'; - btn.label = 'APPROVING'; - btn.glyph = ; - } - } else if (forceSuggestions || !isTranslator) { - // Suggest button, will send an unreviewed translation. - btn = { - id: 'editor-EditorMenu--button-suggest', - className: 'action-suggest', - action: () => props.sendTranslation(), - title: 'Suggest Translation (Enter)', - label: 'SUGGEST', - glyph: null, - }; - - if (isRunningRequest) { - btn.id = 'editor-EditorMenu--button-suggesting'; - btn.label = 'SUGGESTING'; - btn.glyph = ; - } - } else { - // Save button, will send an approved translation. - btn = { - id: 'editor-EditorMenu--button-save', - className: 'action-save', - action: () => props.sendTranslation(), - title: 'Save Translation (Enter)', - label: 'SAVE', - glyph: null, - }; - - if (isRunningRequest) { - btn.id = 'editor-EditorMenu--button-saving'; - btn.label = 'SAVING'; - btn.glyph = ; - } + if (isRunningRequest) { + btn.id = 'editor-EditorMenu--button-approving'; + btn.label = 'APPROVING'; + btn.glyph = ; } + } else if (forceSuggestions || !isTranslator) { + // Suggest button, will send an unreviewed translation. + btn = { + id: 'editor-EditorMenu--button-suggest', + className: 'action-suggest', + action: () => props.sendTranslation(), + title: 'Suggest Translation (Enter)', + label: 'SUGGEST', + glyph: null, + }; - return ( - - - - ); + if (isRunningRequest) { + btn.id = 'editor-EditorMenu--button-suggesting'; + btn.label = 'SUGGESTING'; + btn.glyph = ; + } + } else { + // Save button, will send an approved translation. + btn = { + id: 'editor-EditorMenu--button-save', + className: 'action-save', + action: () => props.sendTranslation(), + title: 'Save Translation (Enter)', + label: 'SAVE', + glyph: null, + }; + + if (isRunningRequest) { + btn.id = 'editor-EditorMenu--button-saving'; + btn.label = 'SAVING'; + btn.glyph = ; + } + } + + return ( + + + + ); } diff --git a/translate/src/core/editor/components/EditorMenu.css b/translate/src/core/editor/components/EditorMenu.css index 38ca67f0c..ad3d444e7 100644 --- a/translate/src/core/editor/components/EditorMenu.css +++ b/translate/src/core/editor/components/EditorMenu.css @@ -1,56 +1,56 @@ .editor-menu { - color: #fff; - padding: 10px; - position: relative; + color: #fff; + padding: 10px; + position: relative; } .editor-menu .actions { - float: right; + float: right; } .editor-menu .actions button { - background: transparent; - border: none; - color: #ebebeb; - height: 40px; - margin: 0 2px; - padding: 10px 3px; + background: transparent; + border: none; + color: #ebebeb; + height: 40px; + margin: 0 2px; + padding: 10px 3px; } .editor-menu .actions button:hover { - color: #7bc876; + color: #7bc876; } .editor-menu .actions button .fa { - margin-right: 4px; + margin-right: 4px; } .editor-menu .actions .action-approve, .editor-menu .actions .action-save, .editor-menu .actions .action-suggest { - background: #7bc876; - border-radius: 3px; - color: #272a2f; - font-weight: 600; - margin-left: 10px; - padding: 10px; + background: #7bc876; + border-radius: 3px; + color: #272a2f; + font-weight: 600; + margin-left: 10px; + padding: 10px; } .editor-menu .actions .action-suggest { - background: #4fc4f6; + background: #4fc4f6; } .editor-menu .actions .action-approve:hover, .editor-menu .actions .action-save:hover, .editor-menu .actions .action-suggest:hover { - color: #ebebeb; + color: #ebebeb; } .editor-menu .banner { - font-style: italic; - line-height: 40px; + font-style: italic; + line-height: 40px; } .editor-menu .banner a { - color: #7bc876; + color: #7bc876; } diff --git a/translate/src/core/editor/components/EditorMenu.test.js b/translate/src/core/editor/components/EditorMenu.test.js index 29b6f1420..9c89c9098 100644 --- a/translate/src/core/editor/components/EditorMenu.test.js +++ b/translate/src/core/editor/components/EditorMenu.test.js @@ -9,91 +9,91 @@ import KeyboardShortcuts from './KeyboardShortcuts'; import TranslationLength from './TranslationLength'; import { - createDefaultUser, - createReduxStore, - mountComponentWithStore, + createDefaultUser, + createReduxStore, + mountComponentWithStore, } from '~/test/store'; const SELECTED_ENTITY = { - pk: 1, - original: 'le test', - original_plural: 'les tests', - translation: [{ string: 'test' }, { string: 'test plural' }], + pk: 1, + original: 'le test', + original_plural: 'les tests', + translation: [{ string: 'test' }, { string: 'test plural' }], }; async function createEditorMenu({ - forceSuggestions = true, - isAuthenticated = true, - entity = SELECTED_ENTITY, - firstItemHook = null, + forceSuggestions = true, + isAuthenticated = true, + entity = SELECTED_ENTITY, + firstItemHook = null, } = {}) { - const store = createReduxStore(); - createDefaultUser(store, { - is_authenticated: isAuthenticated, - settings: { - force_suggestions: forceSuggestions, - }, - }); + const store = createReduxStore(); + createDefaultUser(store, { + is_authenticated: isAuthenticated, + settings: { + force_suggestions: forceSuggestions, + }, + }); - store.dispatch(entities.actions.receive([entity], false)); + store.dispatch(entities.actions.receive([entity], false)); - const wrapper = mountComponentWithStore(EditorMenu, store, { - firstItemHook, - }); + const wrapper = mountComponentWithStore(EditorMenu, store, { + firstItemHook, + }); - await store.dispatch( - navigation.actions.updateEntity(store.getState().router, 1), - ); + await store.dispatch( + navigation.actions.updateEntity(store.getState().router, 1), + ); - return wrapper; + return wrapper; } function expectHiddenSettingsAndActions(wrapper) { - expect(wrapper.find('button')).toHaveLength(0); - expect(wrapper.find(EditorSettings)).toHaveLength(0); - expect(wrapper.find(KeyboardShortcuts)).toHaveLength(0); - expect(wrapper.find(TranslationLength)).toHaveLength(0); - expect(wrapper.find('#editor-EditorMenu--button-copy')).toHaveLength(0); + expect(wrapper.find('button')).toHaveLength(0); + expect(wrapper.find(EditorSettings)).toHaveLength(0); + expect(wrapper.find(KeyboardShortcuts)).toHaveLength(0); + expect(wrapper.find(TranslationLength)).toHaveLength(0); + expect(wrapper.find('#editor-EditorMenu--button-copy')).toHaveLength(0); } describe('', () => { - it('renders correctly', async () => { - const wrapper = await createEditorMenu(); + it('renders correctly', async () => { + const wrapper = await createEditorMenu(); - // 3 buttons to control the editor. - expect(wrapper.find('.action-copy').exists()).toBeTruthy(); - expect(wrapper.find('.action-clear').exists()).toBeTruthy(); - expect(wrapper.find('EditorMainAction')).toHaveLength(1); - }); + // 3 buttons to control the editor. + expect(wrapper.find('.action-copy').exists()).toBeTruthy(); + expect(wrapper.find('.action-clear').exists()).toBeTruthy(); + expect(wrapper.find('EditorMainAction')).toHaveLength(1); + }); - it('hides the settings and actions when the user is logged out', async () => { - const wrapper = await createEditorMenu({ isAuthenticated: false }); + it('hides the settings and actions when the user is logged out', async () => { + const wrapper = await createEditorMenu({ isAuthenticated: false }); - expectHiddenSettingsAndActions(wrapper); + expectHiddenSettingsAndActions(wrapper); - expect( - wrapper.find('#editor-EditorMenu--sign-in-to-translate'), - ).toHaveLength(1); - }); + expect( + wrapper.find('#editor-EditorMenu--sign-in-to-translate'), + ).toHaveLength(1); + }); - it('hides the settings and actions when the entity is read-only', async () => { - const entity = { - ...SELECTED_ENTITY, - readonly: true, - }; - const wrapper = await createEditorMenu({ entity }); + it('hides the settings and actions when the entity is read-only', async () => { + const entity = { + ...SELECTED_ENTITY, + readonly: true, + }; + const wrapper = await createEditorMenu({ entity }); - expectHiddenSettingsAndActions(wrapper); + expectHiddenSettingsAndActions(wrapper); - expect( - wrapper.find('#editor-EditorMenu--read-only-localization'), - ).toHaveLength(1); - }); + expect( + wrapper.find('#editor-EditorMenu--read-only-localization'), + ).toHaveLength(1); + }); - it('accepts a firstItemHook and shows it as its first child', async () => { - const firstItemHook =

    Hello

    ; - const wrapper = await createEditorMenu({ firstItemHook }); + it('accepts a firstItemHook and shows it as its first child', async () => { + const firstItemHook =

    Hello

    ; + const wrapper = await createEditorMenu({ firstItemHook }); - expect(wrapper.find('menu').children().first().text()).toEqual('Hello'); - }); + expect(wrapper.find('menu').children().first().text()).toEqual('Hello'); + }); }); diff --git a/translate/src/core/editor/components/EditorMenu.tsx b/translate/src/core/editor/components/EditorMenu.tsx index 6aad8c1f0..2870f276f 100644 --- a/translate/src/core/editor/components/EditorMenu.tsx +++ b/translate/src/core/editor/components/EditorMenu.tsx @@ -14,11 +14,11 @@ import FailedChecks from './FailedChecks'; import KeyboardShortcuts from './KeyboardShortcuts'; type Props = { - firstItemHook?: React.ReactNode; - translationLengthHook?: React.ReactNode; - clearEditor: () => void; - copyOriginalIntoEditor: () => void; - sendTranslation: (ignoreWarnings?: boolean) => void; + firstItemHook?: React.ReactNode; + translationLengthHook?: React.ReactNode; + clearEditor: () => void; + copyOriginalIntoEditor: () => void; + sendTranslation: (ignoreWarnings?: boolean) => void; }; /** @@ -29,81 +29,75 @@ type Props = { * Otherise, shows the various tools to control the editor. */ export default function EditorMenu(props: Props): React.ReactElement<'menu'> { - return ( - - {props.firstItemHook} - - - - - ); + return ( + + {props.firstItemHook} + + + + + ); } function MenuContent(props: Props) { - const dispatch = useAppDispatch(); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const userState = useAppSelector((state) => state.user); - - if (!userState.isAuthenticated) { - return ( - }} - > -

    {'Sign in to translate.'}

    -
    - ); - } - - if (entity && entity.readonly) { - return ( - -

    This is a read-only localization.

    -
    - ); - } - - function updateSetting(setting: string, value: boolean) { - dispatch(user.actions.saveSetting(setting, value, userState.username)); - } + const dispatch = useAppDispatch(); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const userState = useAppSelector((state) => state.user); + if (!userState.isAuthenticated) { return ( - <> - - - {props.translationLengthHook} -
    - - - - - - - -
    - + }} + > +

    {'Sign in to translate.'}

    +
    ); + } + + if (entity && entity.readonly) { + return ( + +

    This is a read-only localization.

    +
    + ); + } + + function updateSetting(setting: string, value: boolean) { + dispatch(user.actions.saveSetting(setting, value, userState.username)); + } + + return ( + <> + + + {props.translationLengthHook} +
    + + + + + + + +
    + + ); } diff --git a/translate/src/core/editor/components/EditorSettings.css b/translate/src/core/editor/components/EditorSettings.css index 90b933d16..a72700a0f 100644 --- a/translate/src/core/editor/components/EditorSettings.css +++ b/translate/src/core/editor/components/EditorSettings.css @@ -1,69 +1,69 @@ .editor-settings { - float: left; - font-size: 14px; - line-height: 22px; + float: left; + font-size: 14px; + line-height: 22px; } .editor-settings .selector { - cursor: pointer; - color: #aaa; - font-size: 22px; - padding: 9px 5px 9px 0; + cursor: pointer; + color: #aaa; + font-size: 22px; + padding: 9px 5px 9px 0; } .editor-settings .selector:hover { - color: #7bc876; + color: #7bc876; } .editor-settings .menu { - background-color: #272a2f; - bottom: auto; - color: #aaa; - list-style: none; - margin: 0; - max-height: 318px; - overflow: auto; - padding: 10px 12px; - position: absolute; - top: 60px; - width: 185px; - z-index: 20; + background-color: #272a2f; + bottom: auto; + color: #aaa; + list-style: none; + margin: 0; + max-height: 318px; + overflow: auto; + padding: 10px 12px; + position: absolute; + top: 60px; + width: 185px; + z-index: 20; } .editor-settings .menu li { - color: #aaa; - cursor: pointer; - font-weight: 300; - padding: 2px 4px; + color: #aaa; + cursor: pointer; + font-weight: 300; + padding: 2px 4px; } .editor-settings .menu li:hover { - background: #3f4752; + background: #3f4752; } .editor-settings .menu li:hover, .editor-settings .menu li:hover a, .editor-settings .menu li:active a { - color: #fff; + color: #fff; } .editor-settings .menu .horizontal-separator { - border-top: 1px solid #525a65; - height: 0; - margin: 5px 0; - padding: 0; + border-top: 1px solid #525a65; + height: 0; + margin: 5px 0; + padding: 0; } .editor-settings .menu .check-box .fa { - margin-right: 6px; + margin-right: 6px; } .editor-settings .menu .check-box .fa:before { - color: #f36; - content: ''; + color: #f36; + content: ''; } .editor-settings .menu .check-box.enabled .fa:before { - color: #7bc876; - content: ''; + color: #7bc876; + content: ''; } diff --git a/translate/src/core/editor/components/EditorSettings.test.js b/translate/src/core/editor/components/EditorSettings.test.js index 463c2e085..012eceae0 100644 --- a/translate/src/core/editor/components/EditorSettings.test.js +++ b/translate/src/core/editor/components/EditorSettings.test.js @@ -5,62 +5,62 @@ import sinon from 'sinon'; import EditorSettingsBase, { EditorSettings } from './EditorSettings'; function createEditorSettings() { - const toggleSettingMock = sinon.stub(); - const wrapper = shallow( - , - ); - return [wrapper, toggleSettingMock]; + const toggleSettingMock = sinon.stub(); + const wrapper = shallow( + , + ); + return [wrapper, toggleSettingMock]; } describe('', () => { - it('toggles the runQualityChecks setting', () => { - const [wrapper, toggleSettingMock] = createEditorSettings(); + it('toggles the runQualityChecks setting', () => { + const [wrapper, toggleSettingMock] = createEditorSettings(); - // Do it once to turn it on. - wrapper.find('.menu li').at(0).simulate('click'); - expect(toggleSettingMock.calledOnce).toBeTruthy(); - expect(toggleSettingMock.calledWith('runQualityChecks')).toBeTruthy(); + // Do it once to turn it on. + wrapper.find('.menu li').at(0).simulate('click'); + expect(toggleSettingMock.calledOnce).toBeTruthy(); + expect(toggleSettingMock.calledWith('runQualityChecks')).toBeTruthy(); - // Do it twice to turn it off. - wrapper.setProps({ settings: { runQualityChecks: true } }); + // Do it twice to turn it off. + wrapper.setProps({ settings: { runQualityChecks: true } }); - wrapper.find('.menu li').at(0).simulate('click'); - expect(toggleSettingMock.calledTwice).toBeTruthy(); - expect(toggleSettingMock.calledWith('runQualityChecks')).toBeTruthy(); - }); + wrapper.find('.menu li').at(0).simulate('click'); + expect(toggleSettingMock.calledTwice).toBeTruthy(); + expect(toggleSettingMock.calledWith('runQualityChecks')).toBeTruthy(); + }); - it('toggles the forceSuggestions setting', () => { - const [wrapper, toggleSettingMock] = createEditorSettings(); + it('toggles the forceSuggestions setting', () => { + const [wrapper, toggleSettingMock] = createEditorSettings(); - // Do it once to turn it on. - wrapper.find('.menu li').at(1).simulate('click'); - expect(toggleSettingMock.calledOnce).toBeTruthy(); - expect(toggleSettingMock.calledWith('forceSuggestions')).toBeTruthy(); + // Do it once to turn it on. + wrapper.find('.menu li').at(1).simulate('click'); + expect(toggleSettingMock.calledOnce).toBeTruthy(); + expect(toggleSettingMock.calledWith('forceSuggestions')).toBeTruthy(); - // Do it twice to turn it off. - wrapper.setProps({ settings: { forceSuggestions: true } }); + // Do it twice to turn it off. + wrapper.setProps({ settings: { forceSuggestions: true } }); - wrapper.find('.menu li').at(1).simulate('click'); - expect(toggleSettingMock.calledTwice).toBeTruthy(); - expect(toggleSettingMock.calledWith('forceSuggestions')).toBeTruthy(); - }); + wrapper.find('.menu li').at(1).simulate('click'); + expect(toggleSettingMock.calledTwice).toBeTruthy(); + expect(toggleSettingMock.calledWith('forceSuggestions')).toBeTruthy(); + }); }); describe('', () => { - it('toggles the settings menu when clicking the gear icon', () => { - const wrapper = shallow(); - expect(wrapper.find('EditorSettings')).toHaveLength(0); + it('toggles the settings menu when clicking the gear icon', () => { + const wrapper = shallow(); + expect(wrapper.find('EditorSettings')).toHaveLength(0); - wrapper.find('.selector').simulate('click'); - expect(wrapper.find('EditorSettings')).toHaveLength(1); + wrapper.find('.selector').simulate('click'); + expect(wrapper.find('EditorSettings')).toHaveLength(1); - wrapper.find('.selector').simulate('click'); - expect(wrapper.find('EditorSettings')).toHaveLength(0); - }); + wrapper.find('.selector').simulate('click'); + expect(wrapper.find('EditorSettings')).toHaveLength(0); + }); }); diff --git a/translate/src/core/editor/components/EditorSettings.tsx b/translate/src/core/editor/components/EditorSettings.tsx index 3905f42c2..a286b619a 100644 --- a/translate/src/core/editor/components/EditorSettings.tsx +++ b/translate/src/core/editor/components/EditorSettings.tsx @@ -8,119 +8,117 @@ import './EditorSettings.css'; import type { Settings } from '~/core/user'; type Props = { - settings: Settings; - updateSetting: (name: string, value: boolean) => void; + settings: Settings; + updateSetting: (name: string, value: boolean) => void; }; type State = { - visible: boolean; + visible: boolean; }; type EditorSettingsProps = { - settings: Settings; - toggleSetting: (name: string) => void; - onDiscard: () => void; + settings: Settings; + toggleSetting: (name: string) => void; + onDiscard: () => void; }; export function EditorSettings({ - settings, - toggleSetting, - onDiscard, + settings, + toggleSetting, + onDiscard, }: EditorSettingsProps): React.ReactElement<'ul'> { - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); - return ( -
      - }} - > -
    • toggleSetting('runQualityChecks')} - > - {'Translate Toolkit Checks'} -
    • -
      + return ( +
        + }} + > +
      • toggleSetting('runQualityChecks')} + > + {'Translate Toolkit Checks'} +
      • +
        - }} - > -
      • toggleSetting('forceSuggestions')} - > - {'Make Suggestions'} -
      • -
        + }} + > +
      • toggleSetting('forceSuggestions')} + > + {'Make Suggestions'} +
      • +
        -
      • -
      • - - {'Change All Settings'} - -
      • -
      - ); +
    • +
    • + + {'Change All Settings'} + +
    • +
    + ); } /* * Renders settings to be used to customize interactions with the Editor. */ export default class EditorSettingsBase extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - visible: false, - }; - } - - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); + constructor(props: Props) { + super(props); + this.state = { + visible: false, }; + } - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; - toggleSetting(name: string) { - this.props.updateSetting(name, !this.props.settings[name]); - this.toggleVisibility(); - } + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; - render(): React.ReactElement<'div'> { - return ( -
    -
    + toggleSetting(name: string) { + this.props.updateSetting(name, !this.props.settings[name]); + this.toggleVisibility(); + } - {this.state.visible && ( - - )} -
    - ); - } + render(): React.ReactElement<'div'> { + return ( +
    +
    + + {this.state.visible && ( + + )} +
    + ); + } } diff --git a/translate/src/core/editor/components/FailedChecks.css b/translate/src/core/editor/components/FailedChecks.css index 2993f318c..b008856c8 100644 --- a/translate/src/core/editor/components/FailedChecks.css +++ b/translate/src/core/editor/components/FailedChecks.css @@ -1,87 +1,87 @@ .failed-checks { - background: #272a2f; - border-top: 1px solid #5e6475; - bottom: 0; - box-sizing: border-box; - left: 0; - line-height: 22px; - min-height: 90px; - padding: 10px; - position: absolute; - text-align: left; - width: 100%; - z-index: 10; + background: #272a2f; + border-top: 1px solid #5e6475; + bottom: 0; + box-sizing: border-box; + left: 0; + line-height: 22px; + min-height: 90px; + padding: 10px; + position: absolute; + text-align: left; + width: 100%; + z-index: 10; } .failed-checks .close { - background: none; - border: none; - color: #aaaaaa; - float: right; - font-size: 24px; - font-weight: 100; - line-height: 22px; - margin-top: -3px; - padding: 0; + background: none; + border: none; + color: #aaaaaa; + float: right; + font-size: 24px; + font-weight: 100; + line-height: 22px; + margin-top: -3px; + padding: 0; } .failed-checks .close:hover { - color: #ffffff; + color: #ffffff; } .failed-checks .title { - color: #f36; - font-size: 14px; - font-weight: 300; - padding-bottom: 5px; + color: #f36; + font-size: 14px; + font-weight: 300; + padding-bottom: 5px; } .failed-checks ul { - font-style: italic; - list-style: none; - margin: 0px; - width: calc(100% - 140px); + font-style: italic; + list-style: none; + margin: 0px; + width: calc(100% - 140px); } .failed-checks ul li:before { - content: ''; - font-family: 'Font Awesome 5 Free'; - font-size: 18px; - font-style: normal; - font-weight: bold; - margin: 0px 8px; - vertical-align: sub; + content: ''; + font-family: 'Font Awesome 5 Free'; + font-size: 18px; + font-style: normal; + font-weight: bold; + margin: 0px 8px; + vertical-align: sub; } .failed-checks .warning { - color: #aaaaaa; + color: #aaaaaa; } .failed-checks .error:before { - color: #f36; + color: #f36; } .failed-checks .warning:before { - color: #4d5967; + color: #4d5967; } .failed-checks .anyway { - background: #7bc876; - border: none; - border-radius: 3px; - bottom: 10px; - color: #272a2f; - font-weight: 600; - margin-left: 10px; - padding: 10px; - position: absolute; - right: 10px; + background: #7bc876; + border: none; + border-radius: 3px; + bottom: 10px; + color: #272a2f; + font-weight: 600; + margin-left: 10px; + padding: 10px; + position: absolute; + right: 10px; } .failed-checks .anyway.suggest { - background: #4fc4f6; + background: #4fc4f6; } .failed-checks .anyway:hover { - color: #ebebeb; + color: #ebebeb; } diff --git a/translate/src/core/editor/components/FailedChecks.test.js b/translate/src/core/editor/components/FailedChecks.test.js index 1f34eecc9..54701c827 100644 --- a/translate/src/core/editor/components/FailedChecks.test.js +++ b/translate/src/core/editor/components/FailedChecks.test.js @@ -6,143 +6,140 @@ import * as project from '~/core/project'; import * as user from '~/core/user'; import { - createDefaultUser, - createReduxStore, - mountComponentWithStore, + createDefaultUser, + createReduxStore, + mountComponentWithStore, } from '~/test/store'; import FailedChecks from './FailedChecks'; function createFailedChecks() { - const store = createReduxStore(); - createDefaultUser(store); - store.dispatch(locale.actions.receive({ code: 'kg' })); - store.dispatch(project.actions.receive({ slug: 'firefox' })); + const store = createReduxStore(); + createDefaultUser(store); + store.dispatch(locale.actions.receive({ code: 'kg' })); + store.dispatch(project.actions.receive({ slug: 'firefox' })); - const comp = mountComponentWithStore(FailedChecks, store, { - sendTranslation: sinon.mock(), - }); + const comp = mountComponentWithStore(FailedChecks, store, { + sendTranslation: sinon.mock(), + }); - return [comp, store]; + return [comp, store]; } describe('', () => { - it('does not render if no errors or warnings present', () => { - const [wrapper] = createFailedChecks(); + it('does not render if no errors or warnings present', () => { + const [wrapper] = createFailedChecks(); - expect(wrapper.find('.failed-checks')).toHaveLength(0); - }); + expect(wrapper.find('.failed-checks')).toHaveLength(0); + }); - it('renders popup with errors and warnings', () => { - const [wrapper, store] = createFailedChecks(); + it('renders popup with errors and warnings', () => { + const [wrapper, store] = createFailedChecks(); - store.dispatch( - editor.actions.updateFailedChecks( - { - clErrors: ['one error'], - pndbWarnings: ['a warning', 'two warnings'], - }, - '', - ), - ); - wrapper.update(); + store.dispatch( + editor.actions.updateFailedChecks( + { + clErrors: ['one error'], + pndbWarnings: ['a warning', 'two warnings'], + }, + '', + ), + ); + wrapper.update(); - expect(wrapper.find('.failed-checks')).toHaveLength(1); - expect(wrapper.find('#editor-FailedChecks--close')).toHaveLength(1); - expect(wrapper.find('#editor-FailedChecks--title')).toHaveLength(1); - expect(wrapper.find('.error')).toHaveLength(1); - expect(wrapper.find('.warning')).toHaveLength(2); - }); + expect(wrapper.find('.failed-checks')).toHaveLength(1); + expect(wrapper.find('#editor-FailedChecks--close')).toHaveLength(1); + expect(wrapper.find('#editor-FailedChecks--title')).toHaveLength(1); + expect(wrapper.find('.error')).toHaveLength(1); + expect(wrapper.find('.warning')).toHaveLength(2); + }); - it('renders save anyway button if translation with warnings submitted', () => { - const [wrapper, store] = createFailedChecks(); + it('renders save anyway button if translation with warnings submitted', () => { + const [wrapper, store] = createFailedChecks(); - store.dispatch( - editor.actions.updateFailedChecks( - { pndbWarnings: ['a warning'] }, - 'submitted', - ), - ); - store.dispatch( - user.actions.update({ - settings: { - force_suggestions: false, - }, - is_authenticated: true, - username: 'Franck', - manager_for_locales: ['kg'], - translator_for_locales: [], - translator_for_projects: {}, - }), - ); - wrapper.update(); + store.dispatch( + editor.actions.updateFailedChecks( + { pndbWarnings: ['a warning'] }, + 'submitted', + ), + ); + store.dispatch( + user.actions.update({ + settings: { + force_suggestions: false, + }, + is_authenticated: true, + username: 'Franck', + manager_for_locales: ['kg'], + translator_for_locales: [], + translator_for_projects: {}, + }), + ); + wrapper.update(); - expect(wrapper.find('.save.anyway')).toHaveLength(1); - }); + expect(wrapper.find('.save.anyway')).toHaveLength(1); + }); - it('renders suggest anyway button if translation with warnings suggested', () => { - const [wrapper, store] = createFailedChecks(); + it('renders suggest anyway button if translation with warnings suggested', () => { + const [wrapper, store] = createFailedChecks(); - store.dispatch( - editor.actions.updateFailedChecks( - { pndbWarnings: ['a warning'] }, - 'submitted', - ), - ); - store.dispatch( - user.actions.update({ - settings: { - force_suggestions: true, - }, - is_authenticated: true, - username: 'Franck', - manager_for_locales: [], - translator_for_locales: [], - translator_for_projects: {}, - }), - ); - wrapper.update(); + store.dispatch( + editor.actions.updateFailedChecks( + { pndbWarnings: ['a warning'] }, + 'submitted', + ), + ); + store.dispatch( + user.actions.update({ + settings: { + force_suggestions: true, + }, + is_authenticated: true, + username: 'Franck', + manager_for_locales: [], + translator_for_locales: [], + translator_for_projects: {}, + }), + ); + wrapper.update(); - expect(wrapper.find('.suggest.anyway')).toHaveLength(1); - }); + expect(wrapper.find('.suggest.anyway')).toHaveLength(1); + }); - it('renders suggest anyway button if user does not have sufficient permissions', () => { - const [wrapper, store] = createFailedChecks(); + it('renders suggest anyway button if user does not have sufficient permissions', () => { + const [wrapper, store] = createFailedChecks(); - store.dispatch( - editor.actions.updateFailedChecks( - { pndbWarnings: ['a warning'] }, - 'submitted', - ), - ); - store.dispatch( - user.actions.update({ - settings: { - force_suggestions: false, - }, - is_authenticated: true, - username: 'Franck', - manager_for_locales: [], - translator_for_locales: [], - translator_for_projects: {}, - }), - ); - wrapper.update(); + store.dispatch( + editor.actions.updateFailedChecks( + { pndbWarnings: ['a warning'] }, + 'submitted', + ), + ); + store.dispatch( + user.actions.update({ + settings: { + force_suggestions: false, + }, + is_authenticated: true, + username: 'Franck', + manager_for_locales: [], + translator_for_locales: [], + translator_for_projects: {}, + }), + ); + wrapper.update(); - expect(wrapper.find('.suggest.anyway')).toHaveLength(1); - }); + expect(wrapper.find('.suggest.anyway')).toHaveLength(1); + }); - it('renders approve anyway button if translation with warnings approved', () => { - const [wrapper, store] = createFailedChecks(); + it('renders approve anyway button if translation with warnings approved', () => { + const [wrapper, store] = createFailedChecks(); - store.dispatch( - editor.actions.updateFailedChecks( - { pndbWarnings: ['a warning'] }, - '', - ), - ); - wrapper.update(); + store.dispatch( + editor.actions.updateFailedChecks({ pndbWarnings: ['a warning'] }, ''), + ); + wrapper.update(); - expect(wrapper.find('.approve.anyway')).toHaveLength(1); - }); + expect(wrapper.find('.approve.anyway')).toHaveLength(1); + }); }); diff --git a/translate/src/core/editor/components/FailedChecks.tsx b/translate/src/core/editor/components/FailedChecks.tsx index 852938f54..4ee2b295f 100644 --- a/translate/src/core/editor/components/FailedChecks.tsx +++ b/translate/src/core/editor/components/FailedChecks.tsx @@ -12,7 +12,7 @@ import type { EditorState } from '~/core/editor'; import type { UserState } from '~/core/user'; type FailedChecksProps = { - sendTranslation: (ignoreWarnings?: boolean) => void; + sendTranslation: (ignoreWarnings?: boolean) => void; }; /** @@ -20,121 +20,118 @@ type FailedChecksProps = { * those checks and proceed anyway. */ export default function FailedChecks( - props: FailedChecksProps, + props: FailedChecksProps, ): null | React.ReactElement<'div'> { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const errors = useAppSelector((state) => state.editor.errors); - const warnings = useAppSelector((state) => state.editor.warnings); - const source = useAppSelector((state) => state.editor.source); - const userState = useAppSelector((state) => state.user); - const isTranslator = useAppSelector(user.selectors.isTranslator); + const errors = useAppSelector((state) => state.editor.errors); + const warnings = useAppSelector((state) => state.editor.warnings); + const source = useAppSelector((state) => state.editor.source); + const userState = useAppSelector((state) => state.user); + const isTranslator = useAppSelector(user.selectors.isTranslator); - const updateTranslationStatus = useUpdateTranslationStatus(); + const updateTranslationStatus = useUpdateTranslationStatus(); - if (!errors.length && !warnings.length) { - return null; + if (!errors.length && !warnings.length) { + return null; + } + + function resetChecks() { + dispatch(actions.resetFailedChecks()); + } + + function approveAnyway() { + if (typeof source === 'number') { + updateTranslationStatus(source, 'approve', true); } + } - function resetChecks() { - dispatch(actions.resetFailedChecks()); - } + function submitAnyway() { + props.sendTranslation(true); + } - function approveAnyway() { - if (typeof source === 'number') { - updateTranslationStatus(source, 'approve', true); - } - } - - function submitAnyway() { - props.sendTranslation(true); - } - - return ( -
    - - - - -

    THE FOLLOWING CHECKS HAVE FAILED

    -
    -
      - {errors.map((error, key) => ( -
    • - {error} -
    • - ))} - {warnings.map((warning, key) => ( -
    • - {warning} -
    • - ))} -
    - -
    - ); + return ( +
    + + + + +

    THE FOLLOWING CHECKS HAVE FAILED

    +
    +
      + {errors.map((error, key) => ( +
    • + {error} +
    • + ))} + {warnings.map((warning, key) => ( +
    • + {warning} +
    • + ))} +
    + +
    + ); } type MainActionProps = { - source: EditorState['source']; - user: UserState; - isTranslator: boolean; - errors: Array; - approveAnyway: () => void; - submitAnyway: () => void; + source: EditorState['source']; + user: UserState; + isTranslator: boolean; + errors: Array; + approveAnyway: () => void; + submitAnyway: () => void; }; /** * Shows a button to ignore failed checks and proceed with the main editor action. */ function MainAction(props: MainActionProps) { - const { source, user, isTranslator, errors, approveAnyway, submitAnyway } = - props; + const { source, user, isTranslator, errors, approveAnyway, submitAnyway } = + props; - if (source === 'stored' || errors.length) { - return null; - } - - if (source !== 'submitted') { - return ( - - - - ); - } - - if (user.settings.forceSuggestions || !isTranslator) { - return ( - - - - ); - } + if (source === 'stored' || errors.length) { + return null; + } + if (source !== 'submitted') { return ( - - - + + + ); + } + + if (user.settings.forceSuggestions || !isTranslator) { + return ( + + + + ); + } + + return ( + + + + ); } diff --git a/translate/src/core/editor/components/KeyboardShortcuts.css b/translate/src/core/editor/components/KeyboardShortcuts.css index 5d3dd5cc8..2adc91558 100644 --- a/translate/src/core/editor/components/KeyboardShortcuts.css +++ b/translate/src/core/editor/components/KeyboardShortcuts.css @@ -1,60 +1,60 @@ .keyboard-shortcuts { - color: #aaaaaa; - cursor: pointer; - float: left; + color: #aaaaaa; + cursor: pointer; + float: left; } .keyboard-shortcuts .selector { - font-size: 22px; - padding: 9px 5px; + font-size: 22px; + padding: 9px 5px; } .keyboard-shortcuts .overlay { - background: #272a2f; - border: 1px solid #333941; - font-size: 13px; - width: 400px; - height: 435px; - left: 0; - top: 0; - bottom: 0; - right: 0; - position: fixed; - z-index: 20; - margin: auto; - padding: 10px 24px; + background: #272a2f; + border: 1px solid #333941; + font-size: 13px; + width: 400px; + height: 435px; + left: 0; + top: 0; + bottom: 0; + right: 0; + position: fixed; + z-index: 20; + margin: auto; + padding: 10px 24px; } .keyboard-shortcuts .overlay h2 { - font-size: 20px; - font-weight: 300; - padding-bottom: 30px; - text-align: center; + font-size: 20px; + font-weight: 300; + padding-bottom: 30px; + text-align: center; } .keyboard-shortcuts .overlay table { - width: 100%; + width: 100%; } .keyboard-shortcuts .overlay td { - padding: 0; + padding: 0; } .keyboard-shortcuts .overlay td:first-child { - font-weight: 300; - height: 35px; - width: 60%; + font-weight: 300; + height: 35px; + width: 60%; } .keyboard-shortcuts .overlay td:last-child { - white-space: nowrap; + white-space: nowrap; } .keyboard-shortcuts .overlay span { - background: #3f4752; - border: 1px solid #5e6475; - border-radius: 2px; - font-weight: 300; - margin: 0 4px; - padding: 0 4px; + background: #3f4752; + border: 1px solid #5e6475; + border-radius: 2px; + font-weight: 300; + margin: 0 4px; + padding: 0 4px; } diff --git a/translate/src/core/editor/components/KeyboardShortcuts.tsx b/translate/src/core/editor/components/KeyboardShortcuts.tsx index 4d5a53745..5e87c99ab 100644 --- a/translate/src/core/editor/components/KeyboardShortcuts.tsx +++ b/translate/src/core/editor/components/KeyboardShortcuts.tsx @@ -8,257 +8,245 @@ import './KeyboardShortcuts.css'; type Props = {}; type State = { - visible: boolean; + visible: boolean; }; type KeyboardShortcutsProps = { - onDiscard: () => void; + onDiscard: () => void; }; function KeyboardShortcuts({ onDiscard }: KeyboardShortcutsProps) { - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); - return ( -
    - -

    KEYBOARD SHORTCUTS

    + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); + return ( +
    + +

    KEYBOARD SHORTCUTS

    +
    + + + + + -
    Save Translation
    - - - - - - , - }} - > - - - - - - - - , - }} - > - - - - - - - - , - mod1: , - }} - > - - - - - - - - , - mod1: , - }} - > - - - - - - - - , - mod1: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - - - - - , - mod1: , - mod2: , - }} - > - - - - -
    Save Translation{'Enter'}
    Cancel Translation{'Esc'}
    Insert A New Line - {'Shift + Enter'} -
    Go To Previous String{'Alt + Up'}
    Go To Next String{'Alt + Down'}
    Copy From Source - { - 'Ctrl + Shift + C' - } -
    Clear Translation - { - 'Ctrl + Shift + Backspace' - } -
    Search Strings - { - 'Ctrl + Shift + F' - } -
    Select All Strings - { - 'Ctrl + Shift + A' - } -
    Copy From Previous Helper - { - 'Ctrl + Shift + Up' - } -
    Copy From Next Helper - { - 'Ctrl + Shift + Down' - } -
    -
    - ); + , + }} + > +
    {'Enter'}
    Cancel Translation{'Esc'}
    Insert A New Line{'Shift + Enter'}
    Go To Previous String{'Alt + Up'}
    Go To Next String{'Alt + Down'}
    Copy From Source + {'Ctrl + Shift + C'} +
    Clear Translation + { + 'Ctrl + Shift + Backspace' + } +
    Search Strings + {'Ctrl + Shift + F'} +
    Select All Strings + {'Ctrl + Shift + A'} +
    Copy From Previous Helper + {'Ctrl + Shift + Up'} +
    Copy From Next Helper + {'Ctrl + Shift + Down'} +
    +
    + ); } /* * Shows a list of keyboard shortcuts. */ export default class KeyboardShortcutsBase extends React.Component< - Props, - State + Props, + State > { - constructor(props: Props) { - super(props); - this.state = { - visible: false, - }; - } - - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); + constructor(props: Props) { + super(props); + this.state = { + visible: false, }; + } - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; - render(): React.ReactElement<'div'> { - return ( -
    - -
    - + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; - {this.state.visible && ( - - )} -
    - ); - } + render(): React.ReactElement<'div'> { + return ( +
    + +
    + + + {this.state.visible && ( + + )} +
    + ); + } } diff --git a/translate/src/core/editor/components/TranslationLength.css b/translate/src/core/editor/components/TranslationLength.css index ce808752a..076b386f2 100644 --- a/translate/src/core/editor/components/TranslationLength.css +++ b/translate/src/core/editor/components/TranslationLength.css @@ -1,10 +1,10 @@ .translation-length { - color: #aaaaaa; - float: left; - line-height: 22px; - padding: 9px 5px; + color: #aaaaaa; + float: left; + line-height: 22px; + padding: 9px 5px; } .translation-length .countdown .overflow { - color: #f36; + color: #f36; } diff --git a/translate/src/core/editor/components/TranslationLength.test.js b/translate/src/core/editor/components/TranslationLength.test.js index bd1314f88..0bdfbe03b 100644 --- a/translate/src/core/editor/components/TranslationLength.test.js +++ b/translate/src/core/editor/components/TranslationLength.test.js @@ -4,108 +4,108 @@ import { shallow } from 'enzyme'; import TranslationLength from './TranslationLength'; describe('', () => { - const LENGTH_ENTITY = { - comment: '', - original: '12345', - original_plural: '123456', - }; + const LENGTH_ENTITY = { + comment: '', + original: '12345', + original_plural: '123456', + }; - const COUNTDOWN_ENTITY = { - comment: 'MAX_LENGTH: 5\nThis is an actual comment.', - format: 'lang', - original: '12345', - }; + const COUNTDOWN_ENTITY = { + comment: 'MAX_LENGTH: 5\nThis is an actual comment.', + format: 'lang', + original: '12345', + }; - it('shows translation length and original string length', () => { - const wrapper = shallow( - , - ); + it('shows translation length and original string length', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.countdown')).toHaveLength(0); - expect( - wrapper.find('.translation-vs-original').childAt(0).text(), - ).toEqual('7'); - expect( - wrapper.find('.translation-vs-original').childAt(1).text(), - ).toEqual('|'); - expect( - wrapper.find('.translation-vs-original').childAt(2).text(), - ).toEqual('5'); - }); + expect(wrapper.find('.countdown')).toHaveLength(0); + expect(wrapper.find('.translation-vs-original').childAt(0).text()).toEqual( + '7', + ); + expect(wrapper.find('.translation-vs-original').childAt(1).text()).toEqual( + '|', + ); + expect(wrapper.find('.translation-vs-original').childAt(2).text()).toEqual( + '5', + ); + }); - it('shows translation length and plural original string length', () => { - const wrapper = shallow( - , - ); + it('shows translation length and plural original string length', () => { + const wrapper = shallow( + , + ); - expect( - wrapper.find('.translation-vs-original').childAt(2).text(), - ).toEqual('6'); - }); + expect(wrapper.find('.translation-vs-original').childAt(2).text()).toEqual( + '6', + ); + }); - it('shows countdown if MAX_LENGTH provided in LANG entity comment', () => { - const wrapper = shallow( - , - ); + it('shows countdown if MAX_LENGTH provided in LANG entity comment', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.translation-vs-original')).toHaveLength(0); - expect(wrapper.find('.countdown span').text()).toEqual('2'); - expect(wrapper.find('.countdown span.overflow')).toHaveLength(0); - }); + expect(wrapper.find('.translation-vs-original')).toHaveLength(0); + expect(wrapper.find('.countdown span').text()).toEqual('2'); + expect(wrapper.find('.countdown span.overflow')).toHaveLength(0); + }); - it('marks countdown overflow', () => { - const wrapper = shallow( - , - ); + it('marks countdown overflow', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.countdown span.overflow')).toHaveLength(1); - }); + expect(wrapper.find('.countdown span.overflow')).toHaveLength(1); + }); - it('strips html from translation when calculating countdown', () => { - const wrapper = shallow( - , - ); + it('strips html from translation when calculating countdown', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.countdown span').text()).toEqual('-1'); - }); + expect(wrapper.find('.countdown span').text()).toEqual('-1'); + }); - it('does not strips html from translation when calculating length', () => { - const wrapper = shallow( - , - ); + it('does not strips html from translation when calculating length', () => { + const wrapper = shallow( + , + ); - expect( - wrapper.find('.translation-vs-original').childAt(0).text(), - ).toEqual('19'); - }); + expect(wrapper.find('.translation-vs-original').childAt(0).text()).toEqual( + '19', + ); + }); }); diff --git a/translate/src/core/editor/components/TranslationLength.tsx b/translate/src/core/editor/components/TranslationLength.tsx index c9c7d3e58..18d69e380 100644 --- a/translate/src/core/editor/components/TranslationLength.tsx +++ b/translate/src/core/editor/components/TranslationLength.tsx @@ -3,10 +3,10 @@ import * as React from 'react'; import './TranslationLength.css'; type Props = { - comment: string; - format: string; - original: string; - translation: string; + comment: string; + format: string; + original: string; + translation: string; }; /** @@ -17,63 +17,59 @@ type Props = { * is provided for strings without HTML tags, so they need to be stripped. */ export default class TranslationLength extends React.Component { - getLimit(): null | number { - const { comment, format } = this.props; + getLimit(): null | number { + const { comment, format } = this.props; - if (format !== 'lang') { - return null; - } - - const parts = comment.split('\n'); - - if (parts[0].startsWith('MAX_LENGTH')) { - try { - return parseInt( - parts[0].split('MAX_LENGTH: ')[1].split(' ')[0], - 10, - ); - } catch (e) { - // Catch unexpected comment structure - } - } - - return null; + if (format !== 'lang') { + return null; } - // Only used for countdown. - // Source: https://stackoverflow.com/a/47140708 - stripHTML(translation: string): string { - const doc = new DOMParser().parseFromString(translation, 'text/html'); + const parts = comment.split('\n'); - if (!doc.body) { - return ''; - } - - return doc.body.textContent || ''; + if (parts[0].startsWith('MAX_LENGTH')) { + try { + return parseInt(parts[0].split('MAX_LENGTH: ')[1].split(' ')[0], 10); + } catch (e) { + // Catch unexpected comment structure + } } - render(): React.ReactElement<'div'> { - const { original, translation } = this.props; + return null; + } - const limit = this.getLimit(); - const translationLength = this.stripHTML(translation).length; - const countdown = limit !== null ? limit - translationLength : null; + // Only used for countdown. + // Source: https://stackoverflow.com/a/47140708 + stripHTML(translation: string): string { + const doc = new DOMParser().parseFromString(translation, 'text/html'); - return ( -
    - {countdown !== null ? ( -
    - - {countdown} - -
    - ) : ( -
    - {translation.length}| - {original.length} -
    - )} -
    - ); + if (!doc.body) { + return ''; } + + return doc.body.textContent || ''; + } + + render(): React.ReactElement<'div'> { + const { original, translation } = this.props; + + const limit = this.getLimit(); + const translationLength = this.stripHTML(translation).length; + const countdown = limit !== null ? limit - translationLength : null; + + return ( +
    + {countdown !== null ? ( +
    + + {countdown} + +
    + ) : ( +
    + {translation.length}|{original.length} +
    + )} +
    + ); + } } diff --git a/translate/src/core/editor/hooks/useAddTextToTranslation.ts b/translate/src/core/editor/hooks/useAddTextToTranslation.ts index b8ba1be4c..f7765723a 100644 --- a/translate/src/core/editor/hooks/useAddTextToTranslation.ts +++ b/translate/src/core/editor/hooks/useAddTextToTranslation.ts @@ -7,15 +7,15 @@ import { actions } from '..'; * Return a function to add text to the content of the editor. */ export default function useAddTextToTranslation(): ( - content: string, - changeSource?: string, + content: string, + changeSource?: string, ) => void { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - return useCallback( - (content: string, changeSource?: string) => { - dispatch(actions.updateSelection(content, changeSource)); - }, - [dispatch], - ); + return useCallback( + (content: string, changeSource?: string) => { + dispatch(actions.updateSelection(content, changeSource)); + }, + [dispatch], + ); } diff --git a/translate/src/core/editor/hooks/useClearEditor.ts b/translate/src/core/editor/hooks/useClearEditor.ts index 9f3dfdb70..fe2a00ced 100644 --- a/translate/src/core/editor/hooks/useClearEditor.ts +++ b/translate/src/core/editor/hooks/useClearEditor.ts @@ -4,9 +4,9 @@ import useUpdateTranslation from './useUpdateTranslation'; * Return a function to clear the content of the editor. */ export default function useClearEditor(): () => void { - const updateTranslation = useUpdateTranslation(); + const updateTranslation = useUpdateTranslation(); - return () => { - updateTranslation(''); - }; + return () => { + updateTranslation(''); + }; } diff --git a/translate/src/core/editor/hooks/useCopyMachineryTranslation.ts b/translate/src/core/editor/hooks/useCopyMachineryTranslation.ts index 0198c93e6..2970cdfca 100644 --- a/translate/src/core/editor/hooks/useCopyMachineryTranslation.ts +++ b/translate/src/core/editor/hooks/useCopyMachineryTranslation.ts @@ -14,68 +14,62 @@ import useUpdateTranslation from './useUpdateTranslation'; * Return a function to copy the original translation into the editor. */ export default function useCopyMachineryTranslation( - entity?: number | null, + entity?: number | null, ): (translation: MachineryTranslation) => void { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const addTextToTranslation = useAddTextToTranslation(); - const updateTranslation = useUpdateTranslation(); - const updateMachinerySources = useCallback( - (machinerySources: Array, machineryTranslation: string) => { - dispatch( - actions.updateMachinerySources( - machinerySources, - machineryTranslation, - ), - ); - }, - [dispatch], - ); + const addTextToTranslation = useAddTextToTranslation(); + const updateTranslation = useUpdateTranslation(); + const updateMachinerySources = useCallback( + (machinerySources: Array, machineryTranslation: string) => { + dispatch( + actions.updateMachinerySources(machinerySources, machineryTranslation), + ); + }, + [dispatch], + ); - const isReadOnlyEditor = useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), - ); - const isFluentTranslationMessage = useAppSelector((state) => - editor.selectors.isFluentTranslationMessage(state), - ); + const isReadOnlyEditor = useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ); + const isFluentTranslationMessage = useAppSelector((state) => + editor.selectors.isFluentTranslationMessage(state), + ); - return useCallback( - (translation: MachineryTranslation) => { - if (isReadOnlyEditor) { - return; - } + return useCallback( + (translation: MachineryTranslation) => { + if (isReadOnlyEditor) { + return; + } - // Ignore if selecting text - if (window.getSelection().toString()) { - return; - } + // Ignore if selecting text + if (window.getSelection().toString()) { + return; + } - // If there is no entity then it is a search term and it is - // added to the editor instead of replacing the editor content - if (!entity) { - addTextToTranslation(translation.translation); - } - // This is a Fluent Message, thus we are in the RichEditor. - // Handle machinery differently. - else if (isFluentTranslationMessage) { - addTextToTranslation(translation.translation, 'machinery'); - } - // By default replace editor content - else { - updateTranslation(translation.translation, 'machinery'); - } - updateMachinerySources( - translation.sources, - translation.translation, - ); - }, - [ - isFluentTranslationMessage, - isReadOnlyEditor, - entity, - addTextToTranslation, - updateTranslation, - updateMachinerySources, - ], - ); + // If there is no entity then it is a search term and it is + // added to the editor instead of replacing the editor content + if (!entity) { + addTextToTranslation(translation.translation); + } + // This is a Fluent Message, thus we are in the RichEditor. + // Handle machinery differently. + else if (isFluentTranslationMessage) { + addTextToTranslation(translation.translation, 'machinery'); + } + // By default replace editor content + else { + updateTranslation(translation.translation, 'machinery'); + } + updateMachinerySources(translation.sources, translation.translation); + }, + [ + isFluentTranslationMessage, + isReadOnlyEditor, + entity, + addTextToTranslation, + updateTranslation, + updateMachinerySources, + ], + ); } diff --git a/translate/src/core/editor/hooks/useCopyOriginalIntoEditor.ts b/translate/src/core/editor/hooks/useCopyOriginalIntoEditor.ts index dfef9f33f..3b2264314 100644 --- a/translate/src/core/editor/hooks/useCopyOriginalIntoEditor.ts +++ b/translate/src/core/editor/hooks/useCopyOriginalIntoEditor.ts @@ -8,22 +8,22 @@ import useUpdateTranslation from './useUpdateTranslation'; * Return a function to copy the original translation into the editor. */ export default function useCopyOriginalIntoEditor(): () => void { - const updateTranslation = useUpdateTranslation(); + const updateTranslation = useUpdateTranslation(); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const pluralForm = useAppSelector((state) => - plural.selectors.getPluralForm(state), - ); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const pluralForm = useAppSelector((state) => + plural.selectors.getPluralForm(state), + ); - return () => { - if (entity) { - if (pluralForm === -1 || pluralForm === 0) { - updateTranslation(entity.original, 'original'); - } else { - updateTranslation(entity.original_plural, 'original'); - } - } - }; + return () => { + if (entity) { + if (pluralForm === -1 || pluralForm === 0) { + updateTranslation(entity.original, 'original'); + } else { + updateTranslation(entity.original_plural, 'original'); + } + } + }; } diff --git a/translate/src/core/editor/hooks/useCopyOtherLocaleTranslation.ts b/translate/src/core/editor/hooks/useCopyOtherLocaleTranslation.ts index a327e8378..a311c1448 100644 --- a/translate/src/core/editor/hooks/useCopyOtherLocaleTranslation.ts +++ b/translate/src/core/editor/hooks/useCopyOtherLocaleTranslation.ts @@ -10,26 +10,26 @@ import useUpdateTranslation from './useUpdateTranslation'; * Return a function to copy the other locale translation into the editor. */ export default function useCopyOtherLocaleTranslation(): ( - translation: OtherLocaleTranslation, + translation: OtherLocaleTranslation, ) => void { - const updateTranslation = useUpdateTranslation(); - const isReadOnlyEditor = useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), - ); + const updateTranslation = useUpdateTranslation(); + const isReadOnlyEditor = useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ); - return useCallback( - (translation: OtherLocaleTranslation) => { - if (isReadOnlyEditor) { - return; - } + return useCallback( + (translation: OtherLocaleTranslation) => { + if (isReadOnlyEditor) { + return; + } - // Ignore if selecting text - if (window.getSelection().toString()) { - return; - } + // Ignore if selecting text + if (window.getSelection().toString()) { + return; + } - updateTranslation(translation.translation, 'otherlocales'); - }, - [isReadOnlyEditor, updateTranslation], - ); + updateTranslation(translation.translation, 'otherlocales'); + }, + [isReadOnlyEditor, updateTranslation], + ); } diff --git a/translate/src/core/editor/hooks/useHandleShortcuts.ts b/translate/src/core/editor/hooks/useHandleShortcuts.ts index 91ad92027..007bff9d8 100644 --- a/translate/src/core/editor/hooks/useHandleShortcuts.ts +++ b/translate/src/core/editor/hooks/useHandleShortcuts.ts @@ -7,166 +7,160 @@ import * as unsavedchanges from '~/modules/unsavedchanges'; * Return a function to handle shortcuts in a translation form. */ export default function useHandleShortcuts(): ( + event: React.KeyboardEvent, + sendTranslation: (ignoreWarnings?: boolean, translation?: string) => void, + clearEditorCustom?: () => void, + copyOriginalIntoEditorCustom?: () => void, +) => void { + const dispatch = useAppDispatch(); + + const clearEditor = editor.useClearEditor(); + const copyMachineryTranslation = editor.useCopyMachineryTranslation(); + const copyOriginalIntoEditor = editor.useCopyOriginalIntoEditor(); + const copyOtherLocaleTranslation = editor.useCopyOtherLocaleTranslation(); + const updateTranslationStatus = editor.useUpdateTranslationStatus(); + + const editorState = useAppSelector((state) => state.editor); + const unsavedChangesShown = useAppSelector( + (state) => state.unsavedchanges.shown, + ); + const isReadOnlyEditor = useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ); + const sameExistingTranslation = useAppSelector((state) => + editor.selectors.sameExistingTranslation(state), + ); + + const machineryTranslations = useAppSelector( + (state) => state.machinery.translations, + ); + const concordanceSearchResults = useAppSelector( + (state) => state.machinery.searchResults, + ); + const otherLocaleTranslations = useAppSelector( + (state) => state.otherlocales.translations, + ); + + return ( event: React.KeyboardEvent, sendTranslation: (ignoreWarnings?: boolean, translation?: string) => void, clearEditorCustom?: () => void, copyOriginalIntoEditorCustom?: () => void, -) => void { - const dispatch = useAppDispatch(); + ) => { + const clearEditorFn = clearEditorCustom || clearEditor; + const copyOriginalIntoEditorFn = + copyOriginalIntoEditorCustom || copyOriginalIntoEditor; - const clearEditor = editor.useClearEditor(); - const copyMachineryTranslation = editor.useCopyMachineryTranslation(); - const copyOriginalIntoEditor = editor.useCopyOriginalIntoEditor(); - const copyOtherLocaleTranslation = editor.useCopyOtherLocaleTranslation(); - const updateTranslationStatus = editor.useUpdateTranslationStatus(); + const key = event.keyCode; - const editorState = useAppSelector((state) => state.editor); - const unsavedChangesShown = useAppSelector( - (state) => state.unsavedchanges.shown, - ); - const isReadOnlyEditor = useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), - ); - const sameExistingTranslation = useAppSelector((state) => - editor.selectors.sameExistingTranslation(state), - ); + // Disable keyboard shortcuts when editor is in read only. + if (isReadOnlyEditor) { + return; + } - const machineryTranslations = useAppSelector( - (state) => state.machinery.translations, - ); - const concordanceSearchResults = useAppSelector( - (state) => state.machinery.searchResults, - ); - const otherLocaleTranslations = useAppSelector( - (state) => state.otherlocales.translations, - ); + // On Enter: + // - If unsaved changes popup is shown, proceed. + // - If failed checks popup is shown after approving a translation, approve it anyway. + // - In other cases, send current translation. + if (key === 13 && !event.ctrlKey && !event.shiftKey && !event.altKey) { + event.preventDefault(); - return ( - event: React.KeyboardEvent, - sendTranslation: ( - ignoreWarnings?: boolean, - translation?: string, - ) => void, - clearEditorCustom?: () => void, - copyOriginalIntoEditorCustom?: () => void, - ) => { - const clearEditorFn = clearEditorCustom || clearEditor; - const copyOriginalIntoEditorFn = - copyOriginalIntoEditorCustom || copyOriginalIntoEditor; + const errors = editorState.errors; + const warnings = editorState.warnings; + const source = editorState.source; + const ignoreWarnings = !!(errors.length || warnings.length); - const key = event.keyCode; + // There are unsaved changes, proceed. + if (unsavedChangesShown) { + dispatch(unsavedchanges.actions.ignore()); + } + // Approve anyway. + else if (typeof source === 'number') { + updateTranslationStatus(source, 'approve', ignoreWarnings); + } else if (sameExistingTranslation && !sameExistingTranslation.approved) { + updateTranslationStatus( + sameExistingTranslation.pk, + 'approve', + ignoreWarnings, + ); + } + // Send translation. + else { + sendTranslation(ignoreWarnings); + } + } - // Disable keyboard shortcuts when editor is in read only. - if (isReadOnlyEditor) { - return; - } + // On Esc, close unsaved changes and failed checks popups if open. + if (key === 27) { + event.preventDefault(); - // On Enter: - // - If unsaved changes popup is shown, proceed. - // - If failed checks popup is shown after approving a translation, approve it anyway. - // - In other cases, send current translation. - if (key === 13 && !event.ctrlKey && !event.shiftKey && !event.altKey) { - event.preventDefault(); + const errors = editorState.errors; + const warnings = editorState.warnings; - const errors = editorState.errors; - const warnings = editorState.warnings; - const source = editorState.source; - const ignoreWarnings = !!(errors.length || warnings.length); + // Close unsaved changes popup + if (unsavedChangesShown) { + dispatch(unsavedchanges.actions.hide()); + } + // Close failed checks popup + else if (errors.length || warnings.length) { + dispatch(editor.actions.resetFailedChecks()); + } + } - // There are unsaved changes, proceed. - if (unsavedChangesShown) { - dispatch(unsavedchanges.actions.ignore()); - } - // Approve anyway. - else if (typeof source === 'number') { - updateTranslationStatus(source, 'approve', ignoreWarnings); - } else if ( - sameExistingTranslation && - !sameExistingTranslation.approved - ) { - updateTranslationStatus( - sameExistingTranslation.pk, - 'approve', - ignoreWarnings, - ); - } - // Send translation. - else { - sendTranslation(ignoreWarnings); - } - } + // On Ctrl + Shift + C, copy the original translation. + if (key === 67 && event.ctrlKey && event.shiftKey && !event.altKey) { + event.preventDefault(); + copyOriginalIntoEditorFn(); + } - // On Esc, close unsaved changes and failed checks popups if open. - if (key === 27) { - event.preventDefault(); + // On Ctrl + Shift + Backspace, clear the content. + if (key === 8 && event.ctrlKey && event.shiftKey && !event.altKey) { + event.preventDefault(); + clearEditorFn(); + } - const errors = editorState.errors; - const warnings = editorState.warnings; + // On Ctrl + Shift + Up/Down, copy next/previous entry from active + // helper tab (Machinery or Locales) into translation. + if (event.ctrlKey && event.shiftKey && !event.altKey) { + if (key !== 38 && key !== 40) { + return; + } - // Close unsaved changes popup - if (unsavedChangesShown) { - dispatch(unsavedchanges.actions.hide()); - } - // Close failed checks popup - else if (errors.length || warnings.length) { - dispatch(editor.actions.resetFailedChecks()); - } - } + let translations; + let searchResults; + let copyTranslationFn; + if (editorState.selectedHelperTabIndex === 0) { + translations = machineryTranslations; + searchResults = concordanceSearchResults; + copyTranslationFn = copyMachineryTranslation; + } else { + translations = otherLocaleTranslations; + copyTranslationFn = copyOtherLocaleTranslation; + } - // On Ctrl + Shift + C, copy the original translation. - if (key === 67 && event.ctrlKey && event.shiftKey && !event.altKey) { - event.preventDefault(); - copyOriginalIntoEditorFn(); - } + const numTranslations = + translations.length + (searchResults?.length || 0); + if (!numTranslations) { + return; + } - // On Ctrl + Shift + Backspace, clear the content. - if (key === 8 && event.ctrlKey && event.shiftKey && !event.altKey) { - event.preventDefault(); - clearEditorFn(); - } + event.preventDefault(); - // On Ctrl + Shift + Up/Down, copy next/previous entry from active - // helper tab (Machinery or Locales) into translation. - if (event.ctrlKey && event.shiftKey && !event.altKey) { - if (key !== 38 && key !== 40) { - return; - } + const currentIdx = editorState.selectedHelperElementIndex; + let nextIdx; + if (key === 40) { + nextIdx = (currentIdx + 1) % numTranslations; + } else { + nextIdx = (currentIdx - 1 + numTranslations) % numTranslations; + } - let translations; - let searchResults; - let copyTranslationFn; - if (editorState.selectedHelperTabIndex === 0) { - translations = machineryTranslations; - searchResults = concordanceSearchResults; - copyTranslationFn = copyMachineryTranslation; - } else { - translations = otherLocaleTranslations; - copyTranslationFn = copyOtherLocaleTranslation; - } + dispatch(editor.actions.selectHelperElementIndex(nextIdx)); - const numTranslations = - translations.length + (searchResults?.length || 0); - if (!numTranslations) { - return; - } - - event.preventDefault(); - - const currentIdx = editorState.selectedHelperElementIndex; - let nextIdx; - if (key === 40) { - nextIdx = (currentIdx + 1) % numTranslations; - } else { - nextIdx = (currentIdx - 1 + numTranslations) % numTranslations; - } - - dispatch(editor.actions.selectHelperElementIndex(nextIdx)); - - const newTranslation = - !searchResults || nextIdx < translations.length - ? translations[nextIdx] - : searchResults[nextIdx - translations.length]; - copyTranslationFn(newTranslation); - } - }; + const newTranslation = + !searchResults || nextIdx < translations.length + ? translations[nextIdx] + : searchResults[nextIdx - translations.length]; + copyTranslationFn(newTranslation); + } + }; } diff --git a/translate/src/core/editor/hooks/useReplaceSelectionContent.ts b/translate/src/core/editor/hooks/useReplaceSelectionContent.ts index 684565a02..0cb6cd613 100644 --- a/translate/src/core/editor/hooks/useReplaceSelectionContent.ts +++ b/translate/src/core/editor/hooks/useReplaceSelectionContent.ts @@ -4,31 +4,28 @@ import { useAppDispatch, useAppSelector } from '~/hooks'; import * as editor from '~/core/editor'; export default function useReplaceSelectionContent( - updateTranslationSelectionWith: (content: string, source: string) => void, + updateTranslationSelectionWith: (content: string, source: string) => void, ) { - const dispatch = useAppDispatch(); - const changeSource = useAppSelector((state) => state.editor.changeSource); - const selectionReplacementContent = useAppSelector( - (state) => state.editor.selectionReplacementContent, - ); + const dispatch = useAppDispatch(); + const changeSource = useAppSelector((state) => state.editor.changeSource); + const selectionReplacementContent = useAppSelector( + (state) => state.editor.selectionReplacementContent, + ); - React.useEffect(() => { - // If there is content to add to the editor, do so, then remove - // the content so it isn't added again. - // This is an abuse of the redux store, because we want to update - // the content differently for each Editor type. Thus each Editor - // must use this hook and pass it a function specific to its needs. - if (selectionReplacementContent) { - updateTranslationSelectionWith( - selectionReplacementContent, - changeSource, - ); - dispatch(editor.actions.resetSelection()); - } - }, [ - changeSource, - selectionReplacementContent, - dispatch, - updateTranslationSelectionWith, - ]); + React.useEffect(() => { + // If there is content to add to the editor, do so, then remove + // the content so it isn't added again. + // This is an abuse of the redux store, because we want to update + // the content differently for each Editor type. Thus each Editor + // must use this hook and pass it a function specific to its needs. + if (selectionReplacementContent) { + updateTranslationSelectionWith(selectionReplacementContent, changeSource); + dispatch(editor.actions.resetSelection()); + } + }, [ + changeSource, + selectionReplacementContent, + dispatch, + updateTranslationSelectionWith, + ]); } diff --git a/translate/src/core/editor/hooks/useSendTranslation.ts b/translate/src/core/editor/hooks/useSendTranslation.ts index ec19dea62..4ae25d95a 100644 --- a/translate/src/core/editor/hooks/useSendTranslation.ts +++ b/translate/src/core/editor/hooks/useSendTranslation.ts @@ -9,69 +9,69 @@ import { actions } from '..'; * Return a function to send a translation to the server. */ export default function useSendTranslation(): ( - ignoreWarnings?: boolean, - content?: string, + ignoreWarnings?: boolean, + content?: string, ) => void { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const translation = useAppSelector((state) => state.editor.translation); - const isRunningRequest = useAppSelector( - (state) => state.editor.isRunningRequest, - ); - const machinerySources = useAppSelector( - (state) => state.editor.machinerySources, - ); - const machineryTranslation = useAppSelector( - (state) => state.editor.machineryTranslation, - ); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const locale = useAppSelector((state) => state.locale); - const user = useAppSelector((state) => state.user); - const router = useAppSelector((state) => state.router); - const pluralForm = useAppSelector((state) => - plural.selectors.getPluralForm(state), - ); - const nextEntity = useAppSelector((state) => - entities.selectors.getNextEntity(state), - ); - const parameters = useAppSelector((state) => - navigation.selectors.getNavigationParams(state), - ); + const translation = useAppSelector((state) => state.editor.translation); + const isRunningRequest = useAppSelector( + (state) => state.editor.isRunningRequest, + ); + const machinerySources = useAppSelector( + (state) => state.editor.machinerySources, + ); + const machineryTranslation = useAppSelector( + (state) => state.editor.machineryTranslation, + ); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const locale = useAppSelector((state) => state.locale); + const user = useAppSelector((state) => state.user); + const router = useAppSelector((state) => state.router); + const pluralForm = useAppSelector((state) => + plural.selectors.getPluralForm(state), + ); + const nextEntity = useAppSelector((state) => + entities.selectors.getNextEntity(state), + ); + const parameters = useAppSelector((state) => + navigation.selectors.getNavigationParams(state), + ); - return (ignoreWarnings?: boolean, content?: string) => { - if (isRunningRequest || !entity || !locale) { - return; - } + return (ignoreWarnings?: boolean, content?: string) => { + if (isRunningRequest || !entity || !locale) { + return; + } - const translationContent = content || translation; + const translationContent = content || translation; - if (typeof translationContent !== 'string') { - throw new Error( - 'Trying to save an unsupported non-string translation: ' + - typeof translationContent, - ); - } + if (typeof translationContent !== 'string') { + throw new Error( + 'Trying to save an unsupported non-string translation: ' + + typeof translationContent, + ); + } - let realMachinerySources = machinerySources; - if (realMachinerySources && machineryTranslation !== translation) { - realMachinerySources = []; - } + let realMachinerySources = machinerySources; + if (realMachinerySources && machineryTranslation !== translation) { + realMachinerySources = []; + } - dispatch( - actions.sendTranslation( - entity, - translationContent, - locale, - pluralForm, - user.settings.forceSuggestions, - nextEntity, - router, - parameters.resource, - ignoreWarnings, - realMachinerySources, - ), - ); - }; + dispatch( + actions.sendTranslation( + entity, + translationContent, + locale, + pluralForm, + user.settings.forceSuggestions, + nextEntity, + router, + parameters.resource, + ignoreWarnings, + realMachinerySources, + ), + ); + }; } diff --git a/translate/src/core/editor/hooks/useUpdateTranslation.ts b/translate/src/core/editor/hooks/useUpdateTranslation.ts index 49cca8696..5f55089bc 100644 --- a/translate/src/core/editor/hooks/useUpdateTranslation.ts +++ b/translate/src/core/editor/hooks/useUpdateTranslation.ts @@ -9,15 +9,15 @@ import { useAppDispatch } from '~/hooks'; * Return a function to update the content of the editor. */ export default function useUpdateTranslation(): ( - translation: Translation, - changeSource?: string, + translation: Translation, + changeSource?: string, ) => void { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - return useCallback( - (translation: Translation, changeSource?: string) => { - dispatch(actions.update(translation, changeSource)); - }, - [dispatch], - ); + return useCallback( + (translation: Translation, changeSource?: string) => { + dispatch(actions.update(translation, changeSource)); + }, + [dispatch], + ); } diff --git a/translate/src/core/editor/hooks/useUpdateTranslationStatus.ts b/translate/src/core/editor/hooks/useUpdateTranslationStatus.ts index 9c75b900b..ed91b3186 100644 --- a/translate/src/core/editor/hooks/useUpdateTranslationStatus.ts +++ b/translate/src/core/editor/hooks/useUpdateTranslationStatus.ts @@ -12,48 +12,48 @@ import type { ChangeOperation } from '~/modules/history'; * Return a function to update the status (approved, rejected... ) of a translation. */ export default function useUpdateTranslationStatus(): ( + translationId: number, + change: ChangeOperation, + ignoreWarnings?: boolean | null | undefined, +) => void { + const dispatch = useAppDispatch(); + + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const locale = useAppSelector((state) => state.locale); + const parameters = useAppSelector((state) => + navigation.selectors.getNavigationParams(state), + ); + const pluralForm = useAppSelector((state) => + plural.selectors.getPluralForm(state), + ); + const nextEntity = useAppSelector((state) => + entities.selectors.getNextEntity(state), + ); + const router = useAppSelector((state) => state.router); + + return ( translationId: number, change: ChangeOperation, - ignoreWarnings?: boolean | null | undefined, -) => void { - const dispatch = useAppDispatch(); - - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const locale = useAppSelector((state) => state.locale); - const parameters = useAppSelector((state) => - navigation.selectors.getNavigationParams(state), - ); - const pluralForm = useAppSelector((state) => - plural.selectors.getPluralForm(state), - ); - const nextEntity = useAppSelector((state) => - entities.selectors.getNextEntity(state), - ); - const router = useAppSelector((state) => state.router); - - return ( - translationId: number, - change: ChangeOperation, - ignoreWarnings: boolean | null | undefined, - ) => { - dispatch(async (dispatch) => { - dispatch(actions.startUpdateTranslation()); - await dispatch( - history.actions.updateStatus( - change, - entity, - locale, - parameters.resource, - pluralForm, - translationId, - nextEntity, - router, - ignoreWarnings, - ), - ); - dispatch(actions.endUpdateTranslation()); - }); - }; + ignoreWarnings: boolean | null | undefined, + ) => { + dispatch(async (dispatch) => { + dispatch(actions.startUpdateTranslation()); + await dispatch( + history.actions.updateStatus( + change, + entity, + locale, + parameters.resource, + pluralForm, + translationId, + nextEntity, + router, + ignoreWarnings, + ), + ); + dispatch(actions.endUpdateTranslation()); + }); + }; } diff --git a/translate/src/core/editor/hooks/useUpdateUnsavedChanges.ts b/translate/src/core/editor/hooks/useUpdateUnsavedChanges.ts index b548edd2d..02e157710 100644 --- a/translate/src/core/editor/hooks/useUpdateUnsavedChanges.ts +++ b/translate/src/core/editor/hooks/useUpdateUnsavedChanges.ts @@ -4,69 +4,63 @@ import { useAppDispatch, useAppSelector } from '~/hooks'; import * as unsavedchanges from '~/modules/unsavedchanges'; export default function useUpdateUnsavedChanges(richEditor: boolean) { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const translation = useAppSelector((state) => state.editor.translation); - const initialTranslation = useAppSelector( - (state) => state.editor.initialTranslation, - ); - const unsavedChangesExist = useAppSelector( - (state) => state.unsavedchanges.exist, - ); - const unsavedChangesShown = useAppSelector( - (state) => state.unsavedchanges.shown, - ); + const translation = useAppSelector((state) => state.editor.translation); + const initialTranslation = useAppSelector( + (state) => state.editor.initialTranslation, + ); + const unsavedChangesExist = useAppSelector( + (state) => state.unsavedchanges.exist, + ); + const unsavedChangesShown = useAppSelector( + (state) => state.unsavedchanges.shown, + ); - // When the translation or the initial translation changes, check for unsaved changes. - React.useEffect(() => { - let exist: boolean; - if (typeof translation === 'string') { - if (richEditor) return; - exist = translation !== initialTranslation; - } else { - exist = - typeof initialTranslation !== 'string' && - !translation.equals(initialTranslation); - } + // When the translation or the initial translation changes, check for unsaved changes. + React.useEffect(() => { + let exist: boolean; + if (typeof translation === 'string') { + if (richEditor) return; + exist = translation !== initialTranslation; + } else { + exist = + typeof initialTranslation !== 'string' && + !translation.equals(initialTranslation); + } - if (exist !== unsavedChangesExist) { - dispatch(unsavedchanges.actions.update(exist)); - } - }, [ - richEditor, - translation, - initialTranslation, - unsavedChangesExist, - dispatch, - ]); + if (exist !== unsavedChangesExist) { + dispatch(unsavedchanges.actions.update(exist)); + } + }, [ + richEditor, + translation, + initialTranslation, + unsavedChangesExist, + dispatch, + ]); - // When the translation changes, hide unsaved changes notice. - // We need to track the translation value, because this hook depends on the - // `shown` value of the unsavedchanges module, but we don't want to hide - // the notice automatically after it's displayed. We thus only update when - // the translation has effectively changed. - const prevTranslation = React.useRef(translation); - React.useEffect(() => { - let sameTranslation; - if (richEditor) { - if (typeof translation === 'string') { - return; - } - sameTranslation = - typeof prevTranslation.current !== 'string' && - translation.equals(prevTranslation.current); - } else { - sameTranslation = prevTranslation.current === translation; - } - if (!sameTranslation && unsavedChangesShown) { - dispatch(unsavedchanges.actions.hide()); - } - prevTranslation.current = translation; - }, [ - richEditor, - translation, - prevTranslation, - unsavedChangesShown, - dispatch, - ]); + // When the translation changes, hide unsaved changes notice. + // We need to track the translation value, because this hook depends on the + // `shown` value of the unsavedchanges module, but we don't want to hide + // the notice automatically after it's displayed. We thus only update when + // the translation has effectively changed. + const prevTranslation = React.useRef(translation); + React.useEffect(() => { + let sameTranslation; + if (richEditor) { + if (typeof translation === 'string') { + return; + } + sameTranslation = + typeof prevTranslation.current !== 'string' && + translation.equals(prevTranslation.current); + } else { + sameTranslation = prevTranslation.current === translation; + } + if (!sameTranslation && unsavedChangesShown) { + dispatch(unsavedchanges.actions.hide()); + } + prevTranslation.current = translation; + }, [richEditor, translation, prevTranslation, unsavedChangesShown, dispatch]); } diff --git a/translate/src/core/editor/reducer.ts b/translate/src/core/editor/reducer.ts index 349c76314..a6aea4e16 100644 --- a/translate/src/core/editor/reducer.ts +++ b/translate/src/core/editor/reducer.ts @@ -1,222 +1,216 @@ import type { SourceType } from '~/core/api'; import { - END_UPDATE_TRANSLATION, - RESET_FAILED_CHECKS, - RESET_SELECTION, - RESET_EDITOR, - RESET_HELPER_ELEMENT_INDEX, - SELECT_HELPER_ELEMENT_INDEX, - SELECT_HELPER_TAB_INDEX, - SET_INITIAL_TRANSLATION, - START_UPDATE_TRANSLATION, - UPDATE, - UPDATE_FAILED_CHECKS, - UPDATE_SELECTION, - UPDATE_MACHINERY_SOURCES, + END_UPDATE_TRANSLATION, + RESET_FAILED_CHECKS, + RESET_SELECTION, + RESET_EDITOR, + RESET_HELPER_ELEMENT_INDEX, + SELECT_HELPER_ELEMENT_INDEX, + SELECT_HELPER_TAB_INDEX, + SET_INITIAL_TRANSLATION, + START_UPDATE_TRANSLATION, + UPDATE, + UPDATE_FAILED_CHECKS, + UPDATE_SELECTION, + UPDATE_MACHINERY_SOURCES, } from './actions'; import type { - FailedChecks, - EndUpdateTranslationAction, - InitialTranslationAction, - ResetEditorAction, - ResetHelperElementIndexAction, - ResetFailedChecksAction, - ResetSelectionAction, - SelectHelperElementIndexAction, - SelectHelperTabIndexAction, - StartUpdateTranslationAction, - Translation, - UpdateAction, - UpdateFailedChecksAction, - UpdateSelectionAction, - UpdateMachinerySourcesAction, + FailedChecks, + EndUpdateTranslationAction, + InitialTranslationAction, + ResetEditorAction, + ResetHelperElementIndexAction, + ResetFailedChecksAction, + ResetSelectionAction, + SelectHelperElementIndexAction, + SelectHelperTabIndexAction, + StartUpdateTranslationAction, + Translation, + UpdateAction, + UpdateFailedChecksAction, + UpdateSelectionAction, + UpdateMachinerySourcesAction, } from './actions'; type Action = - | EndUpdateTranslationAction - | InitialTranslationAction - | ResetEditorAction - | ResetHelperElementIndexAction - | ResetFailedChecksAction - | ResetSelectionAction - | SelectHelperElementIndexAction - | SelectHelperTabIndexAction - | StartUpdateTranslationAction - | UpdateAction - | UpdateFailedChecksAction - | UpdateSelectionAction - | UpdateMachinerySourcesAction; + | EndUpdateTranslationAction + | InitialTranslationAction + | ResetEditorAction + | ResetHelperElementIndexAction + | ResetFailedChecksAction + | ResetSelectionAction + | SelectHelperElementIndexAction + | SelectHelperTabIndexAction + | StartUpdateTranslationAction + | UpdateAction + | UpdateFailedChecksAction + | UpdateSelectionAction + | UpdateMachinerySourcesAction; export type EditorState = { - readonly translation: Translation; - // Used for storing the initial translation in Fluent editor, - // needed for detecting unsaved changes. - readonly initialTranslation: Translation; + readonly translation: Translation; + // Used for storing the initial translation in Fluent editor, + // needed for detecting unsaved changes. + readonly initialTranslation: Translation; - // Source of the current change: 'internal' if from inside the Editor, - // 'history', 'machinery', 'otherlocales' if copied from corresponding - // Helper tab, 'external' otherwise. This allows the Editor to behave - // differently depending on the type of change. - readonly changeSource: string; + // Source of the current change: 'internal' if from inside the Editor, + // 'history', 'machinery', 'otherlocales' if copied from corresponding + // Helper tab, 'external' otherwise. This allows the Editor to behave + // differently depending on the type of change. + readonly changeSource: string; - readonly machinerySources: Array; + readonly machinerySources: Array; - readonly machineryTranslation: string; + readonly machineryTranslation: string; - // Order to replace the currently selected text inside the Editor with - // this content. This is reset after that change has been made. Because - // we have different Editor implementations, we need to let those components - // perform the actual replacement logic. - readonly selectionReplacementContent: string; + // Order to replace the currently selected text inside the Editor with + // this content. This is reset after that change has been made. Because + // we have different Editor implementations, we need to let those components + // perform the actual replacement logic. + readonly selectionReplacementContent: string; - readonly errors: Array; - readonly warnings: Array; - // A source of failed checks (errors and warnings). Possible values: - // - '': no failed checks are displayed (default) - // - 'stored': failed checks of the translation stored in the DB - // - 'submitted': failed checks of the submitted translation - // - number (translationId): failed checks of the approved translation - readonly source: '' | 'stored' | 'submitted' | number; + readonly errors: Array; + readonly warnings: Array; + // A source of failed checks (errors and warnings). Possible values: + // - '': no failed checks are displayed (default) + // - 'stored': failed checks of the translation stored in the DB + // - 'submitted': failed checks of the submitted translation + // - number (translationId): failed checks of the approved translation + readonly source: '' | 'stored' | 'submitted' | number; - // True when there is a request to send a new translation running, false - // otherwise. Used to prevent duplicate actions from users spamming their - // keyboard or mouse. - readonly isRunningRequest: boolean; + // True when there is a request to send a new translation running, false + // otherwise. Used to prevent duplicate actions from users spamming their + // keyboard or mouse. + readonly isRunningRequest: boolean; - // Index of selected item in the helpers box - readonly selectedHelperElementIndex: number; - // Index of selected tab in the helpers box. Assumes the following: - // 0 -> Machinery - // 1 -> Other Locales - readonly selectedHelperTabIndex: number; + // Index of selected item in the helpers box + readonly selectedHelperElementIndex: number; + // Index of selected tab in the helpers box. Assumes the following: + // 0 -> Machinery + // 1 -> Other Locales + readonly selectedHelperTabIndex: number; }; /** * Return a list of failed check messages of a given type. */ function extractFailedChecksOfType( - failedChecks: FailedChecks, - type: 'Errors' | 'Warnings', + failedChecks: FailedChecks, + type: 'Errors' | 'Warnings', ): Array { - let extractedFailedChecks = []; - const keys = Object.keys(failedChecks); + let extractedFailedChecks = []; + const keys = Object.keys(failedChecks); - for (const key of keys) { - if (key.endsWith(type)) { - for (const message of failedChecks[key]) { - extractedFailedChecks.push(message); - } - } + for (const key of keys) { + if (key.endsWith(type)) { + for (const message of failedChecks[key]) { + extractedFailedChecks.push(message); + } } + } - return extractedFailedChecks; + return extractedFailedChecks; } const initial: EditorState = { - translation: '', - initialTranslation: '', - changeSource: 'reset', - machinerySources: [], - machineryTranslation: '', - selectionReplacementContent: '', - errors: [], - warnings: [], - source: '', - isRunningRequest: false, - selectedHelperElementIndex: -1, - selectedHelperTabIndex: 0, + translation: '', + initialTranslation: '', + changeSource: 'reset', + machinerySources: [], + machineryTranslation: '', + selectionReplacementContent: '', + errors: [], + warnings: [], + source: '', + isRunningRequest: false, + selectedHelperElementIndex: -1, + selectedHelperTabIndex: 0, }; export default function reducer( - state: EditorState = initial, - action: Action, + state: EditorState = initial, + action: Action, ): EditorState { - switch (action.type) { - case END_UPDATE_TRANSLATION: - return { - ...state, - isRunningRequest: false, - }; - case UPDATE: - return { - ...state, - translation: action.translation, - changeSource: action.changeSource, - source: '', - }; - case UPDATE_FAILED_CHECKS: - return { - ...state, - errors: extractFailedChecksOfType( - action.failedChecks, - 'Errors', - ), - warnings: extractFailedChecksOfType( - action.failedChecks, - 'Warnings', - ), - source: action.source, - }; - case UPDATE_SELECTION: - return { - ...state, - selectionReplacementContent: action.content, - changeSource: action.changeSource, - }; - case SET_INITIAL_TRANSLATION: - return { - ...state, - initialTranslation: action.translation, - machineryTranslation: '', - machinerySources: [], - }; - case START_UPDATE_TRANSLATION: - return { - ...state, - isRunningRequest: true, - }; - case RESET_FAILED_CHECKS: - return { - ...state, - errors: [], - warnings: [], - }; - case RESET_SELECTION: - return { - ...state, - selectionReplacementContent: '', - changeSource: 'internal', - }; - case RESET_EDITOR: - return { - ...initial, - isRunningRequest: state.isRunningRequest, - selectedHelperTabIndex: state.selectedHelperTabIndex, - }; - case UPDATE_MACHINERY_SOURCES: - return { - ...state, - machineryTranslation: action.machineryTranslation, - machinerySources: action.machinerySources, - }; - case RESET_HELPER_ELEMENT_INDEX: - return { - ...state, - selectedHelperElementIndex: -1, - }; - case SELECT_HELPER_ELEMENT_INDEX: - return { - ...state, - selectedHelperElementIndex: action.index, - }; - case SELECT_HELPER_TAB_INDEX: - return { - ...state, - selectedHelperTabIndex: action.index, - }; - default: - return state; - } + switch (action.type) { + case END_UPDATE_TRANSLATION: + return { + ...state, + isRunningRequest: false, + }; + case UPDATE: + return { + ...state, + translation: action.translation, + changeSource: action.changeSource, + source: '', + }; + case UPDATE_FAILED_CHECKS: + return { + ...state, + errors: extractFailedChecksOfType(action.failedChecks, 'Errors'), + warnings: extractFailedChecksOfType(action.failedChecks, 'Warnings'), + source: action.source, + }; + case UPDATE_SELECTION: + return { + ...state, + selectionReplacementContent: action.content, + changeSource: action.changeSource, + }; + case SET_INITIAL_TRANSLATION: + return { + ...state, + initialTranslation: action.translation, + machineryTranslation: '', + machinerySources: [], + }; + case START_UPDATE_TRANSLATION: + return { + ...state, + isRunningRequest: true, + }; + case RESET_FAILED_CHECKS: + return { + ...state, + errors: [], + warnings: [], + }; + case RESET_SELECTION: + return { + ...state, + selectionReplacementContent: '', + changeSource: 'internal', + }; + case RESET_EDITOR: + return { + ...initial, + isRunningRequest: state.isRunningRequest, + selectedHelperTabIndex: state.selectedHelperTabIndex, + }; + case UPDATE_MACHINERY_SOURCES: + return { + ...state, + machineryTranslation: action.machineryTranslation, + machinerySources: action.machinerySources, + }; + case RESET_HELPER_ELEMENT_INDEX: + return { + ...state, + selectedHelperElementIndex: -1, + }; + case SELECT_HELPER_ELEMENT_INDEX: + return { + ...state, + selectedHelperElementIndex: action.index, + }; + case SELECT_HELPER_TAB_INDEX: + return { + ...state, + selectedHelperTabIndex: action.index, + }; + default: + return state; + } } diff --git a/translate/src/core/editor/selectors.test.js b/translate/src/core/editor/selectors.test.js index fc7ff40b5..49ede6529 100644 --- a/translate/src/core/editor/selectors.test.js +++ b/translate/src/core/editor/selectors.test.js @@ -3,168 +3,164 @@ import { fluent } from '~/core/utils'; import { _existingTranslation } from './selectors'; const EDITOR = { - initialTranslation: 'something', + initialTranslation: 'something', }; const EDITOR_FLUENT = { - initialTranslation: fluent.parser.parseEntry('msg = something'), + initialTranslation: fluent.parser.parseEntry('msg = something'), }; const ENTITY = { - format: 'po', + format: 'po', }; const ENTITY_FLUENT = { - original: 'msg = Allez Morty !', - format: 'ftl', + original: 'msg = Allez Morty !', + format: 'ftl', }; const ACTIVE_TRANSLATION = { pk: 1 }; const HISTORY = { - translations: [ - { - pk: 12, - string: 'I was there before', - }, - { - pk: 98, - string: 'hello, world!', - }, - { - pk: 10010, - string: '', - }, - ], + translations: [ + { + pk: 12, + string: 'I was there before', + }, + { + pk: 98, + string: 'hello, world!', + }, + { + pk: 10010, + string: '', + }, + ], }; const HISTORY_FLUENT = { - translations: [ - { - pk: 12, - string: 'msg = I like { -brand }', - }, - { - pk: 98, - string: 'msg = hello, world!', - }, - { - pk: 431, - string: 'msg = Come on Morty!\n', - }, - ], + translations: [ + { + pk: 12, + string: 'msg = I like { -brand }', + }, + { + pk: 98, + string: 'msg = hello, world!', + }, + { + pk: 431, + string: 'msg = Come on Morty!\n', + }, + ], }; describe('sameExistingTranslation', () => { - it('finds identical initial/active translation', () => { - expect( - _existingTranslation( - { ...EDITOR, translation: EDITOR.initialTranslation }, - ACTIVE_TRANSLATION, - HISTORY, - ENTITY, - ), - ).toEqual(ACTIVE_TRANSLATION); - }); + it('finds identical initial/active translation', () => { + expect( + _existingTranslation( + { ...EDITOR, translation: EDITOR.initialTranslation }, + ACTIVE_TRANSLATION, + HISTORY, + ENTITY, + ), + ).toEqual(ACTIVE_TRANSLATION); + }); - it('finds identical Fluent initial/active translation', () => { - expect( - _existingTranslation( - { - ...EDITOR_FLUENT, - translation: EDITOR_FLUENT.initialTranslation, - }, - ACTIVE_TRANSLATION, - HISTORY_FLUENT, - ENTITY_FLUENT, - ), - ).toEqual(ACTIVE_TRANSLATION); - }); + it('finds identical Fluent initial/active translation', () => { + expect( + _existingTranslation( + { + ...EDITOR_FLUENT, + translation: EDITOR_FLUENT.initialTranslation, + }, + ACTIVE_TRANSLATION, + HISTORY_FLUENT, + ENTITY_FLUENT, + ), + ).toEqual(ACTIVE_TRANSLATION); + }); - it('finds empty initial/active translation', () => { - expect( - _existingTranslation( - { translation: '', initialTranslation: '' }, - ACTIVE_TRANSLATION, - HISTORY, - ENTITY, - ), - ).toEqual(ACTIVE_TRANSLATION); - }); + it('finds empty initial/active translation', () => { + expect( + _existingTranslation( + { translation: '', initialTranslation: '' }, + ACTIVE_TRANSLATION, + HISTORY, + ENTITY, + ), + ).toEqual(ACTIVE_TRANSLATION); + }); - it('finds identical translation in history', () => { - expect( - _existingTranslation( - { ...EDITOR, translation: HISTORY.translations[0].string }, - ACTIVE_TRANSLATION, - HISTORY, - ENTITY, - ), - ).toEqual(HISTORY.translations[0]); + it('finds identical translation in history', () => { + expect( + _existingTranslation( + { ...EDITOR, translation: HISTORY.translations[0].string }, + ACTIVE_TRANSLATION, + HISTORY, + ENTITY, + ), + ).toEqual(HISTORY.translations[0]); - expect( - _existingTranslation( - { ...EDITOR, translation: HISTORY.translations[1].string }, - ACTIVE_TRANSLATION, - HISTORY, - ENTITY, - ), - ).toEqual(HISTORY.translations[1]); - }); + expect( + _existingTranslation( + { ...EDITOR, translation: HISTORY.translations[1].string }, + ACTIVE_TRANSLATION, + HISTORY, + ENTITY, + ), + ).toEqual(HISTORY.translations[1]); + }); - it('finds identical Fluent translation in history', () => { - expect( - _existingTranslation( - { - ...EDITOR_FLUENT, - translation: fluent.flattenMessage( - fluent.parser.parseEntry( - HISTORY_FLUENT.translations[0].string, - ), - ), - }, - ACTIVE_TRANSLATION, - HISTORY_FLUENT, - ENTITY_FLUENT, - ), - ).toEqual(HISTORY_FLUENT.translations[0]); + it('finds identical Fluent translation in history', () => { + expect( + _existingTranslation( + { + ...EDITOR_FLUENT, + translation: fluent.flattenMessage( + fluent.parser.parseEntry(HISTORY_FLUENT.translations[0].string), + ), + }, + ACTIVE_TRANSLATION, + HISTORY_FLUENT, + ENTITY_FLUENT, + ), + ).toEqual(HISTORY_FLUENT.translations[0]); - expect( - _existingTranslation( - { - ...EDITOR_FLUENT, - translation: fluent.flattenMessage( - fluent.parser.parseEntry( - HISTORY_FLUENT.translations[1].string, - ), - ), - }, - ACTIVE_TRANSLATION, - HISTORY_FLUENT, - ENTITY_FLUENT, - ), - ).toEqual(HISTORY_FLUENT.translations[1]); - }); + expect( + _existingTranslation( + { + ...EDITOR_FLUENT, + translation: fluent.flattenMessage( + fluent.parser.parseEntry(HISTORY_FLUENT.translations[1].string), + ), + }, + ACTIVE_TRANSLATION, + HISTORY_FLUENT, + ENTITY_FLUENT, + ), + ).toEqual(HISTORY_FLUENT.translations[1]); + }); - it('finds empty translation in history', () => { - expect( - _existingTranslation( - { ...EDITOR, translation: '' }, - ACTIVE_TRANSLATION, - HISTORY, - ENTITY, - ), - ).toEqual(HISTORY.translations[2]); - }); + it('finds empty translation in history', () => { + expect( + _existingTranslation( + { ...EDITOR, translation: '' }, + ACTIVE_TRANSLATION, + HISTORY, + ENTITY, + ), + ).toEqual(HISTORY.translations[2]); + }); - it('finds a Fluent translation in history from a simplified Fluent string', () => { - expect( - _existingTranslation( - { initialTranslation: '', translation: 'Come on Morty!' }, - ACTIVE_TRANSLATION, - HISTORY_FLUENT, - ENTITY_FLUENT, - ), - ).toEqual(HISTORY_FLUENT.translations[2]); - }); + it('finds a Fluent translation in history from a simplified Fluent string', () => { + expect( + _existingTranslation( + { initialTranslation: '', translation: 'Come on Morty!' }, + ACTIVE_TRANSLATION, + HISTORY_FLUENT, + ENTITY_FLUENT, + ), + ).toEqual(HISTORY_FLUENT.translations[2]); + }); }); diff --git a/translate/src/core/editor/selectors.ts b/translate/src/core/editor/selectors.ts index 1cab2a2a9..46abca7fe 100644 --- a/translate/src/core/editor/selectors.ts +++ b/translate/src/core/editor/selectors.ts @@ -12,70 +12,70 @@ const editorSelector = (state: RootState) => state[NAME]; const historySelector = (state: RootState) => state[history.NAME]; export function _existingTranslation( - editor: ReturnType, - activeTranslation: ReturnType< - typeof plural.selectors.getTranslationForSelectedEntity - >, - history: ReturnType, - entity: ReturnType, + editor: ReturnType, + activeTranslation: ReturnType< + typeof plural.selectors.getTranslationForSelectedEntity + >, + history: ReturnType, + entity: ReturnType, ) { - const { translation, initialTranslation } = editor; + const { translation, initialTranslation } = editor; - if ( - activeTranslation && - activeTranslation.pk && - // If translation is a string, from the generic editor. - (translation === initialTranslation || - // If translation is a FluentMessage, from the fluent editor. - (typeof translation !== 'string' && - typeof initialTranslation !== 'string' && - translation.equals(initialTranslation))) - ) { - return activeTranslation; - } - if (history.translations.length === 0) { - return undefined; - } - // If translation is a FluentMessage, from the fluent editor. - if (typeof translation !== 'string') { - // We apply a bunch of logic on the stored translation, to - // make it work with our Editor. So here, we need to - // re-serialize it and re-parse it to make sure the translation - // object is a "clean" one, as produced by Fluent. Otherwise - // we encounter bugs when comparing it with the history items. - const fluentTranslation = fluent.parser.parseEntry( - fluent.serializer.serializeEntry(translation), - ); - return history.translations.find((t) => - fluentTranslation.equals(fluent.parser.parseEntry(t.string)), - ); - } + if ( + activeTranslation && + activeTranslation.pk && // If translation is a string, from the generic editor. - // Except it might actually be a Fluent message from the Simple or Source - // editors. - if (entity.format === 'ftl') { - // For Fluent files, the translation can be stored as a simple string - // when the Source editor or the Simple editor are on. Because of that, - // we want to turn the string into a Fluent message, as that's simpler - // to handle and less prone to errors. We do the same for each history - // entry. - let fluentTranslation = fluent.parser.parseEntry(translation); - if (fluentTranslation.type === 'Junk') { - // If the message was junk, it means we are likely in the Simple - // editor, and we thus want to reconstruct the Fluent message. - // Note that if the user is actually in the Source editor, and - // entered an invalid value (which creates this junk entry), - // it doesn't matter as there shouldn't be anything matching anyway. - fluentTranslation = fluent.getReconstructedMessage( - entity.original, - translation, - ); - } - return history.translations.find((t) => - fluentTranslation.equals(fluent.parser.parseEntry(t.string)), - ); + (translation === initialTranslation || + // If translation is a FluentMessage, from the fluent editor. + (typeof translation !== 'string' && + typeof initialTranslation !== 'string' && + translation.equals(initialTranslation))) + ) { + return activeTranslation; + } + if (history.translations.length === 0) { + return undefined; + } + // If translation is a FluentMessage, from the fluent editor. + if (typeof translation !== 'string') { + // We apply a bunch of logic on the stored translation, to + // make it work with our Editor. So here, we need to + // re-serialize it and re-parse it to make sure the translation + // object is a "clean" one, as produced by Fluent. Otherwise + // we encounter bugs when comparing it with the history items. + const fluentTranslation = fluent.parser.parseEntry( + fluent.serializer.serializeEntry(translation), + ); + return history.translations.find((t) => + fluentTranslation.equals(fluent.parser.parseEntry(t.string)), + ); + } + // If translation is a string, from the generic editor. + // Except it might actually be a Fluent message from the Simple or Source + // editors. + if (entity.format === 'ftl') { + // For Fluent files, the translation can be stored as a simple string + // when the Source editor or the Simple editor are on. Because of that, + // we want to turn the string into a Fluent message, as that's simpler + // to handle and less prone to errors. We do the same for each history + // entry. + let fluentTranslation = fluent.parser.parseEntry(translation); + if (fluentTranslation.type === 'Junk') { + // If the message was junk, it means we are likely in the Simple + // editor, and we thus want to reconstruct the Fluent message. + // Note that if the user is actually in the Source editor, and + // entered an invalid value (which creates this junk entry), + // it doesn't matter as there shouldn't be anything matching anyway. + fluentTranslation = fluent.getReconstructedMessage( + entity.original, + translation, + ); } - return history.translations.find((t) => t.string === translation); + return history.translations.find((t) => + fluentTranslation.equals(fluent.parser.parseEntry(t.string)), + ); + } + return history.translations.find((t) => t.string === translation); } /** @@ -86,26 +86,26 @@ export function _existingTranslation( * Othewise, it returns null. */ export const sameExistingTranslation = createSelector( - editorSelector, - plural.selectors.getTranslationForSelectedEntity, - historySelector, - entities.selectors.getSelectedEntity, - _existingTranslation, + editorSelector, + plural.selectors.getTranslationForSelectedEntity, + historySelector, + entities.selectors.getSelectedEntity, + _existingTranslation, ); function _isFluentMessage(editorState: ReturnType) { - return typeof editorState.translation !== 'string'; + return typeof editorState.translation !== 'string'; } /** * Returns `true` if the current editor translation contains a Fluent message. */ const isFluentTranslationMessage = createSelector( - editorSelector, - _isFluentMessage, + editorSelector, + _isFluentMessage, ); export default { - sameExistingTranslation, - isFluentTranslationMessage, + sameExistingTranslation, + isFluentTranslationMessage, }; diff --git a/translate/src/core/entities/actions.ts b/translate/src/core/entities/actions.ts index 70638f532..a1f296804 100644 --- a/translate/src/core/entities/actions.ts +++ b/translate/src/core/entities/actions.ts @@ -9,143 +9,143 @@ export const REQUEST: 'entities/REQUEST' = 'entities/REQUEST'; export const RESET: 'entities/RESET' = 'entities/RESET'; export const UPDATE: 'entities/UPDATE' = 'entities/UPDATE'; export const RECEIVE_SIBLINGS: 'entities/RECEIVE_SIBLINGS' = - 'entities/RECEIVE_SIBLINGS'; + 'entities/RECEIVE_SIBLINGS'; /** * Indicate that entities are currently being fetched. */ export type RequestAction = { - type: typeof REQUEST; + type: typeof REQUEST; }; export function request(): RequestAction { - return { - type: REQUEST, - }; + return { + type: REQUEST, + }; } /** * Update entities to a new set. */ export type ReceiveAction = { - type: typeof RECEIVE; - entities: Entities; - hasMore: boolean; + type: typeof RECEIVE; + entities: Entities; + hasMore: boolean; }; export function receive(entities: Entities, hasMore: boolean): ReceiveAction { - return { - type: RECEIVE, - entities, - hasMore, - }; + return { + type: RECEIVE, + entities, + hasMore, + }; } /** * Update the siblings of an entity. */ export type ReceiveSiblingsAction = { - type: typeof RECEIVE_SIBLINGS; - siblings: EntitySiblings; - entity: number; + type: typeof RECEIVE_SIBLINGS; + siblings: EntitySiblings; + entity: number; }; export function receiveSiblings( - siblings: EntitySiblings, - entity: number, + siblings: EntitySiblings, + entity: number, ): ReceiveSiblingsAction { - return { - type: RECEIVE_SIBLINGS, - siblings, - entity, - }; + return { + type: RECEIVE_SIBLINGS, + siblings, + entity, + }; } /** * Update the active translation of an entity. */ export type UpdateAction = { - type: typeof UPDATE; - entity: number; - pluralForm: number; - translation: EntityTranslation; + type: typeof UPDATE; + entity: number; + pluralForm: number; + translation: EntityTranslation; }; export function updateEntityTranslation( - entity: number, - pluralForm: number, - translation: EntityTranslation, + entity: number, + pluralForm: number, + translation: EntityTranslation, ): UpdateAction { - return { - type: UPDATE, - entity, - pluralForm, - translation, - }; + return { + type: UPDATE, + entity, + pluralForm, + translation, + }; } /** * Fetch entities and their translation. */ export function get( - locale: string, - project: string, - resource: string, - entityIds: Array | null | undefined, - exclude: Array, - entity: string | null | undefined, - search: string | null | undefined, - status: string | null | undefined, - extra: string | null | undefined, - tag: string | null | undefined, - author: string | null | undefined, - time: string | null | undefined, + locale: string, + project: string, + resource: string, + entityIds: Array | null | undefined, + exclude: Array, + entity: string | null | undefined, + search: string | null | undefined, + status: string | null | undefined, + extra: string | null | undefined, + tag: string | null | undefined, + author: string | null | undefined, + time: string | null | undefined, ) { - return async (dispatch: AppDispatch) => { - dispatch(request()); + return async (dispatch: AppDispatch) => { + dispatch(request()); - const content = await api.entity.getEntities( - locale, - project, - resource, - entityIds, - exclude, - entity, - search, - status, - extra, - tag, - author, - time, - false, - ); + const content = await api.entity.getEntities( + locale, + project, + resource, + entityIds, + exclude, + entity, + search, + status, + extra, + tag, + author, + time, + false, + ); - if (content.entities) { - dispatch(receive(content.entities, content.has_next)); - dispatch(stats.actions.update(content.stats)); - } - }; + if (content.entities) { + dispatch(receive(content.entities, content.has_next)); + dispatch(stats.actions.update(content.stats)); + } + }; } export function getSiblingEntities(entity: number, locale: string) { - return async (dispatch: AppDispatch) => { - const siblings = await api.entity.getSiblingEntities(entity, locale); - if (siblings) { - dispatch(receiveSiblings(siblings, entity)); - } - }; + return async (dispatch: AppDispatch) => { + const siblings = await api.entity.getSiblingEntities(entity, locale); + if (siblings) { + dispatch(receiveSiblings(siblings, entity)); + } + }; } export type ResetAction = { - type: typeof RESET; + type: typeof RESET; }; export function reset(): ResetAction { - return { - type: RESET, - }; + return { + type: RESET, + }; } export default { - get, - getSiblingEntities, - receive, - request, - reset, - receiveSiblings, - updateEntityTranslation, + get, + getSiblingEntities, + receive, + request, + reset, + receiveSiblings, + updateEntityTranslation, }; diff --git a/translate/src/core/entities/reducer.ts b/translate/src/core/entities/reducer.ts index fd9b9cbfe..a8d62d116 100644 --- a/translate/src/core/entities/reducer.ts +++ b/translate/src/core/entities/reducer.ts @@ -2,137 +2,137 @@ import { RECEIVE, REQUEST, RESET, UPDATE, RECEIVE_SIBLINGS } from './actions'; import type { Entities, EntityTranslation, EntitySiblings } from '~/core/api'; import type { - ReceiveAction, - RequestAction, - ResetAction, - UpdateAction, - ReceiveSiblingsAction, + ReceiveAction, + RequestAction, + ResetAction, + UpdateAction, + ReceiveSiblingsAction, } from './actions'; export type Action = - | ReceiveAction - | RequestAction - | ResetAction - | UpdateAction - | ReceiveSiblingsAction; + | ReceiveAction + | RequestAction + | ResetAction + | UpdateAction + | ReceiveSiblingsAction; // Read-only state. export type EntitiesState = { - readonly entities: Entities; - readonly fetching: boolean; - readonly fetchCount: number; - readonly hasMore: boolean; + readonly entities: Entities; + readonly fetching: boolean; + readonly fetchCount: number; + readonly hasMore: boolean; }; function updateEntityTranslation( - state: EntitiesState, - entity: number, - pluralForm: number, - translation: EntityTranslation, + state: EntitiesState, + entity: number, + pluralForm: number, + translation: EntityTranslation, ): Entities { - return state.entities.map((item) => { - if (item.pk !== entity) { - return item; - } + return state.entities.map((item) => { + if (item.pk !== entity) { + return item; + } - const translations = [...item.translation]; + const translations = [...item.translation]; - // If the plural form is -1, then there's no plural and we should - // simply update the first translation. - const plural = pluralForm === -1 ? 0 : pluralForm; - translations[plural] = translation; + // If the plural form is -1, then there's no plural and we should + // simply update the first translation. + const plural = pluralForm === -1 ? 0 : pluralForm; + translations[plural] = translation; - return { - ...item, - translation: translations, - }; - }); + return { + ...item, + translation: translations, + }; + }); } function injectSiblingEntities( - entities: Entities, - siblings: EntitySiblings, - entity: number, + entities: Entities, + siblings: EntitySiblings, + entity: number, ): Entities { - const index = entities.findIndex((item) => item.pk === entity); - let parentEntity = entities[index]; - const currentPKs = entities.map((e) => e.pk); - let newEntitiesState = entities.slice(); + const index = entities.findIndex((item) => item.pk === entity); + let parentEntity = entities[index]; + const currentPKs = entities.map((e) => e.pk); + let newEntitiesState = entities.slice(); - // filtering out parent entities already present in the list - const precedingSiblings = siblings.preceding.filter( - (sibling) => !currentPKs.includes(sibling.pk), - ); + // filtering out parent entities already present in the list + const precedingSiblings = siblings.preceding.filter( + (sibling) => !currentPKs.includes(sibling.pk), + ); - const succeedingSiblings = siblings.succeeding.filter( - (sibling) => !currentPKs.includes(sibling.pk), - ); + const succeedingSiblings = siblings.succeeding.filter( + (sibling) => !currentPKs.includes(sibling.pk), + ); - let list = [...precedingSiblings, parentEntity, ...succeedingSiblings]; + let list = [...precedingSiblings, parentEntity, ...succeedingSiblings]; - newEntitiesState.splice(index, 1, ...list); + newEntitiesState.splice(index, 1, ...list); - // This block removes duplicated siblings - newEntitiesState = newEntitiesState.filter( - (currentValue, index, array) => - array.findIndex((e) => e.pk === currentValue.pk) === index, - ); - return newEntitiesState; + // This block removes duplicated siblings + newEntitiesState = newEntitiesState.filter( + (currentValue, index, array) => + array.findIndex((e) => e.pk === currentValue.pk) === index, + ); + return newEntitiesState; } const initial: EntitiesState = { - entities: [], - fetching: false, - fetchCount: 0, - hasMore: true, + entities: [], + fetching: false, + fetchCount: 0, + hasMore: true, }; export default function reducer( - state: EntitiesState = initial, - action: Action, + state: EntitiesState = initial, + action: Action, ): EntitiesState { - switch (action.type) { - case RECEIVE: - return { - ...state, - entities: [...state.entities, ...action.entities], - fetching: false, - fetchCount: state.fetchCount + 1, - hasMore: action.hasMore, - }; - case REQUEST: - return { - ...state, - fetching: true, - hasMore: false, - }; - case RESET: - return { - ...state, - entities: [], - fetching: false, - hasMore: true, - }; - case UPDATE: - return { - ...state, - entities: updateEntityTranslation( - state, - action.entity, - action.pluralForm, - action.translation, - ), - }; - case RECEIVE_SIBLINGS: - return { - ...state, - entities: injectSiblingEntities( - state.entities, - action.siblings, - action.entity, - ), - }; - default: - return state; - } + switch (action.type) { + case RECEIVE: + return { + ...state, + entities: [...state.entities, ...action.entities], + fetching: false, + fetchCount: state.fetchCount + 1, + hasMore: action.hasMore, + }; + case REQUEST: + return { + ...state, + fetching: true, + hasMore: false, + }; + case RESET: + return { + ...state, + entities: [], + fetching: false, + hasMore: true, + }; + case UPDATE: + return { + ...state, + entities: updateEntityTranslation( + state, + action.entity, + action.pluralForm, + action.translation, + ), + }; + case RECEIVE_SIBLINGS: + return { + ...state, + entities: injectSiblingEntities( + state.entities, + action.siblings, + action.entity, + ), + }; + default: + return state; + } } diff --git a/translate/src/core/entities/selectors.test.js b/translate/src/core/entities/selectors.test.js index b87c8e5cb..eb00b8082 100644 --- a/translate/src/core/entities/selectors.test.js +++ b/translate/src/core/entities/selectors.test.js @@ -1,121 +1,121 @@ import { - _getNextEntity, - _getPreviousEntity, - _getSelectedEntity, - _isReadOnlyEditor, + _getNextEntity, + _getPreviousEntity, + _getSelectedEntity, + _isReadOnlyEditor, } from './selectors'; describe('selectors', () => { - describe('getNextEntity', () => { - const ENTITIES = [{ pk: 1 }, { pk: 2 }, { pk: 3 }]; + describe('getNextEntity', () => { + const ENTITIES = [{ pk: 1 }, { pk: 2 }, { pk: 3 }]; - it('returns the next entity in the list', () => { - const params = { entity: 1 }; - const res = _getNextEntity(ENTITIES, params); - expect(res.pk).toEqual(2); - }); - - it('returns the first entity when the last one is selected', () => { - const params = { entity: 3 }; - const res = _getNextEntity(ENTITIES, params); - expect(res.pk).toEqual(1); - }); - - it('returns null when the current entity does not exist', () => { - const params = { entity: 5 }; - const res = _getNextEntity(ENTITIES, params); - expect(res).toBeUndefined(); - }); + it('returns the next entity in the list', () => { + const params = { entity: 1 }; + const res = _getNextEntity(ENTITIES, params); + expect(res.pk).toEqual(2); }); - describe('getPreviousEntity', () => { - const ENTITIES = [{ pk: 1 }, { pk: 2 }, { pk: 3 }]; - - it('returns the previous entity in the list', () => { - const params = { entity: 2 }; - const res = _getPreviousEntity(ENTITIES, params); - expect(res.pk).toEqual(1); - }); - - it('returns the last entity when the first one is selected', () => { - const params = { entity: 1 }; - const res = _getPreviousEntity(ENTITIES, params); - expect(res.pk).toEqual(3); - }); - - it('returns null when the current entity does not exist', () => { - const params = { entity: 5 }; - const res = _getPreviousEntity(ENTITIES, params); - expect(res).toBeUndefined(); - }); + it('returns the first entity when the last one is selected', () => { + const params = { entity: 3 }; + const res = _getNextEntity(ENTITIES, params); + expect(res.pk).toEqual(1); }); - describe('getSelectedEntity', () => { - const entities = [ - { - pk: 1, - original: 'hello', - }, - { - pk: 2, - original: 'world', - }, - ]; + it('returns null when the current entity does not exist', () => { + const params = { entity: 5 }; + const res = _getNextEntity(ENTITIES, params); + expect(res).toBeUndefined(); + }); + }); - it('returns the selected entity', () => { - const navigation = { - entity: 2, - }; - const res = _getSelectedEntity(entities, navigation); + describe('getPreviousEntity', () => { + const ENTITIES = [{ pk: 1 }, { pk: 2 }, { pk: 3 }]; - expect(res.original).toEqual('world'); - }); - - it('returns undefined if the entity is missing', () => { - const navigation = { - entity: 3, - }; - const res = _getSelectedEntity(entities, navigation); - - expect(res).toEqual(undefined); - }); + it('returns the previous entity in the list', () => { + const params = { entity: 2 }; + const res = _getPreviousEntity(ENTITIES, params); + expect(res.pk).toEqual(1); }); - describe('isReadOnlyEditor', () => { - it('returns true if user not authenticated', () => { - const entity = { - readonly: false, - }; - const user = { - isAuthenticated: false, - }; - const res = _isReadOnlyEditor(entity, user); - - expect(res).toBeTruthy(); - }); - - it('returns true if entity read-only', () => { - const entity = { - readonly: true, - }; - const user = { - isAuthenticated: true, - }; - const res = _isReadOnlyEditor(entity, user); - - expect(res).toBeTruthy(); - }); - - it('returns false if entity not read-only and user authenticated', () => { - const entity = { - readonly: false, - }; - const user = { - isAuthenticated: true, - }; - const res = _isReadOnlyEditor(entity, user); - - expect(res).toBeFalsy(); - }); + it('returns the last entity when the first one is selected', () => { + const params = { entity: 1 }; + const res = _getPreviousEntity(ENTITIES, params); + expect(res.pk).toEqual(3); }); + + it('returns null when the current entity does not exist', () => { + const params = { entity: 5 }; + const res = _getPreviousEntity(ENTITIES, params); + expect(res).toBeUndefined(); + }); + }); + + describe('getSelectedEntity', () => { + const entities = [ + { + pk: 1, + original: 'hello', + }, + { + pk: 2, + original: 'world', + }, + ]; + + it('returns the selected entity', () => { + const navigation = { + entity: 2, + }; + const res = _getSelectedEntity(entities, navigation); + + expect(res.original).toEqual('world'); + }); + + it('returns undefined if the entity is missing', () => { + const navigation = { + entity: 3, + }; + const res = _getSelectedEntity(entities, navigation); + + expect(res).toEqual(undefined); + }); + }); + + describe('isReadOnlyEditor', () => { + it('returns true if user not authenticated', () => { + const entity = { + readonly: false, + }; + const user = { + isAuthenticated: false, + }; + const res = _isReadOnlyEditor(entity, user); + + expect(res).toBeTruthy(); + }); + + it('returns true if entity read-only', () => { + const entity = { + readonly: true, + }; + const user = { + isAuthenticated: true, + }; + const res = _isReadOnlyEditor(entity, user); + + expect(res).toBeTruthy(); + }); + + it('returns false if entity not read-only and user authenticated', () => { + const entity = { + readonly: false, + }; + const user = { + isAuthenticated: true, + }; + const res = _isReadOnlyEditor(entity, user); + + expect(res).toBeFalsy(); + }); + }); }); diff --git a/translate/src/core/entities/selectors.ts b/translate/src/core/entities/selectors.ts index 8f745236f..ab484eaf9 100644 --- a/translate/src/core/entities/selectors.ts +++ b/translate/src/core/entities/selectors.ts @@ -14,74 +14,73 @@ const entitiesSelector = (state: RootState) => state[NAME].entities; const userSelector = (state: RootState) => state[user.NAME]; export function _getSelectedEntity( - entities: ReturnType, - params: NavigationParams, + entities: ReturnType, + params: NavigationParams, ): Entity | undefined { - return entities.find((element) => element.pk === params.entity); + return entities.find((element) => element.pk === params.entity); } /** * Return the currently selected Entity object. */ export const getSelectedEntity = createSelector( - entitiesSelector, - navigation.selectors.getNavigationParams, - _getSelectedEntity, + entitiesSelector, + navigation.selectors.getNavigationParams, + _getSelectedEntity, ); export function _getNextEntity( - entities: Entities, - params: NavigationParams, + entities: Entities, + params: NavigationParams, ): Entity | undefined { - const currentIndex = entities.findIndex( - (element) => element.pk === params.entity, - ); + const currentIndex = entities.findIndex( + (element) => element.pk === params.entity, + ); - if (currentIndex === -1) { - return undefined; - } + if (currentIndex === -1) { + return undefined; + } - const next = currentIndex + 1 >= entities.length ? 0 : currentIndex + 1; - return entities[next]; + const next = currentIndex + 1 >= entities.length ? 0 : currentIndex + 1; + return entities[next]; } /** * Return the Entity that follows the current one in the list. */ export const getNextEntity = createSelector( - entitiesSelector, - navigation.selectors.getNavigationParams, - _getNextEntity, + entitiesSelector, + navigation.selectors.getNavigationParams, + _getNextEntity, ); export function _getPreviousEntity( - entities: Entities, - params: NavigationParams, + entities: Entities, + params: NavigationParams, ): Entity | undefined { - const currentIndex = entities.findIndex( - (element) => element.pk === params.entity, - ); + const currentIndex = entities.findIndex( + (element) => element.pk === params.entity, + ); - if (currentIndex === -1) { - return undefined; - } + if (currentIndex === -1) { + return undefined; + } - const previous = - currentIndex === 0 ? entities.length - 1 : currentIndex - 1; - return entities[previous]; + const previous = currentIndex === 0 ? entities.length - 1 : currentIndex - 1; + return entities[previous]; } /** * Return the Entity that preceeds the current one in the list. */ export const getPreviousEntity = createSelector( - entitiesSelector, - navigation.selectors.getNavigationParams, - _getPreviousEntity, + entitiesSelector, + navigation.selectors.getNavigationParams, + _getPreviousEntity, ); export function _isReadOnlyEditor(entity: Entity, user: UserState): boolean { - return (entity && entity.readonly) || !user.isAuthenticated; + return (entity && entity.readonly) || !user.isAuthenticated; } /** @@ -90,14 +89,14 @@ export function _isReadOnlyEditor(entity: Entity, user: UserState): boolean { * - the user is not authenticated */ export const isReadOnlyEditor = createSelector( - getSelectedEntity, - userSelector, - _isReadOnlyEditor, + getSelectedEntity, + userSelector, + _isReadOnlyEditor, ); export default { - getNextEntity, - getPreviousEntity, - getSelectedEntity, - isReadOnlyEditor, + getNextEntity, + getPreviousEntity, + getSelectedEntity, + isReadOnlyEditor, }; diff --git a/translate/src/core/l10n/actions.ts b/translate/src/core/l10n/actions.ts index e3df736e9..505a7f6a9 100644 --- a/translate/src/core/l10n/actions.ts +++ b/translate/src/core/l10n/actions.ts @@ -16,26 +16,26 @@ export const REQUEST: 'l10n/REQUEST' = 'l10n/REQUEST'; * Notify that translations for the UI are being fetched. */ export type RequestAction = { - readonly type: typeof REQUEST; + readonly type: typeof REQUEST; }; export function request(): RequestAction { - return { - type: REQUEST, - }; + return { + type: REQUEST, + }; } /** * Receive translations for a locale. */ export type ReceiveAction = { - readonly type: typeof RECEIVE; - readonly localization: ReactLocalization; + readonly type: typeof RECEIVE; + readonly localization: ReactLocalization; }; export function receive(localization: ReactLocalization): ReceiveAction { - return { - type: RECEIVE, - localization, - }; + return { + type: RECEIVE, + localization, + }; } /** @@ -45,58 +45,55 @@ export function receive(localization: ReactLocalization): ReceiveAction { * those and store them to be used in showing a localized interface. */ export function get(locales: ReadonlyArray) { - return async (dispatch: AppDispatch) => { - dispatch(request()); + return async (dispatch: AppDispatch) => { + dispatch(request()); - // Pseudo localization shows a weirdly translated UI, based on English. - // This is a development only tool that helps verifying that our UI - // is properly localized. - const urlParams = new URLSearchParams(window.location.search); - const usePseudoLocalization = - urlParams.has('pseudolocalization') && - (urlParams.get('pseudolocalization') === 'accented' || - urlParams.get('pseudolocalization') === 'bidi'); - // Setting defaultLocale to `en-US` means that it will always be the - // last fallback locale, thus making sure the UI is always working. - let languages = negotiateLanguages(locales, AVAILABLE_LOCALES, { - defaultLocale: 'en-US', + // Pseudo localization shows a weirdly translated UI, based on English. + // This is a development only tool that helps verifying that our UI + // is properly localized. + const urlParams = new URLSearchParams(window.location.search); + const usePseudoLocalization = + urlParams.has('pseudolocalization') && + (urlParams.get('pseudolocalization') === 'accented' || + urlParams.get('pseudolocalization') === 'bidi'); + // Setting defaultLocale to `en-US` means that it will always be the + // last fallback locale, thus making sure the UI is always working. + let languages = negotiateLanguages(locales, AVAILABLE_LOCALES, { + defaultLocale: 'en-US', + }); + + // For pseudo localization, we only want to serve English. + if (usePseudoLocalization) { + languages = ['en-US']; + } + + const bundles = await Promise.all( + languages.map((locale) => { + return api.l10n.get(locale).then((content) => { + let bundleOptions = {}; + + // We know this is English, let's make it weird before bundling it. + if (usePseudoLocalization) { + bundleOptions = { + transform: PSEUDO_STRATEGIES[urlParams.get('pseudolocalization')], + }; + } + + const bundle = new FluentBundle(locale, bundleOptions); + let resource = new FluentResource(content); + bundle.addResource(resource); + return bundle; }); + }), + ); - // For pseudo localization, we only want to serve English. - if (usePseudoLocalization) { - languages = ['en-US']; - } - - const bundles = await Promise.all( - languages.map((locale) => { - return api.l10n.get(locale).then((content) => { - let bundleOptions = {}; - - // We know this is English, let's make it weird before bundling it. - if (usePseudoLocalization) { - bundleOptions = { - transform: - PSEUDO_STRATEGIES[ - urlParams.get('pseudolocalization') - ], - }; - } - - const bundle = new FluentBundle(locale, bundleOptions); - let resource = new FluentResource(content); - bundle.addResource(resource); - return bundle; - }); - }), - ); - - const localization = new ReactLocalization(bundles); - dispatch(receive(localization)); - }; + const localization = new ReactLocalization(bundles); + dispatch(receive(localization)); + }; } export default { - get, - receive, - request, + get, + receive, + request, }; diff --git a/translate/src/core/l10n/components/AppLocalizationProvider.test.js b/translate/src/core/l10n/components/AppLocalizationProvider.test.js index 2c8b845ae..5c8cf4f4c 100644 --- a/translate/src/core/l10n/components/AppLocalizationProvider.test.js +++ b/translate/src/core/l10n/components/AppLocalizationProvider.test.js @@ -7,50 +7,50 @@ import { shallowUntilTarget } from '~/test/utils'; import { actions } from '..'; import AppLocalizationProvider, { - AppLocalizationProviderBase, + AppLocalizationProviderBase, } from './AppLocalizationProvider'; describe('', () => { - beforeAll(() => { - const getMock = sinon.stub(actions, 'get'); - getMock.returns({ type: 'whatever' }); - }); + beforeAll(() => { + const getMock = sinon.stub(actions, 'get'); + getMock.returns({ type: 'whatever' }); + }); - afterEach(() => { - // Make sure tests do not pollute one another. - actions.get.resetHistory(); - }); + afterEach(() => { + // Make sure tests do not pollute one another. + actions.get.resetHistory(); + }); - afterAll(() => { - actions.get.restore(); - }); + afterAll(() => { + actions.get.restore(); + }); - it('fetches a locale when the component mounts', () => { - const store = createReduxStore(); + it('fetches a locale when the component mounts', () => { + const store = createReduxStore(); - shallowUntilTarget( - -
    - , - AppLocalizationProviderBase, - ); + shallowUntilTarget( + +
    + , + AppLocalizationProviderBase, + ); - expect(actions.get.callCount).toEqual(1); - }); + expect(actions.get.callCount).toEqual(1); + }); - it('renders its children when locales are loaded', () => { - const store = createReduxStore(); - store.dispatch(actions.receive(new ReactLocalization([]))); + it('renders its children when locales are loaded', () => { + const store = createReduxStore(); + store.dispatch(actions.receive(new ReactLocalization([]))); - const wrapper = shallowUntilTarget( - -
    - , - AppLocalizationProviderBase, - ); + const wrapper = shallowUntilTarget( + +
    + , + AppLocalizationProviderBase, + ); - expect( - wrapper.find('#content-test-AppLocalizationProvider'), - ).toHaveLength(1); - }); + expect(wrapper.find('#content-test-AppLocalizationProvider')).toHaveLength( + 1, + ); + }); }); diff --git a/translate/src/core/l10n/components/AppLocalizationProvider.tsx b/translate/src/core/l10n/components/AppLocalizationProvider.tsx index adb0e7e8f..bb38edc88 100644 --- a/translate/src/core/l10n/components/AppLocalizationProvider.tsx +++ b/translate/src/core/l10n/components/AppLocalizationProvider.tsx @@ -6,12 +6,12 @@ import * as l10n from '~/core/l10n'; import { AppDispatch, RootState } from '~/store'; type Props = { - l10n: l10n.L10nState; + l10n: l10n.L10nState; }; type InternalProps = Props & { - children: React.ReactNode; - dispatch: AppDispatch; + children: React.ReactNode; + dispatch: AppDispatch; }; /** @@ -23,37 +23,37 @@ type InternalProps = Props & { * Until the translations are received, a loader is displayed. */ export class AppLocalizationProviderBase extends React.Component { - componentDidMount() { - // By default, we want to use the user's browser preferences to choose - // which locales to fetch and show. - let locales = navigator.languages; + componentDidMount() { + // By default, we want to use the user's browser preferences to choose + // which locales to fetch and show. + let locales = navigator.languages; - // However, if the user has chosen a specific locale, we want to - // fetch and show that instead. - // We use the `` attribute in the index.html file - // to pass the user defined locale if there is one. - if (document.documentElement && document.documentElement.lang) { - locales = [document.documentElement.lang]; - } - - this.props.dispatch(l10n.actions.get(locales)); + // However, if the user has chosen a specific locale, we want to + // fetch and show that instead. + // We use the `` attribute in the index.html file + // to pass the user defined locale if there is one. + if (document.documentElement && document.documentElement.lang) { + locales = [document.documentElement.lang]; } - render(): React.ReactElement { - const { children, l10n } = this.props; + this.props.dispatch(l10n.actions.get(locales)); + } - return ( - - {children} - - ); - } + render(): React.ReactElement { + const { children, l10n } = this.props; + + return ( + + {children} + + ); + } } const mapStateToProps = (state: RootState): Props => { - return { - l10n: state[l10n.NAME], - }; + return { + l10n: state[l10n.NAME], + }; }; export default connect(mapStateToProps)(AppLocalizationProviderBase) as any; diff --git a/translate/src/core/l10n/pseudolocalization.ts b/translate/src/core/l10n/pseudolocalization.ts index 8e0c49d48..0f4fb5c4c 100644 --- a/translate/src/core/l10n/pseudolocalization.ts +++ b/translate/src/core/l10n/pseudolocalization.ts @@ -35,77 +35,77 @@ */ const ACCENTED_MAP = { - // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ - caps: [ - 550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, - 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824, - ], - // ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ - small: [ - 551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, - 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825, - ], + // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ + caps: [ + 550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, + 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824, + ], + // ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ + small: [ + 551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, + 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825, + ], }; const FLIPPED_MAP = { - // ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z - caps: [ - 8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, - 78, 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90, - ], - // ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz - small: [ - 592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, - 111, 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122, - ], + // ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z + caps: [ + 8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, 78, + 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90, + ], + // ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz + small: [ + 592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, 111, + 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122, + ], }; function transformString( - map, - elongate = false, - prefix = '', - postfix = '', - msg, + map, + elongate = false, + prefix = '', + postfix = '', + msg, ) { - // Exclude access-keys and other single-char messages - if (msg.length === 1) { - return msg; - } - // XML entities (‪) and XML tags. - const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/; + // Exclude access-keys and other single-char messages + if (msg.length === 1) { + return msg; + } + // XML entities (‪) and XML tags. + const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/; - const parts = msg.split(reExcluded); - const modified = parts.map((part) => { - if (reExcluded.test(part)) { - return part; + const parts = msg.split(reExcluded); + const modified = parts.map((part) => { + if (reExcluded.test(part)) { + return part; + } + return ( + prefix + + part.replace(/[a-z]/gi, (ch) => { + let cc = ch.charCodeAt(0); + if (cc >= 97 && cc <= 122) { + const newChar = String.fromCodePoint(map.small[cc - 97]); + // duplicate "a", "e", "o" and "u" to emulate ~30% longer text + if ( + elongate && + (cc === 97 || cc === 101 || cc === 111 || cc === 117) + ) { + return newChar + newChar; + } + return newChar; } - return ( - prefix + - part.replace(/[a-z]/gi, (ch) => { - let cc = ch.charCodeAt(0); - if (cc >= 97 && cc <= 122) { - const newChar = String.fromCodePoint(map.small[cc - 97]); - // duplicate "a", "e", "o" and "u" to emulate ~30% longer text - if ( - elongate && - (cc === 97 || cc === 101 || cc === 111 || cc === 117) - ) { - return newChar + newChar; - } - return newChar; - } - if (cc >= 65 && cc <= 90) { - return String.fromCodePoint(map.caps[cc - 65]); - } - return ch; - }) + - postfix - ); - }); - return modified.join(''); + if (cc >= 65 && cc <= 90) { + return String.fromCodePoint(map.caps[cc - 65]); + } + return ch; + }) + + postfix + ); + }); + return modified.join(''); } export default { - accented: transformString.bind(null, ACCENTED_MAP, true, '', ''), - bidi: transformString.bind(null, FLIPPED_MAP, false, '\u202e', '\u202c'), + accented: transformString.bind(null, ACCENTED_MAP, true, '', ''), + bidi: transformString.bind(null, FLIPPED_MAP, false, '\u202e', '\u202c'), }; diff --git a/translate/src/core/l10n/reducer.test.js b/translate/src/core/l10n/reducer.test.js index f7657e8f9..f2f7a76d2 100644 --- a/translate/src/core/l10n/reducer.test.js +++ b/translate/src/core/l10n/reducer.test.js @@ -4,33 +4,33 @@ import reducer from './reducer'; import { RECEIVE, REQUEST } from './actions'; describe('reducer', () => { - it('returns the initial state', () => { - const res = reducer(undefined, {}); - const expected = { - fetching: false, - localization: new ReactLocalization([]), - }; - expect(res).toEqual(expected); - }); + it('returns the initial state', () => { + const res = reducer(undefined, {}); + const expected = { + fetching: false, + localization: new ReactLocalization([]), + }; + expect(res).toEqual(expected); + }); - it('handles the REQUEST action', () => { - const res = reducer({}, { type: REQUEST }); - expect(res.fetching).toEqual(true); - }); + it('handles the REQUEST action', () => { + const res = reducer({}, { type: REQUEST }); + expect(res.fetching).toEqual(true); + }); - it('handles the RECEIVE action', () => { - const initial = { - fetching: true, - localization: new ReactLocalization([]), - }; - const localization = new ReactLocalization([]); + it('handles the RECEIVE action', () => { + const initial = { + fetching: true, + localization: new ReactLocalization([]), + }; + const localization = new ReactLocalization([]); - const res = reducer(initial, { type: RECEIVE, localization }); + const res = reducer(initial, { type: RECEIVE, localization }); - const expected = { - fetching: false, - localization, - }; - expect(res).toEqual(expected); - }); + const expected = { + fetching: false, + localization, + }; + expect(res).toEqual(expected); + }); }); diff --git a/translate/src/core/l10n/reducer.ts b/translate/src/core/l10n/reducer.ts index 53876d1c5..391dfd6e3 100644 --- a/translate/src/core/l10n/reducer.ts +++ b/translate/src/core/l10n/reducer.ts @@ -7,32 +7,32 @@ import type { ReceiveAction, RequestAction } from './actions'; type Action = ReceiveAction | RequestAction; export type L10nState = { - readonly fetching: boolean; - readonly localization: ReactLocalization; + readonly fetching: boolean; + readonly localization: ReactLocalization; }; const initial: L10nState = { - fetching: false, - localization: new ReactLocalization([]), + fetching: false, + localization: new ReactLocalization([]), }; export default function reducer( - state: L10nState = initial, - action: Action, + state: L10nState = initial, + action: Action, ): L10nState { - switch (action.type) { - case REQUEST: - return { - ...state, - fetching: true, - }; - case RECEIVE: - return { - ...state, - fetching: false, - localization: action.localization, - }; - default: - return state; - } + switch (action.type) { + case REQUEST: + return { + ...state, + fetching: true, + }; + case RECEIVE: + return { + ...state, + fetching: false, + localization: action.localization, + }; + default: + return state; + } } diff --git a/translate/src/core/lightbox/actions.ts b/translate/src/core/lightbox/actions.ts index 9d7b3a58c..a03aab1e0 100644 --- a/translate/src/core/lightbox/actions.ts +++ b/translate/src/core/lightbox/actions.ts @@ -5,29 +5,29 @@ export const OPEN: 'lightbox/OPEN' = 'lightbox/OPEN'; * Open the lightbox to show the specified image. */ export type OpenAction = { - type: typeof OPEN; - image: string; + type: typeof OPEN; + image: string; }; export function open(image: string): OpenAction { - return { - type: OPEN, - image, - }; + return { + type: OPEN, + image, + }; } /** * Hide the lightbox. */ export type CloseAction = { - type: typeof CLOSE; + type: typeof CLOSE; }; export function close(): CloseAction { - return { - type: CLOSE, - }; + return { + type: CLOSE, + }; } export default { - close, - open, + close, + open, }; diff --git a/translate/src/core/lightbox/components/Lightbox.css b/translate/src/core/lightbox/components/Lightbox.css index ed657f0ad..b56cac583 100644 --- a/translate/src/core/lightbox/components/Lightbox.css +++ b/translate/src/core/lightbox/components/Lightbox.css @@ -1,17 +1,17 @@ .lightbox { - align-items: center; - background-color: rgba(0, 0, 0, 0.8); - cursor: zoom-out; - display: flex; - height: 100%; - justify-content: center; - left: 0; - position: fixed; - top: 0; - width: 100%; + align-items: center; + background-color: rgba(0, 0, 0, 0.8); + cursor: zoom-out; + display: flex; + height: 100%; + justify-content: center; + left: 0; + position: fixed; + top: 0; + width: 100%; } .lightbox img { - max-height: 90%; - max-width: 90%; + max-height: 90%; + max-width: 90%; } diff --git a/translate/src/core/lightbox/components/Lightbox.test.js b/translate/src/core/lightbox/components/Lightbox.test.js index 4904d1265..f3468fb59 100644 --- a/translate/src/core/lightbox/components/Lightbox.test.js +++ b/translate/src/core/lightbox/components/Lightbox.test.js @@ -9,88 +9,88 @@ import * as actions from '../actions'; import Lightbox, { LightboxBase } from './Lightbox'; describe('', () => { - it('renders the image correctly', () => { - const state = { - image: 'http://example.org/empty.png', - isOpen: true, - }; - const wrapper = shallow().dive(); + it('renders the image correctly', () => { + const state = { + image: 'http://example.org/empty.png', + isOpen: true, + }; + const wrapper = shallow().dive(); - expect(wrapper.find('img')).toHaveLength(1); - expect(wrapper.find('img').props().src).toEqual( - 'http://example.org/empty.png', - ); - }); + expect(wrapper.find('img')).toHaveLength(1); + expect(wrapper.find('img').props().src).toEqual( + 'http://example.org/empty.png', + ); + }); }); describe('', () => { - it('is hidden by default', () => { - const store = createReduxStore(); - const wrapper = mount(); + it('is hidden by default', () => { + const store = createReduxStore(); + const wrapper = mount(); - expect(wrapper.find('img')).toHaveLength(0); - }); - - it('hides after the close action is called', () => { - const store = createReduxStore(); - store.dispatch(actions.open('http://example.org/empty.png')); - - const wrapper = mount(); - expect(wrapper.find('img')).toHaveLength(1); - - act(() => { - store.dispatch(actions.close()); - }); - expect(wrapper.update().find('img')).toHaveLength(0); + expect(wrapper.find('img')).toHaveLength(0); + }); + + it('hides after the close action is called', () => { + const store = createReduxStore(); + store.dispatch(actions.open('http://example.org/empty.png')); + + const wrapper = mount(); + expect(wrapper.find('img')).toHaveLength(1); + + act(() => { + store.dispatch(actions.close()); }); + expect(wrapper.update().find('img')).toHaveLength(0); + }); }); describe('', () => { - beforeAll(() => { - const closeMock = sinon.stub(actions, 'close'); - closeMock.returns({ type: 'whatever' }); + beforeAll(() => { + const closeMock = sinon.stub(actions, 'close'); + closeMock.returns({ type: 'whatever' }); + }); + + afterEach(() => { + // Make sure tests do not pollute one another. + actions.close.resetHistory(); + }); + + afterAll(() => { + actions.close.restore(); + }); + + it('closes on click', () => { + const store = createReduxStore(); + store.dispatch(actions.open('http://example.org/empty.png')); + + const wrapper = mount(); + + expect(wrapper.update().find('img')).toHaveLength(1); + wrapper.simulate('click'); + expect(actions.close.calledOnce).toEqual(true); + }); + + it('closes on key presses if opened before', () => { + const store = createReduxStore(); + store.dispatch(actions.open('http://example.org/empty.png')); + + // Simulating the key presses on `document`. + // See https://github.com/airbnb/enzyme/issues/426 + const eventsMap = {}; + document.addEventListener = sinon.spy((event, cb) => { + eventsMap[event] = cb; }); - afterEach(() => { - // Make sure tests do not pollute one another. - actions.close.resetHistory(); - }); + mount(); - afterAll(() => { - actions.close.restore(); - }); + eventsMap.keydown({ keyCode: 13 }); + expect(actions.close.callCount).toEqual(1); - it('closes on click', () => { - const store = createReduxStore(); - store.dispatch(actions.open('http://example.org/empty.png')); + eventsMap.keydown({ keyCode: 27 }); + expect(actions.close.callCount).toEqual(2); - const wrapper = mount(); - - expect(wrapper.update().find('img')).toHaveLength(1); - wrapper.simulate('click'); - expect(actions.close.calledOnce).toEqual(true); - }); - - it('closes on key presses if opened before', () => { - const store = createReduxStore(); - store.dispatch(actions.open('http://example.org/empty.png')); - - // Simulating the key presses on `document`. - // See https://github.com/airbnb/enzyme/issues/426 - const eventsMap = {}; - document.addEventListener = sinon.spy((event, cb) => { - eventsMap[event] = cb; - }); - - mount(); - - eventsMap.keydown({ keyCode: 13 }); - expect(actions.close.callCount).toEqual(1); - - eventsMap.keydown({ keyCode: 27 }); - expect(actions.close.callCount).toEqual(2); - - eventsMap.keydown({ keyCode: 32 }); - expect(actions.close.callCount).toEqual(3); - }); + eventsMap.keydown({ keyCode: 32 }); + expect(actions.close.callCount).toEqual(3); + }); }); diff --git a/translate/src/core/lightbox/components/Lightbox.tsx b/translate/src/core/lightbox/components/Lightbox.tsx index 983558c78..19975ef50 100644 --- a/translate/src/core/lightbox/components/Lightbox.tsx +++ b/translate/src/core/lightbox/components/Lightbox.tsx @@ -10,16 +10,16 @@ import type { LightboxState } from '../reducer'; import { AppDispatch, RootState } from '~/store'; type Props = { - lightbox: LightboxState; + lightbox: LightboxState; }; type InternalProps = Props & { - dispatch: AppDispatch; + dispatch: AppDispatch; }; type ContentProps = { - image: string; - onClose: () => void; + image: string; + onClose: () => void; }; /** @@ -29,57 +29,57 @@ type ContentProps = { * Click or press a key to close. */ function LightboxContent({ image, onClose }: ContentProps) { - const handleKeyDown = React.useCallback( - (event: KeyboardEvent) => { - // On keys: - // - 13: Enter - // - 27: Escape - // - 32: Space - if ( - event.keyCode === 13 || - event.keyCode === 27 || - event.keyCode === 32 - ) { - onClose(); - } - }, - [onClose], - ); + const handleKeyDown = React.useCallback( + (event: KeyboardEvent) => { + // On keys: + // - 13: Enter + // - 27: Escape + // - 32: Space + if ( + event.keyCode === 13 || + event.keyCode === 27 || + event.keyCode === 32 + ) { + onClose(); + } + }, + [onClose], + ); - React.useEffect(() => { - window.document.addEventListener('keydown', handleKeyDown); - return () => { - window.document.removeEventListener('keydown', handleKeyDown); - }; - }, [handleKeyDown]); + React.useEffect(() => { + window.document.addEventListener('keydown', handleKeyDown); + return () => { + window.document.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); - return ( -
    - -
    - ); + return ( +
    + +
    + ); } export class LightboxBase extends React.Component { - close: () => void = () => { - this.props.dispatch(close()); - }; + close: () => void = () => { + this.props.dispatch(close()); + }; - render(): null | React.ReactElement { - const { lightbox } = this.props; + render(): null | React.ReactElement { + const { lightbox } = this.props; - if (!lightbox.isOpen) { - return null; - } - - return ; + if (!lightbox.isOpen) { + return null; } + + return ; + } } const mapStateToProps = (state: RootState): Props => { - return { - lightbox: state[NAME], - }; + return { + lightbox: state[NAME], + }; }; export default connect(mapStateToProps)(LightboxBase) as any; diff --git a/translate/src/core/lightbox/reducer.ts b/translate/src/core/lightbox/reducer.ts index 17ced40ea..f65e4c8bc 100644 --- a/translate/src/core/lightbox/reducer.ts +++ b/translate/src/core/lightbox/reducer.ts @@ -6,31 +6,31 @@ type Action = CloseAction | OpenAction; // Read-only state. export type LightboxState = { - readonly image: string; - readonly isOpen: boolean; + readonly image: string; + readonly isOpen: boolean; }; const initial: LightboxState = { - image: '', - isOpen: false, + image: '', + isOpen: false, }; export default function reducer( - state: LightboxState = initial, - action: Action, + state: LightboxState = initial, + action: Action, ): LightboxState { - switch (action.type) { - case OPEN: - return { - image: action.image, - isOpen: true, - }; - case CLOSE: - return { - image: '', - isOpen: false, - }; - default: - return state; - } + switch (action.type) { + case OPEN: + return { + image: action.image, + isOpen: true, + }; + case CLOSE: + return { + image: '', + isOpen: false, + }; + default: + return state; + } } diff --git a/translate/src/core/linkify/index.test.js b/translate/src/core/linkify/index.test.js index 9d4185a84..604385632 100644 --- a/translate/src/core/linkify/index.test.js +++ b/translate/src/core/linkify/index.test.js @@ -3,94 +3,93 @@ import { shallow } from 'enzyme'; import { Linkify, getImageURLs } from '.'; describe('Linkify', () => { - it('renders links as expected on plain text', () => { - const wrapper = shallow( - See pontoon.mozilla.org for more., - ); - const links = wrapper.find('a'); - expect(links.length).toBe(1); - const link = links.at(0); - expect(link.text()).toBe('pontoon.mozilla.org'); - expect(link.prop('href')).toBe('http://pontoon.mozilla.org'); - expect(link.prop('target')).toBeUndefined(); - }); - it('sets rel and target', () => { - const wrapper = shallow( - - more pontoon.mozilla.org content - , - ); - const links = wrapper.find('a'); - expect(links.length).toBe(1); - const link = links.at(0); - expect(link.text()).toBe('pontoon.mozilla.org'); - expect(link.prop('href')).toBe('http://pontoon.mozilla.org'); - expect(link.prop('target')).toBe('_blank'); - expect(link.prop('rel')).toContain('noopener'); - expect(link.prop('rel')).toContain('noreferrer'); - }); - it('leaves existing links alone', () => { - const wrapper = shallow( - - more pontoon.mozilla.org{' '} - content - , - ); - const links = wrapper.find('a'); - expect(links.length).toBe(1); - const link = links.at(0); - expect(link.text()).toBe('pontoon.mozilla.org'); - expect(link.prop('href')).toBe('https://example.com'); - }); + it('renders links as expected on plain text', () => { + const wrapper = shallow( + See pontoon.mozilla.org for more., + ); + const links = wrapper.find('a'); + expect(links.length).toBe(1); + const link = links.at(0); + expect(link.text()).toBe('pontoon.mozilla.org'); + expect(link.prop('href')).toBe('http://pontoon.mozilla.org'); + expect(link.prop('target')).toBeUndefined(); + }); + it('sets rel and target', () => { + const wrapper = shallow( + + more pontoon.mozilla.org content + , + ); + const links = wrapper.find('a'); + expect(links.length).toBe(1); + const link = links.at(0); + expect(link.text()).toBe('pontoon.mozilla.org'); + expect(link.prop('href')).toBe('http://pontoon.mozilla.org'); + expect(link.prop('target')).toBe('_blank'); + expect(link.prop('rel')).toContain('noopener'); + expect(link.prop('rel')).toContain('noreferrer'); + }); + it('leaves existing links alone', () => { + const wrapper = shallow( + + more pontoon.mozilla.org content + , + ); + const links = wrapper.find('a'); + expect(links.length).toBe(1); + const link = links.at(0); + expect(link.text()).toBe('pontoon.mozilla.org'); + expect(link.prop('href')).toBe('https://example.com'); + }); }); describe('getImageURLs', () => { - it('finds an image when there is an image URL in the source', () => { - const source = 'That is an image URL: http://link.to/image.png'; - const urls = getImageURLs(source); + it('finds an image when there is an image URL in the source', () => { + const source = 'That is an image URL: http://link.to/image.png'; + const urls = getImageURLs(source); - expect(urls).toEqual(['http://link.to/image.png']); - }); + expect(urls).toEqual(['http://link.to/image.png']); + }); - it('finds several images', () => { - const source = ` + it('finds several images', () => { + const source = ` Here we have 2 images: http://link.to/image.png and https://example.org/test.jpg `; - const urls = getImageURLs(source); + const urls = getImageURLs(source); - const expectedURLs = [ - 'http://link.to/image.png', - 'https://example.org/test.jpg', - ]; + const expectedURLs = [ + 'http://link.to/image.png', + 'https://example.org/test.jpg', + ]; - expect(urls).toEqual(expectedURLs); - }); + expect(urls).toEqual(expectedURLs); + }); - it('does not find non PNG or JPG images', () => { - const source = - 'That is a non-supported image URL: http://link.to/image.bmp'; - const urls = getImageURLs(source); + it('does not find non PNG or JPG images', () => { + const source = + 'That is a non-supported image URL: http://link.to/image.bmp'; + const urls = getImageURLs(source); - expect(urls).toHaveLength(0); - }); + expect(urls).toHaveLength(0); + }); - it('does not find non image links', () => { - const source = 'That is not an image URL: http://link.to/image.php'; - const urls = getImageURLs(source); + it('does not find non image links', () => { + const source = 'That is not an image URL: http://link.to/image.php'; + const urls = getImageURLs(source); - expect(urls).toHaveLength(0); - }); + expect(urls).toHaveLength(0); + }); - it('does returns an empty array when no URLs are found', () => { - const source = 'That is not interesting at all'; - const urls = getImageURLs(source); + it('does returns an empty array when no URLs are found', () => { + const source = 'That is not interesting at all'; + const urls = getImageURLs(source); - expect(urls).toHaveLength(0); - }); + expect(urls).toHaveLength(0); + }); }); diff --git a/translate/src/core/linkify/index.ts b/translate/src/core/linkify/index.ts index d6c3a4d23..5bfb93aea 100644 --- a/translate/src/core/linkify/index.ts +++ b/translate/src/core/linkify/index.ts @@ -12,11 +12,11 @@ const linkify = new LinkifyIt(); linkify.tlds(tlds); export function getImageURLs(source: string, locale: string) { - const matches = linkify.match(source); - if (!matches) { - return []; - } - return matches - .filter((match) => /(https?:\/\/.*\.(?:png|jpg))/im.test(match.url)) - .map((match) => match.url.replace(/en-US\//gi, locale + '/')); + const matches = linkify.match(source); + if (!matches) { + return []; + } + return matches + .filter((match) => /(https?:\/\/.*\.(?:png|jpg))/im.test(match.url)) + .map((match) => match.url.replace(/en-US\//gi, locale + '/')); } diff --git a/translate/src/core/loaders/components/SkeletonLoader.css b/translate/src/core/loaders/components/SkeletonLoader.css index cb558cb82..f6d7f8aaf 100644 --- a/translate/src/core/loaders/components/SkeletonLoader.css +++ b/translate/src/core/loaders/components/SkeletonLoader.css @@ -1,38 +1,38 @@ .skeleton-loader { - animation: fading 1.5s infinite; + animation: fading 1.5s infinite; } .skeleton-loader.entities { - pointer-events: none; - overflow: hidden; - height: 100%; + pointer-events: none; + overflow: hidden; + height: 100%; } .skeleton-loader.entities.scroll { - height: auto; + height: auto; } .skeleton-loader p { - height: 0.8125rem; - background-color: #e2e2e2; - padding-bottom: 3px; + height: 0.8125rem; + background-color: #e2e2e2; + padding-bottom: 3px; } .skeleton-loader .text-2 { - width: 50%; - display: inline-block; + width: 50%; + display: inline-block; } @keyframes fading { - 0% { - opacity: 0.1; - } + 0% { + opacity: 0.1; + } - 50% { - opacity: 0.2; - } + 50% { + opacity: 0.2; + } - 100% { - opacity: 0.1; - } + 100% { + opacity: 0.1; + } } diff --git a/translate/src/core/loaders/components/SkeletonLoader.tsx b/translate/src/core/loaders/components/SkeletonLoader.tsx index b8cf43334..a71eb9e31 100644 --- a/translate/src/core/loaders/components/SkeletonLoader.tsx +++ b/translate/src/core/loaders/components/SkeletonLoader.tsx @@ -3,30 +3,26 @@ import React from 'react'; import './SkeletonLoader.css'; export default function SkeletonLoader(props) { - const firstLoad = props.items.length === 0; - const itemCount = firstLoad ? 30 : 2; - const list = Array.from(Array(itemCount).keys()); + const firstLoad = props.items.length === 0; + const itemCount = firstLoad ? 30 : 2; + const list = Array.from(Array(itemCount).keys()); - return ( -
      - {list.map((i) => { - const classes = `entity missing ${ - i === 0 && firstLoad ? 'selected' : null - }`; - return ( -
    • - -
      -

      -

      -
      -
    • - ); - })} -
    - ); + return ( +
      + {list.map((i) => { + const classes = `entity missing ${ + i === 0 && firstLoad ? 'selected' : null + }`; + return ( +
    • + +
      +

      +

      +
      +
    • + ); + })} +
    + ); } diff --git a/translate/src/core/loaders/components/WaveLoader.css b/translate/src/core/loaders/components/WaveLoader.css index 0bc8210db..18b85f912 100644 --- a/translate/src/core/loaders/components/WaveLoader.css +++ b/translate/src/core/loaders/components/WaveLoader.css @@ -4,102 +4,102 @@ */ .wave-loader { - position: fixed; - z-index: 14; - display: table; - overflow: hidden; - width: 100%; - height: 100%; - background: #3f4752; + position: fixed; + z-index: 14; + display: table; + overflow: hidden; + width: 100%; + height: 100%; + background: #3f4752; } .wave-loader .animation { - margin: 20px auto; - width: 100px; - height: 60px; - text-align: center; - font-size: 20px; + margin: 20px auto; + width: 100px; + height: 60px; + text-align: center; + font-size: 20px; } .wave-loader .animation > div { - display: inline-block; - width: 12px; - height: 100%; - background-color: #7bc876; + display: inline-block; + width: 12px; + height: 100%; + background-color: #7bc876; - -webkit-animation: stretchdelay 1.2s infinite ease-in-out; - animation: stretchdelay 1.2s infinite ease-in-out; + -webkit-animation: stretchdelay 1.2s infinite ease-in-out; + animation: stretchdelay 1.2s infinite ease-in-out; } .wave-loader .animation div:nth-child(2) { - -webkit-animation-delay: -1.1s; - animation-delay: -1.1s; + -webkit-animation-delay: -1.1s; + animation-delay: -1.1s; } .wave-loader .animation div:nth-child(3) { - -webkit-animation-delay: -1s; - animation-delay: -1s; + -webkit-animation-delay: -1s; + animation-delay: -1s; } .wave-loader .animation div:nth-child(4) { - -webkit-animation-delay: -0.9s; - animation-delay: -0.9s; + -webkit-animation-delay: -0.9s; + animation-delay: -0.9s; } .wave-loader .animation div:nth-child(5) { - -webkit-animation-delay: -0.8s; - animation-delay: -0.8s; + -webkit-animation-delay: -0.8s; + animation-delay: -0.8s; } @-webkit-keyframes stretchdelay { - 0%, - 40%, - 100% { - -webkit-transform: scaleY(0.4); - } - 20% { - -webkit-transform: scaleY(1); - } + 0%, + 40%, + 100% { + -webkit-transform: scaleY(0.4); + } + 20% { + -webkit-transform: scaleY(1); + } } @keyframes stretchdelay { - 0%, - 40%, - 100% { - transform: scaleY(0.4); - -webkit-transform: scaleY(0.4); - } - 20% { - transform: scaleY(1); - -webkit-transform: scaleY(1); - } + 0%, + 40%, + 100% { + transform: scaleY(0.4); + -webkit-transform: scaleY(0.4); + } + 20% { + transform: scaleY(1); + -webkit-transform: scaleY(1); + } } .wave-loader .inner { - display: table-cell; - vertical-align: middle; + display: table-cell; + vertical-align: middle; } /* * Loading text */ .wave-loader .text { - display: block; - text-align: center; - font-size: 1.5em; - opacity: 0; - animation: fadeInLoaderText 1s forwards; - animation-delay: 3s; + display: block; + text-align: center; + font-size: 1.5em; + opacity: 0; + animation: fadeInLoaderText 1s forwards; + animation-delay: 3s; } @keyframes fadeInLoaderText { - 0% { - display: None; - opacity: 0; - } + 0% { + display: None; + opacity: 0; + } - 100% { - display: block; - opacity: 1; - } + 100% { + display: block; + opacity: 1; + } } diff --git a/translate/src/core/loaders/components/WaveLoader.tsx b/translate/src/core/loaders/components/WaveLoader.tsx index 904ccdb34..c66368e99 100644 --- a/translate/src/core/loaders/components/WaveLoader.tsx +++ b/translate/src/core/loaders/components/WaveLoader.tsx @@ -3,21 +3,21 @@ import * as React from 'react'; import './WaveLoader.css'; export const WaveLoader = (): React.ReactElement => ( -
    -
    -
    -
    -   -
    -   -
    -   -
    -   -
    -
    -
    +
    +
    +
    +
    +   +
    +   +
    +   +
    +   +
    +
    +
    ); export default WaveLoader; diff --git a/translate/src/core/locale/actions.ts b/translate/src/core/locale/actions.ts index 10b3d0954..1927c6751 100644 --- a/translate/src/core/locale/actions.ts +++ b/translate/src/core/locale/actions.ts @@ -6,67 +6,65 @@ export const RECEIVE: 'locale/RECEIVE' = 'locale/RECEIVE'; export const REQUEST: 'locale/REQUEST' = 'locale/REQUEST'; export type Localization = { - readonly totalStrings: number; - readonly approvedStrings: number; - readonly stringsWithWarnings: number; - readonly project: { - readonly slug: string; - readonly name: string; - }; + readonly totalStrings: number; + readonly approvedStrings: number; + readonly stringsWithWarnings: number; + readonly project: { + readonly slug: string; + readonly name: string; + }; }; export type Locale = { - readonly code: string; - readonly name: string; - readonly cldrPlurals: Array; - readonly pluralRule: string; - readonly direction: string; - readonly script: string; - readonly googleTranslateCode: string; - readonly msTranslatorCode: string; - readonly systranTranslateCode: string; - readonly msTerminologyCode: string; - readonly localizations: Array; + readonly code: string; + readonly name: string; + readonly cldrPlurals: Array; + readonly pluralRule: string; + readonly direction: string; + readonly script: string; + readonly googleTranslateCode: string; + readonly msTranslatorCode: string; + readonly systranTranslateCode: string; + readonly msTerminologyCode: string; + readonly localizations: Array; }; export type RequestAction = { - type: typeof REQUEST; + type: typeof REQUEST; }; export function request(): RequestAction { - return { - type: REQUEST, - }; + return { + type: REQUEST, + }; } export type ReceiveAction = { - type: typeof RECEIVE; - locale: Locale; + type: typeof RECEIVE; + locale: Locale; }; export function receive(locale: Locale): ReceiveAction { - return { - type: RECEIVE, - locale, - }; + return { + type: RECEIVE, + locale, + }; } export function get(code: string) { - return async (dispatch: AppDispatch) => { - dispatch(request()); - const results = await api.locale.get(code); - const data = results.data.locale; - const locale = { - ...data, - direction: data.direction.toLowerCase(), - cldrPlurals: data.cldrPlurals - .split(',') - .map((i) => parseInt(i, 10)), - }; - dispatch(receive(locale)); + return async (dispatch: AppDispatch) => { + dispatch(request()); + const results = await api.locale.get(code); + const data = results.data.locale; + const locale = { + ...data, + direction: data.direction.toLowerCase(), + cldrPlurals: data.cldrPlurals.split(',').map((i) => parseInt(i, 10)), }; + dispatch(receive(locale)); + }; } export default { - receive, - request, - get, + receive, + request, + get, }; diff --git a/translate/src/core/locale/getPluralExamples.test.js b/translate/src/core/locale/getPluralExamples.test.js index d761f8e66..9e4fb2260 100644 --- a/translate/src/core/locale/getPluralExamples.test.js +++ b/translate/src/core/locale/getPluralExamples.test.js @@ -1,37 +1,35 @@ import getPluralExamples from './getPluralExamples'; describe('getPluralExamples', () => { - it('returns a map of Slovenian plural examples', () => { - const locale = { - cldrPlurals: [1, 2, 3, 5], - pluralRule: - '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)', - }; - const res = getPluralExamples(locale); - const expected = { - 1: 1, - 2: 2, - 3: 3, - 5: 0, - }; - expect(res).toEqual(expected); - }); + it('returns a map of Slovenian plural examples', () => { + const locale = { + cldrPlurals: [1, 2, 3, 5], + pluralRule: + '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)', + }; + const res = getPluralExamples(locale); + const expected = { + 1: 1, + 2: 2, + 3: 3, + 5: 0, + }; + expect(res).toEqual(expected); + }); - it('prevents infinite loop if locale plurals are not configured properly', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const locale = { - cldrPlurals: [0, 1, 2, 3, 4, 5], - pluralRule: '(n != 1)', - }; - try { - const res = getPluralExamples(locale); - const expected = { 0: 1, 1: 2 }; - expect(res).toEqual(expected); - expect(spy).toHaveBeenCalledWith( - 'Unable to generate plural examples.', - ); - } finally { - spy.mockRestore(); - } - }); + it('prevents infinite loop if locale plurals are not configured properly', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const locale = { + cldrPlurals: [0, 1, 2, 3, 4, 5], + pluralRule: '(n != 1)', + }; + try { + const res = getPluralExamples(locale); + const expected = { 0: 1, 1: 2 }; + expect(res).toEqual(expected); + expect(spy).toHaveBeenCalledWith('Unable to generate plural examples.'); + } finally { + spy.mockRestore(); + } + }); }); diff --git a/translate/src/core/locale/getPluralExamples.ts b/translate/src/core/locale/getPluralExamples.ts index 8eaa7abc3..f684d7e25 100644 --- a/translate/src/core/locale/getPluralExamples.ts +++ b/translate/src/core/locale/getPluralExamples.ts @@ -15,32 +15,32 @@ import type { Locale } from './actions'; * @returns {Object} A map of locale's cldrPlurals and their plural examples. */ export default function getPluralExamples( - locale: Locale, + locale: Locale, ): Record { - const pluralsCount = locale.cldrPlurals.length; - const examples = {}; + const pluralsCount = locale.cldrPlurals.length; + const examples = {}; - if (pluralsCount === 2) { - examples[locale.cldrPlurals[0]] = 1; - examples[locale.cldrPlurals[1]] = 2; - } else { - const getRule = new Function('n', `return ${locale.pluralRule}`) as ( - n: number, - ) => number | boolean; - let n = 0; - while (Object.keys(examples).length < pluralsCount) { - const rule = locale.cldrPlurals[Number(getRule(n))]; - if (!examples[rule]) { - examples[rule] = n; - } - n++; - // Protection against infinite loop - if (n === 1000) { - console.error('Unable to generate plural examples.'); - break; - } - } + if (pluralsCount === 2) { + examples[locale.cldrPlurals[0]] = 1; + examples[locale.cldrPlurals[1]] = 2; + } else { + const getRule = new Function('n', `return ${locale.pluralRule}`) as ( + n: number, + ) => number | boolean; + let n = 0; + while (Object.keys(examples).length < pluralsCount) { + const rule = locale.cldrPlurals[Number(getRule(n))]; + if (!examples[rule]) { + examples[rule] = n; + } + n++; + // Protection against infinite loop + if (n === 1000) { + console.error('Unable to generate plural examples.'); + break; + } } + } - return examples; + return examples; } diff --git a/translate/src/core/locale/reducer.test.js b/translate/src/core/locale/reducer.test.js index e4b9f8852..557ec9df2 100644 --- a/translate/src/core/locale/reducer.test.js +++ b/translate/src/core/locale/reducer.test.js @@ -2,37 +2,37 @@ import reducer from './reducer'; import { RECEIVE, REQUEST } from './actions'; describe('reducer', () => { - it('returns the initial state', () => { - const res = reducer(undefined, {}); - const expected = { - code: '', - name: '', - cldrPlurals: [], - pluralRule: '', - direction: '', - script: '', - googleTranslateCode: '', - msTranslatorCode: '', - systranTranslateCode: '', - msTerminologyCode: '', - localizations: [], - fetching: false, - }; - expect(res).toEqual(expected); - }); + it('returns the initial state', () => { + const res = reducer(undefined, {}); + const expected = { + code: '', + name: '', + cldrPlurals: [], + pluralRule: '', + direction: '', + script: '', + googleTranslateCode: '', + msTranslatorCode: '', + systranTranslateCode: '', + msTerminologyCode: '', + localizations: [], + fetching: false, + }; + expect(res).toEqual(expected); + }); - it('handles the REQUEST action', () => { - const res = reducer({}, { type: REQUEST }); - expect(res.fetching).toEqual(true); - }); + it('handles the REQUEST action', () => { + const res = reducer({}, { type: REQUEST }); + expect(res.fetching).toEqual(true); + }); - it('handles the RECEIVE action', () => { - const LOCALE = { code: 'kg' }; - const res = reducer({}, { type: RECEIVE, locale: LOCALE }); - const expected = { - ...LOCALE, - fetching: false, - }; - expect(res).toEqual(expected); - }); + it('handles the RECEIVE action', () => { + const LOCALE = { code: 'kg' }; + const res = reducer({}, { type: RECEIVE, locale: LOCALE }); + const expected = { + ...LOCALE, + fetching: false, + }; + expect(res).toEqual(expected); + }); }); diff --git a/translate/src/core/locale/reducer.ts b/translate/src/core/locale/reducer.ts index b8f3cf500..5a883e6ab 100644 --- a/translate/src/core/locale/reducer.ts +++ b/translate/src/core/locale/reducer.ts @@ -5,41 +5,41 @@ import type { Locale, ReceiveAction, RequestAction } from './actions'; type Action = ReceiveAction | RequestAction; export type LocaleState = Locale & { - readonly fetching: boolean; + readonly fetching: boolean; }; const initial: LocaleState = { - code: '', - name: '', - cldrPlurals: [], - pluralRule: '', - direction: '', - script: '', - googleTranslateCode: '', - msTranslatorCode: '', - systranTranslateCode: '', - msTerminologyCode: '', - localizations: [], - fetching: false, + code: '', + name: '', + cldrPlurals: [], + pluralRule: '', + direction: '', + script: '', + googleTranslateCode: '', + msTranslatorCode: '', + systranTranslateCode: '', + msTerminologyCode: '', + localizations: [], + fetching: false, }; export default function reducer( - state: LocaleState = initial, - action: Action, + state: LocaleState = initial, + action: Action, ): LocaleState { - switch (action.type) { - case RECEIVE: - return { - ...state, - ...action.locale, - fetching: false, - }; - case REQUEST: - return { - ...state, - fetching: true, - }; - default: - return state; - } + switch (action.type) { + case RECEIVE: + return { + ...state, + ...action.locale, + fetching: false, + }; + case REQUEST: + return { + ...state, + fetching: true, + }; + default: + return state; + } } diff --git a/translate/src/core/navigation/actions.ts b/translate/src/core/navigation/actions.ts index cb23d8335..eb6577d45 100644 --- a/translate/src/core/navigation/actions.ts +++ b/translate/src/core/navigation/actions.ts @@ -15,48 +15,48 @@ import type { AppDispatch } from '~/store'; * @param {boolean} replaceHistory Whether or not to push a new URL or replace the current one in the browser history. */ export function update( - router: Record, - params: Record, - replaceHistory?: boolean, + router: Record, + params: Record, + replaceHistory?: boolean, ) { - return (dispatch: AppDispatch) => { - const queryString = router.location.search; - const currentParams = new URLSearchParams(queryString); + return (dispatch: AppDispatch) => { + const queryString = router.location.search; + const currentParams = new URLSearchParams(queryString); - Object.keys(params).forEach((param: string) => { - const prev = currentParams.get(param); - const value = params[param]; + Object.keys(params).forEach((param: string) => { + const prev = currentParams.get(param); + const value = params[param]; - if (value === prev || (!value && !prev)) { - return; - } + if (value === prev || (!value && !prev)) { + return; + } - if (!value) { - currentParams.delete(param); - } else { - currentParams.set(param, value); - } - }); + if (!value) { + currentParams.delete(param); + } else { + currentParams.set(param, value); + } + }); - // If the URL did not change, don't do anything. - if (queryString === '?' + currentParams.toString()) { - return; - } + // If the URL did not change, don't do anything. + if (queryString === '?' + currentParams.toString()) { + return; + } - // When we change the URL, we want to remove the `string` parameter - // because with the new results, that entity might not be available - // anymore. - if (!params['string']) { - currentParams.delete('string'); - } + // When we change the URL, we want to remove the `string` parameter + // because with the new results, that entity might not be available + // anymore. + if (!params['string']) { + currentParams.delete('string'); + } - let updateMethod = push; - if (replaceHistory) { - updateMethod = replace; - } + let updateMethod = push; + if (replaceHistory) { + updateMethod = replace; + } - dispatch(updateMethod('?' + currentParams.toString())); - }; + dispatch(updateMethod('?' + currentParams.toString())); + }; } /** @@ -68,10 +68,10 @@ export function update( * It keeps all other parameters in the URL the same. */ export function updateAuthor( - router: Record, - author: string | null | undefined, + router: Record, + author: string | null | undefined, ) { - return update(router, { author }); + return update(router, { author }); } /** @@ -80,11 +80,11 @@ export function updateAuthor( * This function keeps all other parameters in the URL the same. */ export function updateEntity( - router: Record, - entity: string, - replaceHistory?: boolean, + router: Record, + entity: string, + replaceHistory?: boolean, ) { - return update(router, { string: entity }, replaceHistory); + return update(router, { string: entity }, replaceHistory); } /** @@ -96,10 +96,10 @@ export function updateEntity( * It keeps all other parameters in the URL the same. */ export function updateExtra( - router: Record, - extra: string | null | undefined, + router: Record, + extra: string | null | undefined, ) { - return update(router, { extra }); + return update(router, { extra }); } /** @@ -111,10 +111,10 @@ export function updateExtra( * It keeps all other parameters in the URL the same. */ export function updateSearch( - router: Record, - search: string | null | undefined, + router: Record, + search: string | null | undefined, ) { - return update(router, { search }); + return update(router, { search }); } /** @@ -126,10 +126,10 @@ export function updateSearch( * It keeps all other parameters in the URL the same. */ export function updateStatus( - router: Record, - status: string | null | undefined, + router: Record, + status: string | null | undefined, ) { - return update(router, { status }); + return update(router, { status }); } /** @@ -141,10 +141,10 @@ export function updateStatus( * It keeps all other parameters in the URL the same. */ export function updateTag( - router: Record, - tag: string | null | undefined, + router: Record, + tag: string | null | undefined, ) { - return update(router, { tag }); + return update(router, { tag }); } /** @@ -156,19 +156,19 @@ export function updateTag( * It keeps all other parameters in the URL the same. */ export function updateTime( - router: Record, - time: string | null | undefined, + router: Record, + time: string | null | undefined, ) { - return update(router, { time }); + return update(router, { time }); } export default { - update, - updateAuthor, - updateEntity, - updateExtra, - updateSearch, - updateStatus, - updateTag, - updateTime, + update, + updateAuthor, + updateEntity, + updateExtra, + updateSearch, + updateStatus, + updateTag, + updateTime, }; diff --git a/translate/src/core/navigation/selectors.test.js b/translate/src/core/navigation/selectors.test.js index e308e6149..012857114 100644 --- a/translate/src/core/navigation/selectors.test.js +++ b/translate/src/core/navigation/selectors.test.js @@ -1,41 +1,41 @@ import { getNavigationParams } from './selectors'; describe('selectors', () => { - describe('getNavigationParams', () => { - it('correctly parses the pathname', () => { - const pathname = '/kg/waterwolf/path/to/RESOURCE.po/'; - const search = ''; + describe('getNavigationParams', () => { + it('correctly parses the pathname', () => { + const pathname = '/kg/waterwolf/path/to/RESOURCE.po/'; + const search = ''; - const fakeState = { - router: { - location: { - pathname, - search, - }, - }, - }; - const res = getNavigationParams(fakeState); + const fakeState = { + router: { + location: { + pathname, + search, + }, + }, + }; + const res = getNavigationParams(fakeState); - expect(res.locale).toEqual('kg'); - expect(res.project).toEqual('waterwolf'); - expect(res.resource).toEqual('path/to/RESOURCE.po'); - }); - - it('correctly parses the query string', () => { - const pathname = '/kg/waterwolf/path/'; - const search = '?string=42'; - - const fakeState = { - router: { - location: { - pathname, - search, - }, - }, - }; - const res = getNavigationParams(fakeState); - - expect(res.entity).toEqual(42); - }); + expect(res.locale).toEqual('kg'); + expect(res.project).toEqual('waterwolf'); + expect(res.resource).toEqual('path/to/RESOURCE.po'); }); + + it('correctly parses the query string', () => { + const pathname = '/kg/waterwolf/path/'; + const search = '?string=42'; + + const fakeState = { + router: { + location: { + pathname, + search, + }, + }, + }; + const res = getNavigationParams(fakeState); + + expect(res.entity).toEqual(42); + }); + }); }); diff --git a/translate/src/core/navigation/selectors.ts b/translate/src/core/navigation/selectors.ts index d6117f5f5..d098dd8a4 100644 --- a/translate/src/core/navigation/selectors.ts +++ b/translate/src/core/navigation/selectors.ts @@ -6,16 +6,16 @@ const pathSelector = (state: RootState) => state.router.location.pathname; const querySelector = (state: RootState) => state.router.location.search; export type NavigationParams = { - locale: string; - project: string; - resource: string; - entity: number; - search: string | null | undefined; - status: string | null | undefined; - extra: string | null | undefined; - tag: string | null | undefined; - author: string | null | undefined; - time: string | null | undefined; + locale: string; + project: string; + resource: string; + entity: number; + search: string | null | undefined; + status: string | null | undefined; + extra: string | null | undefined; + tag: string | null | undefined; + author: string | null | undefined; + time: string | null | undefined; }; /** @@ -23,43 +23,43 @@ export type NavigationParams = { * current URL. */ export const getNavigationParams = createSelector( - pathSelector, - querySelector, - (path: string, query: string): NavigationParams => { - const parts = path.split('/'); - // Because pathname always starts and finishes with a '/', - // the first and last elements of `parts` are empty strings. - parts.shift(); - parts.pop(); + pathSelector, + querySelector, + (path: string, query: string): NavigationParams => { + const parts = path.split('/'); + // Because pathname always starts and finishes with a '/', + // the first and last elements of `parts` are empty strings. + parts.shift(); + parts.pop(); - const locale = parts.shift(); - const project = parts.shift(); - const resource = parts.join('/'); + const locale = parts.shift(); + const project = parts.shift(); + const resource = parts.join('/'); - const params = new URLSearchParams(query); - const entity = Number(params.get('string')); - const search = params.get('search'); - const status = params.get('status'); - const extra = params.get('extra'); - const tag = params.get('tag'); - const author = params.get('author'); - const time = params.get('time'); + const params = new URLSearchParams(query); + const entity = Number(params.get('string')); + const search = params.get('search'); + const status = params.get('status'); + const extra = params.get('extra'); + const tag = params.get('tag'); + const author = params.get('author'); + const time = params.get('time'); - return { - locale, - project, - resource, - entity, - search, - status, - extra, - tag, - author, - time, - }; - }, + return { + locale, + project, + resource, + entity, + search, + status, + extra, + tag, + author, + time, + }; + }, ); export default { - getNavigationParams, + getNavigationParams, }; diff --git a/translate/src/core/notification/actions.ts b/translate/src/core/notification/actions.ts index cbe6705cf..df04bee41 100644 --- a/translate/src/core/notification/actions.ts +++ b/translate/src/core/notification/actions.ts @@ -1,18 +1,16 @@ export const ADD: 'notification/ADD' = 'notification/ADD'; export type NotificationType = - | 'debug' - | 'error' - | 'info' - | 'success' - | 'warning'; + | 'debug' + | 'error' + | 'info' + | 'success' + | 'warning'; export type NotificationMessage = { - readonly type: NotificationType; - readonly content: - | string - | React.ReactElement, any>; - readonly key?: string; + readonly type: NotificationType; + readonly content: string | React.ReactElement, any>; + readonly key?: string; }; /** @@ -21,22 +19,22 @@ export type NotificationMessage = { * Accepts a notification message as defined in `./messages.js`. */ export type AddAction = { - type: typeof ADD; - message: NotificationMessage; + type: typeof ADD; + message: NotificationMessage; }; let keyCounter = 0; export function add(message: NotificationMessage): AddAction { - // This unique key is a mechanism to force React to re-render a notification - // when the same one is added twice in a row. - if (!message.key) { - message = { ...message, key: String(++keyCounter) }; - } + // This unique key is a mechanism to force React to re-render a notification + // when the same one is added twice in a row. + if (!message.key) { + message = { ...message, key: String(++keyCounter) }; + } - return { - type: ADD, - message, - }; + return { + type: ADD, + message, + }; } /** @@ -49,10 +47,10 @@ export function add(message: NotificationMessage): AddAction { * that come from a different system (like django) and are not localizable yet. */ export function addRaw(content: string, type: NotificationType): AddAction { - return add({ content, type }); + return add({ content, type }); } export default { - add, - addRaw, + add, + addRaw, }; diff --git a/translate/src/core/notification/components/NotificationPanel.css b/translate/src/core/notification/components/NotificationPanel.css index 909ec76f8..45cb38c84 100644 --- a/translate/src/core/notification/components/NotificationPanel.css +++ b/translate/src/core/notification/components/NotificationPanel.css @@ -1,30 +1,30 @@ .notification-panel { - background: rgba(51, 57, 65, 0.9); - cursor: pointer; - font-size: 14px; - font-style: italic; - left: 0; - line-height: 60px; - list-style: none; - margin: 0; - position: fixed; - text-align: center; - top: -60px; - width: 100%; - z-index: 100; - transition: 200ms; + background: rgba(51, 57, 65, 0.9); + cursor: pointer; + font-size: 14px; + font-style: italic; + left: 0; + line-height: 60px; + list-style: none; + margin: 0; + position: fixed; + text-align: center; + top: -60px; + width: 100%; + z-index: 100; + transition: 200ms; } .notification-panel .info, .notification-panel .success { - color: #7bc876; + color: #7bc876; } .notification-panel .error { - color: #f36; + color: #f36; } /* Showing and hiding animations. */ .notification-panel.showing { - top: 0; + top: 0; } diff --git a/translate/src/core/notification/components/NotificationPanel.test.js b/translate/src/core/notification/components/NotificationPanel.test.js index 79f7f7b21..1952e84dd 100644 --- a/translate/src/core/notification/components/NotificationPanel.test.js +++ b/translate/src/core/notification/components/NotificationPanel.test.js @@ -4,57 +4,53 @@ import { shallow } from 'enzyme'; import NotificationPanel from './NotificationPanel'; describe('', () => { - const EMPTY_NOTIF = { - message: null, - }; - const NOTIF = { - message: { - type: 'info', - content: 'Hello, World!', - }, - }; + const EMPTY_NOTIF = { + message: null, + }; + const NOTIF = { + message: { + type: 'info', + content: 'Hello, World!', + }, + }; - it('returns an empty element when there is no notification', () => { - const wrapper = shallow( - , - ); - expect(wrapper.children()).toHaveLength(1); - expect(wrapper.find('span').text()).toEqual(''); - }); + it('returns an empty element when there is no notification', () => { + const wrapper = shallow(); + expect(wrapper.children()).toHaveLength(1); + expect(wrapper.find('span').text()).toEqual(''); + }); - it('shows a message when there is a notification', () => { - const wrapper = shallow(); - expect(wrapper.find('span').text()).toEqual(NOTIF.message.content); - }); + it('shows a message when there is a notification', () => { + const wrapper = shallow(); + expect(wrapper.find('span').text()).toEqual(NOTIF.message.content); + }); - it('hides a message after a delay', () => { - jest.useFakeTimers(); + it('hides a message after a delay', () => { + jest.useFakeTimers(); - // Create a NotificationPanel with no message. - const wrapper = shallow( - , - ); + // Create a NotificationPanel with no message. + const wrapper = shallow(); - expect(wrapper.find('span').text()).toEqual(''); + expect(wrapper.find('span').text()).toEqual(''); - // Add a message to the NotificationPanel, that message is shown. - wrapper.setProps({ notification: NOTIF }); + // Add a message to the NotificationPanel, that message is shown. + wrapper.setProps({ notification: NOTIF }); - expect(wrapper.find('span').text()).toEqual(NOTIF.message.content); - expect(wrapper.find('.showing')).toHaveLength(1); + expect(wrapper.find('span').text()).toEqual(NOTIF.message.content); + expect(wrapper.find('.showing')).toHaveLength(1); - // Run time forward, the message with disappear. - jest.runAllTimers(); + // Run time forward, the message with disappear. + jest.runAllTimers(); - expect(wrapper.children()).toHaveLength(1); - expect(wrapper.find('.showing')).toHaveLength(0); - }); + expect(wrapper.children()).toHaveLength(1); + expect(wrapper.find('.showing')).toHaveLength(0); + }); - it('hides a message on click', () => { - const wrapper = shallow(); + it('hides a message on click', () => { + const wrapper = shallow(); - expect(wrapper.find('.showing')).toHaveLength(1); - wrapper.simulate('click'); - expect(wrapper.find('.showing')).toHaveLength(0); - }); + expect(wrapper.find('.showing')).toHaveLength(1); + wrapper.simulate('click'); + expect(wrapper.find('.showing')).toHaveLength(0); + }); }); diff --git a/translate/src/core/notification/components/NotificationPanel.tsx b/translate/src/core/notification/components/NotificationPanel.tsx index 6392eaa75..744582808 100644 --- a/translate/src/core/notification/components/NotificationPanel.tsx +++ b/translate/src/core/notification/components/NotificationPanel.tsx @@ -5,11 +5,11 @@ import './NotificationPanel.css'; import type { NotificationState } from '../reducer'; type Props = { - notification: NotificationState; + notification: NotificationState; }; type State = { - hiding: boolean; + hiding: boolean; }; /** @@ -20,57 +20,54 @@ type State = { * clicked. */ export default class NotificationPanel extends React.Component { - hideTimeout: number; + hideTimeout: number; - constructor(props: Props) { - super(props); + constructor(props: Props) { + super(props); - // This state is used to start the in and out animations for - // notifications. - this.state = { - hiding: false, - }; - } - - componentDidUpdate(prevProps: Props) { - if ( - this.props.notification.message && - prevProps.notification.message !== this.props.notification.message - ) { - this.setState({ hiding: false }); - clearTimeout(this.hideTimeout); - this.hideTimeout = window.setTimeout(() => { - this.hide(); - }, 2000); - } - } - - hide: () => void = () => { - clearTimeout(this.hideTimeout); - this.setState({ hiding: true }); + // This state is used to start the in and out animations for + // notifications. + this.state = { + hiding: false, }; + } - render(): React.ReactElement<'div'> { - const { notification } = this.props; - - let hideClass = ''; - if (notification.message && !this.state.hiding) { - hideClass = ' showing'; - } - - const notif = notification.message; - - return ( -
    - {!notif ? ( - - ) : ( - {notif.content} - )} -
    - ); + componentDidUpdate(prevProps: Props) { + if ( + this.props.notification.message && + prevProps.notification.message !== this.props.notification.message + ) { + this.setState({ hiding: false }); + clearTimeout(this.hideTimeout); + this.hideTimeout = window.setTimeout(() => { + this.hide(); + }, 2000); } + } + + hide: () => void = () => { + clearTimeout(this.hideTimeout); + this.setState({ hiding: true }); + }; + + render(): React.ReactElement<'div'> { + const { notification } = this.props; + + let hideClass = ''; + if (notification.message && !this.state.hiding) { + hideClass = ' showing'; + } + + const notif = notification.message; + + return ( +
    + {!notif ? ( + + ) : ( + {notif.content} + )} +
    + ); + } } diff --git a/translate/src/core/notification/messages.tsx b/translate/src/core/notification/messages.tsx index d072d6404..db27d0b49 100644 --- a/translate/src/core/notification/messages.tsx +++ b/translate/src/core/notification/messages.tsx @@ -3,166 +3,164 @@ import { Localized } from '@fluent/react'; import type { NotificationMessage } from './actions'; const messages: Record = { - TRANSLATION_APPROVED: { - content: ( - - Translation approved - - ), - type: 'info', - }, - TRANSLATION_UNAPPROVED: { - content: ( - - Translation unapproved - - ), - type: 'info', - }, - TRANSLATION_REJECTED: { - content: ( - - Translation rejected - - ), - type: 'info', - }, - TRANSLATION_UNREJECTED: { - content: ( - - Translation unrejected - - ), - type: 'info', - }, - TRANSLATION_DELETED: { - content: ( - - Translation deleted - - ), - type: 'info', - }, - TRANSLATION_SAVED: { - content: ( - - Translation saved - - ), - type: 'info', - }, - UNABLE_TO_APPROVE_TRANSLATION: { - content: ( - - Unable to approve translation - - ), - type: 'error', - }, - UNABLE_TO_UNAPPROVE_TRANSLATION: { - content: ( - - Unable to unapprove translation - - ), - type: 'error', - }, - UNABLE_TO_REJECT_TRANSLATION: { - content: ( - - Unable to reject translation - - ), - type: 'error', - }, - UNABLE_TO_UNREJECT_TRANSLATION: { - content: ( - - Unable to unreject translation - - ), - type: 'error', - }, - UNABLE_TO_DELETE_TRANSLATION: { - content: ( - - Unable to delete translation - - ), - type: 'error', - }, - SAME_TRANSLATION: { - content: ( - - Same translation already exists - - ), - type: 'error', - }, - CHECKS_ENABLED: { - content: ( - - Translate Toolkit Checks enabled - - ), - type: 'info', - }, - CHECKS_DISABLED: { - content: ( - - Translate Toolkit Checks disabled - - ), - type: 'info', - }, - SUGGESTIONS_ENABLED: { - content: ( - - Make Suggestions enabled - - ), - type: 'info', - }, - SUGGESTIONS_DISABLED: { - content: ( - - Make Suggestions disabled - - ), - type: 'info', - }, - FTL_NOT_SUPPORTED_RICH_EDITOR: { - content: ( - - Translation not supported in rich editor - - ), - type: 'error', - }, - ENTITY_NOT_FOUND: { - content: ( - - Can’t load specified string - - ), - type: 'error', - }, - STRING_LINK_COPIED: { - content: ( - - Link copied to clipboard - - ), - type: 'info', - }, - COMMENT_ADDED: { - content: ( - - Comment added - - ), - type: 'info', - }, + TRANSLATION_APPROVED: { + content: ( + + Translation approved + + ), + type: 'info', + }, + TRANSLATION_UNAPPROVED: { + content: ( + + Translation unapproved + + ), + type: 'info', + }, + TRANSLATION_REJECTED: { + content: ( + + Translation rejected + + ), + type: 'info', + }, + TRANSLATION_UNREJECTED: { + content: ( + + Translation unrejected + + ), + type: 'info', + }, + TRANSLATION_DELETED: { + content: ( + + Translation deleted + + ), + type: 'info', + }, + TRANSLATION_SAVED: { + content: ( + + Translation saved + + ), + type: 'info', + }, + UNABLE_TO_APPROVE_TRANSLATION: { + content: ( + + Unable to approve translation + + ), + type: 'error', + }, + UNABLE_TO_UNAPPROVE_TRANSLATION: { + content: ( + + Unable to unapprove translation + + ), + type: 'error', + }, + UNABLE_TO_REJECT_TRANSLATION: { + content: ( + + Unable to reject translation + + ), + type: 'error', + }, + UNABLE_TO_UNREJECT_TRANSLATION: { + content: ( + + Unable to unreject translation + + ), + type: 'error', + }, + UNABLE_TO_DELETE_TRANSLATION: { + content: ( + + Unable to delete translation + + ), + type: 'error', + }, + SAME_TRANSLATION: { + content: ( + + Same translation already exists + + ), + type: 'error', + }, + CHECKS_ENABLED: { + content: ( + + Translate Toolkit Checks enabled + + ), + type: 'info', + }, + CHECKS_DISABLED: { + content: ( + + Translate Toolkit Checks disabled + + ), + type: 'info', + }, + SUGGESTIONS_ENABLED: { + content: ( + + Make Suggestions enabled + + ), + type: 'info', + }, + SUGGESTIONS_DISABLED: { + content: ( + + Make Suggestions disabled + + ), + type: 'info', + }, + FTL_NOT_SUPPORTED_RICH_EDITOR: { + content: ( + + Translation not supported in rich editor + + ), + type: 'error', + }, + ENTITY_NOT_FOUND: { + content: ( + + Can’t load specified string + + ), + type: 'error', + }, + STRING_LINK_COPIED: { + content: ( + + Link copied to clipboard + + ), + type: 'info', + }, + COMMENT_ADDED: { + content: ( + Comment added + ), + type: 'info', + }, }; export default messages; diff --git a/translate/src/core/notification/reducer.ts b/translate/src/core/notification/reducer.ts index c92094a13..8c59181a5 100644 --- a/translate/src/core/notification/reducer.ts +++ b/translate/src/core/notification/reducer.ts @@ -5,23 +5,23 @@ import type { AddAction, NotificationMessage } from './actions'; type Action = AddAction; export type NotificationState = { - readonly message: NotificationMessage | null | undefined; + readonly message: NotificationMessage | null | undefined; }; const initial: NotificationState = { - message: null, + message: null, }; export default function reducer( - state: NotificationState = initial, - action: Action, + state: NotificationState = initial, + action: Action, ): NotificationState { - switch (action.type) { - case ADD: - return { - message: action.message, - }; - default: - return state; - } + switch (action.type) { + case ADD: + return { + message: action.message, + }; + default: + return state; + } } diff --git a/translate/src/core/placeable/components/WithPlaceables.css b/translate/src/core/placeable/components/WithPlaceables.css index 67de0da84..e85cf3df2 100644 --- a/translate/src/core/placeable/components/WithPlaceables.css +++ b/translate/src/core/placeable/components/WithPlaceables.css @@ -1,18 +1,18 @@ mark.placeable { - background: #4d5967; - border: 1px solid #5e6475; - border-radius: 2px; - color: #cccccc; - font-style: normal; - font-weight: normal; - margin: 0 1px; + background: #4d5967; + border: 1px solid #5e6475; + border-radius: 2px; + color: #cccccc; + font-style: normal; + font-weight: normal; + margin: 0 1px; } mark.placeable [aria-hidden='true'] { - user-select: none; + user-select: none; } mark.placeable .hidden-source { - display: inline-block; - width: 0; + display: inline-block; + width: 0; } diff --git a/translate/src/core/placeable/components/WithPlaceables.test.js b/translate/src/core/placeable/components/WithPlaceables.test.js index 3605a8035..f053be71a 100644 --- a/translate/src/core/placeable/components/WithPlaceables.test.js +++ b/translate/src/core/placeable/components/WithPlaceables.test.js @@ -4,11 +4,11 @@ import { shallow } from 'enzyme'; import WithPlaceables from './WithPlaceables'; describe('Test parser order', () => { - it('matches JSON placeholder', () => { - const content = 'You have created $COUNT$ aliases'; - const wrapper = shallow({content}); + it('matches JSON placeholder', () => { + const content = 'You have created $COUNT$ aliases'; + const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toContain('$COUNT$'); - }); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toContain('$COUNT$'); + }); }); diff --git a/translate/src/core/placeable/components/WithPlaceables.ts b/translate/src/core/placeable/components/WithPlaceables.ts index f39561bec..921fb7b4d 100644 --- a/translate/src/core/placeable/components/WithPlaceables.ts +++ b/translate/src/core/placeable/components/WithPlaceables.ts @@ -34,49 +34,49 @@ import xmlTag from '../parsers/xmlTag'; // Note: the order of these MATTERS! export const rules = [ - newlineEscape, - newlineCharacter, - tabCharacter, - escapeSequence, + newlineEscape, + newlineCharacter, + tabCharacter, + escapeSequence, - // The spaces placeable can match '\n ' and mask the newline, - // so it has to come later. - leadingSpace, - unusualSpace, - nonBreakingSpace, - narrowNonBreakingSpace, - thinSpace, - multipleSpaces, + // The spaces placeable can match '\n ' and mask the newline, + // so it has to come later. + leadingSpace, + unusualSpace, + nonBreakingSpace, + narrowNonBreakingSpace, + thinSpace, + multipleSpaces, - // The XML placeables must be marked before variable placeables - // to avoid marking variables, but leaving out tags. See: - // https://bugzilla.mozilla.org/show_bug.cgi?id=1334926 - xmlTag, - altAttribute, - xmlEntity, + // The XML placeables must be marked before variable placeables + // to avoid marking variables, but leaving out tags. See: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1334926 + xmlTag, + altAttribute, + xmlEntity, - pythonFormatNamedString, - pythonFormatString, - pythonFormattingVariable, - javaFormattingVariable, - stringFormattingVariable, - // JSON Placeholder parser Must come before NSIS Variable parser, - // otherwise JSON Placeholders are marked up without the trailing $ - jsonPlaceholder, - nsisVariable, + pythonFormatNamedString, + pythonFormatString, + pythonFormattingVariable, + javaFormattingVariable, + stringFormattingVariable, + // JSON Placeholder parser Must come before NSIS Variable parser, + // otherwise JSON Placeholders are marked up without the trailing $ + jsonPlaceholder, + nsisVariable, - // The Qt variables can consume the %1 in %1$s which will mask a printf - // placeable, so it has to come later. - qtFormatting, + // The Qt variables can consume the %1 in %1$s which will mask a printf + // placeable, so it has to come later. + qtFormatting, - uriPattern, - filePattern, - emailPattern, - shortCapitalNumberString, - camelCaseString, - optionPattern, - punctuation, - numberString, + uriPattern, + filePattern, + emailPattern, + shortCapitalNumberString, + camelCaseString, + optionPattern, + punctuation, + numberString, ]; /** diff --git a/translate/src/core/placeable/components/WithPlaceablesForFluent.test.js b/translate/src/core/placeable/components/WithPlaceablesForFluent.test.js index ff2895039..c989823d8 100644 --- a/translate/src/core/placeable/components/WithPlaceablesForFluent.test.js +++ b/translate/src/core/placeable/components/WithPlaceablesForFluent.test.js @@ -5,25 +5,25 @@ import each from 'jest-each'; import WithPlaceablesForFluent from './WithPlaceablesForFluent'; describe('', () => { - each([ - ['Fluent string expression', '{"world"}', 'Hello {"world"}'], - ['Fluent term', '{ -brand-name }', 'Hello { -brand-name }'], - [ - 'Fluent parametrized term', - '{ -count($items) }', - 'We have { -count($items) } things', - ], - [ - 'Fluent function', - '{ COUNT(items: []) }', - 'I have { COUNT(items: []) } things', - ], - ]).it('matches a %s', (type, mark, content) => { - const wrapper = shallow( - {content}, - ); + each([ + ['Fluent string expression', '{"world"}', 'Hello {"world"}'], + ['Fluent term', '{ -brand-name }', 'Hello { -brand-name }'], + [ + 'Fluent parametrized term', + '{ -count($items) }', + 'We have { -count($items) } things', + ], + [ + 'Fluent function', + '{ COUNT(items: []) }', + 'I have { COUNT(items: []) } things', + ], + ]).it('matches a %s', (type, mark, content) => { + const wrapper = shallow( + {content}, + ); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toContain(mark); - }); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toContain(mark); + }); }); diff --git a/translate/src/core/placeable/components/WithPlaceablesForFluent.ts b/translate/src/core/placeable/components/WithPlaceablesForFluent.ts index 335479d54..2cfca1c31 100644 --- a/translate/src/core/placeable/components/WithPlaceablesForFluent.ts +++ b/translate/src/core/placeable/components/WithPlaceablesForFluent.ts @@ -11,16 +11,16 @@ import fluentTerm from '../parsers/fluentTerm'; import multipleSpaces from '../parsers/multipleSpaces'; export function getRulesWithFluent(rules: Array): Array { - const newRules = [...rules]; + const newRules = [...rules]; - // Insert after the last space-related rule. - let insertAfter = newRules.indexOf(multipleSpaces); - newRules.splice(insertAfter, 0, fluentFunction); - newRules.splice(insertAfter++, 0, fluentString); - newRules.splice(insertAfter++, 0, fluentParametrizedTerm); - newRules.splice(insertAfter++, 0, fluentTerm); + // Insert after the last space-related rule. + let insertAfter = newRules.indexOf(multipleSpaces); + newRules.splice(insertAfter, 0, fluentFunction); + newRules.splice(insertAfter++, 0, fluentString); + newRules.splice(insertAfter++, 0, fluentParametrizedTerm); + newRules.splice(insertAfter++, 0, fluentTerm); - return newRules; + return newRules; } /** diff --git a/translate/src/core/placeable/components/WithPlaceablesForFluentNoLeadingSpace.ts b/translate/src/core/placeable/components/WithPlaceablesForFluentNoLeadingSpace.ts index e5e1dbb4d..c3926c8f8 100644 --- a/translate/src/core/placeable/components/WithPlaceablesForFluentNoLeadingSpace.ts +++ b/translate/src/core/placeable/components/WithPlaceablesForFluentNoLeadingSpace.ts @@ -13,7 +13,7 @@ import { getRulesWithoutLeadingSpace } from './WithPlaceablesNoLeadingSpace'; * See ./WithPlaceablesNoLeadingSpace.js for documentation. */ const WithPlaceablesForFluentNoLeadingSpace: any = createMarker( - getRulesWithFluent(getRulesWithoutLeadingSpace(rules)), + getRulesWithFluent(getRulesWithoutLeadingSpace(rules)), ); export default WithPlaceablesForFluentNoLeadingSpace; diff --git a/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.test.js b/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.test.js index 55fc08c02..197c1203d 100644 --- a/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.test.js +++ b/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.test.js @@ -4,26 +4,22 @@ import { shallow } from 'enzyme'; import WithPlaceablesNoLeadingSpace from './WithPlaceablesNoLeadingSpace'; describe('', () => { - it('matches newlines in a string', () => { - const content = 'Hello\nworld'; - const wrapper = shallow( - - {content} - , - ); + it('matches newlines in a string', () => { + const content = 'Hello\nworld'; + const wrapper = shallow( + {content}, + ); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toContain('\n'); - }); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toContain('\n'); + }); - it('does not match spaces at the beginning of a string', () => { - const content = ' Hello world'; - const wrapper = shallow( - - {content} - , - ); + it('does not match spaces at the beginning of a string', () => { + const content = ' Hello world'; + const wrapper = shallow( + {content}, + ); - expect(wrapper.text()).toEqual(content); - }); + expect(wrapper.text()).toEqual(content); + }); }); diff --git a/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.ts b/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.ts index 50cb7c015..c37db189e 100644 --- a/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.ts +++ b/translate/src/core/placeable/components/WithPlaceablesNoLeadingSpace.ts @@ -8,12 +8,12 @@ import leadingSpace from '../parsers/leadingSpace'; import unusualSpace from '../parsers/unusualSpace'; export function getRulesWithoutLeadingSpace( - rules: Array, + rules: Array, ): Array { - let newRules = [...rules]; - newRules.splice(newRules.indexOf(leadingSpace), 1); - newRules.splice(newRules.indexOf(unusualSpace), 1); - return newRules; + let newRules = [...rules]; + newRules.splice(newRules.indexOf(leadingSpace), 1); + newRules.splice(newRules.indexOf(unusualSpace), 1); + return newRules; } /** @@ -28,7 +28,7 @@ export function getRulesWithoutLeadingSpace( * combination with other parsing tools (like diff). */ const WithPlaceablesNoLeadingSpace: any = createMarker( - getRulesWithoutLeadingSpace(rules), + getRulesWithoutLeadingSpace(rules), ); export default WithPlaceablesNoLeadingSpace; diff --git a/translate/src/core/placeable/parsers/altAttribute.test.js b/translate/src/core/placeable/parsers/altAttribute.test.js index 6ca30182e..0a173cdd9 100644 --- a/translate/src/core/placeable/parsers/altAttribute.test.js +++ b/translate/src/core/placeable/parsers/altAttribute.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import altAttribute from './altAttribute'; describe('altAttribute', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([altAttribute]); - const content = 'alt="hello"'; + it('marks the right parts of a string', () => { + const Marker = createMarker([altAttribute]); + const content = 'alt="hello"'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('alt="hello"'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('alt="hello"'); + }); }); diff --git a/translate/src/core/placeable/parsers/altAttribute.tsx b/translate/src/core/placeable/parsers/altAttribute.tsx index 9900fc53d..c7da6b7d1 100644 --- a/translate/src/core/placeable/parsers/altAttribute.tsx +++ b/translate/src/core/placeable/parsers/altAttribute.tsx @@ -13,23 +13,20 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L55 */ const altAttribute = { - rule: /(alt=".*?")/i as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(alt=".*?")/i as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default altAttribute; diff --git a/translate/src/core/placeable/parsers/camelCaseString.test.js b/translate/src/core/placeable/parsers/camelCaseString.test.js index 69aea5f13..f4761f41c 100644 --- a/translate/src/core/placeable/parsers/camelCaseString.test.js +++ b/translate/src/core/placeable/parsers/camelCaseString.test.js @@ -7,24 +7,24 @@ import createMarker from 'react-content-marker'; import camelCaseString from './camelCaseString'; describe('camelCaseString', () => { - each([ - ['CamelCase', 'Hello CamelCase'], - ['iPod', 'Hello iPod'], - ['DokuWiki', 'Hello DokuWiki'], - ['KBabel', 'Hello KBabel'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([camelCaseString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['CamelCase', 'Hello CamelCase'], + ['iPod', 'Hello iPod'], + ['DokuWiki', 'Hello DokuWiki'], + ['KBabel', 'Hello KBabel'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([camelCaseString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['_Bug'], ['NOTCAMEL']]).it( - 'does not mark anything in `%s`', - (content) => { - const Marker = createMarker([camelCaseString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }, - ); + each([['_Bug'], ['NOTCAMEL']]).it( + 'does not mark anything in `%s`', + (content) => { + const Marker = createMarker([camelCaseString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/camelCaseString.tsx b/translate/src/core/placeable/parsers/camelCaseString.tsx index 41bcf108c..b9ecf245f 100644 --- a/translate/src/core/placeable/parsers/camelCaseString.tsx +++ b/translate/src/core/placeable/parsers/camelCaseString.tsx @@ -14,20 +14,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L274 */ const camelCaseString = { - rule: /(\b([a-z]+[A-Z]|[A-Z]+[a-z]+[A-Z]|[A-Z]{2,}[a-z])[a-zA-Z0-9]*\b)/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(\b([a-z]+[A-Z]|[A-Z]+[a-z]+[A-Z]|[A-Z]{2,}[a-z])[a-zA-Z0-9]*\b)/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default camelCaseString; diff --git a/translate/src/core/placeable/parsers/emailPattern.test.js b/translate/src/core/placeable/parsers/emailPattern.test.js index d2fd70cfe..076112ce3 100644 --- a/translate/src/core/placeable/parsers/emailPattern.test.js +++ b/translate/src/core/placeable/parsers/emailPattern.test.js @@ -7,13 +7,13 @@ import createMarker from 'react-content-marker'; import emailPattern from './emailPattern'; describe('emailPattern', () => { - each([ - ['lisa@example.org', 'Hello lisa@example.org'], - ['mailto:lisa@name.me', 'Hello mailto:lisa@name.me'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([emailPattern]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['lisa@example.org', 'Hello lisa@example.org'], + ['mailto:lisa@name.me', 'Hello mailto:lisa@name.me'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([emailPattern]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/emailPattern.tsx b/translate/src/core/placeable/parsers/emailPattern.tsx index 31fc2a1f7..2fc258e68 100644 --- a/translate/src/core/placeable/parsers/emailPattern.tsx +++ b/translate/src/core/placeable/parsers/emailPattern.tsx @@ -14,20 +14,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L220 */ const emailPattern = { - rule: /(((mailto:)|)[A-Za-z0-9]+[-a-zA-Z0-9._%]*@(([-A-Za-z0-9]+)\.)+[a-zA-Z]{2,4})/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(((mailto:)|)[A-Za-z0-9]+[-a-zA-Z0-9._%]*@(([-A-Za-z0-9]+)\.)+[a-zA-Z]{2,4})/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default emailPattern; diff --git a/translate/src/core/placeable/parsers/escapeSequence.test.js b/translate/src/core/placeable/parsers/escapeSequence.test.js index 7cc0315c9..bf0398cd8 100644 --- a/translate/src/core/placeable/parsers/escapeSequence.test.js +++ b/translate/src/core/placeable/parsers/escapeSequence.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import escapeSequence from './escapeSequence'; describe('escapeSequence', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([escapeSequence]); - const content = 'hello,\\tworld'; + it('marks the right parts of a string', () => { + const Marker = createMarker([escapeSequence]); + const content = 'hello,\\tworld'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\\'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\\'); + }); }); diff --git a/translate/src/core/placeable/parsers/escapeSequence.tsx b/translate/src/core/placeable/parsers/escapeSequence.tsx index 4108a4e08..78728f261 100644 --- a/translate/src/core/placeable/parsers/escapeSequence.tsx +++ b/translate/src/core/placeable/parsers/escapeSequence.tsx @@ -5,19 +5,16 @@ import { Localized } from '@fluent/react'; * Marks the escape character "\". */ const escapeSequence = { - rule: '\\', - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: '\\', + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default escapeSequence; diff --git a/translate/src/core/placeable/parsers/filePattern.test.js b/translate/src/core/placeable/parsers/filePattern.test.js index 02e3c4c44..b8ac41124 100644 --- a/translate/src/core/placeable/parsers/filePattern.test.js +++ b/translate/src/core/placeable/parsers/filePattern.test.js @@ -7,22 +7,22 @@ import createMarker from 'react-content-marker'; import filePattern from './filePattern'; describe('filePattern', () => { - each([ - ['/home', '/home'], - ['/home/lisa', 'Hello /home/lisa'], - ['/home', 'The path /home leads to your home'], - ['~/user', 'Hello ~/user'], - ['/home/homer/budget.md', 'The money is in /home/homer/budget.md'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([filePattern]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['/home', '/home'], + ['/home/lisa', 'Hello /home/lisa'], + ['/home', 'The path /home leads to your home'], + ['~/user', 'Hello ~/user'], + ['/home/homer/budget.md', 'The money is in /home/homer/budget.md'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([filePattern]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['Pause/Resume']]).it('does not mark anything in `%s`', (content) => { - const Marker = createMarker([filePattern]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }); + each([['Pause/Resume']]).it('does not mark anything in `%s`', (content) => { + const Marker = createMarker([filePattern]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }); }); diff --git a/translate/src/core/placeable/parsers/filePattern.tsx b/translate/src/core/placeable/parsers/filePattern.tsx index 96c01cf9e..a7521fbf6 100644 --- a/translate/src/core/placeable/parsers/filePattern.tsx +++ b/translate/src/core/placeable/parsers/filePattern.tsx @@ -14,20 +14,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L208 */ const filePattern = { - rule: /(^|\s)((~\/|\/|\.\/)([-A-Za-z0-9_$.+!*(),;:@&=?/~#%]|\\){3,})/ as RegExp, - matchIndex: 2, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(^|\s)((~\/|\/|\.\/)([-A-Za-z0-9_$.+!*(),;:@&=?/~#%]|\\){3,})/ as RegExp, + matchIndex: 2, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default filePattern; diff --git a/translate/src/core/placeable/parsers/fluentFunction.test.js b/translate/src/core/placeable/parsers/fluentFunction.test.js index c674cfb59..7aa79df33 100644 --- a/translate/src/core/placeable/parsers/fluentFunction.test.js +++ b/translate/src/core/placeable/parsers/fluentFunction.test.js @@ -6,31 +6,31 @@ import each from 'jest-each'; import fluentFunction from './fluentFunction'; describe('fluentFunction', () => { - each([ - ['{COPY()}', 'Hello {COPY()}'], - ['{ DATETIME($date) }', 'Hello { DATETIME($date) }'], - [ - '{ NUMBER($ratio, minimumFractionDigits: 2) }', - 'Hello { NUMBER($ratio, minimumFractionDigits: 2) }', - ], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([fluentFunction]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{COPY()}', 'Hello {COPY()}'], + ['{ DATETIME($date) }', 'Hello { DATETIME($date) }'], + [ + '{ NUMBER($ratio, minimumFractionDigits: 2) }', + 'Hello { NUMBER($ratio, minimumFractionDigits: 2) }', + ], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([fluentFunction]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([ - [ - '{ DATETIME($date) }', - '{ COPY() }', - 'Hello { DATETIME($date) } and { COPY() }', - ], - ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { - const Marker = createMarker([fluentFunction]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(2); - expect(wrapper.find('mark').at(0).text()).toEqual(mark1); - expect(wrapper.find('mark').at(1).text()).toEqual(mark2); - }); + each([ + [ + '{ DATETIME($date) }', + '{ COPY() }', + 'Hello { DATETIME($date) } and { COPY() }', + ], + ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { + const Marker = createMarker([fluentFunction]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(2); + expect(wrapper.find('mark').at(0).text()).toEqual(mark1); + expect(wrapper.find('mark').at(1).text()).toEqual(mark2); + }); }); diff --git a/translate/src/core/placeable/parsers/fluentFunction.tsx b/translate/src/core/placeable/parsers/fluentFunction.tsx index 4c3f736af..1bd85d806 100644 --- a/translate/src/core/placeable/parsers/fluentFunction.tsx +++ b/translate/src/core/placeable/parsers/fluentFunction.tsx @@ -13,19 +13,16 @@ import { Localized } from '@fluent/react'; * { NUMBER($ratio, minimumFractionDigits: 2) } */ const fluentFunction = { - rule: /({ ?[A-W0-9\-_]+[^}]* ?})/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /({ ?[A-W0-9\-_]+[^}]* ?})/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default fluentFunction; diff --git a/translate/src/core/placeable/parsers/fluentParametrizedTerm.test.js b/translate/src/core/placeable/parsers/fluentParametrizedTerm.test.js index 0cfdfdc29..c3813ee57 100644 --- a/translate/src/core/placeable/parsers/fluentParametrizedTerm.test.js +++ b/translate/src/core/placeable/parsers/fluentParametrizedTerm.test.js @@ -6,34 +6,31 @@ import each from 'jest-each'; import fluentParametrizedTerm from './fluentParametrizedTerm'; describe('fluentParametrizedTerm', () => { - each([ - ['{-brand(case: "test")}', 'Hello {-brand(case: "test")}'], - [ - '{ -brand(case: "what ever") }', - 'Hello { -brand(case: "what ever") }', - ], - [ - '{ -brand-name(foo-bar: "now that\'s a value!") }', - 'Hello { -brand-name(foo-bar: "now that\'s a value!") }', - ], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([fluentParametrizedTerm]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{-brand(case: "test")}', 'Hello {-brand(case: "test")}'], + ['{ -brand(case: "what ever") }', 'Hello { -brand(case: "what ever") }'], + [ + '{ -brand-name(foo-bar: "now that\'s a value!") }', + 'Hello { -brand-name(foo-bar: "now that\'s a value!") }', + ], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([fluentParametrizedTerm]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([ - [ - '{-brand(case: "test")}', - '{-vendor(case: "right")}', - 'Hello {-brand(case: "test")} and {-vendor(case: "right")}', - ], - ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { - const Marker = createMarker([fluentParametrizedTerm]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(2); - expect(wrapper.find('mark').at(0).text()).toEqual(mark1); - expect(wrapper.find('mark').at(1).text()).toEqual(mark2); - }); + each([ + [ + '{-brand(case: "test")}', + '{-vendor(case: "right")}', + 'Hello {-brand(case: "test")} and {-vendor(case: "right")}', + ], + ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { + const Marker = createMarker([fluentParametrizedTerm]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(2); + expect(wrapper.find('mark').at(0).text()).toEqual(mark1); + expect(wrapper.find('mark').at(1).text()).toEqual(mark2); + }); }); diff --git a/translate/src/core/placeable/parsers/fluentParametrizedTerm.tsx b/translate/src/core/placeable/parsers/fluentParametrizedTerm.tsx index 22934e6ec..ddc329b10 100644 --- a/translate/src/core/placeable/parsers/fluentParametrizedTerm.tsx +++ b/translate/src/core/placeable/parsers/fluentParametrizedTerm.tsx @@ -13,24 +13,20 @@ import { Localized } from '@fluent/react'; * { -brand-name(foo-bar: "now that's a value!") } */ const fluentParametrizedTerm = { - rule: /({ ?-[^}]*([^}]*: ?[^}]*) ?})/ as RegExp, - matchIndex: 1, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /({ ?-[^}]*([^}]*: ?[^}]*) ?})/ as RegExp, + matchIndex: 1, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default fluentParametrizedTerm; diff --git a/translate/src/core/placeable/parsers/fluentString.test.js b/translate/src/core/placeable/parsers/fluentString.test.js index a39cfb47d..b269ec4a3 100644 --- a/translate/src/core/placeable/parsers/fluentString.test.js +++ b/translate/src/core/placeable/parsers/fluentString.test.js @@ -6,28 +6,24 @@ import each from 'jest-each'; import fluentString from './fluentString'; describe('fluentString', () => { - each([ - ['{""}', 'Hello {""}'], - ['{ "" }', 'Hello { "" }'], - ['{ "world!" }', 'Hello { "world!" }'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([fluentString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{""}', 'Hello {""}'], + ['{ "" }', 'Hello { "" }'], + ['{ "world!" }', 'Hello { "world!" }'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([fluentString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([ - [ - '{ "hello!" }', - '{ "world!" }', - 'Hello { "hello!" } from { "world!" }', - ], - ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { - const Marker = createMarker([fluentString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(2); - expect(wrapper.find('mark').at(0).text()).toEqual(mark1); - expect(wrapper.find('mark').at(1).text()).toEqual(mark2); - }); + each([ + ['{ "hello!" }', '{ "world!" }', 'Hello { "hello!" } from { "world!" }'], + ]).it('marks `%s` and `%s` in `%s`', (mark1, mark2, content) => { + const Marker = createMarker([fluentString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(2); + expect(wrapper.find('mark').at(0).text()).toEqual(mark1); + expect(wrapper.find('mark').at(1).text()).toEqual(mark2); + }); }); diff --git a/translate/src/core/placeable/parsers/fluentString.tsx b/translate/src/core/placeable/parsers/fluentString.tsx index 981104fee..bf17eeffd 100644 --- a/translate/src/core/placeable/parsers/fluentString.tsx +++ b/translate/src/core/placeable/parsers/fluentString.tsx @@ -12,23 +12,16 @@ import { Localized } from '@fluent/react'; * { "Hello, World" } */ const fluentString = { - rule: /({ ?"[^}]*" ?})/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /({ ?"[^}]*" ?})/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default fluentString; diff --git a/translate/src/core/placeable/parsers/fluentTerm.test.js b/translate/src/core/placeable/parsers/fluentTerm.test.js index 9227a753f..3d6eda2a6 100644 --- a/translate/src/core/placeable/parsers/fluentTerm.test.js +++ b/translate/src/core/placeable/parsers/fluentTerm.test.js @@ -6,25 +6,25 @@ import each from 'jest-each'; import fluentTerm from './fluentTerm'; describe('fluentTerm', () => { - each([ - ['{-brand}', 'Hello {-brand}'], - ['{ -brand }', 'Hello { -brand }'], - ['{ -brand-name }', 'Hello { -brand-name }'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([fluentTerm]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{-brand}', 'Hello {-brand}'], + ['{ -brand }', 'Hello { -brand }'], + ['{ -brand-name }', 'Hello { -brand-name }'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([fluentTerm]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['{-brand}', '{-vendor}', 'Hello {-brand} from {-vendor}']]).it( - 'marks `%s` and `%s` in `%s`', - (mark1, mark2, content) => { - const Marker = createMarker([fluentTerm]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(2); - expect(wrapper.find('mark').at(0).text()).toEqual(mark1); - expect(wrapper.find('mark').at(1).text()).toEqual(mark2); - }, - ); + each([['{-brand}', '{-vendor}', 'Hello {-brand} from {-vendor}']]).it( + 'marks `%s` and `%s` in `%s`', + (mark1, mark2, content) => { + const Marker = createMarker([fluentTerm]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(2); + expect(wrapper.find('mark').at(0).text()).toEqual(mark1); + expect(wrapper.find('mark').at(1).text()).toEqual(mark2); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/fluentTerm.tsx b/translate/src/core/placeable/parsers/fluentTerm.tsx index 8ccf9178e..35c83c682 100644 --- a/translate/src/core/placeable/parsers/fluentTerm.tsx +++ b/translate/src/core/placeable/parsers/fluentTerm.tsx @@ -13,16 +13,16 @@ import { Localized } from '@fluent/react'; * { -brand-name } */ const fluentTerm = { - rule: /({ ?-[^}]* ?})/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /({ ?-[^}]* ?})/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default fluentTerm; diff --git a/translate/src/core/placeable/parsers/javaFormattingVariable.test.js b/translate/src/core/placeable/parsers/javaFormattingVariable.test.js index d330a9a11..dbb8fa799 100644 --- a/translate/src/core/placeable/parsers/javaFormattingVariable.test.js +++ b/translate/src/core/placeable/parsers/javaFormattingVariable.test.js @@ -7,15 +7,15 @@ import createMarker from 'react-content-marker'; import javaFormattingVariable from './javaFormattingVariable'; describe('javaFormattingVariable', () => { - each([ - ['{1,time}', 'At {1,time}'], - ['{1,date}', 'on {1,date}, '], - ['{2}', 'there was {2} '], - ['{0,number,integer}', 'n planet {0,number,integer}.'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([javaFormattingVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{1,time}', 'At {1,time}'], + ['{1,date}', 'on {1,date}, '], + ['{2}', 'there was {2} '], + ['{0,number,integer}', 'n planet {0,number,integer}.'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([javaFormattingVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/javaFormattingVariable.tsx b/translate/src/core/placeable/parsers/javaFormattingVariable.tsx index 83e400c9f..a90e48649 100644 --- a/translate/src/core/placeable/parsers/javaFormattingVariable.tsx +++ b/translate/src/core/placeable/parsers/javaFormattingVariable.tsx @@ -22,24 +22,24 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L127 */ const javaFormattingVariable = { - rule: /({[0-9]+(,\s*(number(,\s*(integer|currency|percent|[-0#.,E;%\u2030\u00a4']+)?)?|(date|time)(,\s*(short|medium|long|full|.+?))?|choice,([^{]+({.+})?)+)?)?})/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /({[0-9]+(,\s*(number(,\s*(integer|currency|percent|[-0#.,E;%\u2030\u00a4']+)?)?|(date|time)(,\s*(short|medium|long|full|.+?))?|choice,([^{]+({.+})?)+)?)?})/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default javaFormattingVariable; diff --git a/translate/src/core/placeable/parsers/jsonPlaceholder.test.js b/translate/src/core/placeable/parsers/jsonPlaceholder.test.js index bcbe5ab62..b0e105635 100644 --- a/translate/src/core/placeable/parsers/jsonPlaceholder.test.js +++ b/translate/src/core/placeable/parsers/jsonPlaceholder.test.js @@ -7,23 +7,23 @@ import createMarker from 'react-content-marker'; import jsonPlaceholder from './jsonPlaceholder'; describe('jsonPlaceholder', () => { - each([ - ['$USER$', 'Hello $USER$'], - ['$USER1$', 'Hello $USER1$'], - ['$FIRST_NAME$', 'Hello $FIRST_NAME$'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([jsonPlaceholder]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['$USER$', 'Hello $USER$'], + ['$USER1$', 'Hello $USER1$'], + ['$FIRST_NAME$', 'Hello $FIRST_NAME$'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([jsonPlaceholder]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['$user$', 'Hello $user$'], ['Hello $USER'], ['Hello USER$']]).it( - 'does not mark anything in `%s`', - (content) => { - const Marker = createMarker([jsonPlaceholder]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }, - ); + each([['$user$', 'Hello $user$'], ['Hello $USER'], ['Hello USER$']]).it( + 'does not mark anything in `%s`', + (content) => { + const Marker = createMarker([jsonPlaceholder]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/jsonPlaceholder.tsx b/translate/src/core/placeable/parsers/jsonPlaceholder.tsx index 7a5faa1d5..1bd871a76 100644 --- a/translate/src/core/placeable/parsers/jsonPlaceholder.tsx +++ b/translate/src/core/placeable/parsers/jsonPlaceholder.tsx @@ -13,19 +13,16 @@ import { Localized } from '@fluent/react'; * $FIRST_NAME$ */ const jsonPlaceholder = { - rule: /(\$[A-Z0-9_]+\$)/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(\$[A-Z0-9_]+\$)/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default jsonPlaceholder; diff --git a/translate/src/core/placeable/parsers/leadingSpace.test.js b/translate/src/core/placeable/parsers/leadingSpace.test.js index 025341af8..431a5b777 100644 --- a/translate/src/core/placeable/parsers/leadingSpace.test.js +++ b/translate/src/core/placeable/parsers/leadingSpace.test.js @@ -7,10 +7,10 @@ import createMarker from 'react-content-marker'; import leadingSpace from './leadingSpace'; describe('leadingSpace', () => { - each([[' ', ' hello world']]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([leadingSpace]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([[' ', ' hello world']]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([leadingSpace]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/leadingSpace.tsx b/translate/src/core/placeable/parsers/leadingSpace.tsx index b016af64d..6e00782f8 100644 --- a/translate/src/core/placeable/parsers/leadingSpace.tsx +++ b/translate/src/core/placeable/parsers/leadingSpace.tsx @@ -9,19 +9,16 @@ import { Localized } from '@fluent/react'; * " Hello, world" */ const leadingSpace = { - rule: /(^ +)/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(^ +)/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default leadingSpace; diff --git a/translate/src/core/placeable/parsers/multipleSpaces.test.js b/translate/src/core/placeable/parsers/multipleSpaces.test.js index e654a0ced..93c5dad84 100644 --- a/translate/src/core/placeable/parsers/multipleSpaces.test.js +++ b/translate/src/core/placeable/parsers/multipleSpaces.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import multipleSpaces from './multipleSpaces'; describe('multipleSpaces', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([multipleSpaces]); - const content = 'hello, world'; + it('marks the right parts of a string', () => { + const Marker = createMarker([multipleSpaces]); + const content = 'hello, world'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(' \u00B7 '); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(' \u00B7 '); + }); }); diff --git a/translate/src/core/placeable/parsers/multipleSpaces.tsx b/translate/src/core/placeable/parsers/multipleSpaces.tsx index 6ca1adbea..912e6b87d 100644 --- a/translate/src/core/placeable/parsers/multipleSpaces.tsx +++ b/translate/src/core/placeable/parsers/multipleSpaces.tsx @@ -5,25 +5,22 @@ import { Localized } from '@fluent/react'; * Marks multiple consecutive spaces and replaces them with a middle dot. */ const multipleSpaces = { - rule: /( +)/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - · - - - ); - }, + rule: /( +)/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + · + + + ); + }, }; export default multipleSpaces; diff --git a/translate/src/core/placeable/parsers/narrowNonBreakingSpace.test.js b/translate/src/core/placeable/parsers/narrowNonBreakingSpace.test.js index 93faba184..024788768 100644 --- a/translate/src/core/placeable/parsers/narrowNonBreakingSpace.test.js +++ b/translate/src/core/placeable/parsers/narrowNonBreakingSpace.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import narrowNonBreakingSpace from './narrowNonBreakingSpace'; describe('narrowNonBreakingSpace', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([narrowNonBreakingSpace]); - const content = 'hello,\u202Fworld'; + it('marks the right parts of a string', () => { + const Marker = createMarker([narrowNonBreakingSpace]); + const content = 'hello,\u202Fworld'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\u202F'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\u202F'); + }); }); diff --git a/translate/src/core/placeable/parsers/narrowNonBreakingSpace.tsx b/translate/src/core/placeable/parsers/narrowNonBreakingSpace.tsx index 80b86fe5c..8b8fc924f 100644 --- a/translate/src/core/placeable/parsers/narrowNonBreakingSpace.tsx +++ b/translate/src/core/placeable/parsers/narrowNonBreakingSpace.tsx @@ -5,23 +5,19 @@ import { Localized } from '@fluent/react'; * Marks the narrow no-break space character (Unicode U+202F). */ const narrowNonBreakingSpace = { - rule: /([\u202F])/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /([\u202F])/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default narrowNonBreakingSpace; diff --git a/translate/src/core/placeable/parsers/newlineCharacter.test.js b/translate/src/core/placeable/parsers/newlineCharacter.test.js index 217206ad9..f093b9abc 100644 --- a/translate/src/core/placeable/parsers/newlineCharacter.test.js +++ b/translate/src/core/placeable/parsers/newlineCharacter.test.js @@ -6,13 +6,13 @@ import createMarker from 'react-content-marker'; import newlineCharacter from './newlineCharacter'; describe('newlineCharacter', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([newlineCharacter]); - const content = `hello, + it('marks the right parts of a string', () => { + const Marker = createMarker([newlineCharacter]); + const content = `hello, world`; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('¶\n'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('¶\n'); + }); }); diff --git a/translate/src/core/placeable/parsers/newlineCharacter.tsx b/translate/src/core/placeable/parsers/newlineCharacter.tsx index 6dd2338b8..e99caa2c4 100644 --- a/translate/src/core/placeable/parsers/newlineCharacter.tsx +++ b/translate/src/core/placeable/parsers/newlineCharacter.tsx @@ -5,25 +5,22 @@ import { Localized } from '@fluent/react'; * Marks the newline character "\n". */ const newlineCharacter = { - rule: '\n', - tag: (x: string): React.ReactElement => { - return ( - - - - {x} - - - ); - }, + rule: '\n', + tag: (x: string): React.ReactElement => { + return ( + + + + {x} + + + ); + }, }; export default newlineCharacter; diff --git a/translate/src/core/placeable/parsers/newlineEscape.test.js b/translate/src/core/placeable/parsers/newlineEscape.test.js index 70dbc57bc..2c795320c 100644 --- a/translate/src/core/placeable/parsers/newlineEscape.test.js +++ b/translate/src/core/placeable/parsers/newlineEscape.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import newlineEscape from './newlineEscape'; describe('newlineEscape', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([newlineEscape]); - const content = '\\n'; + it('marks the right parts of a string', () => { + const Marker = createMarker([newlineEscape]); + const content = '\\n'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\\n'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\\n'); + }); }); diff --git a/translate/src/core/placeable/parsers/newlineEscape.tsx b/translate/src/core/placeable/parsers/newlineEscape.tsx index b743d4808..f0bc21748 100644 --- a/translate/src/core/placeable/parsers/newlineEscape.tsx +++ b/translate/src/core/placeable/parsers/newlineEscape.tsx @@ -5,19 +5,16 @@ import { Localized } from '@fluent/react'; * Marks escaped newline characters. */ const newlineEscape = { - rule: '\\n', - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: '\\n', + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default newlineEscape; diff --git a/translate/src/core/placeable/parsers/nonBreakingSpace.test.js b/translate/src/core/placeable/parsers/nonBreakingSpace.test.js index 1a7b37091..6c3bb8528 100644 --- a/translate/src/core/placeable/parsers/nonBreakingSpace.test.js +++ b/translate/src/core/placeable/parsers/nonBreakingSpace.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import nonBreakingSpace from './nonBreakingSpace'; describe('nonBreakingSpace', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([nonBreakingSpace]); - const content = 'hello,\u00A0world'; + it('marks the right parts of a string', () => { + const Marker = createMarker([nonBreakingSpace]); + const content = 'hello,\u00A0world'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\u00A0'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\u00A0'); + }); }); diff --git a/translate/src/core/placeable/parsers/nonBreakingSpace.tsx b/translate/src/core/placeable/parsers/nonBreakingSpace.tsx index 4018dcec3..f1c04ed23 100644 --- a/translate/src/core/placeable/parsers/nonBreakingSpace.tsx +++ b/translate/src/core/placeable/parsers/nonBreakingSpace.tsx @@ -5,23 +5,16 @@ import { Localized } from '@fluent/react'; * Marks the no-break space character (Unicode U+00A0). */ const nonBreakingSpace = { - rule: '\u00A0', - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: '\u00A0', + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default nonBreakingSpace; diff --git a/translate/src/core/placeable/parsers/nsisVariable.test.js b/translate/src/core/placeable/parsers/nsisVariable.test.js index 8ad7ebf24..3e77d340a 100644 --- a/translate/src/core/placeable/parsers/nsisVariable.test.js +++ b/translate/src/core/placeable/parsers/nsisVariable.test.js @@ -7,23 +7,23 @@ import createMarker from 'react-content-marker'; import nsisVariable from './nsisVariable'; describe('nsisVariable', () => { - each([ - ['$Brand', '$Brand'], - ['$BrandName', 'Welcome to $BrandName'], - ['$MyVar13', 'I am $MyVar13'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([nsisVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['$Brand', '$Brand'], + ['$BrandName', 'Welcome to $BrandName'], + ['$MyVar13', 'I am $MyVar13'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([nsisVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['$10'], ['foo$bar']]).it( - 'does not mark anything in `%s`', - (content) => { - const Marker = createMarker([nsisVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }, - ); + each([['$10'], ['foo$bar']]).it( + 'does not mark anything in `%s`', + (content) => { + const Marker = createMarker([nsisVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/nsisVariable.tsx b/translate/src/core/placeable/parsers/nsisVariable.tsx index 341321055..42f3f1fdf 100644 --- a/translate/src/core/placeable/parsers/nsisVariable.tsx +++ b/translate/src/core/placeable/parsers/nsisVariable.tsx @@ -10,20 +10,17 @@ import { Localized } from '@fluent/react'; * $BrandShortName */ const nsisVariable = { - rule: /(^|\s)(\$[a-zA-Z][\w]*)/ as RegExp, - matchIndex: 2, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(^|\s)(\$[a-zA-Z][\w]*)/ as RegExp, + matchIndex: 2, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default nsisVariable; diff --git a/translate/src/core/placeable/parsers/numberString.test.js b/translate/src/core/placeable/parsers/numberString.test.js index 3f6190f8d..2cbb3dbb2 100644 --- a/translate/src/core/placeable/parsers/numberString.test.js +++ b/translate/src/core/placeable/parsers/numberString.test.js @@ -7,23 +7,23 @@ import createMarker from 'react-content-marker'; import numberString from './numberString'; describe('numberString', () => { - each([ - ['25', 'Here is a 25 number'], - ['-25', 'Here is a -25 number'], - ['+25', 'Here is a +25 number'], - ['25.00', 'Here is a 25.00 number'], - ['2,500.00', 'Here is a 2,500.00 number'], - ['1\u00A0000,99', 'Here is a 1\u00A0000,99 number'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([numberString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['25', 'Here is a 25 number'], + ['-25', 'Here is a -25 number'], + ['+25', 'Here is a +25 number'], + ['25.00', 'Here is a 25.00 number'], + ['2,500.00', 'Here is a 2,500.00 number'], + ['1\u00A0000,99', 'Here is a 1\u00A0000,99 number'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([numberString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['3D game']]).it('does not mark anything in `%s`', (content) => { - const Marker = createMarker([numberString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }); + each([['3D game']]).it('does not mark anything in `%s`', (content) => { + const Marker = createMarker([numberString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }); }); diff --git a/translate/src/core/placeable/parsers/numberString.tsx b/translate/src/core/placeable/parsers/numberString.tsx index 32c485a2f..3282d87fa 100644 --- a/translate/src/core/placeable/parsers/numberString.tsx +++ b/translate/src/core/placeable/parsers/numberString.tsx @@ -15,20 +15,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L72 */ const numberString = { - rule: /([-+]?[0-9]+([\u00A0.,][0-9]+)*)\b/u as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /([-+]?[0-9]+([\u00A0.,][0-9]+)*)\b/u as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default numberString; diff --git a/translate/src/core/placeable/parsers/optionPattern.test.js b/translate/src/core/placeable/parsers/optionPattern.test.js index 86be59ddc..b001a1092 100644 --- a/translate/src/core/placeable/parsers/optionPattern.test.js +++ b/translate/src/core/placeable/parsers/optionPattern.test.js @@ -7,13 +7,13 @@ import createMarker from 'react-content-marker'; import optionPattern from './optionPattern'; describe('optionPattern', () => { - each([ - ['--help', 'Type --help for this help'], - ['-S', 'Short -S ones also'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([optionPattern]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['--help', 'Type --help for this help'], + ['-S', 'Short -S ones also'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([optionPattern]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/optionPattern.tsx b/translate/src/core/placeable/parsers/optionPattern.tsx index 725811a14..da0c47c39 100644 --- a/translate/src/core/placeable/parsers/optionPattern.tsx +++ b/translate/src/core/placeable/parsers/optionPattern.tsx @@ -13,24 +13,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L317 */ const optionPattern = { - rule: /(\B(-[a-zA-Z]|--[a-z-]+)\b)/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(\B(-[a-zA-Z]|--[a-z-]+)\b)/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default optionPattern; diff --git a/translate/src/core/placeable/parsers/punctuation.test.js b/translate/src/core/placeable/parsers/punctuation.test.js index 77adc9f7c..01a403c80 100644 --- a/translate/src/core/placeable/parsers/punctuation.test.js +++ b/translate/src/core/placeable/parsers/punctuation.test.js @@ -7,30 +7,30 @@ import createMarker from 'react-content-marker'; import punctuation from './punctuation'; describe('punctuation', () => { - each([ - ['™', 'Pontoon™'], - ['℉', '9℉ OMG so cold'], - ['π', 'She had π cats'], - ['ʼ', 'Please use the correct quote: ʼ'], - ['«', 'Here comes the French: «'], - ['€', 'Gimme the €'], - ['…', 'Downloading…'], - ['—', 'Hello — Lisa'], - ['–', 'Hello – Lisa'], - [' ', 'Hello\u202Fworld'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([punctuation]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['™', 'Pontoon™'], + ['℉', '9℉ OMG so cold'], + ['π', 'She had π cats'], + ['ʼ', 'Please use the correct quote: ʼ'], + ['«', 'Here comes the French: «'], + ['€', 'Gimme the €'], + ['…', 'Downloading…'], + ['—', 'Hello — Lisa'], + ['–', 'Hello – Lisa'], + [' ', 'Hello\u202Fworld'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([punctuation]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['These, are not. Special: punctuation; marks! Or are "they"?']]).it( - 'does not mark anything in `%s`', - (content) => { - const Marker = createMarker([punctuation]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }, - ); + each([['These, are not. Special: punctuation; marks! Or are "they"?']]).it( + 'does not mark anything in `%s`', + (content) => { + const Marker = createMarker([punctuation]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/punctuation.tsx b/translate/src/core/placeable/parsers/punctuation.tsx index f980b6e5e..cee1ad2bd 100644 --- a/translate/src/core/placeable/parsers/punctuation.tsx +++ b/translate/src/core/placeable/parsers/punctuation.tsx @@ -8,35 +8,32 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L229 */ const punctuation = { - rule: new RegExp( - '(' + - '(' + - /[™©®]|/.source + // Marks - /[℃℉°]|/.source + // Degree related - /[±πθ×÷−√∞∆Σ′″]|/.source + // Maths - /[‘’ʼ‚‛“”„‟]|/.source + // Quote characters - /[«»]|/.source + // Guillemets - /[£¥€]|/.source + // Currencies - /…|/.source + // U2026 - horizontal ellipsis - /—|/.source + // U2014 - em dash - /–|/.source + // U2013 - en dash - /[\u202F]/.source + // U202F - narrow no-break space - ')+' + - ')', - ) as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: new RegExp( + '(' + + '(' + + /[™©®]|/.source + // Marks + /[℃℉°]|/.source + // Degree related + /[±πθ×÷−√∞∆Σ′″]|/.source + // Maths + /[‘’ʼ‚‛“”„‟]|/.source + // Quote characters + /[«»]|/.source + // Guillemets + /[£¥€]|/.source + // Currencies + /…|/.source + // U2026 - horizontal ellipsis + /—|/.source + // U2014 - em dash + /–|/.source + // U2013 - en dash + /[\u202F]/.source + // U202F - narrow no-break space + ')+' + + ')', + ) as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default punctuation; diff --git a/translate/src/core/placeable/parsers/pythonFormatNamedString.test.js b/translate/src/core/placeable/parsers/pythonFormatNamedString.test.js index 758d626f1..c2ed8f96e 100644 --- a/translate/src/core/placeable/parsers/pythonFormatNamedString.test.js +++ b/translate/src/core/placeable/parsers/pythonFormatNamedString.test.js @@ -7,15 +7,15 @@ import createMarker from 'react-content-marker'; import pythonFormatNamedString from './pythonFormatNamedString'; describe('pythonFormatNamedString', () => { - each([ - ['%(name)s', 'Hello %(name)s'], - ['%(number)d', 'Rolling %(number)d dices'], - ['%(name)S', 'Hello %(name)S'], - ['%(number)D', 'Rolling %(number)D dices'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([pythonFormatNamedString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['%(name)s', 'Hello %(name)s'], + ['%(number)d', 'Rolling %(number)d dices'], + ['%(name)S', 'Hello %(name)S'], + ['%(number)D', 'Rolling %(number)D dices'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([pythonFormatNamedString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/pythonFormatNamedString.tsx b/translate/src/core/placeable/parsers/pythonFormatNamedString.tsx index cc9c8a0b0..7281fe7f2 100644 --- a/translate/src/core/placeable/parsers/pythonFormatNamedString.tsx +++ b/translate/src/core/placeable/parsers/pythonFormatNamedString.tsx @@ -10,23 +10,19 @@ import { Localized } from '@fluent/react'; * %(number)D */ const pythonFormatNamedString = { - rule: /(%\([[\w\d!.,[\]%:$<>+\-= ]*\)[+|-|0\d+|#]?[.\d+]?[s|d|e|f|g|o|x|c|%])/i as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(%\([[\w\d!.,[\]%:$<>+\-= ]*\)[+|-|0\d+|#]?[.\d+]?[s|d|e|f|g|o|x|c|%])/i as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default pythonFormatNamedString; diff --git a/translate/src/core/placeable/parsers/pythonFormatString.test.js b/translate/src/core/placeable/parsers/pythonFormatString.test.js index 65da74b42..db7f26d5f 100644 --- a/translate/src/core/placeable/parsers/pythonFormatString.test.js +++ b/translate/src/core/placeable/parsers/pythonFormatString.test.js @@ -7,16 +7,16 @@ import createMarker from 'react-content-marker'; import pythonFormatString from './pythonFormatString'; describe('pythonFormatString', () => { - each([ - ['{0}', 'hello, {0}'], - ['{name}', 'hello, {name}'], - ['{name!s}', 'hello, {name!s}'], - ['{someone.name}', 'hello, {someone.name}'], - ['{name[0]}', 'hello, {name[0]}'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([pythonFormatString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['{0}', 'hello, {0}'], + ['{name}', 'hello, {name}'], + ['{name!s}', 'hello, {name!s}'], + ['{someone.name}', 'hello, {someone.name}'], + ['{name[0]}', 'hello, {name[0]}'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([pythonFormatString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/pythonFormatString.tsx b/translate/src/core/placeable/parsers/pythonFormatString.tsx index e26064525..fef5bc3e0 100644 --- a/translate/src/core/placeable/parsers/pythonFormatString.tsx +++ b/translate/src/core/placeable/parsers/pythonFormatString.tsx @@ -14,23 +14,19 @@ import { Localized } from '@fluent/react'; * {foo[42]} */ const pythonFormatString = { - rule: /(\{{?[\w\d!.,[\]%:$<>+-= ]*\}?})/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(\{{?[\w\d!.,[\]%:$<>+-= ]*\}?})/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default pythonFormatString; diff --git a/translate/src/core/placeable/parsers/pythonFormattingVariable.test.js b/translate/src/core/placeable/parsers/pythonFormattingVariable.test.js index 48bdab189..fe1df9b13 100644 --- a/translate/src/core/placeable/parsers/pythonFormattingVariable.test.js +++ b/translate/src/core/placeable/parsers/pythonFormattingVariable.test.js @@ -7,34 +7,34 @@ import createMarker from 'react-content-marker'; import pythonFormattingVariable from './pythonFormattingVariable'; describe('pythonFormattingVariable', () => { - each([ - ['%%', '100%% correct'], - ['%s', 'There were %s'], - ['%(number)d', 'There were %(number)d cows'], - ['%(cows.number)d', 'There were %(cows.number)d cows'], - ['%(number of cows)d', 'There were %(number of cows)d cows'], - ['%(number)03d', 'There were %(number)03d cows'], - ['%(number)*d', 'There were %(number)*d cows'], - ['%(number)3.1d', 'There were %(number)3.1d cows'], - ['%(number)Ld', 'There were %(number)Ld cows'], - ['%s', 'path/to/file_%s.png'], - ['%s', 'path/to/%sfile.png'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([pythonFormattingVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['%%', '100%% correct'], + ['%s', 'There were %s'], + ['%(number)d', 'There were %(number)d cows'], + ['%(cows.number)d', 'There were %(cows.number)d cows'], + ['%(number of cows)d', 'There were %(number of cows)d cows'], + ['%(number)03d', 'There were %(number)03d cows'], + ['%(number)*d', 'There were %(number)*d cows'], + ['%(number)3.1d', 'There were %(number)3.1d cows'], + ['%(number)Ld', 'There were %(number)Ld cows'], + ['%s', 'path/to/file_%s.png'], + ['%s', 'path/to/%sfile.png'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([pythonFormattingVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([ - ['10 % complete'], - // We used to match '%(number) 3d' here, but don't anymore to avoid - // false positives. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1251186 - ['There were %(number) 3d cows'], - ]).it('does not mark anything in `%s`', (content) => { - const Marker = createMarker([pythonFormattingVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }); + each([ + ['10 % complete'], + // We used to match '%(number) 3d' here, but don't anymore to avoid + // false positives. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1251186 + ['There were %(number) 3d cows'], + ]).it('does not mark anything in `%s`', (content) => { + const Marker = createMarker([pythonFormattingVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }); }); diff --git a/translate/src/core/placeable/parsers/pythonFormattingVariable.tsx b/translate/src/core/placeable/parsers/pythonFormattingVariable.tsx index 02878fabc..0ad9d9b29 100644 --- a/translate/src/core/placeable/parsers/pythonFormattingVariable.tsx +++ b/translate/src/core/placeable/parsers/pythonFormattingVariable.tsx @@ -17,24 +17,24 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L115 */ const pythonFormattingVariable = { - rule: /(%(%|(\([^)]+\)){0,1}[-+0#]{0,1}(\d+|\*){0,1}(\.(\d+|\*)){0,1}[hlL]{0,1}[diouxXeEfFgGcrs]{1}))/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(%(%|(\([^)]+\)){0,1}[-+0#]{0,1}(\d+|\*){0,1}(\.(\d+|\*)){0,1}[hlL]{0,1}[diouxXeEfFgGcrs]{1}))/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default pythonFormattingVariable; diff --git a/translate/src/core/placeable/parsers/qtFormatting.test.js b/translate/src/core/placeable/parsers/qtFormatting.test.js index 96ec0da77..e25d9d515 100644 --- a/translate/src/core/placeable/parsers/qtFormatting.test.js +++ b/translate/src/core/placeable/parsers/qtFormatting.test.js @@ -7,14 +7,14 @@ import createMarker from 'react-content-marker'; import qtFormatting from './qtFormatting'; describe('qtFormatting', () => { - each([ - ['%1', 'Hello, %1'], - ['%99', 'Hello, %99'], - ['%L1', 'Hello, %L1'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([qtFormatting]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['%1', 'Hello, %1'], + ['%99', 'Hello, %99'], + ['%L1', 'Hello, %L1'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([qtFormatting]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/qtFormatting.tsx b/translate/src/core/placeable/parsers/qtFormatting.tsx index 5c22afefe..ab03805d1 100644 --- a/translate/src/core/placeable/parsers/qtFormatting.tsx +++ b/translate/src/core/placeable/parsers/qtFormatting.tsx @@ -24,24 +24,21 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L80 */ const qtFormatting = { - rule: /(%L?[1-9]\d{0,1}(?=([^\d]|$)))/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(%L?[1-9]\d{0,1}(?=([^\d]|$)))/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default qtFormatting; diff --git a/translate/src/core/placeable/parsers/shortCapitalNumberString.test.js b/translate/src/core/placeable/parsers/shortCapitalNumberString.test.js index 875d74a3a..48e34efb3 100644 --- a/translate/src/core/placeable/parsers/shortCapitalNumberString.test.js +++ b/translate/src/core/placeable/parsers/shortCapitalNumberString.test.js @@ -7,22 +7,22 @@ import createMarker from 'react-content-marker'; import shortCapitalNumberString from './shortCapitalNumberString'; describe('shortCapitalNumberString', () => { - each([ - ['3D', '3D'], - ['A4', 'Use the A4 paper'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([shortCapitalNumberString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['3D', '3D'], + ['A4', 'Use the A4 paper'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([shortCapitalNumberString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); - each([['I am'], ['3d'], ['3DS']]).it( - 'does not mark anything in `%s`', - (content) => { - const Marker = createMarker([shortCapitalNumberString]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }, - ); + each([['I am'], ['3d'], ['3DS']]).it( + 'does not mark anything in `%s`', + (content) => { + const Marker = createMarker([shortCapitalNumberString]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }, + ); }); diff --git a/translate/src/core/placeable/parsers/shortCapitalNumberString.tsx b/translate/src/core/placeable/parsers/shortCapitalNumberString.tsx index 708a1934f..553621b2a 100644 --- a/translate/src/core/placeable/parsers/shortCapitalNumberString.tsx +++ b/translate/src/core/placeable/parsers/shortCapitalNumberString.tsx @@ -11,23 +11,23 @@ import { Localized } from '@fluent/react'; * A4 */ const shortCapitalNumberString = { - rule: /(\b([A-Z][0-9])|([0-9][A-Z])\b)/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(\b([A-Z][0-9])|([0-9][A-Z])\b)/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default shortCapitalNumberString; diff --git a/translate/src/core/placeable/parsers/stringFormattingVariable.test.js b/translate/src/core/placeable/parsers/stringFormattingVariable.test.js index c73ea6f97..f2a71047b 100644 --- a/translate/src/core/placeable/parsers/stringFormattingVariable.test.js +++ b/translate/src/core/placeable/parsers/stringFormattingVariable.test.js @@ -7,41 +7,41 @@ import createMarker from 'react-content-marker'; import stringFormattingVariable from './stringFormattingVariable'; describe('stringFormattingVariable', () => { - each([ - ['%d', 'There were %d cows', 1], - ['%Id', 'There were %Id cows', 1], - [['%d', '%s'], 'There were %d %s', 2], - [['%1$s', '%2$s'], '%1$s was kicked by %2$s', 2], - ['%Id', 'There were %Id cows', 1], - ["%'f", "There were %'f cows", 1], - ['%#x', 'There were %#x cows', 1], - ['%3d', 'There were %3d cows', 1], - ['%33d', 'There were %33d cows', 1], - ['%*d', 'There were %*d cows', 1], - ['%1$d', 'There were %1$d cows', 1], - [null, 'There were %\u00a0d cows', 0], - ]).it('marks `%s` in `%s`', (mark, content, matchesNumber) => { - const Marker = createMarker([stringFormattingVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(matchesNumber); - if (matchesNumber === 1) { - expect(wrapper.find('mark').text()).toEqual(mark); - } else if (matchesNumber > 1) { - for (let i = 0; i < matchesNumber; i++) { - expect(wrapper.find('mark').at(i).text()).toEqual(mark[i]); - } - } - }); + each([ + ['%d', 'There were %d cows', 1], + ['%Id', 'There were %Id cows', 1], + [['%d', '%s'], 'There were %d %s', 2], + [['%1$s', '%2$s'], '%1$s was kicked by %2$s', 2], + ['%Id', 'There were %Id cows', 1], + ["%'f", "There were %'f cows", 1], + ['%#x', 'There were %#x cows', 1], + ['%3d', 'There were %3d cows', 1], + ['%33d', 'There were %33d cows', 1], + ['%*d', 'There were %*d cows', 1], + ['%1$d', 'There were %1$d cows', 1], + [null, 'There were %\u00a0d cows', 0], + ]).it('marks `%s` in `%s`', (mark, content, matchesNumber) => { + const Marker = createMarker([stringFormattingVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(matchesNumber); + if (matchesNumber === 1) { + expect(wrapper.find('mark').text()).toEqual(mark); + } else if (matchesNumber > 1) { + for (let i = 0; i < matchesNumber; i++) { + expect(wrapper.find('mark').at(i).text()).toEqual(mark[i]); + } + } + }); - each([ - ['10 % complete'], - // We used to match '% d' here, but don't anymore to avoid - // false positives. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1251186 - ['There were % d cows'], - ]).it('does not mark anything in `%s`', (content) => { - const Marker = createMarker([stringFormattingVariable]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(0); - }); + each([ + ['10 % complete'], + // We used to match '% d' here, but don't anymore to avoid + // false positives. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1251186 + ['There were % d cows'], + ]).it('does not mark anything in `%s`', (content) => { + const Marker = createMarker([stringFormattingVariable]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(0); + }); }); diff --git a/translate/src/core/placeable/parsers/stringFormattingVariable.tsx b/translate/src/core/placeable/parsers/stringFormattingVariable.tsx index 6b2c34f2d..08d4b20a5 100644 --- a/translate/src/core/placeable/parsers/stringFormattingVariable.tsx +++ b/translate/src/core/placeable/parsers/stringFormattingVariable.tsx @@ -16,24 +16,24 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L154 */ const stringFormattingVariable = { - rule: /(%(\d+\$)?[-+0#'I]?((\d+)|[*])?(\.\d+)?[hlI]?[cCdiouxXeEfgGnpsS])/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(%(\d+\$)?[-+0#'I]?((\d+)|[*])?(\.\d+)?[hlI]?[cCdiouxXeEfgGnpsS])/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default stringFormattingVariable; diff --git a/translate/src/core/placeable/parsers/tabCharacter.test.js b/translate/src/core/placeable/parsers/tabCharacter.test.js index 93216dbe5..51803e6eb 100644 --- a/translate/src/core/placeable/parsers/tabCharacter.test.js +++ b/translate/src/core/placeable/parsers/tabCharacter.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import tabCharacter from './tabCharacter'; describe('tabCharacter', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([tabCharacter]); - const content = 'hello,\tworld'; + it('marks the right parts of a string', () => { + const Marker = createMarker([tabCharacter]); + const content = 'hello,\tworld'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\t\u2192'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\t\u2192'); + }); }); diff --git a/translate/src/core/placeable/parsers/tabCharacter.tsx b/translate/src/core/placeable/parsers/tabCharacter.tsx index 33ef324a0..d5641915d 100644 --- a/translate/src/core/placeable/parsers/tabCharacter.tsx +++ b/translate/src/core/placeable/parsers/tabCharacter.tsx @@ -5,25 +5,22 @@ import { Localized } from '@fluent/react'; * Marks the tab character "\t". */ const tabCharacter = { - rule: '\t', - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - - ); - }, + rule: '\t', + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + + ); + }, }; export default tabCharacter; diff --git a/translate/src/core/placeable/parsers/thinSpace.test.js b/translate/src/core/placeable/parsers/thinSpace.test.js index ffefe03f2..a7d6815c4 100644 --- a/translate/src/core/placeable/parsers/thinSpace.test.js +++ b/translate/src/core/placeable/parsers/thinSpace.test.js @@ -6,12 +6,12 @@ import createMarker from 'react-content-marker'; import thinSpace from './thinSpace'; describe('thinSpace', () => { - it('marks the right parts of a string', () => { - const Marker = createMarker([thinSpace]); - const content = 'hello,\u2009world'; + it('marks the right parts of a string', () => { + const Marker = createMarker([thinSpace]); + const content = 'hello,\u2009world'; - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('\u2009'); - }); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('\u2009'); + }); }); diff --git a/translate/src/core/placeable/parsers/thinSpace.tsx b/translate/src/core/placeable/parsers/thinSpace.tsx index bf7eeca55..f6ea67ced 100644 --- a/translate/src/core/placeable/parsers/thinSpace.tsx +++ b/translate/src/core/placeable/parsers/thinSpace.tsx @@ -5,16 +5,16 @@ import { Localized } from '@fluent/react'; * Marks the thin space character (Unicode U+2009). */ const thinSpace = { - rule: /([\u2009])/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /([\u2009])/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default thinSpace; diff --git a/translate/src/core/placeable/parsers/unusualSpace.test.js b/translate/src/core/placeable/parsers/unusualSpace.test.js index ac22ca3d0..bb0b68ea4 100644 --- a/translate/src/core/placeable/parsers/unusualSpace.test.js +++ b/translate/src/core/placeable/parsers/unusualSpace.test.js @@ -7,14 +7,14 @@ import createMarker from 'react-content-marker'; import unusualSpace from './unusualSpace'; describe('unusualSpace', () => { - each([ - [' ', 'hello world '], - [' ', 'hello\n world'], - [' ', 'hello world'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([unusualSpace]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + [' ', 'hello world '], + [' ', 'hello\n world'], + [' ', 'hello world'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([unusualSpace]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/unusualSpace.tsx b/translate/src/core/placeable/parsers/unusualSpace.tsx index 71f357b9a..5d4845dc4 100644 --- a/translate/src/core/placeable/parsers/unusualSpace.tsx +++ b/translate/src/core/placeable/parsers/unusualSpace.tsx @@ -14,19 +14,16 @@ import { Localized } from '@fluent/react'; * "hello world" */ const unusualSpace = { - rule: /( +$|[\r\n\t]( +)| {2,})/ as RegExp, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /( +$|[\r\n\t]( +)| {2,})/ as RegExp, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default unusualSpace; diff --git a/translate/src/core/placeable/parsers/uriPattern.test.js b/translate/src/core/placeable/parsers/uriPattern.test.js index 2fd2090fa..bcd835fdd 100644 --- a/translate/src/core/placeable/parsers/uriPattern.test.js +++ b/translate/src/core/placeable/parsers/uriPattern.test.js @@ -7,24 +7,24 @@ import createMarker from 'react-content-marker'; import uriPattern from './uriPattern'; describe('uriPattern', () => { - each([ - ['http://example.org/'], - ['https://example.org/'], - ['ftp://example.org/'], - ['nttp://example.org/'], - ['file://example.org/'], - ['irc://example.org/'], - ['www.example.org/'], - ['ftp.example.org/'], - ['http://example.org:8888'], - ['http://example.org:8888/?'], - ['http://example.org/path/to/resource?var1=$@3!?%=iwdu8'], - ['http://example.org/path/to/resource?var1=$@3!?%=iwdu8&var2=bar'], - ['HTTP://EXAMPLE.org/'], - ]).it('correctly marks URI `%s`', (uri) => { - const Marker = createMarker([uriPattern]); - const wrapper = shallow({uri}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(uri); - }); + each([ + ['http://example.org/'], + ['https://example.org/'], + ['ftp://example.org/'], + ['nttp://example.org/'], + ['file://example.org/'], + ['irc://example.org/'], + ['www.example.org/'], + ['ftp.example.org/'], + ['http://example.org:8888'], + ['http://example.org:8888/?'], + ['http://example.org/path/to/resource?var1=$@3!?%=iwdu8'], + ['http://example.org/path/to/resource?var1=$@3!?%=iwdu8&var2=bar'], + ['HTTP://EXAMPLE.org/'], + ]).it('correctly marks URI `%s`', (uri) => { + const Marker = createMarker([uriPattern]); + const wrapper = shallow({uri}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(uri); + }); }); diff --git a/translate/src/core/placeable/parsers/uriPattern.tsx b/translate/src/core/placeable/parsers/uriPattern.tsx index 330b800ab..ce028dc19 100644 --- a/translate/src/core/placeable/parsers/uriPattern.tsx +++ b/translate/src/core/placeable/parsers/uriPattern.tsx @@ -14,32 +14,32 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L192 */ const uriPattern = { - rule: new RegExp( - '(' + - '(' + - '(' + - /((news|nttp|file|https?|ftp|irc):\/\/)/.source + // has to start with a protocol - /|((www|ftp)[-A-Za-z0-9]*\.)/.source + // or www... or ftp... hostname - ')' + - /([-A-Za-z0-9]+(\.[-A-Za-z0-9]+)*)/.source + // hostname - /|(\d{1,3}(\.\d{1,3}){3,3})/.source + // or IP address - ')' + - /(:[0-9]{1,5})?/.source + // optional port - /(\/[a-zA-Z0-9-_$.+!*(),;:@&=?/~#%]*)?/.source + // optional trailing path - /(?=$|\s|([\]'}>),"]))/.source + - ')', - 'i', // This one is not case sensitive. - ) as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: new RegExp( + '(' + + '(' + + '(' + + /((news|nttp|file|https?|ftp|irc):\/\/)/.source + // has to start with a protocol + /|((www|ftp)[-A-Za-z0-9]*\.)/.source + // or www... or ftp... hostname + ')' + + /([-A-Za-z0-9]+(\.[-A-Za-z0-9]+)*)/.source + // hostname + /|(\d{1,3}(\.\d{1,3}){3,3})/.source + // or IP address + ')' + + /(:[0-9]{1,5})?/.source + // optional port + /(\/[a-zA-Z0-9-_$.+!*(),;:@&=?/~#%]*)?/.source + // optional trailing path + /(?=$|\s|([\]'}>),"]))/.source + + ')', + 'i', // This one is not case sensitive. + ) as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default uriPattern; diff --git a/translate/src/core/placeable/parsers/xmlEntity.test.js b/translate/src/core/placeable/parsers/xmlEntity.test.js index 731dc3fdc..18addf841 100644 --- a/translate/src/core/placeable/parsers/xmlEntity.test.js +++ b/translate/src/core/placeable/parsers/xmlEntity.test.js @@ -7,14 +7,14 @@ import createMarker from 'react-content-marker'; import xmlEntity from './xmlEntity'; describe('xmlEntity', () => { - each([ - ['&brandShortName;', 'Welcome to &brandShortName;'], - ['Ӓ', 'hello, Ӓ'], - ['&xDEAD;', 'hello, &xDEAD;'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([xmlEntity]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['&brandShortName;', 'Welcome to &brandShortName;'], + ['Ӓ', 'hello, Ӓ'], + ['&xDEAD;', 'hello, &xDEAD;'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([xmlEntity]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/xmlEntity.tsx b/translate/src/core/placeable/parsers/xmlEntity.tsx index fb50320a2..468a73fb2 100644 --- a/translate/src/core/placeable/parsers/xmlEntity.tsx +++ b/translate/src/core/placeable/parsers/xmlEntity.tsx @@ -13,17 +13,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L254 */ const xmlEntity = { - rule: /(&(([a-zA-Z][a-zA-Z0-9.-]*)|([#](\d{1,5}|x[a-fA-F0-9]{1,5})+));)/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(&(([a-zA-Z][a-zA-Z0-9.-]*)|([#](\d{1,5}|x[a-fA-F0-9]{1,5})+));)/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default xmlEntity; diff --git a/translate/src/core/placeable/parsers/xmlTag.test.js b/translate/src/core/placeable/parsers/xmlTag.test.js index f1a192022..e5da2855e 100644 --- a/translate/src/core/placeable/parsers/xmlTag.test.js +++ b/translate/src/core/placeable/parsers/xmlTag.test.js @@ -7,17 +7,17 @@ import createMarker from 'react-content-marker'; import xmlTag from './xmlTag'; describe('xmlTag', () => { - each([ - ['', 'hello, John'], - ['', 'hello, '], - ['', 'hello, '], - ["", "hello, "], - ["", "hello, "], - ['', 'Happy !'], - ]).it('marks `%s` in `%s`', (mark, content) => { - const Marker = createMarker([xmlTag]); - const wrapper = shallow({content}); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual(mark); - }); + each([ + ['', 'hello, John'], + ['', 'hello, '], + ['', 'hello, '], + ["", "hello, "], + ["", "hello, "], + ['', 'Happy !'], + ]).it('marks `%s` in `%s`', (mark, content) => { + const Marker = createMarker([xmlTag]); + const wrapper = shallow({content}); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual(mark); + }); }); diff --git a/translate/src/core/placeable/parsers/xmlTag.tsx b/translate/src/core/placeable/parsers/xmlTag.tsx index 5a320b5cd..900de6783 100644 --- a/translate/src/core/placeable/parsers/xmlTag.tsx +++ b/translate/src/core/placeable/parsers/xmlTag.tsx @@ -14,17 +14,17 @@ import { Localized } from '@fluent/react'; * https://github.com/translate/translate/blob/2.3.1/translate/storage/placeables/general.py#L301 */ const xmlTag = { - rule: /(<[\w.:]+(\s([\w.:-]+=((".*?")|('.*?')))?)*\/?>|<\/[\w.]+>)/ as RegExp, - matchIndex: 0, - tag: (x: string): React.ReactElement => { - return ( - - - {x} - - - ); - }, + rule: /(<[\w.:]+(\s([\w.:-]+=((".*?")|('.*?')))?)*\/?>|<\/[\w.]+>)/ as RegExp, + matchIndex: 0, + tag: (x: string): React.ReactElement => { + return ( + + + {x} + + + ); + }, }; export default xmlTag; diff --git a/translate/src/core/plural/actions.ts b/translate/src/core/plural/actions.ts index 5695be2d3..0a5f44b3a 100644 --- a/translate/src/core/plural/actions.ts +++ b/translate/src/core/plural/actions.ts @@ -10,42 +10,42 @@ export const SELECT: 'plural/SELECT' = 'plural/SELECT'; * Move to next Entity or pluralForm. */ export function moveToNextTranslation( - dispatch: AppDispatch, - router: Record, - entity: number, - nextEntity: number, - pluralForm: number, - locale: Locale, + dispatch: AppDispatch, + router: Record, + entity: number, + nextEntity: number, + pluralForm: number, + locale: Locale, ): void { - if (pluralForm !== -1 && pluralForm < locale.cldrPlurals.length - 1) { - dispatch(select(pluralForm + 1)); - } else if (nextEntity !== entity) { - dispatch(navActions.updateEntity(router, nextEntity.toString())); - } + if (pluralForm !== -1 && pluralForm < locale.cldrPlurals.length - 1) { + dispatch(select(pluralForm + 1)); + } else if (nextEntity !== entity) { + dispatch(navActions.updateEntity(router, nextEntity.toString())); + } } export type ResetAction = { - type: typeof RESET; + type: typeof RESET; }; export function reset(): ResetAction { - return { - type: RESET, - }; + return { + type: RESET, + }; } export type SelectAction = { - type: typeof SELECT; - pluralForm: number; + type: typeof SELECT; + pluralForm: number; }; export function select(pluralForm: number): SelectAction { - return { - type: SELECT, - pluralForm, - }; + return { + type: SELECT, + pluralForm, + }; } export default { - moveToNextTranslation, - reset, - select, + moveToNextTranslation, + reset, + select, }; diff --git a/translate/src/core/plural/components/PluralSelector.css b/translate/src/core/plural/components/PluralSelector.css index 1b325bdd2..8827a3b40 100644 --- a/translate/src/core/plural/components/PluralSelector.css +++ b/translate/src/core/plural/components/PluralSelector.css @@ -1,42 +1,42 @@ .plural-selector ul { - display: table; - line-height: 22px; - table-layout: fixed; - width: 100%; + display: table; + line-height: 22px; + table-layout: fixed; + width: 100%; } .plural-selector ul li { - background: #4d5967; - border: 1px solid #5e6475; - border-left: none; - display: table-cell; - text-align: center; + background: #4d5967; + border: 1px solid #5e6475; + border-left: none; + display: table-cell; + text-align: center; } .plural-selector ul li.active, .plural-selector ul li:hover { - background: #3f4752; + background: #3f4752; } .plural-selector ul li.active { - border-bottom: none; + border-bottom: none; } .plural-selector ul li button { - background: none; - border: none; - color: #ccc; - cursor: pointer; - display: block; - font-weight: 300; - padding: 10px; - text-transform: uppercase; - width: 100%; + background: none; + border: none; + color: #ccc; + cursor: pointer; + display: block; + font-weight: 300; + padding: 10px; + text-transform: uppercase; + width: 100%; } .plural-selector ul li button sup { - color: #7bc876; - font-size: 11px; - font-weight: 400; - padding-left: 1px; + color: #7bc876; + font-size: 11px; + font-weight: 400; + padding-left: 1px; } diff --git a/translate/src/core/plural/components/PluralSelector.test.js b/translate/src/core/plural/components/PluralSelector.test.js index 134c009da..04c4dc9e7 100644 --- a/translate/src/core/plural/components/PluralSelector.test.js +++ b/translate/src/core/plural/components/PluralSelector.test.js @@ -9,101 +9,101 @@ import { actions } from '..'; import PluralSelector, { PluralSelectorBase } from './PluralSelector'; function createShallowPluralSelector(plural, locale) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('returns null when the locale is missing', () => { - const wrapper = createShallowPluralSelector(0, null); - expect(wrapper.type()).toBeNull(); + it('returns null when the locale is missing', () => { + const wrapper = createShallowPluralSelector(0, null); + expect(wrapper.type()).toBeNull(); + }); + + it('returns null when the locale has only one plural form', () => { + const wrapper = createShallowPluralSelector(0, { cldrPlurals: [5] }); + expect(wrapper.type()).toBeNull(); + }); + + it('returns null when the selected plural form is -1', () => { + // If pluralForm is -1, it means the entity has no plural string. + const wrapper = createShallowPluralSelector(-1, { + cldrPlurals: [1, 5], + }); + expect(wrapper.type()).toBeNull(); + }); + + it('shows the correct list of plural choices for locale with 2 forms', () => { + const wrapper = createShallowPluralSelector(0, { cldrPlurals: [1, 5] }); + + expect(wrapper.find('li')).toHaveLength(2); + expect(wrapper.find('ul').text()).toEqual('one1other2'); + }); + + it('shows the correct list of plural choices for locale with all 6 forms', () => { + const wrapper = createShallowPluralSelector(0, { + cldrPlurals: [0, 1, 2, 3, 4, 5], + // This is the pluralRule for Arabic. + pluralRule: + '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5)', }); - it('returns null when the locale has only one plural form', () => { - const wrapper = createShallowPluralSelector(0, { cldrPlurals: [5] }); - expect(wrapper.type()).toBeNull(); - }); + expect(wrapper.find('li')).toHaveLength(6); + expect(wrapper.find('ul').text()).toEqual( + 'zero0one1two2few3many11other100', + ); + }); - it('returns null when the selected plural form is -1', () => { - // If pluralForm is -1, it means the entity has no plural string. - const wrapper = createShallowPluralSelector(-1, { - cldrPlurals: [1, 5], - }); - expect(wrapper.type()).toBeNull(); - }); + it('marks the right choice as active', () => { + const wrapper = createShallowPluralSelector(1, { cldrPlurals: [1, 5] }); - it('shows the correct list of plural choices for locale with 2 forms', () => { - const wrapper = createShallowPluralSelector(0, { cldrPlurals: [1, 5] }); - - expect(wrapper.find('li')).toHaveLength(2); - expect(wrapper.find('ul').text()).toEqual('one1other2'); - }); - - it('shows the correct list of plural choices for locale with all 6 forms', () => { - const wrapper = createShallowPluralSelector(0, { - cldrPlurals: [0, 1, 2, 3, 4, 5], - // This is the pluralRule for Arabic. - pluralRule: - '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5)', - }); - - expect(wrapper.find('li')).toHaveLength(6); - expect(wrapper.find('ul').text()).toEqual( - 'zero0one1two2few3many11other100', - ); - }); - - it('marks the right choice as active', () => { - const wrapper = createShallowPluralSelector(1, { cldrPlurals: [1, 5] }); - - expect(wrapper.find('li.active').text()).toEqual('other2'); - }); + expect(wrapper.find('li.active').text()).toEqual('other2'); + }); }); describe('', () => { - it('selects the correct form when clicking a choice', () => { - const initialState = { - plural: { - pluralForm: 1, - }, - locale: { - code: 'kg', - cldrPlurals: [1, 5], - }, - router: { - location: { - pathname: '/kg/firefox/all-resources/', - search: '?string=42', - }, - // `action` is required because - // https://github.com/supasate/connected-react-router/issues/312#issuecomment-500968504 - // Please note the initial `LOCATION_CHANGE` action can and must - // be supressed via the `noInitialPop` prop in - // `ConnectedRouter`, otherwise it'll cause side-effects like - // executing reducers and hence altering initial state values. - action: 'some-string-to-please-connected-react-router', - }, - }; - const store = createReduxStore(initialState); - const dispatchSpy = sinon.spy(store, 'dispatch'); + it('selects the correct form when clicking a choice', () => { + const initialState = { + plural: { + pluralForm: 1, + }, + locale: { + code: 'kg', + cldrPlurals: [1, 5], + }, + router: { + location: { + pathname: '/kg/firefox/all-resources/', + search: '?string=42', + }, + // `action` is required because + // https://github.com/supasate/connected-react-router/issues/312#issuecomment-500968504 + // Please note the initial `LOCATION_CHANGE` action can and must + // be supressed via the `noInitialPop` prop in + // `ConnectedRouter`, otherwise it'll cause side-effects like + // executing reducers and hence altering initial state values. + action: 'some-string-to-please-connected-react-router', + }, + }; + const store = createReduxStore(initialState); + const dispatchSpy = sinon.spy(store, 'dispatch'); - const root = mountComponentWithStore(PluralSelector, store, { - resetEditor: sinon.spy(), - }); - const wrapper = root.find(PluralSelectorBase); - - const button = wrapper.find('button').first(); - // `simulate()` doesn't quite work in conjunction with `mount()`, so - // invoking the `prop()` callback directly is the way to go as suggested - // by the enzyme maintainer... - button.prop('onClick')(); - - const expectedAction = actions.select(0); - expect(dispatchSpy.calledWith(expectedAction)).toBeTruthy(); + const root = mountComponentWithStore(PluralSelector, store, { + resetEditor: sinon.spy(), }); + const wrapper = root.find(PluralSelectorBase); + + const button = wrapper.find('button').first(); + // `simulate()` doesn't quite work in conjunction with `mount()`, so + // invoking the `prop()` callback directly is the way to go as suggested + // by the enzyme maintainer... + button.prop('onClick')(); + + const expectedAction = actions.select(0); + expect(dispatchSpy.calledWith(expectedAction)).toBeTruthy(); + }); }); diff --git a/translate/src/core/plural/components/PluralSelector.tsx b/translate/src/core/plural/components/PluralSelector.tsx index 104f321c6..79690f047 100644 --- a/translate/src/core/plural/components/PluralSelector.tsx +++ b/translate/src/core/plural/components/PluralSelector.tsx @@ -12,19 +12,19 @@ import type { AppDispatch } from '~/store'; import type { Locale } from '~/core/locale'; type Props = { - locale: Locale; - pluralForm: number; + locale: Locale; + pluralForm: number; }; type WrapperProps = { - resetEditor: () => void; + resetEditor: () => void; }; type InternalProps = Props & - WrapperProps & { - dispatch: AppDispatch; - store: AppStore; - }; + WrapperProps & { + dispatch: AppDispatch; + store: AppStore; + }; /** * Plural form picker component. @@ -33,81 +33,76 @@ type InternalProps = Props & * to change selected plural form. */ export class PluralSelectorBase extends React.Component { - selectPluralForm(pluralForm: number) { - if (this.props.pluralForm === pluralForm) { - return; - } - - const { dispatch } = this.props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - this.props.resetEditor(); - dispatch(actions.select(pluralForm)); - }, - ), - ); + selectPluralForm(pluralForm: number) { + if (this.props.pluralForm === pluralForm) { + return; } - render(): null | React.ReactElement<'nav'> { - const props = this.props; - const { pluralForm } = props; + const { dispatch } = this.props; - if ( - pluralForm === -1 || - !props.locale || - props.locale.cldrPlurals.length <= 1 - ) { - return null; - } + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - const examples = locale.getPluralExamples(props.locale); + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + this.props.resetEditor(); + dispatch(actions.select(pluralForm)); + }, + ), + ); + } - return ( - - ); + render(): null | React.ReactElement<'nav'> { + const props = this.props; + const { pluralForm } = props; + + if ( + pluralForm === -1 || + !props.locale || + props.locale.cldrPlurals.length <= 1 + ) { + return null; } + + const examples = locale.getPluralExamples(props.locale); + + return ( + + ); + } } export default function PluralSelector( - props: WrapperProps, + props: WrapperProps, ): React.ReactElement { - const state = { - locale: useAppSelector((state) => state[locale.NAME]), - pluralForm: useAppSelector((state) => selectors.getPluralForm(state)), - }; + const state = { + locale: useAppSelector((state) => state[locale.NAME]), + pluralForm: useAppSelector((state) => selectors.getPluralForm(state)), + }; - return ( - - ); + return ( + + ); } diff --git a/translate/src/core/plural/index.ts b/translate/src/core/plural/index.ts index 42a2ed5fc..f74ec4583 100644 --- a/translate/src/core/plural/index.ts +++ b/translate/src/core/plural/index.ts @@ -8,12 +8,12 @@ export type { PluralState } from './reducer'; // List of available CLDR plural categories. export const CLDR_PLURALS: Array = [ - 'zero', - 'one', - 'two', - 'few', - 'many', - 'other', + 'zero', + 'one', + 'two', + 'few', + 'many', + 'other', ]; // Name of this module. diff --git a/translate/src/core/plural/reducer.test.js b/translate/src/core/plural/reducer.test.js index e756d78ba..ae995e736 100644 --- a/translate/src/core/plural/reducer.test.js +++ b/translate/src/core/plural/reducer.test.js @@ -2,21 +2,21 @@ import reducer from './reducer'; import { RESET, SELECT } from './actions'; describe('reducer', () => { - it('returns the initial state', () => { - const res = reducer(undefined, {}); - const expected = { - pluralForm: -1, - }; - expect(res).toEqual(expected); - }); + it('returns the initial state', () => { + const res = reducer(undefined, {}); + const expected = { + pluralForm: -1, + }; + expect(res).toEqual(expected); + }); - it('handles the SELECT action', () => { - const res = reducer({}, { type: SELECT, pluralForm: 2 }); - expect(res).toEqual({ pluralForm: 2 }); - }); + it('handles the SELECT action', () => { + const res = reducer({}, { type: SELECT, pluralForm: 2 }); + expect(res).toEqual({ pluralForm: 2 }); + }); - it('handles the RESET action', () => { - const res = reducer({}, { type: RESET }); - expect(res).toEqual({ pluralForm: -1 }); - }); + it('handles the RESET action', () => { + const res = reducer({}, { type: RESET }); + expect(res).toEqual({ pluralForm: -1 }); + }); }); diff --git a/translate/src/core/plural/reducer.ts b/translate/src/core/plural/reducer.ts index ef0b53a8f..2c99505ab 100644 --- a/translate/src/core/plural/reducer.ts +++ b/translate/src/core/plural/reducer.ts @@ -8,29 +8,29 @@ import type { ResetAction, SelectAction } from './actions'; type Action = LocationChangeAction | ResetAction | SelectAction; export type PluralState = { - readonly pluralForm: number; + readonly pluralForm: number; }; const initial: PluralState = { - pluralForm: -1, + pluralForm: -1, }; export default function reducer( - state: PluralState = initial, - action: Action, + state: PluralState = initial, + action: Action, ): PluralState { - switch (action.type) { - case SELECT: - return { - ...state, - pluralForm: action.pluralForm, - }; - case LOCATION_CHANGE: - case RESET: - return { - ...initial, - }; - default: - return state; - } + switch (action.type) { + case SELECT: + return { + ...state, + pluralForm: action.pluralForm, + }; + case LOCATION_CHANGE: + case RESET: + return { + ...initial, + }; + default: + return state; + } } diff --git a/translate/src/core/plural/selectors.test.js b/translate/src/core/plural/selectors.test.js index 389a485e5..2ca4d1b4a 100644 --- a/translate/src/core/plural/selectors.test.js +++ b/translate/src/core/plural/selectors.test.js @@ -1,69 +1,69 @@ import { - _getPluralForm, - _getTranslationForSelectedEntity, - _getTranslationStringForSelectedEntity, + _getPluralForm, + _getTranslationForSelectedEntity, + _getTranslationStringForSelectedEntity, } from './selectors'; describe('getPluralForm', () => { - it('returns the plural form', () => { - expect(_getPluralForm(3, null)).toEqual(3); - expect(_getPluralForm(-1, null)).toEqual(-1); - expect(_getPluralForm(-1, { original_plural: '' })).toEqual(-1); - }); + it('returns the plural form', () => { + expect(_getPluralForm(3, null)).toEqual(3); + expect(_getPluralForm(-1, null)).toEqual(-1); + expect(_getPluralForm(-1, { original_plural: '' })).toEqual(-1); + }); - it('corrects the plural number', () => { - expect( - _getPluralForm(-1, { original_plural: 'I have a plural!' }), - ).toEqual(0); - }); + it('corrects the plural number', () => { + expect(_getPluralForm(-1, { original_plural: 'I have a plural!' })).toEqual( + 0, + ); + }); }); const ENTITIES = [ - { - translation: [ - { - string: 'world', - }, - ], - }, - { - translation: [ - { - string: 'wat', - rejected: true, - }, - ], - }, + { + translation: [ + { + string: 'world', + }, + ], + }, + { + translation: [ + { + string: 'wat', + rejected: true, + }, + ], + }, ]; describe('getTranslationForSelectedEntity', () => { - it('returns the correct string', () => { - const entity = ENTITIES[0]; - const res = _getTranslationForSelectedEntity(entity, -1); + it('returns the correct string', () => { + const entity = ENTITIES[0]; + const res = _getTranslationForSelectedEntity(entity, -1); - expect(res).toEqual({ string: 'world' }); - }); + expect(res).toEqual({ string: 'world' }); + }); - it('does not return rejected translations', () => { - const entity = ENTITIES[1]; - const res = _getTranslationForSelectedEntity(entity, -1); + it('does not return rejected translations', () => { + const entity = ENTITIES[1]; + const res = _getTranslationForSelectedEntity(entity, -1); - expect(res).toEqual(null); - }); + expect(res).toEqual(null); + }); }); describe('getTranslationStringForSelectedEntity', () => { - it('returns the correct string', () => { - const entity = ENTITIES[0]; - const res = _getTranslationStringForSelectedEntity(entity, -1); + it('returns the correct string', () => { + const entity = ENTITIES[0]; + const res = _getTranslationStringForSelectedEntity(entity, -1); - expect(res).toEqual('world'); - }); + expect(res).toEqual('world'); + }); - it('does not return rejected translations', () => { - const entity = ENTITIES[1]; - const res = _getTranslationStringForSelectedEntity(entity, -1); + it('does not return rejected translations', () => { + const entity = ENTITIES[1]; + const res = _getTranslationStringForSelectedEntity(entity, -1); - expect(res).toEqual(''); - }); + expect(res).toEqual(''); + }); }); diff --git a/translate/src/core/plural/selectors.ts b/translate/src/core/plural/selectors.ts index 4a2ff74e5..519f13eac 100644 --- a/translate/src/core/plural/selectors.ts +++ b/translate/src/core/plural/selectors.ts @@ -8,13 +8,13 @@ import type { RootState } from '../../store'; const pluralSelector = (state: RootState) => state.plural.pluralForm; export function _getPluralForm( - pluralForm: number, - entity: Entity | null | undefined, + pluralForm: number, + entity: Entity | null | undefined, ): number { - if (pluralForm === -1 && entity && entity.original_plural) { - return 0; - } - return pluralForm; + if (pluralForm === -1 && entity && entity.original_plural) { + return 0; + } + return pluralForm; } /** @@ -25,28 +25,28 @@ export function _getPluralForm( * the plural form as stored in the state. */ export const getPluralForm = createSelector( - pluralSelector, - (state: RootState) => entities.selectors.getSelectedEntity(state), - _getPluralForm, + pluralSelector, + (state: RootState) => entities.selectors.getSelectedEntity(state), + _getPluralForm, ); export function _getTranslationForSelectedEntity( - entity: Entity, - pluralForm: number, + entity: Entity, + pluralForm: number, ): EntityTranslation | null | undefined { - if (pluralForm === -1) { - pluralForm = 0; - } + if (pluralForm === -1) { + pluralForm = 0; + } - if ( - entity && - entity.translation[pluralForm] && - !entity.translation[pluralForm].rejected - ) { - return entity.translation[pluralForm]; - } + if ( + entity && + entity.translation[pluralForm] && + !entity.translation[pluralForm].rejected + ) { + return entity.translation[pluralForm]; + } - return null; + return null; } /** @@ -57,20 +57,20 @@ export function _getTranslationForSelectedEntity( * most recent non-rejected one. */ export const getTranslationForSelectedEntity = createSelector( - (state: RootState) => entities.selectors.getSelectedEntity(state), - getPluralForm, - _getTranslationForSelectedEntity, + (state: RootState) => entities.selectors.getSelectedEntity(state), + getPluralForm, + _getTranslationForSelectedEntity, ); export function _getTranslationStringForSelectedEntity( - entity: Entity, - pluralForm: number, + entity: Entity, + pluralForm: number, ): string { - const translation = _getTranslationForSelectedEntity(entity, pluralForm); - if (translation && translation.string) { - return translation.string; - } - return ''; + const translation = _getTranslationForSelectedEntity(entity, pluralForm); + if (translation && translation.string) { + return translation.string; + } + return ''; } /** @@ -81,13 +81,13 @@ export function _getTranslationStringForSelectedEntity( * most recent non-rejected one. */ export const getTranslationStringForSelectedEntity = createSelector( - (state: RootState) => entities.selectors.getSelectedEntity(state), - getPluralForm, - _getTranslationStringForSelectedEntity, + (state: RootState) => entities.selectors.getSelectedEntity(state), + getPluralForm, + _getTranslationStringForSelectedEntity, ); export default { - getPluralForm, - getTranslationForSelectedEntity, - getTranslationStringForSelectedEntity, + getPluralForm, + getTranslationForSelectedEntity, + getTranslationStringForSelectedEntity, }; diff --git a/translate/src/core/project/actions.ts b/translate/src/core/project/actions.ts index 710020f5e..781e9639e 100644 --- a/translate/src/core/project/actions.ts +++ b/translate/src/core/project/actions.ts @@ -6,67 +6,67 @@ export const RECEIVE: 'project/RECEIVE' = 'project/RECEIVE'; export const REQUEST: 'project/REQUEST' = 'project/REQUEST'; export type Tag = { - readonly slug: string; - readonly name: string; - readonly priority: number; + readonly slug: string; + readonly name: string; + readonly priority: number; }; type Project = { - slug: string; - name: string; - info: string; - tags: Array; + slug: string; + name: string; + info: string; + tags: Array; }; /** * Notify that project data is being fetched. */ export type RequestAction = { - readonly type: typeof REQUEST; + readonly type: typeof REQUEST; }; export function request(): RequestAction { - return { - type: REQUEST, - }; + return { + type: REQUEST, + }; } /** * Receive project data. */ export type ReceiveAction = { - readonly type: typeof RECEIVE; - readonly slug: string; - readonly name: string; - readonly info: string; - readonly tags: Array; + readonly type: typeof RECEIVE; + readonly slug: string; + readonly name: string; + readonly info: string; + readonly tags: Array; }; export function receive(project: Project): ReceiveAction { - return { - type: RECEIVE, - slug: project.slug, - name: project.name, - info: project.info, - tags: project.tags, - }; + return { + type: RECEIVE, + slug: project.slug, + name: project.name, + info: project.info, + tags: project.tags, + }; } /** * Get data about the current project. */ export function get(slug: string) { - return async (dispatch: AppDispatch) => { - // When 'all-projects' are selected, we do not fetch data. - if (slug === 'all-projects') { - return; - } - dispatch(request()); - const results = await api.project.get(slug); - dispatch(receive(results.data.project)); - }; + return async (dispatch: AppDispatch) => { + // When 'all-projects' are selected, we do not fetch data. + if (slug === 'all-projects') { + return; + } + dispatch(request()); + const results = await api.project.get(slug); + dispatch(receive(results.data.project)); + }; } export default { - get, - receive, - request, + get, + receive, + request, }; diff --git a/translate/src/core/project/components/ProjectItem.css b/translate/src/core/project/components/ProjectItem.css index 096c0f643..c9215b1c4 100644 --- a/translate/src/core/project/components/ProjectItem.css +++ b/translate/src/core/project/components/ProjectItem.css @@ -1,18 +1,18 @@ .project-menu .menu li.no-results, .project-menu .menu li a { - display: block; - padding: 2px 4px; + display: block; + padding: 2px 4px; } .project-menu .menu li a:hover { - background: #3f4752; - color: #fff; + background: #3f4752; + color: #fff; } .project-menu .menu li a .project { - display: inline-block; - max-width: 550px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + display: inline-block; + max-width: 550px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/translate/src/core/project/components/ProjectItem.test.js b/translate/src/core/project/components/ProjectItem.test.js index 73e6c7c5a..216e69ce6 100644 --- a/translate/src/core/project/components/ProjectItem.test.js +++ b/translate/src/core/project/components/ProjectItem.test.js @@ -5,40 +5,40 @@ import ProjectItem from './ProjectItem'; import ProjectPercent from './ProjectPercent'; function createShallowProjectItem({ slug = 'slug' } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders correctly', () => { - const wrapper = createShallowProjectItem(); - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('a')).toHaveLength(1); - expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find(ProjectPercent)).toHaveLength(1); - expect(wrapper.find('a').prop('href')).toEqual( - '/locale/slug/all-resources/', - ); - }); + it('renders correctly', () => { + const wrapper = createShallowProjectItem(); + expect(wrapper.find('li')).toHaveLength(1); + expect(wrapper.find('a')).toHaveLength(1); + expect(wrapper.find('span')).toHaveLength(1); + expect(wrapper.find(ProjectPercent)).toHaveLength(1); + expect(wrapper.find('a').prop('href')).toEqual( + '/locale/slug/all-resources/', + ); + }); - it('sets the className correctly', () => { - let wrapper = createShallowProjectItem(); - expect(wrapper.find('li.current')).toHaveLength(0); + it('sets the className correctly', () => { + let wrapper = createShallowProjectItem(); + expect(wrapper.find('li.current')).toHaveLength(0); - wrapper = createShallowProjectItem({ slug: 'project' }); - expect(wrapper.find('li.current')).toHaveLength(1); - }); + wrapper = createShallowProjectItem({ slug: 'project' }); + expect(wrapper.find('li.current')).toHaveLength(1); + }); }); diff --git a/translate/src/core/project/components/ProjectItem.tsx b/translate/src/core/project/components/ProjectItem.tsx index d637e57c8..cdf1213f5 100644 --- a/translate/src/core/project/components/ProjectItem.tsx +++ b/translate/src/core/project/components/ProjectItem.tsx @@ -8,30 +8,30 @@ import type { Localization } from '~/core/locale'; import type { NavigationParams } from '~/core/navigation'; type Props = { - parameters: NavigationParams; - localization: Localization; - navigateToPath: (arg0: React.MouseEvent) => void; + parameters: NavigationParams; + localization: Localization; + navigateToPath: (arg0: React.MouseEvent) => void; }; /** * Render a project menu item. */ export default function ProjectItem(props: Props): React.ReactElement<'li'> { - const { parameters, localization, navigateToPath } = props; - const project = localization.project; - const className = parameters.project === project.slug ? 'current' : null; + const { parameters, localization, navigateToPath } = props; + const project = localization.project; + const className = parameters.project === project.slug ? 'current' : null; - return ( -
  • - - - {project.name} - - - -
  • - ); + return ( +
  • + + + {project.name} + + + +
  • + ); } diff --git a/translate/src/core/project/components/ProjectMenu.css b/translate/src/core/project/components/ProjectMenu.css index db5c7f84c..cdeb7bb2a 100644 --- a/translate/src/core/project/components/ProjectMenu.css +++ b/translate/src/core/project/components/ProjectMenu.css @@ -1,125 +1,125 @@ .project-menu { - font-weight: 100; + font-weight: 100; } .project-menu .selector { - border-radius: 2px; - cursor: pointer; - line-height: 24px; - margin: 14px 6px; - float: right; - padding: 4px 6px; + border-radius: 2px; + cursor: pointer; + line-height: 24px; + margin: 14px 6px; + float: right; + padding: 4px 6px; } .project-menu .selector.unselectable { - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; } .project-menu.closed .selector:hover { - background: #333941; + background: #333941; } .project-menu .selector .icon { - color: #7bc876; - padding-left: 7px; + color: #7bc876; + padding-left: 7px; } .project-menu .menu { - background-color: #272a2f; - border: 1px solid #333941; - border-top: none; - color: #aaaaaa; - line-height: 1.231em; - list-style: none; - padding: 10px 12px; - position: absolute; - top: 59px; - width: 600px; - z-index: 20; + background-color: #272a2f; + border: 1px solid #333941; + border-top: none; + color: #aaaaaa; + line-height: 1.231em; + list-style: none; + padding: 10px 12px; + position: absolute; + top: 59px; + width: 600px; + z-index: 20; } .project-menu .menu .search-wrapper { - margin-bottom: 10px; - position: relative; - font-size: 13px; + margin-bottom: 10px; + position: relative; + font-size: 13px; } .project-menu .menu .search-wrapper .icon { - color: #aaaaaa; - font-size: 1.2em; - position: absolute; - left: 6px; - top: 6px; - z-index: 20; + color: #aaaaaa; + font-size: 1.2em; + position: absolute; + left: 6px; + top: 6px; + z-index: 20; } .project-menu .menu .search-wrapper input[type='search'] { - color: #ffffff; - padding: 4px 3px 3px 25px; - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - font-weight: 300; - height: 28px; - position: relative; - width: 100%; - z-index: 10; + color: #ffffff; + padding: 4px 3px 3px 25px; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + font-weight: 300; + height: 28px; + position: relative; + width: 100%; + z-index: 10; } /* Remove highlight in Chrome */ .project-menu .menu .search-wrapper input[type='search']:focus { - outline: none; + outline: none; } .project-menu - .menu - .search-wrapper - input[type='search']::-webkit-search-decoration, + .menu + .search-wrapper + input[type='search']::-webkit-search-decoration, .project-menu - .menu - .search-wrapper - input[type='search']::-webkit-search-cancel-button { - display: none; + .menu + .search-wrapper + input[type='search']::-webkit-search-cancel-button { + display: none; } .project-menu .menu ul { - max-height: 318px; - overflow: auto; + max-height: 318px; + overflow: auto; } .project-menu .menu .static-links { - border-top: 1px solid #5e6475; - margin-top: 5px; - padding-top: 5px; + border-top: 1px solid #5e6475; + margin-top: 5px; + padding-top: 5px; } .project-menu .menu .current a { - color: #7bc876; + color: #7bc876; } .project-menu .menu .header { - border-bottom: 1px solid #5e6475; - padding: 5px 4px; - overflow: auto; - font-size: 13px; - font-weight: bold; - cursor: pointer; - margin-bottom: 5px; + border-bottom: 1px solid #5e6475; + padding: 5px 4px; + overflow: auto; + font-size: 13px; + font-weight: bold; + cursor: pointer; + margin-bottom: 5px; } .project-menu .menu .header .project { - float: left; + float: left; } .project-menu .menu .header .progress { - float: right; + float: right; } .project-menu .menu .header .icon { - margin: 1px 5px; + margin: 1px 5px; } diff --git a/translate/src/core/project/components/ProjectMenu.test.js b/translate/src/core/project/components/ProjectMenu.test.js index 9b57b69ad..740f928e6 100644 --- a/translate/src/core/project/components/ProjectMenu.test.js +++ b/translate/src/core/project/components/ProjectMenu.test.js @@ -6,124 +6,122 @@ import ProjectItem from './ProjectItem'; import ProjectMenuBase, { ProjectMenu } from './ProjectMenu'; function createShallowProjectMenu({ - project = { - slug: 'project', - name: 'Project', - }, + project = { + slug: 'project', + name: 'Project', + }, } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders properly', () => { - const wrapper = createShallowProjectMenu(); + it('renders properly', () => { + const wrapper = createShallowProjectMenu(); - expect(wrapper.find('.menu .search-wrapper')).toHaveLength(1); - expect(wrapper.find('.menu > ul')).toHaveLength(1); - expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(1); + expect(wrapper.find('.menu .search-wrapper')).toHaveLength(1); + expect(wrapper.find('.menu > ul')).toHaveLength(1); + expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(1); + }); + + it('returns no results for non-matching searches', () => { + const SEARCH_NO_MATCH = 'bc'; + const wrapper = createShallowProjectMenu({ + project: ALL_PROJECTS, }); - it('returns no results for non-matching searches', () => { - const SEARCH_NO_MATCH = 'bc'; - const wrapper = createShallowProjectMenu({ - project: ALL_PROJECTS, - }); + wrapper + .find('.menu .search-wrapper input') + .simulate('change', { currentTarget: { value: SEARCH_NO_MATCH } }); - wrapper - .find('.menu .search-wrapper input') - .simulate('change', { currentTarget: { value: SEARCH_NO_MATCH } }); + expect(wrapper.find('.menu .search-wrapper input').prop('value')).toEqual( + SEARCH_NO_MATCH, + ); + expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(0); + }); - expect( - wrapper.find('.menu .search-wrapper input').prop('value'), - ).toEqual(SEARCH_NO_MATCH); - expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(0); + it('searches project items correctly', () => { + const SEARCH_MATCH = 'roj'; + const wrapper = createShallowProjectMenu({ + project: ALL_PROJECTS, }); - it('searches project items correctly', () => { - const SEARCH_MATCH = 'roj'; - const wrapper = createShallowProjectMenu({ - project: ALL_PROJECTS, - }); + wrapper + .find('.menu .search-wrapper input') + .simulate('change', { currentTarget: { value: SEARCH_MATCH } }); - wrapper - .find('.menu .search-wrapper input') - .simulate('change', { currentTarget: { value: SEARCH_MATCH } }); - - expect( - wrapper.find('.menu .search-wrapper input').prop('value'), - ).toEqual(SEARCH_MATCH); - expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(1); - }); + expect(wrapper.find('.menu .search-wrapper input').prop('value')).toEqual( + SEARCH_MATCH, + ); + expect(wrapper.find('.menu > ul').find(ProjectItem)).toHaveLength(1); + }); }); function createShallowProjectMenuBase({ - project = { - slug: 'project', - name: 'Project', - }, + project = { + slug: 'project', + name: 'Project', + }, } = {}) { - return shallow( - , - ); + return shallow( + , + ); } const ALL_PROJECTS = { - slug: 'all-projects', - name: 'All Projects', + slug: 'all-projects', + name: 'All Projects', }; describe('', () => { - it('shows a link to localization dashboard in regular view', () => { - const wrapper = createShallowProjectMenuBase(); + it('shows a link to localization dashboard in regular view', () => { + const wrapper = createShallowProjectMenuBase(); - expect(wrapper.text()).toContain('Project'); - expect(wrapper.find('a').prop('href')).toEqual('/locale/project/'); - }); + expect(wrapper.text()).toContain('Project'); + expect(wrapper.find('a').prop('href')).toEqual('/locale/project/'); + }); - it('shows project selector in all projects view', () => { - const wrapper = createShallowProjectMenuBase({ project: ALL_PROJECTS }); + it('shows project selector in all projects view', () => { + const wrapper = createShallowProjectMenuBase({ project: ALL_PROJECTS }); - expect(wrapper.find('.project-menu .selector')).toHaveLength(1); - expect(wrapper.find('#project-ProjectMenu--all-projects')).toHaveLength( - 1, - ); - expect(wrapper.find('.project-menu .selector .icon')).toHaveLength(1); - }); + expect(wrapper.find('.project-menu .selector')).toHaveLength(1); + expect(wrapper.find('#project-ProjectMenu--all-projects')).toHaveLength(1); + expect(wrapper.find('.project-menu .selector .icon')).toHaveLength(1); + }); - it('renders the project menu upon clicking on all projects', () => { - const wrapper = createShallowProjectMenuBase({ project: ALL_PROJECTS }); - wrapper.find('.selector').simulate('click'); + it('renders the project menu upon clicking on all projects', () => { + const wrapper = createShallowProjectMenuBase({ project: ALL_PROJECTS }); + wrapper.find('.selector').simulate('click'); - expect(wrapper.find('ProjectMenu')).toHaveLength(1); - }); + expect(wrapper.find('ProjectMenu')).toHaveLength(1); + }); }); diff --git a/translate/src/core/project/components/ProjectMenu.tsx b/translate/src/core/project/components/ProjectMenu.tsx index a6be39043..ea74cb125 100644 --- a/translate/src/core/project/components/ProjectMenu.tsx +++ b/translate/src/core/project/components/ProjectMenu.tsx @@ -12,169 +12,165 @@ import type { NavigationParams } from '~/core/navigation'; import type { ProjectState } from '~/core/project'; type Props = { - parameters: NavigationParams; - locale: LocaleState; - project: ProjectState; - navigateToPath: (arg0: string) => void; + parameters: NavigationParams; + locale: LocaleState; + project: ProjectState; + navigateToPath: (arg0: string) => void; }; type State = { - visible: boolean; + visible: boolean; }; type ProjectMenuProps = { - locale: LocaleState; - parameters: NavigationParams; - onDiscard: () => void; - onNavigate: (e: React.MouseEvent) => void; + locale: LocaleState; + parameters: NavigationParams; + onDiscard: () => void; + onNavigate: (e: React.MouseEvent) => void; }; export function ProjectMenu({ - locale, - parameters, - onDiscard, - onNavigate, + locale, + parameters, + onDiscard, + onNavigate, }: ProjectMenuProps): React.ReactElement<'div'> { - // Searching - const [search, setSearch] = React.useState(''); + // Searching + const [search, setSearch] = React.useState(''); - const updateProjectList = (e: React.SyntheticEvent) => { - setSearch(e.currentTarget.value); - }; + const updateProjectList = (e: React.SyntheticEvent) => { + setSearch(e.currentTarget.value); + }; - const localizationElements = locale.localizations.filter( - (localization) => - localization.project.name - .toLowerCase() - .indexOf(search.toLowerCase()) > -1, - ); + const localizationElements = locale.localizations.filter( + (localization) => + localization.project.name.toLowerCase().indexOf(search.toLowerCase()) > + -1, + ); - // Sorting - const [sortActive, setSortActive] = React.useState('project'); - const [sortAsc, setSortAsc] = React.useState(true); + // Sorting + const [sortActive, setSortActive] = React.useState('project'); + const [sortAsc, setSortAsc] = React.useState(true); - const sortByProject = () => { - setSortActive('project'); - setSortAsc(sortActive !== 'project' || !sortAsc); - }; - const sortByProgress = () => { - setSortActive('progress'); - setSortAsc(sortActive !== 'progress' || !sortAsc); - }; + const sortByProject = () => { + setSortActive('project'); + setSortAsc(sortActive !== 'project' || !sortAsc); + }; + const sortByProgress = () => { + setSortActive('progress'); + setSortAsc(sortActive !== 'progress' || !sortAsc); + }; - const getProgress = (local: Localization) => { - const completeStrings = - local.approvedStrings + local.stringsWithWarnings; - const percent = Math.floor( - (completeStrings / local.totalStrings) * 100, - ); - return percent; - }; + const getProgress = (local: Localization) => { + const completeStrings = local.approvedStrings + local.stringsWithWarnings; + const percent = Math.floor((completeStrings / local.totalStrings) * 100); + return percent; + }; - const getProject = (local: Localization) => { - return local.project.name; - }; + const getProject = (local: Localization) => { + return local.project.name; + }; - const sort = sortAsc ? 'fa fa-caret-up' : 'fa fa-caret-down'; - const projectClass = sortActive === 'project' ? sort : ''; - const progressClass = sortActive === 'progress' ? sort : ''; + const sort = sortAsc ? 'fa fa-caret-up' : 'fa fa-caret-down'; + const projectClass = sortActive === 'project' ? sort : ''; + const progressClass = sortActive === 'progress' ? sort : ''; - // Discarding menu - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); + // Discarding menu + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); - return ( -
    -
    -
    - - - -
    + return ( +
    +
    +
    + + + +
    -
    - - - PROJECT - - - - - - PROGRESS - - - -
    +
    + + + PROJECT + + + + + + PROGRESS + + + +
    -
      - {localizationElements.length ? ( - (sortActive === 'project' - ? localizationElements.sort((a, b) => { - const projectA = getProject(a); - const projectB = getProject(b); +
        + {localizationElements.length ? ( + (sortActive === 'project' + ? localizationElements.sort((a, b) => { + const projectA = getProject(a); + const projectB = getProject(b); - let result = 0; + let result = 0; - if (projectA < projectB) { - result = -1; - } - if (projectA > projectB) { - result = 1; - } + if (projectA < projectB) { + result = -1; + } + if (projectA > projectB) { + result = 1; + } - return sortAsc ? result : result * -1; - }) - : localizationElements.sort((a, b) => { - const percentA = getProgress(a); - const percentB = getProgress(b); + return sortAsc ? result : result * -1; + }) + : localizationElements.sort((a, b) => { + const percentA = getProgress(a); + const percentB = getProgress(b); - let result = 0; + let result = 0; - if (percentA < percentB) { - result = -1; - } - if (percentA > percentB) { - result = 1; - } + if (percentA < percentB) { + result = -1; + } + if (percentA > percentB) { + result = 1; + } - return sortAsc ? result : result * -1; - }) - ).map((localization, index) => { - return ( - - ); - }) - ) : ( - // No projects found - -
      • No results
      • -
        - )} -
      -
    - ); + return sortAsc ? result : result * -1; + }) + ).map((localization, index) => { + return ( + + ); + }) + ) : ( + // No projects found + +
  • No results
  • +
    + )} + +
    + ); } /** @@ -184,75 +180,70 @@ export function ProjectMenu({ * regular view without reloading the Translate app. */ export default class ProjectMenuBase extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - visible: false, - }; + constructor(props: Props) { + super(props); + this.state = { + visible: false, + }; + } + + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; + + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; + + navigateToPath: (event: React.MouseEvent) => void = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + const path = event.currentTarget.pathname; + this.props.navigateToPath(path); + + this.setState({ + visible: false, + }); + }; + + render(): React.ReactElement<'li'> { + const { locale, parameters, project } = this.props; + + if (parameters.project !== 'all-projects') { + return ( +
  • + {project.name} +
  • + ); } - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); - }; - - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; - - navigateToPath: (event: React.MouseEvent) => void = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - - const path = event.currentTarget.pathname; - this.props.navigateToPath(path); - - this.setState({ - visible: false, - }); - }; - - render(): React.ReactElement<'li'> { - const { locale, parameters, project } = this.props; - - if (parameters.project !== 'all-projects') { - return ( -
  • - - {project.name} - -
  • - ); - } - - let className = 'project-menu'; - if (!this.state.visible) { - className += ' closed'; - } - return ( -
  • -
    - - All Projects - - -
    - {this.state.visible && ( - - )} -
  • - ); + let className = 'project-menu'; + if (!this.state.visible) { + className += ' closed'; } + return ( +
  • +
    + + All Projects + + +
    + {this.state.visible && ( + + )} +
  • + ); + } } diff --git a/translate/src/core/project/components/ProjectPercent.css b/translate/src/core/project/components/ProjectPercent.css index 34e207836..97f902182 100644 --- a/translate/src/core/project/components/ProjectPercent.css +++ b/translate/src/core/project/components/ProjectPercent.css @@ -1,4 +1,4 @@ .project-menu .menu .percent { - color: #7bc876; - float: right; + color: #7bc876; + float: right; } diff --git a/translate/src/core/project/components/ProjectPercent.test.js b/translate/src/core/project/components/ProjectPercent.test.js index 07ab1dc83..cf9055873 100644 --- a/translate/src/core/project/components/ProjectPercent.test.js +++ b/translate/src/core/project/components/ProjectPercent.test.js @@ -4,14 +4,14 @@ import { shallow } from 'enzyme'; import ProjectPercent from './ProjectPercent'; describe('', () => { - const LOCALIZATION = { - approvedStrings: 2, - stringsWithWarnings: 3, - totalStrings: 10, - }; + const LOCALIZATION = { + approvedStrings: 2, + stringsWithWarnings: 3, + totalStrings: 10, + }; - it('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper.find('.percent').text()).toEqual('50%'); - }); + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.find('.percent').text()).toEqual('50%'); + }); }); diff --git a/translate/src/core/project/components/ProjectPercent.tsx b/translate/src/core/project/components/ProjectPercent.tsx index 1f4d2a8ae..a086968d2 100644 --- a/translate/src/core/project/components/ProjectPercent.tsx +++ b/translate/src/core/project/components/ProjectPercent.tsx @@ -5,20 +5,20 @@ import './ProjectPercent.css'; import type { Localization } from '~/core/locale'; type Props = { - localization: Localization; + localization: Localization; }; /** * Render a project item percentage. */ export default function ProjectPercent( - props: Props, + props: Props, ): React.ReactElement<'span'> { - const { approvedStrings, stringsWithWarnings, totalStrings } = - props.localization; - const completeStrings = approvedStrings + stringsWithWarnings; + const { approvedStrings, stringsWithWarnings, totalStrings } = + props.localization; + const completeStrings = approvedStrings + stringsWithWarnings; - const percent = Math.floor((completeStrings / totalStrings) * 100) + '%'; + const percent = Math.floor((completeStrings / totalStrings) * 100) + '%'; - return {percent}; + return {percent}; } diff --git a/translate/src/core/project/reducer.ts b/translate/src/core/project/reducer.ts index 5dda55571..6170f9469 100644 --- a/translate/src/core/project/reducer.ts +++ b/translate/src/core/project/reducer.ts @@ -5,41 +5,41 @@ import type { ReceiveAction, RequestAction, Tag } from './actions'; type Action = ReceiveAction | RequestAction; export type ProjectState = { - readonly fetching: boolean; - readonly slug: string; - readonly name: string; - readonly info: string; - readonly tags: Array; + readonly fetching: boolean; + readonly slug: string; + readonly name: string; + readonly info: string; + readonly tags: Array; }; const initial: ProjectState = { - fetching: false, - slug: '', - name: '', - info: '', - tags: [], + fetching: false, + slug: '', + name: '', + info: '', + tags: [], }; export default function reducer( - state: ProjectState = initial, - action: Action, + state: ProjectState = initial, + action: Action, ): ProjectState { - switch (action.type) { - case REQUEST: - return { - ...state, - fetching: true, - }; - case RECEIVE: - return { - ...state, - fetching: false, - slug: action.slug, - name: action.name, - info: action.info, - tags: action.tags, - }; - default: - return state; - } + switch (action.type) { + case REQUEST: + return { + ...state, + fetching: true, + }; + case RECEIVE: + return { + ...state, + fetching: false, + slug: action.slug, + name: action.name, + info: action.info, + tags: action.tags, + }; + default: + return state; + } } diff --git a/translate/src/core/resource/actions.ts b/translate/src/core/resource/actions.ts index 1d32b9b9c..110f59347 100644 --- a/translate/src/core/resource/actions.ts +++ b/translate/src/core/resource/actions.ts @@ -6,67 +6,67 @@ export const RECEIVE: 'resource/RECEIVE' = 'resource/RECEIVE'; export const UPDATE: 'resource/UPDATE' = 'resource/UPDATE'; export type Resource = { - readonly path: string; - readonly approvedStrings: number; - readonly stringsWithWarnings: number; - readonly totalStrings: number; + readonly path: string; + readonly approvedStrings: number; + readonly stringsWithWarnings: number; + readonly totalStrings: number; }; export type UpdateAction = { - type: typeof UPDATE; - resourcePath: string; - approvedStrings: number; - stringsWithWarnings: number; + type: typeof UPDATE; + resourcePath: string; + approvedStrings: number; + stringsWithWarnings: number; }; export function update( - resourcePath: string, - approvedStrings: number, - stringsWithWarnings: number, + resourcePath: string, + approvedStrings: number, + stringsWithWarnings: number, ): UpdateAction { - return { - type: UPDATE, - resourcePath, - approvedStrings, - stringsWithWarnings, - }; + return { + type: UPDATE, + resourcePath, + approvedStrings, + stringsWithWarnings, + }; } export type ReceiveAction = { - type: typeof RECEIVE; - resources: Array; - allResources: Resource; + type: typeof RECEIVE; + resources: Array; + allResources: Resource; }; export function receive( - resources: Array, - allResources: Resource, + resources: Array, + allResources: Resource, ): ReceiveAction { - return { - type: RECEIVE, - resources, - allResources, - }; + return { + type: RECEIVE, + resources, + allResources, + }; } export function get(locale: string, project: string) { - return async (dispatch: AppDispatch) => { - const results = await api.resource.getAll(locale, project); + return async (dispatch: AppDispatch) => { + const results = await api.resource.getAll(locale, project); - const resources = results.map((resource) => { - return { - path: resource.title, - approvedStrings: resource.approved_strings, - stringsWithWarnings: resource.strings_with_warnings, - totalStrings: resource.resource__total_strings, - }; - }); + const resources = results.map((resource) => { + return { + path: resource.title, + approvedStrings: resource.approved_strings, + stringsWithWarnings: resource.strings_with_warnings, + totalStrings: resource.resource__total_strings, + }; + }); - const allResources = resources.pop(); + const allResources = resources.pop(); - dispatch(receive(resources, allResources)); - }; + dispatch(receive(resources, allResources)); + }; } export default { - get, - update, + get, + update, }; diff --git a/translate/src/core/resource/components/ResourceItem.css b/translate/src/core/resource/components/ResourceItem.css index aa6b32429..279ccafa5 100644 --- a/translate/src/core/resource/components/ResourceItem.css +++ b/translate/src/core/resource/components/ResourceItem.css @@ -1,18 +1,18 @@ .resource-menu .menu li.no-results, .resource-menu .menu li a { - display: block; - padding: 2px 4px; + display: block; + padding: 2px 4px; } .resource-menu .menu li a:hover { - background: #3f4752; - color: #fff; + background: #3f4752; + color: #fff; } .resource-menu .menu li a .path { - display: inline-block; - max-width: 550px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + display: inline-block; + max-width: 550px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/translate/src/core/resource/components/ResourceItem.test.js b/translate/src/core/resource/components/ResourceItem.test.js index da194b1cb..6a69d2601 100644 --- a/translate/src/core/resource/components/ResourceItem.test.js +++ b/translate/src/core/resource/components/ResourceItem.test.js @@ -5,35 +5,35 @@ import ResourceItem from './ResourceItem'; import ResourcePercent from './ResourcePercent'; function createShallowResourceItem({ path = 'path' } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders correctly', () => { - const wrapper = createShallowResourceItem(); - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('a')).toHaveLength(1); - expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find(ResourcePercent)).toHaveLength(1); - expect(wrapper.find('a').prop('href')).toEqual('/locale/project/path/'); - }); + it('renders correctly', () => { + const wrapper = createShallowResourceItem(); + expect(wrapper.find('li')).toHaveLength(1); + expect(wrapper.find('a')).toHaveLength(1); + expect(wrapper.find('span')).toHaveLength(1); + expect(wrapper.find(ResourcePercent)).toHaveLength(1); + expect(wrapper.find('a').prop('href')).toEqual('/locale/project/path/'); + }); - it('sets the className correctly', () => { - let wrapper = createShallowResourceItem(); - expect(wrapper.find('li.current')).toHaveLength(0); + it('sets the className correctly', () => { + let wrapper = createShallowResourceItem(); + expect(wrapper.find('li.current')).toHaveLength(0); - wrapper = createShallowResourceItem({ path: 'resource' }); - expect(wrapper.find('li.current')).toHaveLength(1); - }); + wrapper = createShallowResourceItem({ path: 'resource' }); + expect(wrapper.find('li.current')).toHaveLength(1); + }); }); diff --git a/translate/src/core/resource/components/ResourceItem.tsx b/translate/src/core/resource/components/ResourceItem.tsx index 9fdeba347..615662dd7 100644 --- a/translate/src/core/resource/components/ResourceItem.tsx +++ b/translate/src/core/resource/components/ResourceItem.tsx @@ -8,29 +8,29 @@ import type { NavigationParams } from '~/core/navigation'; import type { Resource } from '..'; type Props = { - parameters: NavigationParams; - resource: Resource; - navigateToPath: (arg0: React.MouseEvent) => void; + parameters: NavigationParams; + resource: Resource; + navigateToPath: (arg0: React.MouseEvent) => void; }; /** * Render a resource menu item. */ export default function ResourceItem(props: Props): React.ReactElement<'li'> { - const { parameters, resource, navigateToPath } = props; - const className = parameters.resource === resource.path ? 'current' : null; + const { parameters, resource, navigateToPath } = props; + const className = parameters.resource === resource.path ? 'current' : null; - return ( -
  • - - - {resource.path} - - - -
  • - ); + return ( +
  • + + + {resource.path} + + + +
  • + ); } diff --git a/translate/src/core/resource/components/ResourceMenu.css b/translate/src/core/resource/components/ResourceMenu.css index 46e2ee644..f9c146a0a 100644 --- a/translate/src/core/resource/components/ResourceMenu.css +++ b/translate/src/core/resource/components/ResourceMenu.css @@ -1,125 +1,125 @@ .resource-menu { - font-weight: 100; + font-weight: 100; } .resource-menu .selector { - border-radius: 2px; - cursor: pointer; - line-height: 24px; - margin: 14px 6px; - float: right; - padding: 4px 6px; + border-radius: 2px; + cursor: pointer; + line-height: 24px; + margin: 14px 6px; + float: right; + padding: 4px 6px; } .resource-menu .selector.unselectable { - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; } .resource-menu.closed .selector:hover { - background: #333941; + background: #333941; } .resource-menu .selector .icon { - color: #7bc876; - padding-left: 7px; + color: #7bc876; + padding-left: 7px; } .resource-menu .menu { - background-color: #272a2f; - border: 1px solid #333941; - border-top: none; - color: #aaaaaa; - line-height: 1.231em; - list-style: none; - padding: 10px 12px; - position: absolute; - top: 59px; - width: 600px; - z-index: 20; + background-color: #272a2f; + border: 1px solid #333941; + border-top: none; + color: #aaaaaa; + line-height: 1.231em; + list-style: none; + padding: 10px 12px; + position: absolute; + top: 59px; + width: 600px; + z-index: 20; } .resource-menu .menu .search-wrapper { - margin-bottom: 10px; - position: relative; - font-size: 13px; + margin-bottom: 10px; + position: relative; + font-size: 13px; } .resource-menu .menu .search-wrapper .icon { - color: #aaaaaa; - font-size: 1.2em; - position: absolute; - left: 6px; - top: 6px; - z-index: 20; + color: #aaaaaa; + font-size: 1.2em; + position: absolute; + left: 6px; + top: 6px; + z-index: 20; } .resource-menu .menu .search-wrapper input[type='search'] { - color: #ffffff; - padding: 4px 3px 3px 25px; - background: #333941; - border: 1px solid #4d5967; - border-radius: 3px; - font-weight: 300; - height: 28px; - position: relative; - width: 100%; - z-index: 10; + color: #ffffff; + padding: 4px 3px 3px 25px; + background: #333941; + border: 1px solid #4d5967; + border-radius: 3px; + font-weight: 300; + height: 28px; + position: relative; + width: 100%; + z-index: 10; } /* Remove highlight in Chrome */ .resource-menu .menu .search-wrapper input[type='search']:focus { - outline: none; + outline: none; } .resource-menu - .menu - .search-wrapper - input[type='search']::-webkit-search-decoration, + .menu + .search-wrapper + input[type='search']::-webkit-search-decoration, .resource-menu - .menu - .search-wrapper - input[type='search']::-webkit-search-cancel-button { - display: none; + .menu + .search-wrapper + input[type='search']::-webkit-search-cancel-button { + display: none; } .resource-menu .menu ul { - max-height: 318px; - overflow: auto; + max-height: 318px; + overflow: auto; } .resource-menu .menu .static-links { - border-top: 1px solid #5e6475; - margin-top: 5px; - padding-top: 5px; + border-top: 1px solid #5e6475; + margin-top: 5px; + padding-top: 5px; } .resource-menu .menu .current a { - color: #7bc876; + color: #7bc876; } .resource-menu .menu .header { - border-bottom: 1px solid #5e6475; - padding: 5px 4px; - overflow: auto; - font-size: 13px; - font-weight: bold; - cursor: pointer; - margin-bottom: 5px; + border-bottom: 1px solid #5e6475; + padding: 5px 4px; + overflow: auto; + font-size: 13px; + font-weight: bold; + cursor: pointer; + margin-bottom: 5px; } .resource-menu .menu .header .resource { - float: left; + float: left; } .resource-menu .menu .header .progress { - float: right; + float: right; } .resource-menu .menu .header .icon { - margin: 1px 5px; + margin: 1px 5px; } diff --git a/translate/src/core/resource/components/ResourceMenu.test.js b/translate/src/core/resource/components/ResourceMenu.test.js index fcee7e253..79b32b210 100644 --- a/translate/src/core/resource/components/ResourceMenu.test.js +++ b/translate/src/core/resource/components/ResourceMenu.test.js @@ -6,128 +6,128 @@ import ResourceItem from './ResourceItem'; import ResourceMenuBase, { ResourceMenu } from './ResourceMenu'; function createShallowResourceMenu({ - project = 'project', - resource = 'path/to.file', + project = 'project', + resource = 'path/to.file', } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders resource menu correctly', () => { - const wrapper = createShallowResourceMenu(); + it('renders resource menu correctly', () => { + const wrapper = createShallowResourceMenu(); - expect(wrapper.find('.menu .search-wrapper')).toHaveLength(1); - expect(wrapper.find('.menu > ul')).toHaveLength(2); - expect(wrapper.find('.menu > ul').find(ResourceItem)).toHaveLength(3); - expect(wrapper.find('.menu .static-links')).toHaveLength(1); - expect( - wrapper.find('.menu #resource-ResourceMenu--all-resources'), - ).toHaveLength(1); - expect( - wrapper.find('.menu #resource-ResourceMenu--all-projects'), - ).toHaveLength(1); - }); + expect(wrapper.find('.menu .search-wrapper')).toHaveLength(1); + expect(wrapper.find('.menu > ul')).toHaveLength(2); + expect(wrapper.find('.menu > ul').find(ResourceItem)).toHaveLength(3); + expect(wrapper.find('.menu .static-links')).toHaveLength(1); + expect( + wrapper.find('.menu #resource-ResourceMenu--all-resources'), + ).toHaveLength(1); + expect( + wrapper.find('.menu #resource-ResourceMenu--all-projects'), + ).toHaveLength(1); + }); - it('searches resource items correctly', () => { - const SEARCH = 'bc'; - const wrapper = createShallowResourceMenu(); - wrapper - .find('.menu .search-wrapper input') - .simulate('change', { currentTarget: { value: SEARCH } }); + it('searches resource items correctly', () => { + const SEARCH = 'bc'; + const wrapper = createShallowResourceMenu(); + wrapper + .find('.menu .search-wrapper input') + .simulate('change', { currentTarget: { value: SEARCH } }); - expect( - wrapper.find('.menu .search-wrapper input').prop('value'), - ).toEqual(SEARCH); - expect(wrapper.find('.menu > ul').find(ResourceItem)).toHaveLength(2); - }); + expect(wrapper.find('.menu .search-wrapper input').prop('value')).toEqual( + SEARCH, + ); + expect(wrapper.find('.menu > ul').find(ResourceItem)).toHaveLength(2); + }); }); function createShallowResourceMenuBase({ - project = 'project', - resource = 'path/to.file', + project = 'project', + resource = 'path/to.file', } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('hides resource selector for all-projects', () => { - const wrapper = createShallowResourceMenuBase({ - project: 'all-projects', - }); - - expect(wrapper.find('.resource-menu .selector')).toHaveLength(0); + it('hides resource selector for all-projects', () => { + const wrapper = createShallowResourceMenuBase({ + project: 'all-projects', }); - it('renders resource selector correctly', () => { - const wrapper = createShallowResourceMenuBase(); + expect(wrapper.find('.resource-menu .selector')).toHaveLength(0); + }); - expect(wrapper.find('.resource-menu .selector')).toHaveLength(1); - expect(wrapper.find('.resource-menu .selector').prop('title')).toEqual( - 'path/to.file', - ); - expect( - wrapper.find('.resource-menu .selector span:first-child').text(), - ).toEqual('to.file'); - expect(wrapper.find('.resource-menu .selector .icon')).toHaveLength(1); + it('renders resource selector correctly', () => { + const wrapper = createShallowResourceMenuBase(); + + expect(wrapper.find('.resource-menu .selector')).toHaveLength(1); + expect(wrapper.find('.resource-menu .selector').prop('title')).toEqual( + 'path/to.file', + ); + expect( + wrapper.find('.resource-menu .selector span:first-child').text(), + ).toEqual('to.file'); + expect(wrapper.find('.resource-menu .selector .icon')).toHaveLength(1); + }); + + it('sets a localized resource name correctly for all-resources', () => { + const wrapper = createShallowResourceMenuBase({ + resource: 'all-resources', }); - it('sets a localized resource name correctly for all-resources', () => { - const wrapper = createShallowResourceMenuBase({ - resource: 'all-resources', - }); + expect(wrapper.find('#resource-ResourceMenu--all-resources')).toHaveLength( + 1, + ); + }); - expect( - wrapper.find('#resource-ResourceMenu--all-resources'), - ).toHaveLength(1); - }); + it('renders resource menu correctly', () => { + const wrapper = createShallowResourceMenuBase(); - it('renders resource menu correctly', () => { - const wrapper = createShallowResourceMenuBase(); - - expect(wrapper.find('ResourceMenu')).toHaveLength(0); - wrapper.find('.selector').simulate('click'); - expect(wrapper.find('ResourceMenu')).toHaveLength(1); - }); + expect(wrapper.find('ResourceMenu')).toHaveLength(0); + wrapper.find('.selector').simulate('click'); + expect(wrapper.find('ResourceMenu')).toHaveLength(1); + }); }); diff --git a/translate/src/core/resource/components/ResourceMenu.tsx b/translate/src/core/resource/components/ResourceMenu.tsx index e7cef936e..604dad669 100644 --- a/translate/src/core/resource/components/ResourceMenu.tsx +++ b/translate/src/core/resource/components/ResourceMenu.tsx @@ -13,192 +13,188 @@ import type { ResourcesState } from '..'; import type { Resource } from '../actions'; type Props = { - parameters: NavigationParams; - resources: ResourcesState; - navigateToPath: (path: string) => void; + parameters: NavigationParams; + resources: ResourcesState; + navigateToPath: (path: string) => void; }; type State = { - visible: boolean; + visible: boolean; }; type ResourceMenuProps = { - parameters: NavigationParams; - resources: ResourcesState; - onDiscard: () => void; - onNavigate: (e: React.MouseEvent) => void; + parameters: NavigationParams; + resources: ResourcesState; + onDiscard: () => void; + onNavigate: (e: React.MouseEvent) => void; }; export function ResourceMenu({ - parameters, - resources, - onDiscard, - onNavigate, + parameters, + resources, + onDiscard, + onNavigate, }: ResourceMenuProps): React.ReactElement<'div'> { - // Searching - const [search, setSearch] = React.useState(''); - const resourceElements = resources.resources.filter( - (resource) => - resource.path.toLowerCase().indexOf(search.toLowerCase()) > -1, - ); + // Searching + const [search, setSearch] = React.useState(''); + const resourceElements = resources.resources.filter( + (resource) => + resource.path.toLowerCase().indexOf(search.toLowerCase()) > -1, + ); - const updateResourceList = (e: React.SyntheticEvent) => { - setSearch(e.currentTarget.value); - }; + const updateResourceList = (e: React.SyntheticEvent) => { + setSearch(e.currentTarget.value); + }; - // Sorting - const [sortActive, setSortActive] = React.useState('resource'); - const [sortAsc, setSortAsc] = React.useState(true); + // Sorting + const [sortActive, setSortActive] = React.useState('resource'); + const [sortAsc, setSortAsc] = React.useState(true); - const sortByResource = () => { - setSortActive('resource'); - setSortAsc(sortActive !== 'resource' || !sortAsc); - }; - const sortByProgress = () => { - setSortActive('progress'); - setSortAsc(sortActive !== 'progress' || !sortAsc); - }; + const sortByResource = () => { + setSortActive('resource'); + setSortAsc(sortActive !== 'resource' || !sortAsc); + }; + const sortByProgress = () => { + setSortActive('progress'); + setSortAsc(sortActive !== 'progress' || !sortAsc); + }; - const getProgress = (res: Resource) => { - const completeStrings = res.approvedStrings + res.stringsWithWarnings; - const percent = Math.floor((completeStrings / res.totalStrings) * 100); - return percent; - }; + const getProgress = (res: Resource) => { + const completeStrings = res.approvedStrings + res.stringsWithWarnings; + const percent = Math.floor((completeStrings / res.totalStrings) * 100); + return percent; + }; - const getResource = (res: Resource) => { - return res.path; - }; + const getResource = (res: Resource) => { + return res.path; + }; - const sort = sortAsc ? 'fa fa-caret-up' : 'fa fa-caret-down'; - const resourceClass = sortActive === 'resource' ? sort : ''; - const progressClass = sortActive === 'progress' ? sort : ''; + const sort = sortAsc ? 'fa fa-caret-up' : 'fa fa-caret-down'; + const resourceClass = sortActive === 'resource' ? sort : ''; + const progressClass = sortActive === 'progress' ? sort : ''; - // Discarding menu - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); + // Discarding menu + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); - return ( -
    -
    -
    - - - -
    + return ( +
    +
    +
    + + + +
    -
    - - - RESOURCE - - - - - - PROGRESS - - - -
    +
    + + + RESOURCE + + + + + + PROGRESS + + + +
    -
      - {resourceElements.length ? ( - (sortActive === 'resource' - ? resourceElements.sort((a, b) => { - const resourceA = getResource(a); - const resourceB = getResource(b); +
        + {resourceElements.length ? ( + (sortActive === 'resource' + ? resourceElements.sort((a, b) => { + const resourceA = getResource(a); + const resourceB = getResource(b); - let result = 0; + let result = 0; - if (resourceA < resourceB) { - result = -1; - } - if (resourceA > resourceB) { - result = 1; - } + if (resourceA < resourceB) { + result = -1; + } + if (resourceA > resourceB) { + result = 1; + } - return sortAsc ? result : result * -1; - }) - : resourceElements.sort((a, b) => { - const percentA = getProgress(a); - const percentB = getProgress(b); + return sortAsc ? result : result * -1; + }) + : resourceElements.sort((a, b) => { + const percentA = getProgress(a); + const percentB = getProgress(b); - let result = 0; + let result = 0; - if (percentA < percentB) { - result = -1; - } - if (percentA > percentB) { - result = 1; - } + if (percentA < percentB) { + result = -1; + } + if (percentA > percentB) { + result = 1; + } - return sortAsc ? result : result * -1; - }) - ).map((resource, index) => { - return ( - - ); - }) - ) : ( - // No resources found - -
      • No results
      • -
        - )} -
      + return sortAsc ? result : result * -1; + }) + ).map((resource, index) => { + return ( + + ); + }) + ) : ( + // No resources found + +
    • No results
    • +
      + )} +
    - -
    - ); + +
    + ); } /** @@ -207,82 +203,82 @@ export function ResourceMenu({ * Allows to switch between resources without reloading the Translate app. */ export default class ResourceMenuBase extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - visible: false, - }; + constructor(props: Props) { + super(props); + this.state = { + visible: false, + }; + } + + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; + + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; + + navigateToPath: (event: React.MouseEvent) => void = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + const path = event.currentTarget.pathname; + this.props.navigateToPath(path); + + this.setState({ + visible: false, + }); + }; + + render(): null | React.ReactElement<'li'> { + const { parameters, resources } = this.props; + + if (parameters.project === 'all-projects') { + return null; } - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); - }; - - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; - - navigateToPath: (event: React.MouseEvent) => void = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - - const path = event.currentTarget.pathname; - this.props.navigateToPath(path); - - this.setState({ - visible: false, - }); - }; - - render(): null | React.ReactElement<'li'> { - const { parameters, resources } = this.props; - - if (parameters.project === 'all-projects') { - return null; - } - - let className = 'resource-menu'; - if (!this.state.visible) { - className += ' closed'; - } - - let resourceName: string | React.ReactElement = parameters.resource - .split('/') - .slice(-1)[0]; - - if (parameters.resource === 'all-resources') { - resourceName = ( - - All Resources - - ); - } - - return ( -
  • -
    - {resourceName} - -
    - - {this.state.visible && ( - - )} -
  • - ); + let className = 'resource-menu'; + if (!this.state.visible) { + className += ' closed'; } + + let resourceName: string | React.ReactElement = parameters.resource + .split('/') + .slice(-1)[0]; + + if (parameters.resource === 'all-resources') { + resourceName = ( + + All Resources + + ); + } + + return ( +
  • +
    + {resourceName} + +
    + + {this.state.visible && ( + + )} +
  • + ); + } } diff --git a/translate/src/core/resource/components/ResourcePercent.css b/translate/src/core/resource/components/ResourcePercent.css index 115cb6c14..6cb827c92 100644 --- a/translate/src/core/resource/components/ResourcePercent.css +++ b/translate/src/core/resource/components/ResourcePercent.css @@ -1,4 +1,4 @@ .resource-menu .menu .percent { - color: #7bc876; - float: right; + color: #7bc876; + float: right; } diff --git a/translate/src/core/resource/components/ResourcePercent.test.js b/translate/src/core/resource/components/ResourcePercent.test.js index a7cde0ea2..611d0e203 100644 --- a/translate/src/core/resource/components/ResourcePercent.test.js +++ b/translate/src/core/resource/components/ResourcePercent.test.js @@ -4,14 +4,14 @@ import { shallow } from 'enzyme'; import ResourcePercent from './ResourcePercent'; describe('', () => { - const RESOURCE = { - approvedStrings: 2, - stringsWithWarnings: 3, - totalStrings: 10, - }; + const RESOURCE = { + approvedStrings: 2, + stringsWithWarnings: 3, + totalStrings: 10, + }; - it('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper.find('.percent').text()).toEqual('50%'); - }); + it('renders correctly', () => { + const wrapper = shallow(); + expect(wrapper.find('.percent').text()).toEqual('50%'); + }); }); diff --git a/translate/src/core/resource/components/ResourcePercent.tsx b/translate/src/core/resource/components/ResourcePercent.tsx index 61b149be3..6557d7c4c 100644 --- a/translate/src/core/resource/components/ResourcePercent.tsx +++ b/translate/src/core/resource/components/ResourcePercent.tsx @@ -5,20 +5,19 @@ import './ResourcePercent.css'; import type { Resource } from '..'; type Props = { - resource: Resource; + resource: Resource; }; /** * Render a resource item percentage. */ export default function ResourcePercent( - props: Props, + props: Props, ): React.ReactElement<'span'> { - const { approvedStrings, stringsWithWarnings, totalStrings } = - props.resource; - const completeStrings = approvedStrings + stringsWithWarnings; + const { approvedStrings, stringsWithWarnings, totalStrings } = props.resource; + const completeStrings = approvedStrings + stringsWithWarnings; - const percent = Math.floor((completeStrings / totalStrings) * 100) + '%'; + const percent = Math.floor((completeStrings / totalStrings) * 100) + '%'; - return {percent}; + return {percent}; } diff --git a/translate/src/core/resource/reducer.test.js b/translate/src/core/resource/reducer.test.js index f78fc5d3c..e683966b5 100644 --- a/translate/src/core/resource/reducer.test.js +++ b/translate/src/core/resource/reducer.test.js @@ -2,89 +2,89 @@ import reducer from './reducer'; import { RECEIVE, UPDATE } from './actions'; describe('reducer', () => { - const RESOURCES = [ - { - path: 'path/to.file', - approvedStrings: 1, - stringsWithWarnings: 1, - totalStrings: 2, - }, - ]; - const ALL_RESOURCES = { - path: [], - approvedStrings: 1, - stringsWithWarnings: 1, - totalStrings: 2, + const RESOURCES = [ + { + path: 'path/to.file', + approvedStrings: 1, + stringsWithWarnings: 1, + totalStrings: 2, + }, + ]; + const ALL_RESOURCES = { + path: [], + approvedStrings: 1, + stringsWithWarnings: 1, + totalStrings: 2, + }; + + it('returns the initial state', () => { + const res = reducer(undefined, {}); + + const expected = { + resources: [], + allResources: { + path: 'all-resources', + approvedStrings: 0, + stringsWithWarnings: 0, + totalStrings: 0, + }, }; - it('returns the initial state', () => { - const res = reducer(undefined, {}); + expect(res).toEqual(expected); + }); - const expected = { - resources: [], - allResources: { - path: 'all-resources', - approvedStrings: 0, - stringsWithWarnings: 0, - totalStrings: 0, - }, - }; + it('handles the RECEIVE action', () => { + const res = reducer( + {}, + { + type: RECEIVE, + resources: RESOURCES, + allResources: ALL_RESOURCES, + }, + ); - expect(res).toEqual(expected); - }); + const expected = { + resources: RESOURCES, + allResources: ALL_RESOURCES, + }; - it('handles the RECEIVE action', () => { - const res = reducer( - {}, - { - type: RECEIVE, - resources: RESOURCES, - allResources: ALL_RESOURCES, - }, - ); + expect(res).toEqual(expected); + }); - const expected = { - resources: RESOURCES, - allResources: ALL_RESOURCES, - }; + it('handles the UPDATE action', () => { + const UPDATED_RESOURCES = [ + { + path: 'path/to.file', + approvedStrings: 2, + stringsWithWarnings: 0, + totalStrings: 2, + }, + ]; + const UPDATED_ALL_RESOURCES = { + path: [], + approvedStrings: 2, + stringsWithWarnings: 0, + totalStrings: 2, + }; - expect(res).toEqual(expected); - }); + const res = reducer( + { + resources: RESOURCES, + allResources: ALL_RESOURCES, + }, + { + type: UPDATE, + resourcePath: 'path/to.file', + approvedStrings: 2, + stringsWithWarnings: 0, + }, + ); - it('handles the UPDATE action', () => { - const UPDATED_RESOURCES = [ - { - path: 'path/to.file', - approvedStrings: 2, - stringsWithWarnings: 0, - totalStrings: 2, - }, - ]; - const UPDATED_ALL_RESOURCES = { - path: [], - approvedStrings: 2, - stringsWithWarnings: 0, - totalStrings: 2, - }; + const expected = { + resources: UPDATED_RESOURCES, + allResources: UPDATED_ALL_RESOURCES, + }; - const res = reducer( - { - resources: RESOURCES, - allResources: ALL_RESOURCES, - }, - { - type: UPDATE, - resourcePath: 'path/to.file', - approvedStrings: 2, - stringsWithWarnings: 0, - }, - ); - - const expected = { - resources: UPDATED_RESOURCES, - allResources: UPDATED_ALL_RESOURCES, - }; - - expect(res).toEqual(expected); - }); + expect(res).toEqual(expected); + }); }); diff --git a/translate/src/core/resource/reducer.ts b/translate/src/core/resource/reducer.ts index bd7a109e8..389c397c0 100644 --- a/translate/src/core/resource/reducer.ts +++ b/translate/src/core/resource/reducer.ts @@ -5,94 +5,93 @@ import type { Resource, ReceiveAction, UpdateAction } from './actions'; type Action = ReceiveAction | UpdateAction; export type ResourcesState = { - readonly resources: Array; - readonly allResources: Resource; + readonly resources: Array; + readonly allResources: Resource; }; function updateResource( - resources: Array, - resourcePath: string, - approvedStrings: number, - stringsWithWarnings: number, + resources: Array, + resourcePath: string, + approvedStrings: number, + stringsWithWarnings: number, ): Array { - return resources.map((item) => { - if (item.path === resourcePath) { - return { - ...item, - approvedStrings, - stringsWithWarnings, - }; - } else { - return item; - } - }); + return resources.map((item) => { + if (item.path === resourcePath) { + return { + ...item, + approvedStrings, + stringsWithWarnings, + }; + } else { + return item; + } + }); } function updateAllResources( - state: ResourcesState, - resourcePath: string, - approvedStrings: number, - stringsWithWarnings: number, + state: ResourcesState, + resourcePath: string, + approvedStrings: number, + stringsWithWarnings: number, ): Resource { - const updatedResource = state.resources.find( - (item) => item.path === resourcePath, - ); + const updatedResource = state.resources.find( + (item) => item.path === resourcePath, + ); - // That can happen in All Projects view - if (!updatedResource) { - return state.allResources; - } + // That can happen in All Projects view + if (!updatedResource) { + return state.allResources; + } - const diffApproved = approvedStrings - updatedResource.approvedStrings; - const diffWarnings = - stringsWithWarnings - updatedResource.stringsWithWarnings; + const diffApproved = approvedStrings - updatedResource.approvedStrings; + const diffWarnings = + stringsWithWarnings - updatedResource.stringsWithWarnings; - return { - ...state.allResources, - approvedStrings: state.allResources.approvedStrings + diffApproved, - stringsWithWarnings: - state.allResources.stringsWithWarnings + diffWarnings, - }; + return { + ...state.allResources, + approvedStrings: state.allResources.approvedStrings + diffApproved, + stringsWithWarnings: state.allResources.stringsWithWarnings + diffWarnings, + }; } const initial: ResourcesState = { - resources: [], - allResources: { - path: 'all-resources', - approvedStrings: 0, - stringsWithWarnings: 0, - totalStrings: 0, - }, + resources: [], + allResources: { + path: 'all-resources', + approvedStrings: 0, + stringsWithWarnings: 0, + totalStrings: 0, + }, }; export default function reducer( - state: ResourcesState = initial, - action: Action, + state: ResourcesState = initial, + action: Action, ): ResourcesState { - switch (action.type) { - case RECEIVE: - return { - ...state, - resources: action.resources, - allResources: action.allResources, - }; - case UPDATE: - return { - ...state, - resources: updateResource( - state.resources, - action.resourcePath, - action.approvedStrings, - action.stringsWithWarnings, - ), - allResources: updateAllResources( - state, - action.resourcePath, - action.approvedStrings, - action.stringsWithWarnings, - ), - }; - default: - return state; - } + switch (action.type) { + case RECEIVE: + return { + ...state, + resources: action.resources, + allResources: action.allResources, + }; + case UPDATE: + return { + ...state, + resources: updateResource( + state.resources, + action.resourcePath, + action.approvedStrings, + action.stringsWithWarnings, + ), + allResources: updateAllResources( + state, + action.resourcePath, + action.approvedStrings, + action.stringsWithWarnings, + ), + }; + default: + return state; + } } diff --git a/translate/src/core/stats/actions.ts b/translate/src/core/stats/actions.ts index 308d46052..a780961ea 100644 --- a/translate/src/core/stats/actions.ts +++ b/translate/src/core/stats/actions.ts @@ -1,38 +1,38 @@ export const UPDATE: 'stats/UPDATE' = 'stats/UPDATE'; export type APIStats = { - approved: number; - fuzzy: number; - warnings: number; - errors: number; - unreviewed: number; - total: number; + approved: number; + fuzzy: number; + warnings: number; + errors: number; + unreviewed: number; + total: number; }; export type Stats = APIStats & { - missing: number; + missing: number; }; export type UpdateAction = { - readonly type: typeof UPDATE; - readonly stats: Stats; + readonly type: typeof UPDATE; + readonly stats: Stats; }; export function update(stats: APIStats): UpdateAction { - const newStats: Stats = { - ...stats, - missing: - stats.total - - stats.approved - - stats.fuzzy - - stats.errors - - stats.warnings, - }; - return { - type: UPDATE, - stats: newStats, - }; + const newStats: Stats = { + ...stats, + missing: + stats.total - + stats.approved - + stats.fuzzy - + stats.errors - + stats.warnings, + }; + return { + type: UPDATE, + stats: newStats, + }; } export default { - update, + update, }; diff --git a/translate/src/core/stats/reducer.ts b/translate/src/core/stats/reducer.ts index bad88f9be..ba7231388 100644 --- a/translate/src/core/stats/reducer.ts +++ b/translate/src/core/stats/reducer.ts @@ -5,22 +5,22 @@ import type { UpdateAction, Stats } from './actions'; type Action = UpdateAction; const initial: Stats = { - approved: 0, - fuzzy: 0, - warnings: 0, - errors: 0, - missing: 0, - unreviewed: 0, - total: 0, + approved: 0, + fuzzy: 0, + warnings: 0, + errors: 0, + missing: 0, + unreviewed: 0, + total: 0, }; export default function reducer(state: Stats = initial, action: Action): Stats { - switch (action.type) { - case UPDATE: - return { - ...action.stats, - }; - default: - return state; - } + switch (action.type) { + case UPDATE: + return { + ...action.stats, + }; + default: + return state; + } } diff --git a/translate/src/core/term/actions.ts b/translate/src/core/term/actions.ts index 1cd451215..3a5c388d7 100644 --- a/translate/src/core/term/actions.ts +++ b/translate/src/core/term/actions.ts @@ -9,48 +9,48 @@ export const RECEIVE: 'terms/RECEIVE' = 'terms/RECEIVE'; export const REQUEST: 'terms/REQUEST' = 'terms/REQUEST'; export type ReceiveAction = { - readonly type: typeof RECEIVE; - readonly terms: Array; + readonly type: typeof RECEIVE; + readonly terms: Array; }; export function receive(terms: Array): ReceiveAction { - return { - type: RECEIVE, - terms, - }; + return { + type: RECEIVE, + terms, + }; } export type RequestAction = { - readonly type: typeof REQUEST; - readonly sourceString: string; + readonly type: typeof REQUEST; + readonly sourceString: string; }; export function request(sourceString: string): RequestAction { - return { - type: REQUEST, - sourceString, - }; + return { + type: REQUEST, + sourceString, + }; } export function get(sourceString: string, locale: string) { - return async (dispatch: AppDispatch) => { - dispatch(request(sourceString)); + return async (dispatch: AppDispatch) => { + dispatch(request(sourceString)); - // Abort all previously running requests. - await api.entity.abort(); + // Abort all previously running requests. + await api.entity.abort(); - let content = await api.entity.getTerms(sourceString, locale); + let content = await api.entity.getTerms(sourceString, locale); - // The default return value of aborted requests is {}, - // which is incompatible with reducer - if (isEmpty(content)) { - content = []; - } + // The default return value of aborted requests is {}, + // which is incompatible with reducer + if (isEmpty(content)) { + content = []; + } - dispatch(receive(content)); - }; + dispatch(receive(content)); + }; } export default { - get, - receive, - request, + get, + receive, + request, }; diff --git a/translate/src/core/term/components/Term.css b/translate/src/core/term/components/Term.css index 1c80d06c0..bc931e7c0 100644 --- a/translate/src/core/term/components/Term.css +++ b/translate/src/core/term/components/Term.css @@ -1,56 +1,56 @@ .terms-list .term { - border-bottom: 1px solid #5e6475; - cursor: pointer; - padding: 10px; + border-bottom: 1px solid #5e6475; + cursor: pointer; + padding: 10px; } .terms-list .term.cannot-copy { - pointer-events: none; + pointer-events: none; } .terms-list .term:not(.cannot-copy):hover { - background: #333941; - border-color: #5e6475; + background: #333941; + border-color: #5e6475; } .terms-list .term .text { - color: #aaa; + color: #aaa; } .terms-list .term .part-of-speech { - color: #aaa; - font-size: 11px; - font-weight: 300; - padding-left: 3px; - text-transform: uppercase; + color: #aaa; + font-size: 11px; + font-weight: 300; + padding-left: 3px; + text-transform: uppercase; } .terms-list .term .part-of-speech::before { - content: '•'; - padding-right: 3px; + content: '•'; + padding-right: 3px; } .terms-list .term .translate { - color: #7bc876; - float: right; - font-size: 11px; - font-weight: 300; - pointer-events: auto; - text-transform: uppercase; + color: #7bc876; + float: right; + font-size: 11px; + font-weight: 300; + pointer-events: auto; + text-transform: uppercase; } .terms-list .term .translation { - color: #ebebeb; + color: #ebebeb; } .terms-list .term .details { - color: #aaa; - font-style: italic; - padding: 5px 0 0 15px; + color: #aaa; + font-style: italic; + padding: 5px 0 0 15px; } .terms-list .term .usage .title { - color: #7bc876; - font-size: 11px; - margin-right: 3px; + color: #7bc876; + font-size: 11px; + margin-right: 3px; } diff --git a/translate/src/core/term/components/Term.test.js b/translate/src/core/term/components/Term.test.js index 818376778..fa4169933 100644 --- a/translate/src/core/term/components/Term.test.js +++ b/translate/src/core/term/components/Term.test.js @@ -5,84 +5,84 @@ import sinon from 'sinon'; import Term from './Term'; describe('', () => { - const TERM = { - text: 'text', - partOfSpeech: 'partOfSpeech', - definition: 'definition', - usage: 'usage', - translation: 'translation', + const TERM = { + text: 'text', + partOfSpeech: 'partOfSpeech', + definition: 'definition', + usage: 'usage', + translation: 'translation', + }; + + let getSelectionBackup; + + beforeAll(() => { + getSelectionBackup = window.getSelection; + window.getSelection = () => { + return { + toString: () => {}, + }; }; + }); - let getSelectionBackup; + afterAll(() => { + window.getSelection = getSelectionBackup; + }); - beforeAll(() => { - getSelectionBackup = window.getSelection; - window.getSelection = () => { - return { - toString: () => {}, - }; - }; - }); + it('renders term correctly', () => { + const wrapper = shallow(); - afterAll(() => { - window.getSelection = getSelectionBackup; - }); + expect(wrapper.find('li')).toHaveLength(1); + expect(wrapper.find('.text').text()).toEqual('text'); + expect(wrapper.find('.part-of-speech').text()).toEqual('partOfSpeech'); + expect(wrapper.find('.definition').text()).toEqual('definition'); + expect(wrapper.find('.usage .content').text()).toEqual('usage'); + expect(wrapper.find('.translation').text()).toEqual('translation'); + }); - it('renders term correctly', () => { - const wrapper = shallow(); + it('calls the addTextToEditorTranslation function on click', () => { + const addTextToEditorTranslationFn = sinon.spy(); - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('.text').text()).toEqual('text'); - expect(wrapper.find('.part-of-speech').text()).toEqual('partOfSpeech'); - expect(wrapper.find('.definition').text()).toEqual('definition'); - expect(wrapper.find('.usage .content').text()).toEqual('usage'); - expect(wrapper.find('.translation').text()).toEqual('translation'); - }); + const wrapper = shallow( + , + ); - it('calls the addTextToEditorTranslation function on click', () => { - const addTextToEditorTranslationFn = sinon.spy(); + wrapper.find('li').simulate('click'); + expect(addTextToEditorTranslationFn.called).toEqual(true); + }); - const wrapper = shallow( - , - ); + it('does not call the addTextToEditorTranslation function if term not translated', () => { + const term = { + ...TERM, + translation: '', + }; + const addTextToEditorTranslationFn = sinon.spy(); - wrapper.find('li').simulate('click'); - expect(addTextToEditorTranslationFn.called).toEqual(true); - }); + const wrapper = shallow( + , + ); - it('does not call the addTextToEditorTranslation function if term not translated', () => { - const term = { - ...TERM, - translation: '', - }; - const addTextToEditorTranslationFn = sinon.spy(); + wrapper.find('li').simulate('click'); + expect(addTextToEditorTranslationFn.called).toEqual(false); + }); - const wrapper = shallow( - , - ); + it('does not call the addTextToEditorTranslation function if read-only editor', () => { + const addTextToEditorTranslationFn = sinon.spy(); - wrapper.find('li').simulate('click'); - expect(addTextToEditorTranslationFn.called).toEqual(false); - }); + const wrapper = shallow( + , + ); - it('does not call the addTextToEditorTranslation function if read-only editor', () => { - const addTextToEditorTranslationFn = sinon.spy(); - - const wrapper = shallow( - , - ); - - wrapper.find('li').simulate('click'); - expect(addTextToEditorTranslationFn.called).toEqual(false); - }); + wrapper.find('li').simulate('click'); + expect(addTextToEditorTranslationFn.called).toEqual(false); + }); }); diff --git a/translate/src/core/term/components/Term.tsx b/translate/src/core/term/components/Term.tsx index 632aa9b38..6783f44f5 100644 --- a/translate/src/core/term/components/Term.tsx +++ b/translate/src/core/term/components/Term.tsx @@ -6,82 +6,81 @@ import './Term.css'; import type { TermType } from '~/core/api'; type Props = { - isReadOnlyEditor: boolean; - locale: string; - term: TermType; - addTextToEditorTranslation: (arg0: string) => void; - navigateToPath: (arg0: string) => void; + isReadOnlyEditor: boolean; + locale: string; + term: TermType; + addTextToEditorTranslation: (arg0: string) => void; + navigateToPath: (arg0: string) => void; }; /** * Shows term entry with its metadata. */ export default function Term( - props: Props, + props: Props, ): React.ReactElement { - const { isReadOnlyEditor, locale, term } = props; + const { isReadOnlyEditor, locale, term } = props; - const copyTermIntoEditor = (translation: string) => { - if (isReadOnlyEditor) { - return; - } + const copyTermIntoEditor = (translation: string) => { + if (isReadOnlyEditor) { + return; + } - // Ignore if term not translated - if (!translation) { - return; - } + // Ignore if term not translated + if (!translation) { + return; + } - // Ignore if selecting text - if (window.getSelection().toString()) { - return; - } + // Ignore if selecting text + if (window.getSelection().toString()) { + return; + } - props.addTextToEditorTranslation(translation); - }; + props.addTextToEditorTranslation(translation); + }; - const navigateToPath = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); + const navigateToPath = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); - const path = event.currentTarget.pathname; - props.navigateToPath(path); - }; + const path = event.currentTarget.pathname; + props.navigateToPath(path); + }; - // Copying into the editor is not allowed - const cannotCopy = - isReadOnlyEditor || !term.translation ? 'cannot-copy' : ''; + // Copying into the editor is not allowed + const cannotCopy = isReadOnlyEditor || !term.translation ? 'cannot-copy' : ''; - return ( - -
  • copyTermIntoEditor(term.translation)} - > -
    - {term.text} - {term.partOfSpeech} - - Translate - -
    -

    {term.translation}

    -
    -

    {term.definition}

    - {!term.usage ? null : ( -

    - - E.G. - - {term.usage} -

    - )} -
    -
  • -
    - ); + return ( + +
  • copyTermIntoEditor(term.translation)} + > +
    + {term.text} + {term.partOfSpeech} + + Translate + +
    +

    {term.translation}

    +
    +

    {term.definition}

    + {!term.usage ? null : ( +

    + + E.G. + + {term.usage} +

    + )} +
    +
  • +
    + ); } diff --git a/translate/src/core/term/components/TermsList.css b/translate/src/core/term/components/TermsList.css index f490095c0..b448ed4b3 100644 --- a/translate/src/core/term/components/TermsList.css +++ b/translate/src/core/term/components/TermsList.css @@ -1,5 +1,5 @@ .terms-list { - line-height: 22px; - list-style: none; - margin-left: 0; + line-height: 22px; + list-style: none; + margin-left: 0; } diff --git a/translate/src/core/term/components/TermsList.test.js b/translate/src/core/term/components/TermsList.test.js index dfd6e0fcd..46045caa7 100644 --- a/translate/src/core/term/components/TermsList.test.js +++ b/translate/src/core/term/components/TermsList.test.js @@ -5,21 +5,21 @@ import Term from './Term'; import TermsList from './TermsList'; describe('', () => { - const TERMS = [ - { - text: 'text1', - }, - { - text: 'text2', - }, - { - text: 'text3', - }, - ]; + const TERMS = [ + { + text: 'text1', + }, + { + text: 'text2', + }, + { + text: 'text3', + }, + ]; - it('renders list of terms correctly', () => { - const wrapper = shallow(); + it('renders list of terms correctly', () => { + const wrapper = shallow(); - expect(wrapper.find(Term)).toHaveLength(3); - }); + expect(wrapper.find(Term)).toHaveLength(3); + }); }); diff --git a/translate/src/core/term/components/TermsList.tsx b/translate/src/core/term/components/TermsList.tsx index 004e86013..878cf1328 100644 --- a/translate/src/core/term/components/TermsList.tsx +++ b/translate/src/core/term/components/TermsList.tsx @@ -7,33 +7,31 @@ import Term from './Term'; import type { TermType } from '~/core/api'; type Props = { - isReadOnlyEditor: boolean; - locale: string; - terms: Array; - addTextToEditorTranslation: (arg0: string) => void; - navigateToPath: (arg0: string) => void; + isReadOnlyEditor: boolean; + locale: string; + terms: Array; + addTextToEditorTranslation: (arg0: string) => void; + navigateToPath: (arg0: string) => void; }; /** * Shows a list of terms. */ export default function TermsList(props: Props): React.ReactElement<'ul'> { - return ( -
      - {props.terms.map((term, i) => { - return ( - - ); - })} -
    - ); + return ( +
      + {props.terms.map((term, i) => { + return ( + + ); + })} +
    + ); } diff --git a/translate/src/core/term/getMarker.test.js b/translate/src/core/term/getMarker.test.js index f4c6c67b6..4235ae34b 100644 --- a/translate/src/core/term/getMarker.test.js +++ b/translate/src/core/term/getMarker.test.js @@ -4,113 +4,113 @@ import { shallow } from 'enzyme'; import getMarker from './getMarker'; describe('markTerms', () => { - it('marks terms properly', () => { - const string = 'foo bar baz'; - const terms = { - terms: [ - { - text: 'bar', - }, - { - text: 'baz', - }, - ], - }; + it('marks terms properly', () => { + const string = 'foo bar baz'; + const terms = { + terms: [ + { + text: 'bar', + }, + { + text: 'baz', + }, + ], + }; - const TermsAndPlaceablesMarker = getMarker(terms); - const wrapper = shallow( - {string}, - ); + const TermsAndPlaceablesMarker = getMarker(terms); + const wrapper = shallow( + {string}, + ); - expect(wrapper.find('mark')).toHaveLength(2); - expect(wrapper.find('mark').at(0).text()).toEqual('bar'); - expect(wrapper.find('mark').at(1).text()).toEqual('baz'); - }); + expect(wrapper.find('mark')).toHaveLength(2); + expect(wrapper.find('mark').at(0).text()).toEqual('bar'); + expect(wrapper.find('mark').at(1).text()).toEqual('baz'); + }); - it('marks entire words on partial match', () => { - const string = 'Download Add-Ons from the web.'; - const terms = { - terms: [ - { - text: 'add-on', - }, - ], - }; + it('marks entire words on partial match', () => { + const string = 'Download Add-Ons from the web.'; + const terms = { + terms: [ + { + text: 'add-on', + }, + ], + }; - const TermsAndPlaceablesMarker = getMarker(terms); - const wrapper = shallow( - {string}, - ); + const TermsAndPlaceablesMarker = getMarker(terms); + const wrapper = shallow( + {string}, + ); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('Add-Ons'); - }); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('Add-Ons'); + }); - it('only marks terms at the beginning of the word', () => { - const string = 'Consider using one of the alternatives.'; - const terms = { - terms: [ - { - text: 'native', - }, - ], - }; + it('only marks terms at the beginning of the word', () => { + const string = 'Consider using one of the alternatives.'; + const terms = { + terms: [ + { + text: 'native', + }, + ], + }; - const TermsAndPlaceablesMarker = getMarker(terms); - const wrapper = shallow( - {string}, - ); + const TermsAndPlaceablesMarker = getMarker(terms); + const wrapper = shallow( + {string}, + ); - expect(wrapper.find('mark')).toHaveLength(0); - }); + expect(wrapper.find('mark')).toHaveLength(0); + }); - it('marks longer terms first', () => { - const string = 'This is a translation tool.'; - const terms = { - terms: [ - { - text: 'translation', - }, - { - text: 'translation tool', - }, - ], - }; + it('marks longer terms first', () => { + const string = 'This is a translation tool.'; + const terms = { + terms: [ + { + text: 'translation', + }, + { + text: 'translation tool', + }, + ], + }; - const TermsAndPlaceablesMarker = getMarker(terms); - const wrapper = shallow( - {string}, - ); + const TermsAndPlaceablesMarker = getMarker(terms); + const wrapper = shallow( + {string}, + ); - expect(wrapper.find('mark')).toHaveLength(1); - expect(wrapper.find('mark').text()).toEqual('translation tool'); - }); + expect(wrapper.find('mark')).toHaveLength(1); + expect(wrapper.find('mark').text()).toEqual('translation tool'); + }); - it('does not mark terms within placeables', () => { - const string = - 'This browser { $version } does not support { $bits }-bit systems.'; - const terms = { - terms: [ - { - text: 'browser', - }, - { - text: 'version', - }, - ], - }; + it('does not mark terms within placeables', () => { + const string = + 'This browser { $version } does not support { $bits }-bit systems.'; + const terms = { + terms: [ + { + text: 'browser', + }, + { + text: 'version', + }, + ], + }; - const TermsAndPlaceablesMarker = getMarker(terms); - const wrapper = shallow( - {string}, - ); + const TermsAndPlaceablesMarker = getMarker(terms); + const wrapper = shallow( + {string}, + ); - expect(wrapper.find('mark')).toHaveLength(3); - expect(wrapper.find('mark').at(0).text()).toEqual('browser'); - expect(wrapper.find('mark').at(0).hasClass('term')); - expect(wrapper.find('mark').at(1).text()).toEqual('{ $version }'); - expect(wrapper.find('mark').at(1).hasClass('placeable')); - expect(wrapper.find('mark').at(2).text()).toEqual('{ $bits }'); - expect(wrapper.find('mark').at(2).hasClass('placeable')); - }); + expect(wrapper.find('mark')).toHaveLength(3); + expect(wrapper.find('mark').at(0).text()).toEqual('browser'); + expect(wrapper.find('mark').at(0).hasClass('term')); + expect(wrapper.find('mark').at(1).text()).toEqual('{ $version }'); + expect(wrapper.find('mark').at(1).hasClass('placeable')); + expect(wrapper.find('mark').at(2).text()).toEqual('{ $bits }'); + expect(wrapper.find('mark').at(2).hasClass('placeable')); + }); }); diff --git a/translate/src/core/term/getMarker.tsx b/translate/src/core/term/getMarker.tsx index 68e488de6..25e970b9f 100644 --- a/translate/src/core/term/getMarker.tsx +++ b/translate/src/core/term/getMarker.tsx @@ -3,9 +3,9 @@ import escapeRegExp from 'lodash.escaperegexp'; import createMarker from 'react-content-marker'; import { - getRulesWithFluent, - getRulesWithoutLeadingSpace, - rules, + getRulesWithFluent, + getRulesWithoutLeadingSpace, + rules, } from '~/core/placeable'; import type { TermState } from '~/core/term'; @@ -13,47 +13,43 @@ import type { TermState } from '~/core/term'; let keyCounter = 0; export default function getMarker( - terms: TermState, - forFluent: boolean = false, + terms: TermState, + forFluent: boolean = false, ): any { - let placeableRules = getRulesWithoutLeadingSpace(rules); + let placeableRules = getRulesWithoutLeadingSpace(rules); - if (forFluent) { - placeableRules = getRulesWithFluent(placeableRules); - } + if (forFluent) { + placeableRules = getRulesWithFluent(placeableRules); + } - if (terms.fetching || !terms.terms) { - return createMarker(placeableRules); - } + if (terms.fetching || !terms.terms) { + return createMarker(placeableRules); + } - const newRules = [...placeableRules]; + const newRules = [...placeableRules]; - // Sort terms by length descendingly. That allows us to mark multi-word terms - // when they consist of words that are terms as well. See test case for the example. - const sortedTerms = terms.terms.sort((a, b) => - a.text.length < b.text.length ? 1 : -1, - ); + // Sort terms by length descendingly. That allows us to mark multi-word terms + // when they consist of words that are terms as well. See test case for the example. + const sortedTerms = terms.terms.sort((a, b) => + a.text.length < b.text.length ? 1 : -1, + ); - for (let term of sortedTerms) { - const text = escapeRegExp(term.text); + for (let term of sortedTerms) { + const text = escapeRegExp(term.text); - const termParser = { - rule: new RegExp(`\\b${text}[a-zA-z]*\\b`, 'gi'), - tag: (x: string) => { - return ( - - {x} - - ); - }, - }; + const termParser = { + rule: new RegExp(`\\b${text}[a-zA-z]*\\b`, 'gi'), + tag: (x: string) => { + return ( + + {x} + + ); + }, + }; - newRules.push(termParser); - } + newRules.push(termParser); + } - return createMarker(newRules); + return createMarker(newRules); } diff --git a/translate/src/core/term/reducer.ts b/translate/src/core/term/reducer.ts index 729224e0e..72c643157 100644 --- a/translate/src/core/term/reducer.ts +++ b/translate/src/core/term/reducer.ts @@ -6,36 +6,36 @@ import type { ReceiveAction, RequestAction } from './actions'; type Action = ReceiveAction | RequestAction; export type TermState = { - readonly fetching: boolean; - readonly sourceString: string; - readonly terms: Array; + readonly fetching: boolean; + readonly sourceString: string; + readonly terms: Array; }; const initialState: TermState = { - fetching: false, - sourceString: '', - terms: [], + fetching: false, + sourceString: '', + terms: [], }; export default function reducer( - state: TermState = initialState, - action: Action, + state: TermState = initialState, + action: Action, ): TermState { - switch (action.type) { - case REQUEST: - return { - ...state, - fetching: true, - sourceString: action.sourceString, - terms: [], - }; - case RECEIVE: - return { - ...state, - fetching: false, - terms: action.terms, - }; - default: - return state; - } + switch (action.type) { + case REQUEST: + return { + ...state, + fetching: true, + sourceString: action.sourceString, + terms: [], + }; + case RECEIVE: + return { + ...state, + fetching: false, + terms: action.terms, + }; + default: + return state; + } } diff --git a/translate/src/core/translation/components/FluentTranslation.tsx b/translate/src/core/translation/components/FluentTranslation.tsx index 7e8fc1475..7565c41ce 100644 --- a/translate/src/core/translation/components/FluentTranslation.tsx +++ b/translate/src/core/translation/components/FluentTranslation.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { withDiff } from '~/core/diff'; import { - WithPlaceablesForFluent, - WithPlaceablesForFluentNoLeadingSpace, + WithPlaceablesForFluent, + WithPlaceablesForFluentNoLeadingSpace, } from '~/core/placeable'; import { fluent } from '~/core/utils'; import { withSearch } from '~/modules/search'; @@ -12,37 +12,37 @@ import type { TranslationProps } from './GenericTranslation'; // @ts-ignore: https://github.com/mozilla/pontoon/issues/2294. const TranslationPlaceablesDiff = withDiff( - WithPlaceablesForFluentNoLeadingSpace, + WithPlaceablesForFluentNoLeadingSpace, ); // @ts-ignore: https://github.com/mozilla/pontoon/issues/2294. const TranslationPlaceablesSearch = withSearch( - WithPlaceablesForFluentNoLeadingSpace, + WithPlaceablesForFluentNoLeadingSpace, ); export default function FluentTranslation({ - content, - diffTarget, - search, + content, + diffTarget, + search, }: TranslationProps): React.ReactElement { - const preview = fluent.getSimplePreview(content); + const preview = fluent.getSimplePreview(content); - if (diffTarget) { - const fluentTarget = fluent.getSimplePreview(diffTarget); - return ( - - {preview} - - ); - } + if (diffTarget) { + const fluentTarget = fluent.getSimplePreview(diffTarget); + return ( + + {preview} + + ); + } - if (search) { - return ( - - {preview} - - ); - } + if (search) { + return ( + + {preview} + + ); + } - return {preview}; + return {preview}; } diff --git a/translate/src/core/translation/components/GenericTranslation.tsx b/translate/src/core/translation/components/GenericTranslation.tsx index a86ca604c..06a1fcf91 100644 --- a/translate/src/core/translation/components/GenericTranslation.tsx +++ b/translate/src/core/translation/components/GenericTranslation.tsx @@ -11,31 +11,31 @@ const TranslationPlaceablesDiff = withDiff(WithPlaceablesNoLeadingSpace); const TranslationPlaceablesSearch = withSearch(WithPlaceablesNoLeadingSpace); export type TranslationProps = { - content: string; - diffTarget?: string | null | undefined; - search?: string | null | undefined; + content: string; + diffTarget?: string | null | undefined; + search?: string | null | undefined; }; export default function GenericTranslation({ - content, - diffTarget, - search, + content, + diffTarget, + search, }: TranslationProps): React.ReactElement { - if (diffTarget) { - return ( - - {content} - - ); - } + if (diffTarget) { + return ( + + {content} + + ); + } - if (search) { - return ( - - {content} - - ); - } + if (search) { + return ( + + {content} + + ); + } - return {content}; + return {content}; } diff --git a/translate/src/core/translation/components/TranslationProxy.tsx b/translate/src/core/translation/components/TranslationProxy.tsx index 9dc4acd35..5f0014baf 100644 --- a/translate/src/core/translation/components/TranslationProxy.tsx +++ b/translate/src/core/translation/components/TranslationProxy.tsx @@ -4,21 +4,20 @@ import FluentTranslation from './FluentTranslation'; import GenericTranslation from './GenericTranslation'; type Props = { - content: string | null | undefined; - diffTarget?: string | null | undefined; - format: string; - search?: string | null | undefined; + content: string | null | undefined; + diffTarget?: string | null | undefined; + format: string; + search?: string | null | undefined; }; export default function TranslationProxy({ - format, - ...props + format, + ...props }: Props): null | React.ReactElement { - if (!props.content) { - return null; - } + if (!props.content) { + return null; + } - const Translation = - format === 'ftl' ? FluentTranslation : GenericTranslation; - return ; + const Translation = format === 'ftl' ? FluentTranslation : GenericTranslation; + return ; } diff --git a/translate/src/core/user/actions.ts b/translate/src/core/user/actions.ts index 154782e45..180690be6 100644 --- a/translate/src/core/user/actions.ts +++ b/translate/src/core/user/actions.ts @@ -9,114 +9,114 @@ export const UPDATE: 'user/UPDATE' = 'user/UPDATE'; export const UPDATE_SETTINGS: 'user/UPDATE_SETTINGS' = 'user/UPDATE_SETTINGS'; export type ReceiveAction = { - readonly type: typeof RECEIVE_USERS; - readonly users: Array; + readonly type: typeof RECEIVE_USERS; + readonly users: Array; }; export function receive(users: Array): ReceiveAction { - return { - type: RECEIVE_USERS, - users, - }; + return { + type: RECEIVE_USERS, + users, + }; } /** * Update Interactive Tour status to a given step. */ export function updateTourStatus(step: number): AppThunk { - return async () => { - await api.user.updateTourStatus(step); - }; + return async () => { + await api.user.updateTourStatus(step); + }; } export type Settings = { - runQualityChecks?: boolean; - forceSuggestions?: boolean; + runQualityChecks?: boolean; + forceSuggestions?: boolean; }; /** * Update the user settings. */ export type UpdateSettingsAction = { - readonly type: typeof UPDATE_SETTINGS; - readonly settings: Settings; + readonly type: typeof UPDATE_SETTINGS; + readonly settings: Settings; }; export function updateSettings(settings: Settings): UpdateSettingsAction { - return { - type: UPDATE_SETTINGS, - settings, - }; + return { + type: UPDATE_SETTINGS, + settings, + }; } /** * Update the user data. */ export type UpdateAction = { - readonly type: typeof UPDATE; - readonly data: Record; + readonly type: typeof UPDATE; + readonly data: Record; }; export function update(data: Record): UpdateAction { - return { - type: UPDATE, - data, - }; + return { + type: UPDATE, + data, + }; } /** * Sign out the current user. */ export function signOut(url: string): AppThunk { - return async (dispatch) => { - await api.user.signOut(url); + return async (dispatch) => { + await api.user.signOut(url); - dispatch(get()); - }; + dispatch(get()); + }; } function _getOperationNotif(setting, value) { - if (setting === 'runQualityChecks' && value) { - return notification.messages.CHECKS_ENABLED; - } - if (setting === 'runQualityChecks' && !value) { - return notification.messages.CHECKS_DISABLED; - } - if (setting === 'forceSuggestions' && value) { - return notification.messages.SUGGESTIONS_ENABLED; - } - if (setting === 'forceSuggestions' && !value) { - return notification.messages.SUGGESTIONS_DISABLED; - } + if (setting === 'runQualityChecks' && value) { + return notification.messages.CHECKS_ENABLED; + } + if (setting === 'runQualityChecks' && !value) { + return notification.messages.CHECKS_DISABLED; + } + if (setting === 'forceSuggestions' && value) { + return notification.messages.SUGGESTIONS_ENABLED; + } + if (setting === 'forceSuggestions' && !value) { + return notification.messages.SUGGESTIONS_DISABLED; + } - throw new Error('Unsupported operation on setting: ' + setting); + throw new Error('Unsupported operation on setting: ' + setting); } export function saveSetting( - setting: string, - value: boolean, - username: string, + setting: string, + value: boolean, + username: string, ): AppThunk { - return async (dispatch) => { - dispatch(updateSettings({ [setting]: value })); + return async (dispatch) => { + dispatch(updateSettings({ [setting]: value })); - await api.user.updateSetting(username, setting, value); + await api.user.updateSetting(username, setting, value); - const notif = _getOperationNotif(setting, value); - dispatch(notification.actions.add(notif)); - }; + const notif = _getOperationNotif(setting, value); + dispatch(notification.actions.add(notif)); + }; } export function markAllNotificationsAsRead(): AppThunk { - return async (dispatch) => { - await api.user.markAllNotificationsAsRead(); + return async (dispatch) => { + await api.user.markAllNotificationsAsRead(); - dispatch(get()); - }; + dispatch(get()); + }; } export function getUsers(): AppThunk { - return async (dispatch) => { - const content = await api.user.getUsers(); - dispatch(receive(content)); - }; + return async (dispatch) => { + const content = await api.user.getUsers(); + dispatch(receive(content)); + }; } /** @@ -126,28 +126,28 @@ export function getUsers(): AppThunk { * and if so, get their information and permissions. */ export function get(): AppThunk { - return async (dispatch) => { - const content = await api.user.get(); - dispatch(update(content)); - }; + return async (dispatch) => { + const content = await api.user.get(); + dispatch(update(content)); + }; } export function dismissAddonPromotion(): AppThunk { - return async (dispatch) => { - await api.user.dismissAddonPromotion(); + return async (dispatch) => { + await api.user.dismissAddonPromotion(); - dispatch(get()); - }; + dispatch(get()); + }; } export default { - dismissAddonPromotion, - get, - getUsers, - markAllNotificationsAsRead, - saveSetting, - signOut, - update, - updateSettings, - updateTourStatus, + dismissAddonPromotion, + get, + getUsers, + markAllNotificationsAsRead, + saveSetting, + signOut, + update, + updateSettings, + updateTourStatus, }; diff --git a/translate/src/core/user/components/FileUpload.css b/translate/src/core/user/components/FileUpload.css index 4d7032642..c83f6793e 100644 --- a/translate/src/core/user/components/FileUpload.css +++ b/translate/src/core/user/components/FileUpload.css @@ -1,10 +1,10 @@ .file-upload label { - line-height: 22px; - display: block; + line-height: 22px; + display: block; } .file-upload input[name='uploadfile'] { - position: absolute; - visibility: hidden; - width: 0; + position: absolute; + visibility: hidden; + width: 0; } diff --git a/translate/src/core/user/components/FileUpload.tsx b/translate/src/core/user/components/FileUpload.tsx index 4024c2322..757d66c92 100644 --- a/translate/src/core/user/components/FileUpload.tsx +++ b/translate/src/core/user/components/FileUpload.tsx @@ -6,73 +6,63 @@ import './FileUpload.css'; import type { NavigationParams } from '~/core/navigation'; type Props = { - parameters: NavigationParams; + parameters: NavigationParams; }; /* * Render a File Upload button. */ export default class FileUpload extends React.Component { - uploadForm: { - current: HTMLFormElement | null | undefined; - }; + uploadForm: { + current: HTMLFormElement | null | undefined; + }; - constructor(props: Props) { - super(props); - this.uploadForm = React.createRef(); + constructor(props: Props) { + super(props); + this.uploadForm = React.createRef(); + } + + submitForm: () => void = () => { + const form = this.uploadForm.current; + if (form) { + form.submit(); + } + }; + + render(): React.ReactElement<'form'> { + const { parameters } = this.props; + + /* TODO: Refactor core.api.base and reuse getCSRFToken() here */ + let csrfToken = ''; + const rootElt = document.getElementById('root'); + if (rootElt) { + csrfToken = rootElt.dataset.csrfToken; } - submitForm: () => void = () => { - const form = this.uploadForm.current; - if (form) { - form.submit(); - } - }; - - render(): React.ReactElement<'form'> { - const { parameters } = this.props; - - /* TODO: Refactor core.api.base and reuse getCSRFToken() here */ - let csrfToken = ''; - const rootElt = document.getElementById('root'); - if (rootElt) { - csrfToken = rootElt.dataset.csrfToken; - } - - return ( -
    - - - - - -
    - ); - } + return ( +
    + + + + + +
    + ); + } } diff --git a/translate/src/core/user/components/SignIn.css b/translate/src/core/user/components/SignIn.css index 654499ea6..2b0a393ce 100644 --- a/translate/src/core/user/components/SignIn.css +++ b/translate/src/core/user/components/SignIn.css @@ -1,16 +1,16 @@ .user-signin a { - border: 1.5px solid #7bc176; - border-radius: 2px; - color: white; - display: inline-block; - font-size: 14px; - height: 17px; - line-height: 16px; - padding: 10px 25px; - text-align: center; - margin: 10px 5px 0 0; + border: 1.5px solid #7bc176; + border-radius: 2px; + color: white; + display: inline-block; + font-size: 14px; + height: 17px; + line-height: 16px; + padding: 10px 25px; + text-align: center; + margin: 10px 5px 0 0; } .user-signin a:hover { - background-color: #7bc176; + background-color: #7bc176; } diff --git a/translate/src/core/user/components/SignIn.tsx b/translate/src/core/user/components/SignIn.tsx index 6a8134e1e..c3ad01422 100644 --- a/translate/src/core/user/components/SignIn.tsx +++ b/translate/src/core/user/components/SignIn.tsx @@ -6,20 +6,20 @@ import './SignIn.css'; import SignInLink from './SignInLink'; type Props = { - url: string; + url: string; }; /* * Render a Sign In link styled as a button. */ export default class SignIn extends React.Component { - render(): React.ReactElement<'span'> { - return ( - - - Sign in - - - ); - } + render(): React.ReactElement<'span'> { + return ( + + + Sign in + + + ); + } } diff --git a/translate/src/core/user/components/SignInLink.tsx b/translate/src/core/user/components/SignInLink.tsx index 7ba5a55bf..6bce57821 100644 --- a/translate/src/core/user/components/SignInLink.tsx +++ b/translate/src/core/user/components/SignInLink.tsx @@ -1,25 +1,25 @@ import * as React from 'react'; type Props = { - children?: React.ReactNode; - url: string; + children?: React.ReactNode; + url: string; }; /* * Render a link to the Sign In process. */ export default class SignInLink extends React.Component { - generateSignInURL(): string { - const absoluteUrl = window.location.origin + this.props.url; - const parsedUrl = new URL(absoluteUrl); - const next = window.location.pathname + window.location.search; + generateSignInURL(): string { + const absoluteUrl = window.location.origin + this.props.url; + const parsedUrl = new URL(absoluteUrl); + const next = window.location.pathname + window.location.search; - parsedUrl.searchParams.set('next', next); + parsedUrl.searchParams.set('next', next); - return parsedUrl.toString(); - } + return parsedUrl.toString(); + } - render(): React.ReactElement<'a'> { - return {this.props.children}; - } + render(): React.ReactElement<'a'> { + return {this.props.children}; + } } diff --git a/translate/src/core/user/components/SignOut.tsx b/translate/src/core/user/components/SignOut.tsx index 8ee897911..9b3d47174 100644 --- a/translate/src/core/user/components/SignOut.tsx +++ b/translate/src/core/user/components/SignOut.tsx @@ -2,27 +2,25 @@ import * as React from 'react'; import { Localized } from '@fluent/react'; type Props = { - signOut: () => void; + signOut: () => void; }; /* * Render a Sign Out link. */ export default class SignOut extends React.Component { - signOut: () => void = () => { - this.props.signOut(); - }; + signOut: () => void = () => { + this.props.signOut(); + }; - render(): React.ReactElement { - return ( - }} - > - - - ); - } + render(): React.ReactElement { + return ( + }} + > + + + ); + } } diff --git a/translate/src/core/user/components/UserAutoUpdater.test.js b/translate/src/core/user/components/UserAutoUpdater.test.js index bd3f17eb0..79c425299 100644 --- a/translate/src/core/user/components/UserAutoUpdater.test.js +++ b/translate/src/core/user/components/UserAutoUpdater.test.js @@ -5,27 +5,27 @@ import { shallow } from 'enzyme'; import UserAutoUpdater from './UserAutoUpdater'; describe('', () => { - it('fetches user data on mount', () => { - const getUserData = sinon.spy(); - shallow(); + it('fetches user data on mount', () => { + const getUserData = sinon.spy(); + shallow(); - expect(getUserData.callCount).toEqual(1); - }); + expect(getUserData.callCount).toEqual(1); + }); - it('fetches user data every 2 minutes', () => { - jest.useFakeTimers(); + it('fetches user data every 2 minutes', () => { + jest.useFakeTimers(); - const getUserData = sinon.spy(); - shallow(); + const getUserData = sinon.spy(); + shallow(); - jest.advanceTimersByTime(2 * 60 * 1000); - expect(getUserData.callCount).toEqual(2); + jest.advanceTimersByTime(2 * 60 * 1000); + expect(getUserData.callCount).toEqual(2); - jest.advanceTimersByTime(2 * 60 * 1000); - expect(getUserData.callCount).toEqual(3); + jest.advanceTimersByTime(2 * 60 * 1000); + expect(getUserData.callCount).toEqual(3); - // If less than 2 minutes have passed, it doesn't trigger. - jest.advanceTimersByTime(60 * 1000); - expect(getUserData.callCount).toEqual(3); - }); + // If less than 2 minutes have passed, it doesn't trigger. + jest.advanceTimersByTime(60 * 1000); + expect(getUserData.callCount).toEqual(3); + }); }); diff --git a/translate/src/core/user/components/UserAutoUpdater.ts b/translate/src/core/user/components/UserAutoUpdater.ts index c2dfcbb91..7fe0fddaa 100644 --- a/translate/src/core/user/components/UserAutoUpdater.ts +++ b/translate/src/core/user/components/UserAutoUpdater.ts @@ -1,32 +1,32 @@ import * as React from 'react'; type Props = { - getUserData: () => void; + getUserData: () => void; }; /** * Regularly fetch user data to keep it up-to-date with the server. */ export default class UserAutoUpdater extends React.Component { - timer: number | null; + timer: number | null; - fetchUserData: () => void = () => { - this.props.getUserData(); - }; + fetchUserData: () => void = () => { + this.props.getUserData(); + }; - componentDidMount() { - this.fetchUserData(); - this.timer = window.setInterval(this.fetchUserData, 2 * 60 * 1000); + componentDidMount() { + this.fetchUserData(); + this.timer = window.setInterval(this.fetchUserData, 2 * 60 * 1000); + } + + componentWillUnmount() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; } + } - componentWillUnmount() { - if (this.timer) { - clearInterval(this.timer); - this.timer = null; - } - } - - render(): null { - return null; - } + render(): null { + return null; + } } diff --git a/translate/src/core/user/components/UserAvatar.tsx b/translate/src/core/user/components/UserAvatar.tsx index 22c350c0a..731bdc3a9 100644 --- a/translate/src/core/user/components/UserAvatar.tsx +++ b/translate/src/core/user/components/UserAvatar.tsx @@ -2,32 +2,27 @@ import * as React from 'react'; import { Localized } from '@fluent/react'; type Props = { - username: string; - title?: string; - imageUrl: string; + username: string; + title?: string; + imageUrl: string; }; export default function UserAvatar(props: Props): React.ReactElement<'div'> { - const { username, title, imageUrl } = props; + const { username, title, imageUrl } = props; - return ( - - ); + return ( + + ); } diff --git a/translate/src/core/user/components/UserControls.css b/translate/src/core/user/components/UserControls.css index b5b51d1f1..1f5178a1c 100644 --- a/translate/src/core/user/components/UserControls.css +++ b/translate/src/core/user/components/UserControls.css @@ -1,3 +1,3 @@ .user-controls { - float: right; + float: right; } diff --git a/translate/src/core/user/components/UserControls.test.js b/translate/src/core/user/components/UserControls.test.js index eec8ba47f..7a4f503cb 100644 --- a/translate/src/core/user/components/UserControls.test.js +++ b/translate/src/core/user/components/UserControls.test.js @@ -5,19 +5,19 @@ import SignIn from './SignIn'; import { UserControlsBase } from './UserControls'; describe('', () => { - it('shows a Sign in link when user is logged out', () => { - const wrapper = shallow( - , - ); + it('shows a Sign in link when user is logged out', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find(SignIn)).toHaveLength(1); - }); + expect(wrapper.find(SignIn)).toHaveLength(1); + }); - it('hides a Sign in link when user is logged in', () => { - const wrapper = shallow( - , - ); + it('hides a Sign in link when user is logged in', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find(SignIn)).toHaveLength(0); - }); + expect(wrapper.find(SignIn)).toHaveLength(0); + }); }); diff --git a/translate/src/core/user/components/UserControls.tsx b/translate/src/core/user/components/UserControls.tsx index 09e217239..1008a9041 100644 --- a/translate/src/core/user/components/UserControls.tsx +++ b/translate/src/core/user/components/UserControls.tsx @@ -19,65 +19,65 @@ import type { UserState } from '~/core/user'; import { AppDispatch, RootState } from '~/store'; type Props = { - isTranslator: boolean; - parameters: NavigationParams; - selectedEntity: Entity; - user: UserState; + isTranslator: boolean; + parameters: NavigationParams; + selectedEntity: Entity; + user: UserState; }; type InternalProps = Props & { - dispatch: AppDispatch; + dispatch: AppDispatch; }; export class UserControlsBase extends React.Component { - getUserData: () => void = () => { - this.props.dispatch(actions.get()); - }; + getUserData: () => void = () => { + this.props.dispatch(actions.get()); + }; - markAllNotificationsAsRead: () => void = () => { - this.props.dispatch(actions.markAllNotificationsAsRead()); - }; + markAllNotificationsAsRead: () => void = () => { + this.props.dispatch(actions.markAllNotificationsAsRead()); + }; - signUserOut: () => void = () => { - const { user } = this.props; - this.props.dispatch(actions.signOut(user.signOutURL)); - }; + signUserOut: () => void = () => { + const { user } = this.props; + this.props.dispatch(actions.signOut(user.signOutURL)); + }; - render(): React.ReactElement<'div'> { - const { isTranslator, parameters, user, selectedEntity } = this.props; + render(): React.ReactElement<'div'> { + const { isTranslator, parameters, user, selectedEntity } = this.props; - const isReadOnly = selectedEntity ? selectedEntity.readonly : true; + const isReadOnly = selectedEntity ? selectedEntity.readonly : true; - return ( -
    - + return ( +
    + - + - + - {user.isAuthenticated ? null : } -
    - ); - } + {user.isAuthenticated ? null : } +
    + ); + } } const mapStateToProps = (state: RootState): Props => { - return { - isTranslator: user.selectors.isTranslator(state), - parameters: navigation.selectors.getNavigationParams(state), - selectedEntity: entities.selectors.getSelectedEntity(state), - user: state[NAME], - }; + return { + isTranslator: user.selectors.isTranslator(state), + parameters: navigation.selectors.getNavigationParams(state), + selectedEntity: entities.selectors.getSelectedEntity(state), + user: state[NAME], + }; }; export default connect(mapStateToProps)(UserControlsBase) as any; diff --git a/translate/src/core/user/components/UserMenu.css b/translate/src/core/user/components/UserMenu.css index 6d4541037..876a2ff71 100644 --- a/translate/src/core/user/components/UserMenu.css +++ b/translate/src/core/user/components/UserMenu.css @@ -1,108 +1,108 @@ .user-menu { - float: right; - height: 60px; + float: right; + height: 60px; } .user-menu .selector { - cursor: pointer; - height: 100%; + cursor: pointer; + height: 100%; } .user-menu .selector img, .user-menu .selector .menu-icon, .user-menu .menu li.details img { - border: 2px solid #4d5967; - border-radius: 6px; + border: 2px solid #4d5967; + border-radius: 6px; } .user-menu .selector img { - height: 44px; - width: 44px; - margin: 6px 5px 6px 5px; + height: 44px; + width: 44px; + margin: 6px 5px 6px 5px; } .user-menu .selector .menu-icon { - font-size: 20px; - height: 20px; - width: 20px; - margin: 6px 5px 6px 5px; - padding: 12px; - text-align: center; + font-size: 20px; + height: 20px; + width: 20px; + margin: 6px 5px 6px 5px; + padding: 12px; + text-align: center; } .user-menu .menu { - background-color: #272a2f; - border: 1px solid #333941; - border-right: none; - border-top: none; - list-style: none; - margin: 0; - padding: 10px 12px; - position: absolute; - right: 0; - top: 59px; - width: 250px; - z-index: 20; + background-color: #272a2f; + border: 1px solid #333941; + border-right: none; + border-top: none; + list-style: none; + margin: 0; + padding: 10px 12px; + position: absolute; + right: 0; + top: 59px; + width: 250px; + z-index: 20; } .user-menu .menu li { - color: #aaa; - cursor: pointer; - font-size: 14px; - font-weight: 300; - padding: 0 4px; + color: #aaa; + cursor: pointer; + font-size: 14px; + font-weight: 300; + padding: 0 4px; } .user-menu .menu li.details { - padding: 10px 4px; - text-align: center; + padding: 10px 4px; + text-align: center; } .user-menu .menu li.details .name { - color: #ebebeb; - font-size: 16px; + color: #ebebeb; + font-size: 16px; } .user-menu .menu li.details .email { - color: #aaaaaa; - font-size: 12px; + color: #aaaaaa; + font-size: 12px; } .user-menu .menu li a, .user-menu .menu li button { - display: block; - line-height: 22px; + display: block; + line-height: 22px; } .user-menu li button { - background: none; - border: none; - color: #aaa; - font-weight: 300; - padding: 0; - text-align: left; - width: 100%; + background: none; + border: none; + color: #aaa; + font-weight: 300; + padding: 0; + text-align: left; + width: 100%; } .user-menu .menu li i.fa, .user-menu .menu li i.fab { - margin: 0 8px 0 -2px; + margin: 0 8px 0 -2px; } .user-menu .menu li:hover { - background: #3f4752; + background: #3f4752; } .user-menu .menu li:hover, .user-menu .menu li:hover a, .user-menu .menu li:hover button, .user-menu .menu li:active a { - color: #fff; + color: #fff; } .user-menu .menu .horizontal-separator { - border-top: 1px solid #525a65; - height: 0; - margin: 5px 0; - padding: 0; + border-top: 1px solid #525a65; + height: 0; + margin: 5px 0; + padding: 0; } diff --git a/translate/src/core/user/components/UserMenu.test.js b/translate/src/core/user/components/UserMenu.test.js index ea7d6f17e..39ec08919 100644 --- a/translate/src/core/user/components/UserMenu.test.js +++ b/translate/src/core/user/components/UserMenu.test.js @@ -8,123 +8,123 @@ import UserMenuBase, { UserMenu } from './UserMenu'; import { findLocalizedById } from '~/test/utils'; function createShallowUserMenu({ - isAdmin = false, - isReadOnly = false, - isTranslator = true, - isAuthenticated = true, - locale = 'mylocale', - project = 'myproject', - resource = 'myresource', + isAdmin = false, + isReadOnly = false, + isTranslator = true, + isAuthenticated = true, + locale = 'mylocale', + project = 'myproject', + resource = 'myresource', } = {}) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('shows the right menu items when the user is logged in', () => { - const wrapper = createShallowUserMenu({ - locale: 'locale', - project: 'project', - }); - - expect(wrapper.find('.details')).toHaveLength(1); - expect(wrapper.find('a[href="/settings/"]')).toHaveLength(1); - expect(wrapper.find(SignOut)).toHaveLength(1); + it('shows the right menu items when the user is logged in', () => { + const wrapper = createShallowUserMenu({ + locale: 'locale', + project: 'project', }); - it('hides the right menu items when the user is logged out', () => { - const wrapper = createShallowUserMenu({ isAuthenticated: false }); + expect(wrapper.find('.details')).toHaveLength(1); + expect(wrapper.find('a[href="/settings/"]')).toHaveLength(1); + expect(wrapper.find(SignOut)).toHaveLength(1); + }); - expect(wrapper.find('.details')).toHaveLength(0); - expect(wrapper.find('a[href="/settings/"]')).toHaveLength(0); - expect(wrapper.find(SignOut)).toHaveLength(0); - }); + it('hides the right menu items when the user is logged out', () => { + const wrapper = createShallowUserMenu({ isAuthenticated: false }); - it('shows upload & download menu items', () => { - const wrapper = createShallowUserMenu(); + expect(wrapper.find('.details')).toHaveLength(0); + expect(wrapper.find('a[href="/settings/"]')).toHaveLength(0); + expect(wrapper.find(SignOut)).toHaveLength(0); + }); - expect(wrapper.find(FileUpload)).toHaveLength(1); - expect( - findLocalizedById(wrapper, 'user-UserMenu--download-translations'), - ).toHaveLength(1); - }); + it('shows upload & download menu items', () => { + const wrapper = createShallowUserMenu(); - it('hides upload & download menu items when translating all projects', () => { - const wrapper = createShallowUserMenu({ project: 'all-projects' }); + expect(wrapper.find(FileUpload)).toHaveLength(1); + expect( + findLocalizedById(wrapper, 'user-UserMenu--download-translations'), + ).toHaveLength(1); + }); - expect(wrapper.find(FileUpload)).toHaveLength(0); - expect( - findLocalizedById(wrapper, 'user-UserMenu--download-translations'), - ).toHaveLength(0); - }); + it('hides upload & download menu items when translating all projects', () => { + const wrapper = createShallowUserMenu({ project: 'all-projects' }); - it('hides upload & download menu items when translating all resources', () => { - const wrapper = createShallowUserMenu({ resource: 'all-resources' }); + expect(wrapper.find(FileUpload)).toHaveLength(0); + expect( + findLocalizedById(wrapper, 'user-UserMenu--download-translations'), + ).toHaveLength(0); + }); - expect(wrapper.find(FileUpload)).toHaveLength(0); - expect( - findLocalizedById(wrapper, 'user-UserMenu--download-translations'), - ).toHaveLength(0); - }); + it('hides upload & download menu items when translating all resources', () => { + const wrapper = createShallowUserMenu({ resource: 'all-resources' }); - it('hides upload menu item for users without permission to review translations', () => { - const wrapper = createShallowUserMenu({ isTranslator: false }); + expect(wrapper.find(FileUpload)).toHaveLength(0); + expect( + findLocalizedById(wrapper, 'user-UserMenu--download-translations'), + ).toHaveLength(0); + }); - expect(wrapper.find(FileUpload)).toHaveLength(0); - }); + it('hides upload menu item for users without permission to review translations', () => { + const wrapper = createShallowUserMenu({ isTranslator: false }); - it('hides upload menu for read-only strings', () => { - const wrapper = createShallowUserMenu({ isReadOnly: true }); + expect(wrapper.find(FileUpload)).toHaveLength(0); + }); - expect(wrapper.find(FileUpload)).toHaveLength(0); - }); + it('hides upload menu for read-only strings', () => { + const wrapper = createShallowUserMenu({ isReadOnly: true }); - it('shows the admin menu items when the user is an admin', () => { - const wrapper = createShallowUserMenu({ isAdmin: true }); + expect(wrapper.find(FileUpload)).toHaveLength(0); + }); - expect(wrapper.find('a[href="/admin/"]')).toHaveLength(1); - expect( - wrapper.find('a[href="/admin/projects/myproject/"]'), - ).toHaveLength(1); - }); + it('shows the admin menu items when the user is an admin', () => { + const wrapper = createShallowUserMenu({ isAdmin: true }); + + expect(wrapper.find('a[href="/admin/"]')).toHaveLength(1); + expect(wrapper.find('a[href="/admin/projects/myproject/"]')).toHaveLength( + 1, + ); + }); }); function createShallowUserMenuBase({ - isAdmin = false, - isAuthenticated = true, + isAdmin = false, + isAuthenticated = true, } = {}) { - return shallow(); + return shallow(); } describe('', () => { - it('shows the user avatar when the user is logged in', () => { - const wrapper = createShallowUserMenuBase(); + it('shows the user avatar when the user is logged in', () => { + const wrapper = createShallowUserMenuBase(); - expect(wrapper.find('img')).toHaveLength(1); - expect(wrapper.find('.menu-icon')).toHaveLength(0); - }); + expect(wrapper.find('img')).toHaveLength(1); + expect(wrapper.find('.menu-icon')).toHaveLength(0); + }); - it('shows the general menu icon when the user is logged out', () => { - const wrapper = createShallowUserMenuBase({ isAuthenticated: false }); + it('shows the general menu icon when the user is logged out', () => { + const wrapper = createShallowUserMenuBase({ isAuthenticated: false }); - expect(wrapper.find('img')).toHaveLength(0); - expect(wrapper.find('.menu-icon')).toHaveLength(1); - }); + expect(wrapper.find('img')).toHaveLength(0); + expect(wrapper.find('.menu-icon')).toHaveLength(1); + }); - it('toggles the user menu when clicking the user avatar', () => { - const wrapper = createShallowUserMenuBase(); - expect(wrapper.find('UserMenu')).toHaveLength(0); + it('toggles the user menu when clicking the user avatar', () => { + const wrapper = createShallowUserMenuBase(); + expect(wrapper.find('UserMenu')).toHaveLength(0); - wrapper.find('.selector').simulate('click'); - expect(wrapper.find('UserMenu')).toHaveLength(1); + wrapper.find('.selector').simulate('click'); + expect(wrapper.find('UserMenu')).toHaveLength(1); - wrapper.find('.selector').simulate('click'); - expect(wrapper.find('UserMenu')).toHaveLength(0); - }); + wrapper.find('.selector').simulate('click'); + expect(wrapper.find('UserMenu')).toHaveLength(0); + }); }); diff --git a/translate/src/core/user/components/UserMenu.tsx b/translate/src/core/user/components/UserMenu.tsx index 80570888c..4991038e6 100644 --- a/translate/src/core/user/components/UserMenu.tsx +++ b/translate/src/core/user/components/UserMenu.tsx @@ -11,281 +11,268 @@ import type { NavigationParams } from '~/core/navigation'; import type { UserState } from '~/core/user'; type Props = { - isReadOnly: boolean; - isTranslator: boolean; - parameters: NavigationParams; - signOut: () => void; - user: UserState; + isReadOnly: boolean; + isTranslator: boolean; + parameters: NavigationParams; + signOut: () => void; + user: UserState; }; type State = { - visible: boolean; + visible: boolean; }; type UserMenuProps = Props & { - onDiscard: () => void; + onDiscard: () => void; }; export function UserMenu({ - user, - parameters, - isTranslator, - isReadOnly, - signOut, - onDiscard, + user, + parameters, + isTranslator, + isReadOnly, + signOut, + onDiscard, }: UserMenuProps): React.ReactElement<'ul'> { - const { locale, project, resource } = parameters; + const { locale, project, resource } = parameters; - const canDownload = - project !== 'all-projects' && resource !== 'all-resources'; + const canDownload = + project !== 'all-projects' && resource !== 'all-resources'; - const canUpload = - /* TODO: Also disable for subpages (in-context l10n) when supported */ - canDownload && isTranslator && !isReadOnly; + const canUpload = + /* TODO: Also disable for subpages (in-context l10n) when supported */ + canDownload && isTranslator && !isReadOnly; - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); - return ( - + ); } /** * Renders user menu. */ export default class UserMenuBase extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - visible: false, - }; - } - - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); + constructor(props: Props) { + super(props); + this.state = { + visible: false, }; + } - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; - render(): React.ReactElement<'div'> { - const { isReadOnly, isTranslator, parameters, signOut, user } = - this.props; + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; - return ( -
    -
    - {user.isAuthenticated ? ( - - ) : ( -
    - )} -
    + render(): React.ReactElement<'div'> { + const { isReadOnly, isTranslator, parameters, signOut, user } = this.props; - {this.state.visible && ( - - )} -
    - ); - } + return ( +
    +
    + {user.isAuthenticated ? ( + + ) : ( +
    + )} +
    + + {this.state.visible && ( + + )} +
    + ); + } } diff --git a/translate/src/core/user/components/UserNotification.css b/translate/src/core/user/components/UserNotification.css index e4e0c2a22..894661eef 100644 --- a/translate/src/core/user/components/UserNotification.css +++ b/translate/src/core/user/components/UserNotification.css @@ -1,87 +1,87 @@ .user-notification { - border-top: 1px solid #333941; - cursor: default; - padding: 0; + border-top: 1px solid #333941; + cursor: default; + padding: 0; } .user-notification:first-child { - border-color: #272a2f; + border-color: #272a2f; } .user-notification:first-child:hover { - border-color: #333941; + border-color: #333941; } .user-notification.unread { - background: #3f4752; + background: #3f4752; } .user-notification.read { - animation-duration: 1s; - animation-name: fadeout-background; + animation-duration: 1s; + animation-name: fadeout-background; } @keyframes fadeout-background { - 0% { - background: #3f4752; - } + 0% { + background: #3f4752; + } - 100% { - background: #272a2f; - } + 100% { + background: #272a2f; + } } .user-notification .item-content { - display: block; - padding: 10px; + display: block; + padding: 10px; } .user-notification span { - color: #ebebeb; - float: none; + color: #ebebeb; + float: none; } .user-notification a { - color: #f36; - display: inline; + color: #f36; + display: inline; } .user-notification .verb { - color: #ebebeb; - padding: 0 3px; + color: #ebebeb; + padding: 0 3px; } .user-notification .description ul { - list-style: inside; - margin-left: 0; - padding: 12px 0 0 4px; + list-style: inside; + margin-left: 0; + padding: 12px 0 0 4px; } .user-notification .message { - padding: 10px; + padding: 10px; } .user-notification .message.trim, .user-notification .message.trim p { - pointer-events: none; - color: #aaaaaa; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + pointer-events: none; + color: #aaaaaa; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .user-notification .item-content:hover .message, .user-notification .item-content:hover .message.trim p, .user-notification .item-content:hover .message.trim a { - color: #ebebeb; + color: #ebebeb; } .user-notification .message.trim a { - color: #aaaaaa; + color: #aaaaaa; } .user-notification .message.trim p { - padding: 0; + padding: 0; } /* Users can include HTML tags in their messages */ @@ -91,41 +91,41 @@ .user-notification .message h4, .user-notification .message h5, .user-notification .message h6 { - color: #ebebeb; - font-size: 14px; - font-style: normal; - font-weight: bold; - letter-spacing: 0; - text-transform: none; + color: #ebebeb; + font-size: 14px; + font-style: normal; + font-weight: bold; + letter-spacing: 0; + text-transform: none; } .user-notification .message h1 { - font-size: 18px; + font-size: 18px; } .user-notification .message h2 { - font-size: 16px; + font-size: 16px; } .user-notification .message p { - padding: 5px 0; + padding: 5px 0; } .user-notification .message ol { - margin-left: 1.2em; + margin-left: 1.2em; } .user-notification .message ul { - list-style: inside; + list-style: inside; } /* End message styling */ .user-notification .timeago { - color: #888888; - display: block; - font-size: 11px; - font-weight: normal; - margin-top: 8px; - text-align: right; - text-transform: uppercase; + color: #888888; + display: block; + font-size: 11px; + font-weight: normal; + margin-top: 8px; + text-align: right; + text-transform: uppercase; } diff --git a/translate/src/core/user/components/UserNotification.tsx b/translate/src/core/user/components/UserNotification.tsx index 9d0996bdc..606501b52 100644 --- a/translate/src/core/user/components/UserNotification.tsx +++ b/translate/src/core/user/components/UserNotification.tsx @@ -7,170 +7,160 @@ import { Linkify } from '~/core/linkify'; import './UserNotification.css'; type Props = { - notification: Record; + notification: Record; }; type State = { - markAsRead: boolean; + markAsRead: boolean; }; /** * Renders a single notification in the notifications menu. */ export default class UserNotification extends React.Component { - constructor(props: Props) { - super(props); + constructor(props: Props) { + super(props); - this.state = { - markAsRead: false, - }; + this.state = { + markAsRead: false, + }; + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.notification.unread && !this.props.notification.unread) { + this.setState({ + markAsRead: true, + }); + } + } + + render(): React.ReactElement<'li'> { + const { notification } = this.props; + + const description = notification.description.content; + const isSuggestion = + description && description.startsWith('Unreviewed suggestions'); + const isComment = !description + ? false + : notification.description.is_comment; + + let className = 'user-notification'; + if (isSuggestion) { + className += ' suggestion'; + } + if (notification.unread) { + className += ' unread'; + } else if (this.state.markAsRead) { + className += ' read'; } - componentDidUpdate(prevProps: Props) { - if (prevProps.notification.unread && !this.props.notification.unread) { - this.setState({ - markAsRead: true, - }); - } + if (isSuggestion) { + return ( +
  • +
    + + + +
    +
  • + ); } - render(): React.ReactElement<'li'> { - const { notification } = this.props; + if (isComment) { + return ( +
  • +
    + {notification.actor.anchor} - const description = notification.description.content; - const isSuggestion = - description && description.startsWith('Unreviewed suggestions'); - const isComment = !description - ? false - : notification.description.is_comment; + + {notification.verb} + - let className = 'user-notification'; - if (isSuggestion) { - className += ' suggestion'; - } - if (notification.unread) { - className += ' unread'; - } else if (this.state.markAsRead) { - className += ' read'; - } + {notification.target.anchor} - if (isSuggestion) { - return ( -
  • -
    - + - -
    -
  • - ); - } - - if (isComment) { - return ( -
  • -
    - - {notification.actor.anchor} - - - - - {notification.verb} - - - - - {notification.target.anchor} - - - - -
    - - {/* We can safely parse description as it is - * sanitized when coming from the DB. See: - * - pontoon.base.forms.AddCommentForm(} - * - pontoon.base.forms.HtmlField() - */} - {parse(description)} - -
    -
    -
  • - ); - } - - return ( -
  • -
    - - - {notification.actor.anchor} - - - - {notification.verb} - - {!notification.target ? null : ( - - - {notification.target.anchor} - - - )} - - - - {!description ? null : ( -
    - )} -
    -
  • - ); +
    + + {/* We can safely parse description as it is + * sanitized when coming from the DB. See: + * - pontoon.base.forms.AddCommentForm(} + * - pontoon.base.forms.HtmlField() + */} + {parse(description)} + +
    +
    + + ); } + + return ( +
  • +
    + + {notification.actor.anchor} + + + {notification.verb} + + {!notification.target ? null : ( + + {notification.target.anchor} + + )} + + + + {!description ? null : ( +
    + )} +
    +
  • + ); + } } diff --git a/translate/src/core/user/components/UserNotificationsMenu.css b/translate/src/core/user/components/UserNotificationsMenu.css index 767c02cf0..74e356505 100644 --- a/translate/src/core/user/components/UserNotificationsMenu.css +++ b/translate/src/core/user/components/UserNotificationsMenu.css @@ -1,104 +1,104 @@ .user-notifications-menu { - float: right; - height: 60px; - margin-right: 3px; - position: relative; + float: right; + height: 60px; + margin-right: 3px; + position: relative; } .user-notifications-menu .selector { - cursor: pointer; - height: 100%; - padding: 0; + cursor: pointer; + height: 100%; + padding: 0; } .user-notifications-menu .selector .icon { - color: #4d5967; - font-size: 26px; - margin-top: 17px; + color: #4d5967; + font-size: 26px; + margin-top: 17px; } .user-notifications-menu .selector .badge { - background: #f36; - border: 3px solid #272a2f; - border-radius: 12px; - color: #fff; - font-size: 12px; - font-style: normal; - font-weight: 400; - height: 14px; - line-height: 16px; - padding: 1px 6px 3px; - position: absolute; - right: -6px; - text-align: center; - top: 4px; + background: #f36; + border: 3px solid #272a2f; + border-radius: 12px; + color: #fff; + font-size: 12px; + font-style: normal; + font-weight: 400; + height: 14px; + line-height: 16px; + padding: 1px 6px 3px; + position: absolute; + right: -6px; + text-align: center; + top: 4px; } .user-notifications-menu .menu { - background-color: #272a2f; - border: 1px solid #333941; - border-top: none; - padding: 0; - position: absolute; - right: 0; - top: 59px; - width: 350px; - z-index: 20; + background-color: #272a2f; + border: 1px solid #333941; + border-top: none; + padding: 0; + position: absolute; + right: 0; + top: 59px; + width: 350px; + z-index: 20; } .user-notifications-menu .menu .notification-list { - list-style: none; - margin: 0; - max-height: 280px; - overflow: auto; + list-style: none; + margin: 0; + max-height: 280px; + overflow: auto; } .user-notifications-menu .menu li { - color: #aaa; - font-size: 14px; - font-weight: 300; + color: #aaa; + font-size: 14px; + font-weight: 300; } .user-notifications-menu .menu li:hover { - background: #3f4752; + background: #3f4752; } .user-notifications-menu .menu li.no { - background: transparent; - color: inherit; - padding: 0 4px 14px; - text-align: center; + background: transparent; + color: inherit; + padding: 0 4px 14px; + text-align: center; } .user-notifications-menu .menu li.no .icon { - color: #333941; - font-size: 92px; - padding-bottom: 8px; + color: #333941; + font-size: 92px; + padding-bottom: 8px; } .user-notifications-menu .menu li.no .title { - color: #ebebeb; - font-size: 16px; + color: #ebebeb; + font-size: 16px; } .user-notifications-menu .menu li.no .description { - color: #aaaaaa; - font-size: 12px; - padding-top: 4px; + color: #aaaaaa; + font-size: 12px; + padding-top: 4px; } .user-notifications-menu .menu .see-all { - border-top: 1px solid #333941; + border-top: 1px solid #333941; } .user-notifications-menu .menu .see-all a { - display: block; - font-size: 14px; - padding: 10px; - text-align: center; + display: block; + font-size: 14px; + padding: 10px; + text-align: center; } .user-notifications-menu .menu .see-all a:hover { - background: #3f4752; - color: #ffffff; + background: #3f4752; + color: #ffffff; } diff --git a/translate/src/core/user/components/UserNotificationsMenu.test.js b/translate/src/core/user/components/UserNotificationsMenu.test.js index 13cfda2eb..3347e1551 100644 --- a/translate/src/core/user/components/UserNotificationsMenu.test.js +++ b/translate/src/core/user/components/UserNotificationsMenu.test.js @@ -5,125 +5,123 @@ import { shallow } from 'enzyme'; import api from '~/core/api'; import UserNotificationsMenuBase, { - UserNotificationsMenu, + UserNotificationsMenu, } from './UserNotificationsMenu'; import UserNotification from './UserNotification'; describe('', () => { - it('shows empty notifications menu if user has no notifications', () => { - const notifications = []; - const wrapper = shallow( - , - ); + it('shows empty notifications menu if user has no notifications', () => { + const notifications = []; + const wrapper = shallow( + , + ); - expect( - wrapper.find('.notification-list .user-notification'), - ).toHaveLength(0); - expect(wrapper.find('.notification-list .no')).toHaveLength(1); - }); + expect(wrapper.find('.notification-list .user-notification')).toHaveLength( + 0, + ); + expect(wrapper.find('.notification-list .no')).toHaveLength(1); + }); - it('shows a notification in the notifications menu', () => { - const notifications = [ - { - id: 0, - level: 'level', - unread: false, - description: 'description', - verb: 'verb', - date: 'Jan 31, 2000 10:20', - date_iso: '2019-01-31T10:20:00+00:00', - actor: { - anchor: 'actor_anchor', - url: 'actor_url', - }, - target: { - anchor: 'target_anchor', - url: 'target_url', - }, - }, - ]; + it('shows a notification in the notifications menu', () => { + const notifications = [ + { + id: 0, + level: 'level', + unread: false, + description: 'description', + verb: 'verb', + date: 'Jan 31, 2000 10:20', + date_iso: '2019-01-31T10:20:00+00:00', + actor: { + anchor: 'actor_anchor', + url: 'actor_url', + }, + target: { + anchor: 'target_anchor', + url: 'target_url', + }, + }, + ]; - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - expect(wrapper.find('.notification-list .no')).toHaveLength(0); - expect(wrapper.find(UserNotification)).toHaveLength(1); - }); + expect(wrapper.find('.notification-list .no')).toHaveLength(0); + expect(wrapper.find(UserNotification)).toHaveLength(1); + }); }); describe('', () => { - const sandbox = sinon.createSandbox(); + const sandbox = sinon.createSandbox(); - beforeEach(function () { - sandbox.spy(api.uxaction, 'log'); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('hides the notifications icon when the user is logged out', () => { - const user = { - isAuthenticated: false, - notifications: { - has_unread: false, - }, - }; - const wrapper = shallow(); - - expect(wrapper.find('.user-notifications-menu')).toHaveLength(0); - }); - - it('shows the notifications icon when the user is logged in', () => { - const user = { - isAuthenticated: true, - notifications: { - notifications: [], - }, - }; - const wrapper = shallow(); - - expect(wrapper.find('.user-notifications-menu')).toHaveLength(1); - }); - - it('shows the notifications badge when the user has unread notifications and call logUxAction', () => { - const user = { - isAuthenticated: true, - notifications: { - has_unread: true, - notifications: [], - unread_count: '5', - }, - }; - const wrapper = shallow(); - - expect(wrapper.find('.user-notifications-menu .badge').text()).toEqual( - '5', - ); - expect(api.uxaction.log.called).toEqual(true); - }); - - it('calls the logUxAction function on click on the icon if menu not visible', () => { - const markAllNotificationsAsRead = sinon.spy(); - const user = { - isAuthenticated: true, - notifications: { - has_unread: true, - notifications: [], - }, - }; - const wrapper = shallow( - , - ); - - wrapper.setState({ - visible: false, - }); - wrapper.find('.selector').simulate('click'); - expect(api.uxaction.log.called).toEqual(true); + beforeEach(function () { + sandbox.spy(api.uxaction, 'log'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('hides the notifications icon when the user is logged out', () => { + const user = { + isAuthenticated: false, + notifications: { + has_unread: false, + }, + }; + const wrapper = shallow(); + + expect(wrapper.find('.user-notifications-menu')).toHaveLength(0); + }); + + it('shows the notifications icon when the user is logged in', () => { + const user = { + isAuthenticated: true, + notifications: { + notifications: [], + }, + }; + const wrapper = shallow(); + + expect(wrapper.find('.user-notifications-menu')).toHaveLength(1); + }); + + it('shows the notifications badge when the user has unread notifications and call logUxAction', () => { + const user = { + isAuthenticated: true, + notifications: { + has_unread: true, + notifications: [], + unread_count: '5', + }, + }; + const wrapper = shallow(); + + expect(wrapper.find('.user-notifications-menu .badge').text()).toEqual('5'); + expect(api.uxaction.log.called).toEqual(true); + }); + + it('calls the logUxAction function on click on the icon if menu not visible', () => { + const markAllNotificationsAsRead = sinon.spy(); + const user = { + isAuthenticated: true, + notifications: { + has_unread: true, + notifications: [], + }, + }; + const wrapper = shallow( + , + ); + + wrapper.setState({ + visible: false, }); + wrapper.find('.selector').simulate('click'); + expect(api.uxaction.log.called).toEqual(true); + }); }); diff --git a/translate/src/core/user/components/UserNotificationsMenu.tsx b/translate/src/core/user/components/UserNotificationsMenu.tsx index 01979531a..d861c0928 100644 --- a/translate/src/core/user/components/UserNotificationsMenu.tsx +++ b/translate/src/core/user/components/UserNotificationsMenu.tsx @@ -12,153 +12,144 @@ import { useOnDiscard } from '~/core/utils'; import type { UserState, Notification } from '~/core/user'; type Props = { - markAllNotificationsAsRead: () => void; - user: UserState; + markAllNotificationsAsRead: () => void; + user: UserState; }; type State = { - visible: boolean; + visible: boolean; }; type UserNotificationsMenuProps = { - notifications: Array; - onDiscard: () => void; + notifications: Array; + onDiscard: () => void; }; export function UserNotificationsMenu({ - notifications, - onDiscard, + notifications, + onDiscard, }: UserNotificationsMenuProps): React.ReactElement<'div'> { - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); - return ( -
    -
      - {notifications.length ? ( - notifications.map((notification) => { - return ( - - ); - }) - ) : ( -
    • - - -

      No new notifications.

      -
      - -

      - Here you’ll see updates for localizations you - contribute to. -

      -
      -
    • - )} -
    + return ( +
    +
      + {notifications.length ? ( + notifications.map((notification) => { + return ( + + ); + }) + ) : ( +
    • + + +

      No new notifications.

      +
      + +

      + Here you’ll see updates for localizations you contribute to. +

      +
      +
    • + )} +
    - -
    - ); + +
    + ); } /** * Renders user notifications. */ export default class UserNotificationsMenuBase extends React.Component< - Props, - State + Props, + State > { - constructor(props: Props) { - super(props); + constructor(props: Props) { + super(props); - this.state = { - visible: false, - }; + this.state = { + visible: false, + }; + } + + componentDidMount() { + if (!this.props.user.isAuthenticated) { + return; } - componentDidMount() { - if (!this.props.user.isAuthenticated) { - return; - } - - if (!this.props.user.notifications.has_unread) { - return; - } - - api.uxaction.log( - 'Render: Unread notifications icon', - 'Notifications 1.0', - { - pathname: window.location.pathname, - }, - ); + if (!this.props.user.notifications.has_unread) { + return; } - handleClick: () => void = () => { - if (!this.state.visible) { - api.uxaction.log('Click: Notifications icon', 'Notifications 1.0', { - pathname: window.location.pathname, - unread: this.props.user.notifications.has_unread, - }); - } + api.uxaction.log('Render: Unread notifications icon', 'Notifications 1.0', { + pathname: window.location.pathname, + }); + } - this.toggleVisibility(); - this.markAllNotificationsAsRead(); - }; - - toggleVisibility: () => void = () => { - this.setState((state) => { - return { visible: !state.visible }; - }); - }; - - markAllNotificationsAsRead: () => void = () => { - if (this.props.user.notifications.has_unread) { - this.props.markAllNotificationsAsRead(); - } - }; - - handleDiscard: () => void = () => { - this.setState({ - visible: false, - }); - }; - - render(): null | React.ReactElement<'div'> { - const { user } = this.props; - - if (!user || !user.isAuthenticated) { - return null; - } - - return ( -
    -
    - - {user.notifications.has_unread && ( - - {user.notifications.unread_count} - - )} -
    - - {this.state.visible && ( - - )} -
    - ); + handleClick: () => void = () => { + if (!this.state.visible) { + api.uxaction.log('Click: Notifications icon', 'Notifications 1.0', { + pathname: window.location.pathname, + unread: this.props.user.notifications.has_unread, + }); } + + this.toggleVisibility(); + this.markAllNotificationsAsRead(); + }; + + toggleVisibility: () => void = () => { + this.setState((state) => { + return { visible: !state.visible }; + }); + }; + + markAllNotificationsAsRead: () => void = () => { + if (this.props.user.notifications.has_unread) { + this.props.markAllNotificationsAsRead(); + } + }; + + handleDiscard: () => void = () => { + this.setState({ + visible: false, + }); + }; + + render(): null | React.ReactElement<'div'> { + const { user } = this.props; + + if (!user || !user.isAuthenticated) { + return null; + } + + return ( +
    +
    + + {user.notifications.has_unread && ( + {user.notifications.unread_count} + )} +
    + + {this.state.visible && ( + + )} +
    + ); + } } diff --git a/translate/src/core/user/reducer.ts b/translate/src/core/user/reducer.ts index e3046fb73..beefaaac7 100644 --- a/translate/src/core/user/reducer.ts +++ b/translate/src/core/user/reducer.ts @@ -1,161 +1,160 @@ import { RECEIVE_USERS, UPDATE, UPDATE_SETTINGS } from './actions'; import type { - ReceiveAction, - UpdateAction, - UpdateSettingsAction, + ReceiveAction, + UpdateAction, + UpdateSettingsAction, } from './actions'; import type { UsersList } from '~/core/api'; type Action = ReceiveAction | UpdateAction | UpdateSettingsAction; export type SettingsState = { - readonly runQualityChecks: boolean; - readonly forceSuggestions: boolean; + readonly runQualityChecks: boolean; + readonly forceSuggestions: boolean; }; const initialSettings: SettingsState = { - runQualityChecks: true, - forceSuggestions: true, + runQualityChecks: true, + forceSuggestions: true, }; function settings( - state: SettingsState = initialSettings, - action: Action, + state: SettingsState = initialSettings, + action: Action, ): SettingsState { - switch (action.type) { - case UPDATE: - if (!action.data.settings) { - return state; - } - return { - runQualityChecks: action.data.settings.quality_checks, - forceSuggestions: action.data.settings.force_suggestions, - }; - case UPDATE_SETTINGS: - return { - ...state, - ...action.settings, - }; - default: - return state; - } + switch (action.type) { + case UPDATE: + if (!action.data.settings) { + return state; + } + return { + runQualityChecks: action.data.settings.quality_checks, + forceSuggestions: action.data.settings.force_suggestions, + }; + case UPDATE_SETTINGS: + return { + ...state, + ...action.settings, + }; + default: + return state; + } } export type Notification = { - readonly id: number; - readonly level: string; - readonly unread: string; - readonly description: { - readonly content: string; - readonly is_comment: boolean; - }; - readonly verb: string; - readonly date: string; - readonly date_iso: string; - readonly actor: { - readonly anchor: string; - readonly url: string; - }; - readonly target: { - readonly anchor: string; - readonly url: string; - }; + readonly id: number; + readonly level: string; + readonly unread: string; + readonly description: { + readonly content: string; + readonly is_comment: boolean; + }; + readonly verb: string; + readonly date: string; + readonly date_iso: string; + readonly actor: { + readonly anchor: string; + readonly url: string; + }; + readonly target: { + readonly anchor: string; + readonly url: string; + }; }; export type Notifications = { - has_unread: boolean; - notifications: Array; - unread_count: string; + has_unread: boolean; + notifications: Array; + unread_count: string; }; export type UserState = { - readonly isAuthenticated: boolean; - readonly isAdmin: boolean; - readonly id: string; - readonly displayName: string; - readonly nameOrEmail: string; - readonly email: string; - readonly username: string; - readonly managerForLocales: Array; - readonly translatorForLocales: Array; - readonly translatorForProjects: Record; - readonly settings: SettingsState; - readonly tourStatus: number | null | undefined; - readonly hasDismissedAddonPromotion: boolean; - readonly signInURL: string; - readonly signOutURL: string; - readonly gravatarURLSmall: string; - readonly gravatarURLBig: string; - readonly notifications: Notifications; - readonly users: Array; + readonly isAuthenticated: boolean; + readonly isAdmin: boolean; + readonly id: string; + readonly displayName: string; + readonly nameOrEmail: string; + readonly email: string; + readonly username: string; + readonly managerForLocales: Array; + readonly translatorForLocales: Array; + readonly translatorForProjects: Record; + readonly settings: SettingsState; + readonly tourStatus: number | null | undefined; + readonly hasDismissedAddonPromotion: boolean; + readonly signInURL: string; + readonly signOutURL: string; + readonly gravatarURLSmall: string; + readonly gravatarURLBig: string; + readonly notifications: Notifications; + readonly users: Array; }; const initial: UserState = { - isAuthenticated: false, - isAdmin: false, - id: '', - displayName: '', - nameOrEmail: '', - email: '', - username: '', - managerForLocales: [], - translatorForLocales: [], - translatorForProjects: {}, - settings: initialSettings, - tourStatus: null, - hasDismissedAddonPromotion: false, - signInURL: '', - signOutURL: '', - gravatarURLSmall: '', - gravatarURLBig: '', - notifications: { - has_unread: false, - notifications: [], - unread_count: '0', - }, - users: [], + isAuthenticated: false, + isAdmin: false, + id: '', + displayName: '', + nameOrEmail: '', + email: '', + username: '', + managerForLocales: [], + translatorForLocales: [], + translatorForProjects: {}, + settings: initialSettings, + tourStatus: null, + hasDismissedAddonPromotion: false, + signInURL: '', + signOutURL: '', + gravatarURLSmall: '', + gravatarURLBig: '', + notifications: { + has_unread: false, + notifications: [], + unread_count: '0', + }, + users: [], }; export default function reducer( - state: UserState = initial, - action: Action, + state: UserState = initial, + action: Action, ): UserState { - switch (action.type) { - case RECEIVE_USERS: - return { - ...state, - users: action.users, - }; - case UPDATE: - return { - ...state, - isAuthenticated: action.data.is_authenticated, - isAdmin: action.data.is_admin, - id: action.data.id, - displayName: action.data.display_name, - nameOrEmail: action.data.name_or_email, - email: action.data.email, - username: action.data.username, - managerForLocales: action.data.manager_for_locales, - translatorForLocales: action.data.translator_for_locales, - translatorForProjects: action.data.translator_for_projects, - settings: settings(state.settings, action), - tourStatus: action.data.tour_status, - hasDismissedAddonPromotion: - action.data.has_dismissed_addon_promotion, - signInURL: action.data.login_url, - signOutURL: action.data.logout_url, - gravatarURLSmall: action.data.gravatar_url_small, - gravatarURLBig: action.data.gravatar_url_big, - notifications: action.data.notifications, - }; - case UPDATE_SETTINGS: - return { - ...state, - settings: settings(state.settings, action), - }; - default: - return state; - } + switch (action.type) { + case RECEIVE_USERS: + return { + ...state, + users: action.users, + }; + case UPDATE: + return { + ...state, + isAuthenticated: action.data.is_authenticated, + isAdmin: action.data.is_admin, + id: action.data.id, + displayName: action.data.display_name, + nameOrEmail: action.data.name_or_email, + email: action.data.email, + username: action.data.username, + managerForLocales: action.data.manager_for_locales, + translatorForLocales: action.data.translator_for_locales, + translatorForProjects: action.data.translator_for_projects, + settings: settings(state.settings, action), + tourStatus: action.data.tour_status, + hasDismissedAddonPromotion: action.data.has_dismissed_addon_promotion, + signInURL: action.data.login_url, + signOutURL: action.data.logout_url, + gravatarURLSmall: action.data.gravatar_url_small, + gravatarURLBig: action.data.gravatar_url_big, + notifications: action.data.notifications, + }; + case UPDATE_SETTINGS: + return { + ...state, + settings: settings(state.settings, action), + }; + default: + return state; + } } diff --git a/translate/src/core/user/selectors.test.js b/translate/src/core/user/selectors.test.js index 4eab24775..0ab5aa6eb 100644 --- a/translate/src/core/user/selectors.test.js +++ b/translate/src/core/user/selectors.test.js @@ -1,58 +1,58 @@ import { _isTranslator } from './selectors'; describe('isTranslator', () => { - it('returns false for non-authenticated users', () => { - expect( - _isTranslator( - { isAuthenticated: false }, - { code: 'mylocale' }, - { slug: 'myproject' }, - ), - ).toBeFalsy(); - }); + it('returns false for non-authenticated users', () => { + expect( + _isTranslator( + { isAuthenticated: false }, + { code: 'mylocale' }, + { slug: 'myproject' }, + ), + ).toBeFalsy(); + }); - it('returns true if user is a manager of the locale', () => { - expect( - _isTranslator( - { - isAuthenticated: true, - managerForLocales: ['mylocale'], - translatorForLocales: [], - translatorForProjects: {}, - }, - { code: 'mylocale' }, - { slug: 'myproject' }, - ), - ).toBeTruthy(); - }); + it('returns true if user is a manager of the locale', () => { + expect( + _isTranslator( + { + isAuthenticated: true, + managerForLocales: ['mylocale'], + translatorForLocales: [], + translatorForProjects: {}, + }, + { code: 'mylocale' }, + { slug: 'myproject' }, + ), + ).toBeTruthy(); + }); - it('returns true if user is a translator of the locale', () => { - expect( - _isTranslator( - { - isAuthenticated: true, - managerForLocales: [], - translatorForLocales: ['mylocale'], - translatorForProjects: {}, - }, - { code: 'mylocale' }, - { slug: 'myproject' }, - ), - ).toBeTruthy(); - }); + it('returns true if user is a translator of the locale', () => { + expect( + _isTranslator( + { + isAuthenticated: true, + managerForLocales: [], + translatorForLocales: ['mylocale'], + translatorForProjects: {}, + }, + { code: 'mylocale' }, + { slug: 'myproject' }, + ), + ).toBeTruthy(); + }); - it('returns true if user is a translator for project-locale', () => { - expect( - _isTranslator( - { - isAuthenticated: true, - managerForLocales: ['localeA'], - translatorForLocales: ['localeB'], - translatorForProjects: { 'mylocale-myproject': true }, - }, - { code: 'mylocale' }, - { slug: 'myproject' }, - ), - ).toBeTruthy(); - }); + it('returns true if user is a translator for project-locale', () => { + expect( + _isTranslator( + { + isAuthenticated: true, + managerForLocales: ['localeA'], + translatorForLocales: ['localeB'], + translatorForProjects: { 'mylocale-myproject': true }, + }, + { code: 'mylocale' }, + { slug: 'myproject' }, + ), + ).toBeTruthy(); + }); }); diff --git a/translate/src/core/user/selectors.ts b/translate/src/core/user/selectors.ts index 13e9bcb49..6bdb1638f 100644 --- a/translate/src/core/user/selectors.ts +++ b/translate/src/core/user/selectors.ts @@ -12,25 +12,25 @@ const localeSelector = (state: RootState) => state[LOCALE_NAME]; const projectSelector = (state: RootState) => state[PROJECT_NAME]; export function _isTranslator( - user: ReturnType, - locale: ReturnType, - project: ReturnType, + user: ReturnType, + locale: ReturnType, + project: ReturnType, ): boolean { - const localeProject = locale.code + '-' + project.slug; + const localeProject = locale.code + '-' + project.slug; - if (!user.isAuthenticated) { - return false; - } + if (!user.isAuthenticated) { + return false; + } - if (user.managerForLocales.indexOf(locale.code) !== -1) { - return true; - } + if (user.managerForLocales.indexOf(locale.code) !== -1) { + return true; + } - if (Object.hasOwnProperty.call(user.translatorForProjects, localeProject)) { - return user.translatorForProjects[localeProject]; - } + if (Object.hasOwnProperty.call(user.translatorForProjects, localeProject)) { + return user.translatorForProjects[localeProject]; + } - return user.translatorForLocales.indexOf(locale.code) !== -1; + return user.translatorForLocales.indexOf(locale.code) !== -1; } /** @@ -38,12 +38,12 @@ export function _isTranslator( * and locale. */ export const isTranslator = createSelector( - userSelector, - localeSelector, - projectSelector, - _isTranslator, + userSelector, + localeSelector, + projectSelector, + _isTranslator, ); export default { - isTranslator, + isTranslator, }; diff --git a/translate/src/core/utils/asLocaleString.ts b/translate/src/core/utils/asLocaleString.ts index 74959af09..d3cb58119 100644 --- a/translate/src/core/utils/asLocaleString.ts +++ b/translate/src/core/utils/asLocaleString.ts @@ -7,5 +7,5 @@ * @returns {string} A language-sensitive representation of the number. */ export default function asLocaleString(number: number): string { - return Number(number).toLocaleString('en-GB'); + return Number(number).toLocaleString('en-GB'); } diff --git a/translate/src/core/utils/components/withActionsDisabled.test.js b/translate/src/core/utils/components/withActionsDisabled.test.js index 3d3be4c01..f4f141f5e 100644 --- a/translate/src/core/utils/components/withActionsDisabled.test.js +++ b/translate/src/core/utils/components/withActionsDisabled.test.js @@ -4,34 +4,34 @@ import { shallow } from 'enzyme'; import withActionsDisabled from './withActionsDisabled'; describe('withActionsDisabled', () => { - class FakeComp extends React.Component {} - const WrappedComp = withActionsDisabled(FakeComp); + class FakeComp extends React.Component {} + const WrappedComp = withActionsDisabled(FakeComp); - it('passes internal props correctly', () => { - const wrapper = shallow(); + it('passes internal props correctly', () => { + const wrapper = shallow(); - expect(wrapper.props().isActionDisabled).toEqual(false); - expect(wrapper.props().disableAction).toBeInstanceOf(Function); - }); + expect(wrapper.props().isActionDisabled).toEqual(false); + expect(wrapper.props().disableAction).toBeInstanceOf(Function); + }); - it('passes other props along', () => { - const wrapper = shallow(); + it('passes other props along', () => { + const wrapper = shallow(); - expect(wrapper.props().foo).toEqual('bar'); - expect(wrapper.props().baz).toEqual(42); - }); + expect(wrapper.props().foo).toEqual('bar'); + expect(wrapper.props().baz).toEqual(42); + }); - it('turns action off until next render', () => { - const wrapper = shallow(); + it('turns action off until next render', () => { + const wrapper = shallow(); - expect(wrapper.props().isActionDisabled).toEqual(false); + expect(wrapper.props().isActionDisabled).toEqual(false); - // Disable the action. - wrapper.instance().disableAction(); - expect(wrapper.props().isActionDisabled).toEqual(true); + // Disable the action. + wrapper.instance().disableAction(); + expect(wrapper.props().isActionDisabled).toEqual(true); - // Trigger a render. - wrapper.setProps({ foo: 'var' }); - expect(wrapper.props().isActionDisabled).toEqual(false); - }); + // Trigger a render. + wrapper.setProps({ foo: 'var' }); + expect(wrapper.props().isActionDisabled).toEqual(false); + }); }); diff --git a/translate/src/core/utils/components/withActionsDisabled.tsx b/translate/src/core/utils/components/withActionsDisabled.tsx index d3c8f2518..89c403e51 100644 --- a/translate/src/core/utils/components/withActionsDisabled.tsx +++ b/translate/src/core/utils/components/withActionsDisabled.tsx @@ -4,52 +4,52 @@ import { $Diff } from 'utility-types'; import * as React from 'react'; type Props = { - isActionDisabled: boolean | void; - disableAction: (() => void) | void; + isActionDisabled: boolean | void; + disableAction: (() => void) | void; }; type State = { - isActionDisabled: boolean; + isActionDisabled: boolean; }; export default function withActionsDisabled( - WrappedComponent: React.ComponentType, + WrappedComponent: React.ComponentType, ): React.ComponentType<$Diff> { - class WithActionsDisabled extends React.Component< - $Diff, - State - > { - constructor(props: $Diff) { - super(props); + class WithActionsDisabled extends React.Component< + $Diff, + State + > { + constructor(props: $Diff) { + super(props); - this.state = { - isActionDisabled: false, - }; - } - - componentDidUpdate(prevProps: $Diff, prevState: State) { - if (prevState.isActionDisabled) { - this.setState({ isActionDisabled: false }); - } - } - - disableAction = () => { - this.setState({ isActionDisabled: true }); - }; - - render() { - return ( - - ); - } + this.state = { + isActionDisabled: false, + }; } - WithActionsDisabled.displayName = `WithActionsDisabled(${ - WrappedComponent.displayName || WrappedComponent.name || 'Component' - })`; - return WithActionsDisabled; + componentDidUpdate(prevProps: $Diff, prevState: State) { + if (prevState.isActionDisabled) { + this.setState({ isActionDisabled: false }); + } + } + + disableAction = () => { + this.setState({ isActionDisabled: true }); + }; + + render() { + return ( + + ); + } + } + + WithActionsDisabled.displayName = `WithActionsDisabled(${ + WrappedComponent.displayName || WrappedComponent.name || 'Component' + })`; + return WithActionsDisabled; } diff --git a/translate/src/core/utils/fluent/areSupportedElements.ts b/translate/src/core/utils/fluent/areSupportedElements.ts index 73372da0b..4daed7700 100644 --- a/translate/src/core/utils/fluent/areSupportedElements.ts +++ b/translate/src/core/utils/fluent/areSupportedElements.ts @@ -9,18 +9,18 @@ import type { PatternElement } from '@fluent/syntax'; * - select expressions, whose variants are simple elements */ export default function areSupportedElements( - elements: Array, + elements: Array, ): boolean { - return elements.every((element) => { - return ( - isSimpleElement(element) || - (element.type === 'Placeable' && - element.expression.type === 'SelectExpression' && - element.expression.variants.every((variant) => { - return variant.value.elements.every((element) => - isSimpleElement(element), - ); - })) - ); - }); + return elements.every((element) => { + return ( + isSimpleElement(element) || + (element.type === 'Placeable' && + element.expression.type === 'SelectExpression' && + element.expression.variants.every((variant) => { + return variant.value.elements.every((element) => + isSimpleElement(element), + ); + })) + ); + }); } diff --git a/translate/src/core/utils/fluent/convertSyntax.test.js b/translate/src/core/utils/fluent/convertSyntax.test.js index df1badc2a..0f0c0ce5e 100644 --- a/translate/src/core/utils/fluent/convertSyntax.test.js +++ b/translate/src/core/utils/fluent/convertSyntax.test.js @@ -1,142 +1,142 @@ import { - getComplexFromRich, - getComplexFromSimple, - getRichFromComplex, - getSimpleFromComplex, + getComplexFromRich, + getComplexFromSimple, + getRichFromComplex, + getSimpleFromComplex, } from './convertSyntax'; import getEmptyMessage from './getEmptyMessage'; import parser from './parser'; describe('getComplexFromRich', () => { - it('converts rich translation to complex', () => { - // Input is a Fluent AST. - const current = parser.parseEntry('title = Mon titre'); - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('converts rich translation to complex', () => { + // Input is a Fluent AST. + const current = parser.parseEntry('title = Mon titre'); + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getComplexFromRich(current, original, initial); + const res = getComplexFromRich(current, original, initial); - expect(res[0]).toEqual('title = Mon titre\n'); - }); + expect(res[0]).toEqual('title = Mon titre\n'); + }); - it('returns the correct initial translation when one is provided', () => { - const current = parser.parseEntry('title = Mon titre'); - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('returns the correct initial translation when one is provided', () => { + const current = parser.parseEntry('title = Mon titre'); + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getComplexFromRich(current, original, initial); + const res = getComplexFromRich(current, original, initial); - expect(res[1]).toEqual('title = Mien titre\n'); - }); + expect(res[1]).toEqual('title = Mien titre\n'); + }); - it('returns the correct initial translation when none exist', () => { - const current = parser.parseEntry('title = Mon titre'); - const original = 'title = My title'; - // Initial is empty, there's no active translation. - const initial = ''; + it('returns the correct initial translation when none exist', () => { + const current = parser.parseEntry('title = Mon titre'); + const original = 'title = My title'; + // Initial is empty, there's no active translation. + const initial = ''; - const res = getComplexFromRich(current, original, initial); + const res = getComplexFromRich(current, original, initial); - expect(res[1]).toEqual('title = \n'); - }); + expect(res[1]).toEqual('title = \n'); + }); }); describe('getComplexFromSimple', () => { - it('converts simple translation to complex', () => { - const current = 'Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('converts simple translation to complex', () => { + const current = 'Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getComplexFromSimple(current, original, initial); + const res = getComplexFromSimple(current, original, initial); - expect(res[0]).toEqual('title = Mon titre\n'); - }); + expect(res[0]).toEqual('title = Mon titre\n'); + }); - it('returns the correct initial translation when one is provided', () => { - const current = 'Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('returns the correct initial translation when one is provided', () => { + const current = 'Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getComplexFromSimple(current, original, initial); + const res = getComplexFromSimple(current, original, initial); - expect(res[1]).toEqual('title = Mien titre'); - }); + expect(res[1]).toEqual('title = Mien titre'); + }); - it('returns the correct initial translation when none exist', () => { - const current = 'Mon titre'; - const original = 'title = My title'; - // Initial is empty, there's no active translation. - const initial = ''; + it('returns the correct initial translation when none exist', () => { + const current = 'Mon titre'; + const original = 'title = My title'; + // Initial is empty, there's no active translation. + const initial = ''; - const res = getComplexFromSimple(current, original, initial); + const res = getComplexFromSimple(current, original, initial); - expect(res[1]).toEqual('title = \n'); - }); + expect(res[1]).toEqual('title = \n'); + }); }); describe('getRichFromComplex', () => { - it('converts complex translation to rich', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('converts complex translation to rich', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getRichFromComplex(current, original, initial); + const res = getRichFromComplex(current, original, initial); - expect(res[0]).toEqual(parser.parseEntry('title = Mon titre\n')); - }); + expect(res[0]).toEqual(parser.parseEntry('title = Mon titre\n')); + }); - it('returns the correct initial translation when one is provided', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('returns the correct initial translation when one is provided', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getRichFromComplex(current, original, initial); + const res = getRichFromComplex(current, original, initial); - expect(res[1]).toEqual(parser.parseEntry('title = Mien titre')); - }); + expect(res[1]).toEqual(parser.parseEntry('title = Mien titre')); + }); - it('returns the correct initial translation when none exist', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - // Initial is empty, there's no active translation. - const initial = ''; + it('returns the correct initial translation when none exist', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + // Initial is empty, there's no active translation. + const initial = ''; - const res = getRichFromComplex(current, original, initial); + const res = getRichFromComplex(current, original, initial); - expect(res[1]).toEqual(getEmptyMessage(parser.parseEntry('title = a'))); - }); + expect(res[1]).toEqual(getEmptyMessage(parser.parseEntry('title = a'))); + }); }); describe('getSimpleFromComplex', () => { - it('converts complex translation to simple', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('converts complex translation to simple', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getSimpleFromComplex(current, original, initial); + const res = getSimpleFromComplex(current, original, initial); - expect(res[0]).toEqual('Mon titre'); - }); + expect(res[0]).toEqual('Mon titre'); + }); - it('returns the correct initial translation when one is provided', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - const initial = 'title = Mien titre'; + it('returns the correct initial translation when one is provided', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + const initial = 'title = Mien titre'; - const res = getSimpleFromComplex(current, original, initial); + const res = getSimpleFromComplex(current, original, initial); - expect(res[1]).toEqual('Mien titre'); - }); + expect(res[1]).toEqual('Mien titre'); + }); - it('returns the correct initial translation when none exist', () => { - const current = 'title = Mon titre'; - const original = 'title = My title'; - // Initial is empty, there's no active translation. - const initial = ''; + it('returns the correct initial translation when none exist', () => { + const current = 'title = Mon titre'; + const original = 'title = My title'; + // Initial is empty, there's no active translation. + const initial = ''; - const res = getSimpleFromComplex(current, original, initial); + const res = getSimpleFromComplex(current, original, initial); - expect(res[1]).toEqual(''); - }); + expect(res[1]).toEqual(''); + }); }); diff --git a/translate/src/core/utils/fluent/convertSyntax.ts b/translate/src/core/utils/fluent/convertSyntax.ts index 09429988c..4fac2284f 100644 --- a/translate/src/core/utils/fluent/convertSyntax.ts +++ b/translate/src/core/utils/fluent/convertSyntax.ts @@ -11,101 +11,98 @@ import type { Locale } from '~/core/locale'; type SyntaxType = 'simple' | 'rich' | 'complex' | ''; export function getSimpleFromComplex( - current: string, - original: string, - initial: string, + current: string, + original: string, + initial: string, ): [string, string] { - let translationContent = getSimplePreview(current); - let initialContent = getSimplePreview(initial); + let translationContent = getSimplePreview(current); + let initialContent = getSimplePreview(initial); - // If any of the contents are junk, discard them. - if (translationContent === current) { - translationContent = ''; - } - if (initialContent === initial) { - initialContent = ''; - } + // If any of the contents are junk, discard them. + if (translationContent === current) { + translationContent = ''; + } + if (initialContent === initial) { + initialContent = ''; + } - return [translationContent, initialContent]; + return [translationContent, initialContent]; } export function getComplexFromSimple( - current: string, - original: string, - initial: string, - locale: Locale, + current: string, + original: string, + initial: string, + locale: Locale, ): [string, string] { - let initialContent = initial; + let initialContent = initial; - const translationContent = serializer.serializeEntry( - getReconstructedMessage(original, current), + const translationContent = serializer.serializeEntry( + getReconstructedMessage(original, current), + ); + + // If there is no active translation (it's an untranslated string) + // we make the initial translation an empty fluent message to avoid + // showing unchanged content warnings. + if (!initialContent) { + initialContent = serializer.serializeEntry( + getEmptyMessage(parser.parseEntry(original), locale), ); + } - // If there is no active translation (it's an untranslated string) - // we make the initial translation an empty fluent message to avoid - // showing unchanged content warnings. - if (!initialContent) { - initialContent = serializer.serializeEntry( - getEmptyMessage(parser.parseEntry(original), locale), - ); - } - - return [translationContent, initialContent]; + return [translationContent, initialContent]; } export function getRichFromComplex( - current: string, - original: string, - initial: string, - locale: Locale, + current: string, + original: string, + initial: string, + locale: Locale, ): [Entry, Entry] { - let translationContent = flattenMessage(parser.parseEntry(current)); + let translationContent = flattenMessage(parser.parseEntry(current)); - // If the parsed content is invalid, create an empty message instead. - // Note that this should be replaced with a check that prevents - // turning back to the Rich editor, in order to avoid losing data. - if (translationContent.type === 'Junk') { - translationContent = getEmptyMessage( - parser.parseEntry(original), - locale, - ); - } + // If the parsed content is invalid, create an empty message instead. + // Note that this should be replaced with a check that prevents + // turning back to the Rich editor, in order to avoid losing data. + if (translationContent.type === 'Junk') { + translationContent = getEmptyMessage(parser.parseEntry(original), locale); + } - let initialContent = parser.parseEntry(initial); + let initialContent = parser.parseEntry(initial); - // If there is no active translation for this entity, create an - // empty message to serve as the reference for unsaved changes. - if (initialContent.type === 'Junk') { - initialContent = getEmptyMessage(parser.parseEntry(original), locale); - } else { - initialContent = flattenMessage(initialContent); - } + // If there is no active translation for this entity, create an + // empty message to serve as the reference for unsaved changes. + if (initialContent.type === 'Junk') { + initialContent = getEmptyMessage(parser.parseEntry(original), locale); + } else { + initialContent = flattenMessage(initialContent); + } - return [translationContent, initialContent]; + return [translationContent, initialContent]; } export function getComplexFromRich( - current: Entry, - original: string, - initial: string, - locale: Locale, + current: Entry, + original: string, + initial: string, + locale: Locale, ): [string, string] { - const translationContent = serializer.serializeEntry(current); + const translationContent = serializer.serializeEntry(current); - let initialEntry: Entry; + let initialEntry: Entry; - // If there is no active translation (it's an untranslated string) - // we make the initial translation an empty fluent message to avoid - // showing unchanged content warnings. - if (!initial) { - initialEntry = getEmptyMessage(parser.parseEntry(original), locale); - } else { - initialEntry = flattenMessage(parser.parseEntry(initial)); - } + // If there is no active translation (it's an untranslated string) + // we make the initial translation an empty fluent message to avoid + // showing unchanged content warnings. + if (!initial) { + initialEntry = getEmptyMessage(parser.parseEntry(original), locale); + } else { + initialEntry = flattenMessage(parser.parseEntry(initial)); + } - const initialContent = serializer.serializeEntry(initialEntry); + const initialContent = serializer.serializeEntry(initialEntry); - return [translationContent, initialContent]; + return [translationContent, initialContent]; } /** @@ -122,40 +119,40 @@ export function getComplexFromRich( * @returns {[ string | Entry, string ]} The converted current translation and initial translation. */ export default function convertSyntax( - fromSyntax: SyntaxType, - toSyntax: SyntaxType, - current: string | Entry, - original: string, - initial: string, - locale: Locale, + fromSyntax: SyntaxType, + toSyntax: SyntaxType, + current: string | Entry, + original: string, + initial: string, + locale: Locale, ): [Entry, Entry] | [string, string] { - if ( - fromSyntax === 'complex' && - toSyntax === 'simple' && - typeof current === 'string' - ) { - return getSimpleFromComplex(current, original, initial); - } else if ( - fromSyntax === 'simple' && - toSyntax === 'complex' && - typeof current === 'string' - ) { - return getComplexFromSimple(current, original, initial, locale); - } else if ( - fromSyntax === 'complex' && - toSyntax === 'rich' && - typeof current === 'string' - ) { - return getRichFromComplex(current, original, initial, locale); - } else if ( - fromSyntax === 'rich' && - toSyntax === 'complex' && - typeof current !== 'string' - ) { - return getComplexFromRich(current, original, initial, locale); - } + if ( + fromSyntax === 'complex' && + toSyntax === 'simple' && + typeof current === 'string' + ) { + return getSimpleFromComplex(current, original, initial); + } else if ( + fromSyntax === 'simple' && + toSyntax === 'complex' && + typeof current === 'string' + ) { + return getComplexFromSimple(current, original, initial, locale); + } else if ( + fromSyntax === 'complex' && + toSyntax === 'rich' && + typeof current === 'string' + ) { + return getRichFromComplex(current, original, initial, locale); + } else if ( + fromSyntax === 'rich' && + toSyntax === 'complex' && + typeof current !== 'string' + ) { + return getComplexFromRich(current, original, initial, locale); + } - throw new Error( - `Unsupported conversion: from '${fromSyntax}' to '${toSyntax}'`, - ); + throw new Error( + `Unsupported conversion: from '${fromSyntax}' to '${toSyntax}'`, + ); } diff --git a/translate/src/core/utils/fluent/extractAccessKeyCandidates.test.js b/translate/src/core/utils/fluent/extractAccessKeyCandidates.test.js index b4957edad..199af3065 100644 --- a/translate/src/core/utils/fluent/extractAccessKeyCandidates.test.js +++ b/translate/src/core/utils/fluent/extractAccessKeyCandidates.test.js @@ -3,71 +3,69 @@ import flattenMessage from './flattenMessage'; import parser from './parser'; describe('extractAccessKeyCandidates', () => { - it('returns null if the message has no attributes', () => { - const message = flattenMessage(parser.parseEntry('title = Title')); - const res = extractAccessKeyCandidates(message); + it('returns null if the message has no attributes', () => { + const message = flattenMessage(parser.parseEntry('title = Title')); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(null); - }); + expect(res).toEqual(null); + }); - it('returns null if the message has no accesskey attribute', () => { - const input = 'title =' + '\n .foo = Bar'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('returns null if the message has no accesskey attribute', () => { + const input = 'title =' + '\n .foo = Bar'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(null); - }); + expect(res).toEqual(null); + }); - it('returns null if the message has no label attribute or value', () => { - const input = 'title =' + '\n .foo = Bar' + '\n .accesskey = B'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('returns null if the message has no label attribute or value', () => { + const input = 'title =' + '\n .foo = Bar' + '\n .accesskey = B'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(null); - }); + expect(res).toEqual(null); + }); - it('returns a list of access keys from the message value', () => { - const input = 'title = Candidates' + '\n .accesskey = B'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('returns a list of access keys from the message value', () => { + const input = 'title = Candidates' + '\n .accesskey = B'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); - }); + expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); + }); - it('returns a list of access keys from the label attribute', () => { - const input = - 'title = Title' + - '\n .label = Candidates' + - '\n .accesskey = B'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('returns a list of access keys from the label attribute', () => { + const input = + 'title = Title' + '\n .label = Candidates' + '\n .accesskey = B'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); - }); + expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); + }); - it('Does not take Placeables into account when generating candidates', () => { - const input = - 'title = Title' + - '\n .label = Candidates { brand }' + - '\n .accesskey = B'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('Does not take Placeables into account when generating candidates', () => { + const input = + 'title = Title' + + '\n .label = Candidates { brand }' + + '\n .accesskey = B'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); - }); + expect(res).toEqual(['C', 'a', 'n', 'd', 'i', 't', 'e', 's']); + }); - it('returns a list of access keys from all label attribute variants', () => { - const input = - 'title = Title' + - '\n .label =' + - '\n { PLATFORM() ->' + - '\n [windows] Ctrl' + - '\n *[other] Cmd' + - '\n }' + - '\n .accesskey = C'; - const message = flattenMessage(parser.parseEntry(input)); - const res = extractAccessKeyCandidates(message); + it('returns a list of access keys from all label attribute variants', () => { + const input = + 'title = Title' + + '\n .label =' + + '\n { PLATFORM() ->' + + '\n [windows] Ctrl' + + '\n *[other] Cmd' + + '\n }' + + '\n .accesskey = C'; + const message = flattenMessage(parser.parseEntry(input)); + const res = extractAccessKeyCandidates(message); - expect(res).toEqual(['C', 't', 'r', 'l', 'm', 'd']); - }); + expect(res).toEqual(['C', 't', 'r', 'l', 'm', 'd']); + }); }); diff --git a/translate/src/core/utils/fluent/extractAccessKeyCandidates.ts b/translate/src/core/utils/fluent/extractAccessKeyCandidates.ts index 30ab2e8f2..f42b4a0e4 100644 --- a/translate/src/core/utils/fluent/extractAccessKeyCandidates.ts +++ b/translate/src/core/utils/fluent/extractAccessKeyCandidates.ts @@ -6,29 +6,29 @@ import type { Entry, PatternElement, TextElement } from '@fluent/syntax'; * Returns a flat list of Text Elements, either standalone or from SelectExpression variants */ function getTextElementsRecursivelly( - elements: Array, + elements: Array, ): TextElement[] { - const textElements = elements.map( - (element): TextElement | TextElement[] | TextElement[][] => { - if (element.type === 'TextElement') { - return element; - } + const textElements = elements.map( + (element): TextElement | TextElement[] | TextElement[][] => { + if (element.type === 'TextElement') { + return element; + } - if ( - element.type === 'Placeable' && - element.expression && - element.expression.type === 'SelectExpression' - ) { - return element.expression.variants.map((variant) => { - return getTextElementsRecursivelly(variant.value.elements); - }); - } + if ( + element.type === 'Placeable' && + element.expression && + element.expression.type === 'SelectExpression' + ) { + return element.expression.variants.map((variant) => { + return getTextElementsRecursivelly(variant.value.elements); + }); + } - return null; - }, - ); + return null; + }, + ); - return flattenDeep(textElements); + return flattenDeep(textElements); } /** @@ -42,59 +42,57 @@ function getTextElementsRecursivelly( * @returns {?Array} A list of access key candidates. */ export default function extractAccessKeyCandidates( - message: Entry, + message: Entry, ): Array | null { - // Safeguard against non-message Fluent entries - if (message.type !== 'Message') { - return null; - } - // If message has no attributes, return null - if (!message.attributes) { - return null; - } + // Safeguard against non-message Fluent entries + if (message.type !== 'Message') { + return null; + } + // If message has no attributes, return null + if (!message.attributes) { + return null; + } - const attributeIDs = message.attributes.map( - (attribute) => attribute.id.name, + const attributeIDs = message.attributes.map((attribute) => attribute.id.name); + + // If message has no accesskey attribute, return null + if (attributeIDs.indexOf('accesskey') === -1) { + return null; + } + + // Generate access key candidates from the 'label' attribute or the message value + let source = null; + if (message.attributes && attributeIDs.indexOf('label') !== -1) { + source = message.attributes.find( + (attribute) => attribute.id.name === 'label', ); + } else if (message.value) { + source = message; + } - // If message has no accesskey attribute, return null - if (attributeIDs.indexOf('accesskey') === -1) { - return null; + if (!source || !source.value) { + return null; + } + + // Only take TextElements + const textElements = getTextElementsRecursivelly(source.value.elements); + + // Collect values of TextElements + const values = textElements.map((element) => { + let value = ''; + if (element && typeof element.value === 'string') { + value = element.value + // Exclude placeables (message is flat). See bug 1447103 for details. + .replace(/{[^}]*}/g, '') + // Exclude whitespace + .replace(/\s/g, ''); } + return value; + }); - // Generate access key candidates from the 'label' attribute or the message value - let source = null; - if (message.attributes && attributeIDs.indexOf('label') !== -1) { - source = message.attributes.find( - (attribute) => attribute.id.name === 'label', - ); - } else if (message.value) { - source = message; - } + // Create a list of single-character keys + const keys = values.join('').split(''); - if (!source || !source.value) { - return null; - } - - // Only take TextElements - const textElements = getTextElementsRecursivelly(source.value.elements); - - // Collect values of TextElements - const values = textElements.map((element) => { - let value = ''; - if (element && typeof element.value === 'string') { - value = element.value - // Exclude placeables (message is flat). See bug 1447103 for details. - .replace(/{[^}]*}/g, '') - // Exclude whitespace - .replace(/\s/g, ''); - } - return value; - }); - - // Create a list of single-character keys - const keys = values.join('').split(''); - - // Extract unique candidates - return keys.filter((key, i, array) => array.indexOf(key) === i); + // Extract unique candidates + return keys.filter((key, i, array) => array.indexOf(key) === i); } diff --git a/translate/src/core/utils/fluent/flattenMessage.test.js b/translate/src/core/utils/fluent/flattenMessage.test.js index 3f0615728..b451297fe 100644 --- a/translate/src/core/utils/fluent/flattenMessage.test.js +++ b/translate/src/core/utils/fluent/flattenMessage.test.js @@ -2,127 +2,123 @@ import flattenMessage from './flattenMessage'; import parser from './parser'; describe('flattenMessage', () => { - it('does not modify value with single element', () => { - const message = parser.parseEntry('title = My Title'); - const res = flattenMessage(message); + it('does not modify value with single element', () => { + const message = parser.parseEntry('title = My Title'); + const res = flattenMessage(message); - expect(res.value.elements).toHaveLength(1); - expect(res.value.elements[0].value).toEqual('My Title'); - }); + expect(res.value.elements).toHaveLength(1); + expect(res.value.elements[0].value).toEqual('My Title'); + }); - it('does not modify attributes with single element', () => { - const message = parser.parseEntry('title =\n .foo = Bar'); - const res = flattenMessage(message); + it('does not modify attributes with single element', () => { + const message = parser.parseEntry('title =\n .foo = Bar'); + const res = flattenMessage(message); - expect(res.attributes).toHaveLength(1); - expect(res.attributes[0].value.elements).toHaveLength(1); - expect(res.attributes[0].value.elements[0].value).toEqual('Bar'); - }); + expect(res.attributes).toHaveLength(1); + expect(res.attributes[0].value.elements).toHaveLength(1); + expect(res.attributes[0].value.elements[0].value).toEqual('Bar'); + }); - it('does not modify value with a single select expression', () => { - const input = - 'my-entry =' + - '\n { PLATFORM() ->' + - '\n [variant] Hello!' + - '\n *[another-variant] World!' + - '\n }'; - const message = parser.parseEntry(input); - const res = flattenMessage(message); + it('does not modify value with a single select expression', () => { + const input = + 'my-entry =' + + '\n { PLATFORM() ->' + + '\n [variant] Hello!' + + '\n *[another-variant] World!' + + '\n }'; + const message = parser.parseEntry(input); + const res = flattenMessage(message); - expect(res.value.elements).toHaveLength(1); - expect( - res.value.elements[0].expression.variants[0].value.elements[0] - .value, - ).toEqual('Hello!'); - expect( - res.value.elements[0].expression.variants[1].value.elements[0] - .value, - ).toEqual('World!'); - }); + expect(res.value.elements).toHaveLength(1); + expect( + res.value.elements[0].expression.variants[0].value.elements[0].value, + ).toEqual('Hello!'); + expect( + res.value.elements[0].expression.variants[1].value.elements[0].value, + ).toEqual('World!'); + }); - it('flattens a value with several elements', () => { - const message = parser.parseEntry('title = My { $awesome } Title'); + it('flattens a value with several elements', () => { + const message = parser.parseEntry('title = My { $awesome } Title'); - expect(message.value.elements).toHaveLength(3); + expect(message.value.elements).toHaveLength(3); - const res = flattenMessage(message); + const res = flattenMessage(message); - expect(res.value.elements).toHaveLength(1); - expect(res.value.elements[0].value).toEqual('My { $awesome } Title'); - }); + expect(res.value.elements).toHaveLength(1); + expect(res.value.elements[0].value).toEqual('My { $awesome } Title'); + }); - it('flattens an attribute with several elements', () => { - const message = parser.parseEntry( - 'title =\n .foo = Bar { -foo } Baz', - ); + it('flattens an attribute with several elements', () => { + const message = parser.parseEntry('title =\n .foo = Bar { -foo } Baz'); - expect(message.attributes[0].value.elements).toHaveLength(3); + expect(message.attributes[0].value.elements).toHaveLength(3); - const res = flattenMessage(message); + const res = flattenMessage(message); - expect(res.attributes).toHaveLength(1); - expect(res.attributes[0].value.elements).toHaveLength(1); - expect(res.attributes[0].value.elements[0].value).toEqual( - 'Bar { -foo } Baz', - ); - }); + expect(res.attributes).toHaveLength(1); + expect(res.attributes[0].value.elements).toHaveLength(1); + expect(res.attributes[0].value.elements[0].value).toEqual( + 'Bar { -foo } Baz', + ); + }); - it('flattens value and attributes', () => { - const input = - 'batman = The { $dark } Knight' + - '\n .weapon = Brain and { -wayne-enterprise }' + - '\n .history = Lost { 2 } parents, has { 1 } "$alfred"'; - const message = parser.parseEntry(input); + it('flattens value and attributes', () => { + const input = + 'batman = The { $dark } Knight' + + '\n .weapon = Brain and { -wayne-enterprise }' + + '\n .history = Lost { 2 } parents, has { 1 } "$alfred"'; + const message = parser.parseEntry(input); - expect(message.value.elements).toHaveLength(3); - expect(message.attributes[0].value.elements).toHaveLength(2); - expect(message.attributes[1].value.elements).toHaveLength(5); + expect(message.value.elements).toHaveLength(3); + expect(message.attributes[0].value.elements).toHaveLength(2); + expect(message.attributes[1].value.elements).toHaveLength(5); - const res = flattenMessage(message); + const res = flattenMessage(message); - expect(res.value.elements).toHaveLength(1); - expect(res.value.elements[0].value).toEqual('The { $dark } Knight'); + expect(res.value.elements).toHaveLength(1); + expect(res.value.elements[0].value).toEqual('The { $dark } Knight'); - expect(res.attributes).toHaveLength(2); + expect(res.attributes).toHaveLength(2); - expect(res.attributes[0].value.elements).toHaveLength(1); - expect(res.attributes[0].value.elements[0].value).toEqual( - 'Brain and { -wayne-enterprise }', - ); + expect(res.attributes[0].value.elements).toHaveLength(1); + expect(res.attributes[0].value.elements[0].value).toEqual( + 'Brain and { -wayne-enterprise }', + ); - expect(res.attributes[1].value.elements).toHaveLength(1); - expect(res.attributes[1].value.elements[0].value).toEqual( - 'Lost { 2 } parents, has { 1 } "$alfred"', - ); - }); + expect(res.attributes[1].value.elements).toHaveLength(1); + expect(res.attributes[1].value.elements[0].value).toEqual( + 'Lost { 2 } parents, has { 1 } "$alfred"', + ); + }); - it('flattens values surrounding a select expression and select expression variants', () => { - const input = - 'my-entry =' + - '\n There { $num ->' + - '\n [one] is one email' + - '\n *[other] are { $num } emails' + - '\n } for { $awesome } { $gender ->' + - '\n *[masculine] him' + - '\n [feminine] her' + - '\n }'; - const message = parser.parseEntry(input); - const res = flattenMessage(message); + it('flattens values surrounding a select expression and select expression variants', () => { + const input = + 'my-entry =' + + '\n There { $num ->' + + '\n [one] is one email' + + '\n *[other] are { $num } emails' + + '\n } for { $awesome } { $gender ->' + + '\n *[masculine] him' + + '\n [feminine] her' + + '\n }'; + const message = parser.parseEntry(input); + const res = flattenMessage(message); - expect(res.value.elements).toHaveLength(3); + expect(res.value.elements).toHaveLength(3); - const selEmail = res.value.elements[0].expression; - expect(selEmail.variants[0].value.elements[0].value).toEqual( - 'There is one email for { $awesome }', - ); - expect(selEmail.variants[1].value.elements[0].value).toEqual( - 'There are { $num } emails for { $awesome }', - ); + const selEmail = res.value.elements[0].expression; + expect(selEmail.variants[0].value.elements[0].value).toEqual( + 'There is one email for { $awesome }', + ); + expect(selEmail.variants[1].value.elements[0].value).toEqual( + 'There are { $num } emails for { $awesome }', + ); - expect(res.value.elements[1].value).toEqual(' '); + expect(res.value.elements[1].value).toEqual(' '); - const selGender = res.value.elements[2].expression; - expect(selGender.variants[0].value.elements[0].value).toEqual('him'); - expect(selGender.variants[1].value.elements[0].value).toEqual('her'); - }); + const selGender = res.value.elements[2].expression; + expect(selGender.variants[0].value.elements[0].value).toEqual('him'); + expect(selGender.variants[1].value.elements[0].value).toEqual('her'); + }); }); diff --git a/translate/src/core/utils/fluent/flattenMessage.ts b/translate/src/core/utils/fluent/flattenMessage.ts index 63d51a909..faf9f6f65 100644 --- a/translate/src/core/utils/fluent/flattenMessage.ts +++ b/translate/src/core/utils/fluent/flattenMessage.ts @@ -14,26 +14,26 @@ import type { Entry } from '@fluent/syntax'; * value and attributes elements. */ export default function flattenMessage(message: Entry): Entry { - const flatMessage = message.clone(); - if (flatMessage.type !== 'Message' && flatMessage.type !== 'Term') { - return flatMessage; - } - - if (flatMessage.value && flatMessage.value.elements.length > 0) { - flatMessage.value.elements = flattenPatternElements( - flatMessage.value.elements, - ); - } - - if (flatMessage.attributes) { - flatMessage.attributes.forEach((attribute) => { - if (attribute.value && attribute.value.elements.length > 0) { - attribute.value.elements = flattenPatternElements( - attribute.value.elements, - ); - } - }); - } - + const flatMessage = message.clone(); + if (flatMessage.type !== 'Message' && flatMessage.type !== 'Term') { return flatMessage; + } + + if (flatMessage.value && flatMessage.value.elements.length > 0) { + flatMessage.value.elements = flattenPatternElements( + flatMessage.value.elements, + ); + } + + if (flatMessage.attributes) { + flatMessage.attributes.forEach((attribute) => { + if (attribute.value && attribute.value.elements.length > 0) { + attribute.value.elements = flattenPatternElements( + attribute.value.elements, + ); + } + }); + } + + return flatMessage; } diff --git a/translate/src/core/utils/fluent/flattenPatternElements.ts b/translate/src/core/utils/fluent/flattenPatternElements.ts index efef3e847..db3f82420 100644 --- a/translate/src/core/utils/fluent/flattenPatternElements.ts +++ b/translate/src/core/utils/fluent/flattenPatternElements.ts @@ -1,8 +1,8 @@ import { - PatternElement, - SelectExpression, - serializeExpression, - TextElement, + PatternElement, + SelectExpression, + serializeExpression, + TextElement, } from '@fluent/syntax'; /** @@ -15,85 +15,83 @@ import { * Placeable (representing select expressions). */ export default function flattenPatternElements( - elements: Array, + elements: Array, ): Array { - const flatElements: PatternElement[] = []; - let textFragment = ''; - let prevSelect: SelectExpression | null = null; + const flatElements: PatternElement[] = []; + let textFragment = ''; + let prevSelect: SelectExpression | null = null; - for (const element of elements) { - if ( - element.type === 'Placeable' && - element.expression && - element.expression.type === 'SelectExpression' - ) { - // In a message with multiple SelectExpressions separated by some - // whitespace, keep that whitespace out of select variants. - if (/^\s+$/.test(textFragment)) { - flatElements.push(new TextElement(textFragment)); - textFragment = ''; - } - - // Flatten SelectExpression variant elements - for (const variant of element.expression.variants) { - variant.value.elements = flattenPatternElements( - variant.value.elements, - ); - - // If there is preceding text, include that for all variants - if (textFragment) { - const { elements } = variant.value; - const first = elements[0]; - if (first?.type === 'TextElement') { - first.value = textFragment + first.value; - } else { - elements.unshift(new TextElement(textFragment)); - } - } - } - if (textFragment) textFragment = ''; - - flatElements.push(element); - prevSelect = element.expression; - } else { - let strValue = - element.type === 'TextElement' - ? element.value - : serializeExpression(element); - if (textFragment) { - strValue = textFragment + strValue; - textFragment = ''; - } - - if (prevSelect) { - // Keep trailing whitespace out of variant values - const wsEnd = strValue.match(/\s+$/); - if (wsEnd) { - strValue = strValue.substring(0, wsEnd.index); - textFragment = wsEnd[0]; - } - - // If there is a preceding SelectExpression, append to each of its variants - for (const variant of prevSelect.variants) { - const { elements } = variant.value; - const last = elements[elements.length - 1]; - if (last?.type === 'TextElement') { - last.value += strValue; - } else { - elements.push(new TextElement(strValue)); - } - } - } else { - // ... otherwise, append to a temporary string - textFragment += strValue; - } - } - } - - // Merge any remaining collected text into a TextElement - if (textFragment || flatElements.length === 0) { + for (const element of elements) { + if ( + element.type === 'Placeable' && + element.expression && + element.expression.type === 'SelectExpression' + ) { + // In a message with multiple SelectExpressions separated by some + // whitespace, keep that whitespace out of select variants. + if (/^\s+$/.test(textFragment)) { flatElements.push(new TextElement(textFragment)); - } + textFragment = ''; + } - return flatElements; + // Flatten SelectExpression variant elements + for (const variant of element.expression.variants) { + variant.value.elements = flattenPatternElements(variant.value.elements); + + // If there is preceding text, include that for all variants + if (textFragment) { + const { elements } = variant.value; + const first = elements[0]; + if (first?.type === 'TextElement') { + first.value = textFragment + first.value; + } else { + elements.unshift(new TextElement(textFragment)); + } + } + } + if (textFragment) textFragment = ''; + + flatElements.push(element); + prevSelect = element.expression; + } else { + let strValue = + element.type === 'TextElement' + ? element.value + : serializeExpression(element); + if (textFragment) { + strValue = textFragment + strValue; + textFragment = ''; + } + + if (prevSelect) { + // Keep trailing whitespace out of variant values + const wsEnd = strValue.match(/\s+$/); + if (wsEnd) { + strValue = strValue.substring(0, wsEnd.index); + textFragment = wsEnd[0]; + } + + // If there is a preceding SelectExpression, append to each of its variants + for (const variant of prevSelect.variants) { + const { elements } = variant.value; + const last = elements[elements.length - 1]; + if (last?.type === 'TextElement') { + last.value += strValue; + } else { + elements.push(new TextElement(strValue)); + } + } + } else { + // ... otherwise, append to a temporary string + textFragment += strValue; + } + } + } + + // Merge any remaining collected text into a TextElement + if (textFragment || flatElements.length === 0) { + flatElements.push(new TextElement(textFragment)); + } + + return flatElements; } diff --git a/translate/src/core/utils/fluent/getEmptyMessage.test.js b/translate/src/core/utils/fluent/getEmptyMessage.test.js index 8172694de..af554e448 100644 --- a/translate/src/core/utils/fluent/getEmptyMessage.test.js +++ b/translate/src/core/utils/fluent/getEmptyMessage.test.js @@ -2,159 +2,152 @@ import getEmptyMessage from './getEmptyMessage'; import parser from './parser'; const LOCALE = { - code: 'sl', - cldrPlurals: [1, 2, 3, 5], + code: 'sl', + cldrPlurals: [1, 2, 3, 5], }; describe('getEmptyMessage', () => { - it('empties a simple value', () => { - const source = parser.parseEntry('my-message = Some value'); - const message = getEmptyMessage(source, LOCALE); + it('empties a simple value', () => { + const source = parser.parseEntry('my-message = Some value'); + const message = getEmptyMessage(source, LOCALE); - expect(message.value.elements[0].value).toEqual(''); - expect(message.value.elements).toHaveLength(1); - }); + expect(message.value.elements[0].value).toEqual(''); + expect(message.value.elements).toHaveLength(1); + }); - it('empties a value with multiple elements', () => { - const source = parser.parseEntry('my-message = Hello { $small } World'); - const message = getEmptyMessage(source, LOCALE); + it('empties a value with multiple elements', () => { + const source = parser.parseEntry('my-message = Hello { $small } World'); + const message = getEmptyMessage(source, LOCALE); - expect(message.value.elements[0].value).toEqual(''); - expect(message.value.elements).toHaveLength(1); - }); + expect(message.value.elements[0].value).toEqual(''); + expect(message.value.elements).toHaveLength(1); + }); - it('empties a single simple attribute', () => { - const source = parser.parseEntry('my-message =\n .my-attr = Hello'); - const message = getEmptyMessage(source, LOCALE); + it('empties a single simple attribute', () => { + const source = parser.parseEntry('my-message =\n .my-attr = Hello'); + const message = getEmptyMessage(source, LOCALE); - expect(message.attributes[0].id.name).toEqual('my-attr'); + expect(message.attributes[0].id.name).toEqual('my-attr'); - expect(message.attributes[0].value.elements[0].value).toEqual(''); - expect(message.attributes[0].value.elements).toHaveLength(1); - }); + expect(message.attributes[0].value.elements[0].value).toEqual(''); + expect(message.attributes[0].value.elements).toHaveLength(1); + }); - it('empties both value and attributes', () => { - const source = parser.parseEntry( - 'my-message = Some value\n .my-attr = Hello', - ); - const message = getEmptyMessage(source, LOCALE); + it('empties both value and attributes', () => { + const source = parser.parseEntry( + 'my-message = Some value\n .my-attr = Hello', + ); + const message = getEmptyMessage(source, LOCALE); - expect(message.value.elements[0].value).toEqual(''); - expect(message.value.elements).toHaveLength(1); + expect(message.value.elements[0].value).toEqual(''); + expect(message.value.elements).toHaveLength(1); - expect(message.attributes[0].id.name).toEqual('my-attr'); - expect(message.attributes[0].value.elements[0].value).toEqual(''); - expect(message.attributes[0].value.elements).toHaveLength(1); - }); + expect(message.attributes[0].id.name).toEqual('my-attr'); + expect(message.attributes[0].value.elements[0].value).toEqual(''); + expect(message.attributes[0].value.elements).toHaveLength(1); + }); - it('empties several attributes', () => { - const source = parser.parseEntry( - 'my-message =\n .my-attr = Hello\n .title = Title', - ); - const message = getEmptyMessage(source, LOCALE); + it('empties several attributes', () => { + const source = parser.parseEntry( + 'my-message =\n .my-attr = Hello\n .title = Title', + ); + const message = getEmptyMessage(source, LOCALE); - expect(message.attributes[0].id.name).toEqual('my-attr'); - expect(message.attributes[0].value.elements[0].value).toEqual(''); - expect(message.attributes[0].value.elements).toHaveLength(1); + expect(message.attributes[0].id.name).toEqual('my-attr'); + expect(message.attributes[0].value.elements[0].value).toEqual(''); + expect(message.attributes[0].value.elements).toHaveLength(1); - expect(message.attributes[1].id.name).toEqual('title'); - expect(message.attributes[1].value.elements[0].value).toEqual(''); - expect(message.attributes[1].value.elements).toHaveLength(1); - }); + expect(message.attributes[1].id.name).toEqual('title'); + expect(message.attributes[1].value.elements[0].value).toEqual(''); + expect(message.attributes[1].value.elements).toHaveLength(1); + }); - it('empties a select expression', () => { - const input = ` + it('empties a select expression', () => { + const input = ` my-entry = { PLATFORM() -> [variant] Hello! *[another-variant] { reference } World! }`; - const source = parser.parseEntry(input); - const message = getEmptyMessage(source, LOCALE); + const source = parser.parseEntry(input); + const message = getEmptyMessage(source, LOCALE); - expect( - message.value.elements[0].expression.variants[0].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[0].value.elements, - ).toHaveLength(1); + expect( + message.value.elements[0].expression.variants[0].value.elements[0].value, + ).toEqual(''); + expect( + message.value.elements[0].expression.variants[0].value.elements, + ).toHaveLength(1); - expect( - message.value.elements[0].expression.variants[1].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[1].value.elements, - ).toHaveLength(1); - }); + expect( + message.value.elements[0].expression.variants[1].value.elements[0].value, + ).toEqual(''); + expect( + message.value.elements[0].expression.variants[1].value.elements, + ).toHaveLength(1); + }); - it('empties custom plural variants and creates empty default locale plural variants', () => { - const input = ` + it('empties custom plural variants and creates empty default locale plural variants', () => { + const input = ` my-entry = { $num -> [0] Yo! [one] Hello! *[other] { reference } World! }`; - const source = parser.parseEntry(input); - const message = getEmptyMessage(source, LOCALE); + const source = parser.parseEntry(input); + const message = getEmptyMessage(source, LOCALE); - expect(message.value.elements[0].expression.variants).toHaveLength(5); + expect(message.value.elements[0].expression.variants).toHaveLength(5); - expect( - message.value.elements[0].expression.variants[0].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[0].key.value, - ).toEqual('0'); - expect( - message.value.elements[0].expression.variants[0].value.elements, - ).toHaveLength(1); + expect( + message.value.elements[0].expression.variants[0].value.elements[0].value, + ).toEqual(''); + expect(message.value.elements[0].expression.variants[0].key.value).toEqual( + '0', + ); + expect( + message.value.elements[0].expression.variants[0].value.elements, + ).toHaveLength(1); - expect( - message.value.elements[0].expression.variants[1].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[1].key.name, - ).toEqual('one'); - expect( - message.value.elements[0].expression.variants[1].value.elements, - ).toHaveLength(1); + expect( + message.value.elements[0].expression.variants[1].value.elements[0].value, + ).toEqual(''); + expect(message.value.elements[0].expression.variants[1].key.name).toEqual( + 'one', + ); + expect( + message.value.elements[0].expression.variants[1].value.elements, + ).toHaveLength(1); - expect( - message.value.elements[0].expression.variants[2].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[2].key.name, - ).toEqual('two'); - expect( - message.value.elements[0].expression.variants[2].value.elements, - ).toHaveLength(1); + expect( + message.value.elements[0].expression.variants[2].value.elements[0].value, + ).toEqual(''); + expect(message.value.elements[0].expression.variants[2].key.name).toEqual( + 'two', + ); + expect( + message.value.elements[0].expression.variants[2].value.elements, + ).toHaveLength(1); - expect( - message.value.elements[0].expression.variants[3].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[3].key.name, - ).toEqual('few'); - expect( - message.value.elements[0].expression.variants[3].value.elements, - ).toHaveLength(1); + expect( + message.value.elements[0].expression.variants[3].value.elements[0].value, + ).toEqual(''); + expect(message.value.elements[0].expression.variants[3].key.name).toEqual( + 'few', + ); + expect( + message.value.elements[0].expression.variants[3].value.elements, + ).toHaveLength(1); - expect( - message.value.elements[0].expression.variants[4].value.elements[0] - .value, - ).toEqual(''); - expect( - message.value.elements[0].expression.variants[4].key.name, - ).toEqual('other'); - expect( - message.value.elements[0].expression.variants[4].value.elements, - ).toBeTruthy(); - }); + expect( + message.value.elements[0].expression.variants[4].value.elements[0].value, + ).toEqual(''); + expect(message.value.elements[0].expression.variants[4].key.name).toEqual( + 'other', + ); + expect( + message.value.elements[0].expression.variants[4].value.elements, + ).toBeTruthy(); + }); }); diff --git a/translate/src/core/utils/fluent/getEmptyMessage.ts b/translate/src/core/utils/fluent/getEmptyMessage.ts index 9728701a9..aa5fa6fb8 100644 --- a/translate/src/core/utils/fluent/getEmptyMessage.ts +++ b/translate/src/core/utils/fluent/getEmptyMessage.ts @@ -1,9 +1,9 @@ import { - Transformer, - BaseNode, - SelectExpression, - TextElement, - Variant, + Transformer, + BaseNode, + SelectExpression, + TextElement, + Variant, } from '@fluent/syntax'; import flattenMessage from './flattenMessage'; @@ -18,52 +18,50 @@ import type { Locale } from '~/core/locale'; * Gather custom (numeric) plural variants */ function getNumericVariants(variants: Variant[]) { - return variants.filter((variant) => { - return variant.key.type === 'NumberLiteral'; - }); + return variants.filter((variant) => { + return variant.key.type === 'NumberLiteral'; + }); } /** * Generate a CLDR template variant */ function getCldrTemplateVariant( - variants: ReadonlyArray, + variants: ReadonlyArray, ): Variant | null | undefined { - return variants.find((variant) => { - const key = variant.key; - return ( - key.type === 'Identifier' && CLDR_PLURALS.indexOf(key.name) !== -1 - ); - }); + return variants.find((variant) => { + const key = variant.key; + return key.type === 'Identifier' && CLDR_PLURALS.indexOf(key.name) !== -1; + }); } /** * Generate locale plural variants from a template */ function getLocaleVariants(locale: Locale, template: Variant) { - return locale.cldrPlurals.map((item) => { - const localeVariant = template.clone(); - if (localeVariant.key.type === 'Identifier') { - localeVariant.key.name = CLDR_PLURALS[item]; - } - localeVariant.default = false; - return localeVariant; - }); + return locale.cldrPlurals.map((item) => { + const localeVariant = template.clone(); + if (localeVariant.key.type === 'Identifier') { + localeVariant.key.name = CLDR_PLURALS[item]; + } + localeVariant.default = false; + return localeVariant; + }); } /** * Return variants with default variant set */ function withDefaultVariant(variants: Array): Array { - const defaultVariant = variants.find((variant) => { - return variant.default === true; - }); + const defaultVariant = variants.find((variant) => { + return variant.default === true; + }); - if (!defaultVariant) { - variants[variants.length - 1].default = true; - } + if (!defaultVariant) { + variants[variants.length - 1].default = true; + } - return variants; + return variants; } /** @@ -82,39 +80,39 @@ function withDefaultVariant(variants: Array): Array { * @returns {Entry} An emptied copy of the source. */ export default function getEmptyMessage(source: Entry, locale: Locale): Entry { - class EmptyTransformer extends Transformer { - // Empty Text Elements - visitTextElement(node: TextElement): TextElement { - node.value = ''; - return node; - } - - // Create empty locale plural variants - visitSelectExpression(node: SelectExpression): BaseNode { - if (isPluralExpression(node)) { - const variants = node.variants; - const numericVariants = getNumericVariants(variants); - - const template = getCldrTemplateVariant(variants); - const localeVariants = template - ? getLocaleVariants(locale, template) - : []; - - node.variants = withDefaultVariant( - numericVariants.concat(localeVariants), - ); - } - - return this.genericVisit(node); - } + class EmptyTransformer extends Transformer { + // Empty Text Elements + visitTextElement(node: TextElement): TextElement { + node.value = ''; + return node; } - const message = source.clone(); + // Create empty locale plural variants + visitSelectExpression(node: SelectExpression): BaseNode { + if (isPluralExpression(node)) { + const variants = node.variants; + const numericVariants = getNumericVariants(variants); - // Convert all simple elements to TextElements - const flatMessage = flattenMessage(message); + const template = getCldrTemplateVariant(variants); + const localeVariants = template + ? getLocaleVariants(locale, template) + : []; - // Empty TextElements - const transformer = new EmptyTransformer(); - return transformer.visit(flatMessage) as any as Entry; + node.variants = withDefaultVariant( + numericVariants.concat(localeVariants), + ); + } + + return this.genericVisit(node); + } + } + + const message = source.clone(); + + // Convert all simple elements to TextElements + const flatMessage = flattenMessage(message); + + // Empty TextElements + const transformer = new EmptyTransformer(); + return transformer.visit(flatMessage) as any as Entry; } diff --git a/translate/src/core/utils/fluent/getReconstructedMessage.test.js b/translate/src/core/utils/fluent/getReconstructedMessage.test.js index 5ee6ea9c6..2fe3c5182 100644 --- a/translate/src/core/utils/fluent/getReconstructedMessage.test.js +++ b/translate/src/core/utils/fluent/getReconstructedMessage.test.js @@ -3,71 +3,71 @@ import parser from './parser'; import serializer from './serializer'; describe('getReconstructedMessage', () => { - it('returns the correct value for a simple message', () => { - const original = 'title = Marvel Cinematic Universe'; - const translation = 'Univers cinématographique Marvel'; - const res = getReconstructedMessage(original, translation); - const expected = parser.parseEntry( - 'title = Univers cinématographique Marvel', - ); - expect(res).toEqual(expected); - }); + it('returns the correct value for a simple message', () => { + const original = 'title = Marvel Cinematic Universe'; + const translation = 'Univers cinématographique Marvel'; + const res = getReconstructedMessage(original, translation); + const expected = parser.parseEntry( + 'title = Univers cinématographique Marvel', + ); + expect(res).toEqual(expected); + }); - it('returns the correct value for a single attribute', () => { - const original = 'spoilers =\n .who-dies = Who dies?'; - const translation = 'Qui meurt ?'; - const res = getReconstructedMessage(original, translation); - const expected = parser.parseEntry( - 'spoilers =\n .who-dies = Qui meurt ?', - ); - expect(res).toEqual(expected); - }); + it('returns the correct value for a single attribute', () => { + const original = 'spoilers =\n .who-dies = Who dies?'; + const translation = 'Qui meurt ?'; + const res = getReconstructedMessage(original, translation); + const expected = parser.parseEntry( + 'spoilers =\n .who-dies = Qui meurt ?', + ); + expect(res).toEqual(expected); + }); - it('returns indented content for a multiline simple message', () => { - const original = 'time-travel = They discovered Time Travel'; - const translation = 'Ils ont inventé le\nvoyage temporel'; - const res = getReconstructedMessage(original, translation); - const expected = parser.parseEntry( - 'time-travel =\n Ils ont inventé le\n voyage temporel', - ); - expect(res).toEqual(expected); - }); + it('returns indented content for a multiline simple message', () => { + const original = 'time-travel = They discovered Time Travel'; + const translation = 'Ils ont inventé le\nvoyage temporel'; + const res = getReconstructedMessage(original, translation); + const expected = parser.parseEntry( + 'time-travel =\n Ils ont inventé le\n voyage temporel', + ); + expect(res).toEqual(expected); + }); - it('returns indented content for a multiline single attribute', () => { - const original = 'slow-walks =\n .title = They walk in slow motion'; - const translation = 'Ils se déplacent\nen mouvement lents'; - const res = getReconstructedMessage(original, translation); - const expected = parser.parseEntry( - 'slow-walks =\n .title =\n Ils se déplacent\n en mouvement lents', - ); - expect(res).toEqual(expected); - }); + it('returns indented content for a multiline single attribute', () => { + const original = 'slow-walks =\n .title = They walk in slow motion'; + const translation = 'Ils se déplacent\nen mouvement lents'; + const res = getReconstructedMessage(original, translation); + const expected = parser.parseEntry( + 'slow-walks =\n .title =\n Ils se déplacent\n en mouvement lents', + ); + expect(res).toEqual(expected); + }); - it('adds the leading dash to the id of Term messages', () => { - const original = '-my-term = My Term'; - const translation = 'Mon Terme'; - const res = getReconstructedMessage(original, translation); - const expected = parser.parseEntry('-my-term = Mon Terme'); - expect(res).toEqual(expected); - }); + it('adds the leading dash to the id of Term messages', () => { + const original = '-my-term = My Term'; + const translation = 'Mon Terme'; + const res = getReconstructedMessage(original, translation); + const expected = parser.parseEntry('-my-term = Mon Terme'); + expect(res).toEqual(expected); + }); - it('empties all but the first text element', () => { - const original = - 'stark = Tony Stark\n .hero = IronMan\n .hair = black'; - const translation = 'Anthony Stark'; - const res = getReconstructedMessage(original, translation); + it('empties all but the first text element', () => { + const original = + 'stark = Tony Stark\n .hero = IronMan\n .hair = black'; + const translation = 'Anthony Stark'; + const res = getReconstructedMessage(original, translation); - expect(res.value.elements[0].value).toEqual('Anthony Stark'); - expect(res.attributes).toHaveLength(0); - }); + expect(res.value.elements[0].value).toEqual('Anthony Stark'); + expect(res.attributes).toHaveLength(0); + }); - it('does not duplicate terms in content', () => { - const original = 'with-term = I am { -term }'; - const translation = 'Je suis { -term }'; - const res = getReconstructedMessage(original, translation); + it('does not duplicate terms in content', () => { + const original = 'with-term = I am { -term }'; + const translation = 'Je suis { -term }'; + const res = getReconstructedMessage(original, translation); - expect(serializer.serializeEntry(res)).toEqual( - 'with-term = Je suis { -term }\n', - ); - }); + expect(serializer.serializeEntry(res)).toEqual( + 'with-term = Je suis { -term }\n', + ); + }); }); diff --git a/translate/src/core/utils/fluent/getReconstructedMessage.ts b/translate/src/core/utils/fluent/getReconstructedMessage.ts index 1f8d7b8aa..c3eae4497 100644 --- a/translate/src/core/utils/fluent/getReconstructedMessage.ts +++ b/translate/src/core/utils/fluent/getReconstructedMessage.ts @@ -7,46 +7,44 @@ import type { Entry } from '@fluent/syntax'; * translated content. */ export default function getReconstructedMessage( - original: string, - translation: string, + original: string, + translation: string, ): Entry { - const message = parser.parseEntry(original); - if (message.type !== 'Message' && message.type !== 'Term') { - throw new Error( - `Unexpected type '${message.type}' in getReconstructedMessage`, - ); - } - let key = message.id.name; + const message = parser.parseEntry(original); + if (message.type !== 'Message' && message.type !== 'Term') { + throw new Error( + `Unexpected type '${message.type}' in getReconstructedMessage`, + ); + } + let key = message.id.name; - // For Terms, the leading dash is removed in the identifier. We need to add - // it back manually. - if (message.type === 'Term') { - key = '-' + key; - } + // For Terms, the leading dash is removed in the identifier. We need to add + // it back manually. + if (message.type === 'Term') { + key = '-' + key; + } - const isMultilineTranslation = translation.indexOf('\n') > -1; + const isMultilineTranslation = translation.indexOf('\n') > -1; - let content: string; + let content: string; - if (message.attributes && message.attributes.length === 1) { - const attribute = message.attributes[0].id.name; + if (message.attributes && message.attributes.length === 1) { + const attribute = message.attributes[0].id.name; - if (isMultilineTranslation) { - content = `${key} =\n .${attribute} =`; - translation - .split('\n') - .forEach((t) => (content += `\n ${t}`)); - } else { - content = `${key} =\n .${attribute} = ${translation}`; - } + if (isMultilineTranslation) { + content = `${key} =\n .${attribute} =`; + translation.split('\n').forEach((t) => (content += `\n ${t}`)); } else { - if (isMultilineTranslation) { - content = `${key} =`; - translation.split('\n').forEach((t) => (content += `\n ${t}`)); - } else { - content = `${key} = ${translation}`; - } + content = `${key} =\n .${attribute} = ${translation}`; } + } else { + if (isMultilineTranslation) { + content = `${key} =`; + translation.split('\n').forEach((t) => (content += `\n ${t}`)); + } else { + content = `${key} = ${translation}`; + } + } - return parser.parseEntry(content); + return parser.parseEntry(content); } diff --git a/translate/src/core/utils/fluent/getSimplePreview.test.js b/translate/src/core/utils/fluent/getSimplePreview.test.js index 72ac2f638..b28205bfe 100644 --- a/translate/src/core/utils/fluent/getSimplePreview.test.js +++ b/translate/src/core/utils/fluent/getSimplePreview.test.js @@ -1,78 +1,78 @@ import getSimplePreview from './getSimplePreview'; describe('getSimplePreview', () => { - it('works for an empty string', () => { - expect(getSimplePreview('')).toEqual(''); - }); + it('works for an empty string', () => { + expect(getSimplePreview('')).toEqual(''); + }); - it('works for a null value', () => { - expect(getSimplePreview(null)).toEqual(''); - }); + it('works for a null value', () => { + expect(getSimplePreview(null)).toEqual(''); + }); - it('works for a non-FTL string', () => { - expect(getSimplePreview('I am inevitable')).toEqual('I am inevitable'); - }); + it('works for a non-FTL string', () => { + expect(getSimplePreview('I am inevitable')).toEqual('I am inevitable'); + }); - it('returns the value for a simple Message', () => { - const message = 'title = Marvel Cinematic Universe'; - const res = getSimplePreview(message); - expect(res).toEqual('Marvel Cinematic Universe'); - }); + it('returns the value for a simple Message', () => { + const message = 'title = Marvel Cinematic Universe'; + const res = getSimplePreview(message); + expect(res).toEqual('Marvel Cinematic Universe'); + }); - it('returns the value for a multiline Message', () => { - const message = `summary = + it('returns the value for a multiline Message', () => { + const message = `summary = Heroes beat the Villain `; - const res = getSimplePreview(message); - expect(res).toEqual('Heroes\nbeat\nthe Villain'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Heroes\nbeat\nthe Villain'); + }); - it('returns the value for a simple Term', () => { - const message = '-team-name = Avengers'; - const res = getSimplePreview(message); - expect(res).toEqual('Avengers'); - }); + it('returns the value for a simple Term', () => { + const message = '-team-name = Avengers'; + const res = getSimplePreview(message); + expect(res).toEqual('Avengers'); + }); - it('returns the attribute when there are no values and an attribute', () => { - const message = `hawkeye = + it('returns the attribute when there are no values and an attribute', () => { + const message = `hawkeye = .real-name = Clint Barton `; - const res = getSimplePreview(message); - expect(res).toEqual('Clint Barton'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Clint Barton'); + }); - it('returns the value when there is a value and an attribute', () => { - const message = `ironman-slogan = I am Ironman! + it('returns the value when there is a value and an attribute', () => { + const message = `ironman-slogan = I am Ironman! .attributed-to = Tony Stark `; - const res = getSimplePreview(message); - expect(res).toEqual('I am Ironman!'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('I am Ironman!'); + }); - it('returns the first attribute when there are several', () => { - const message = `thor = + it('returns the first attribute when there are several', () => { + const message = `thor = .first-movie = Thor .second-movie = The Dark World .third-movie = Ragnarok `; - const res = getSimplePreview(message); - expect(res).toEqual('Thor'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Thor'); + }); - it('returns the default value for plurals 2', () => { - const message = `key = + it('returns the default value for plurals 2', () => { + const message = `key = { $number -> [1] Simple String *[other] Other Simple String }`; - const res = getSimplePreview(message); - expect(res).toEqual('Other Simple String'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Other Simple String'); + }); - it('returns the default value for plurals', () => { - const message = `stones-number = + it('returns the default value for plurals', () => { + const message = `stones-number = Thanos has { $number -> [0] no Stones [1] 1 Stone @@ -80,77 +80,77 @@ describe('getSimplePreview', () => { *[other] { $number } Stones } `; - const res = getSimplePreview(message); - expect(res).toEqual('Thanos has { $number } Stones'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Thanos has { $number } Stones'); + }); - it('returns the default value for selectors', () => { - const message = `who-dies = + it('returns the default value for selectors', () => { + const message = `who-dies = { $who -> [female] Black Widow [male] Hawkeye *[other] Everyone } will die `; - const res = getSimplePreview(message); - expect(res).toEqual('Everyone will die'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Everyone will die'); + }); - it('returns the default value for a selector in an attribute', () => { - const message = `ironman = + it('returns the default value for a selector in an attribute', () => { + const message = `ironman = .talking-ia = { PLATFORM() -> [win] Friday *[other] Jarvis } `; - const res = getSimplePreview(message); - expect(res).toEqual('Jarvis'); - }); + const res = getSimplePreview(message); + expect(res).toEqual('Jarvis'); + }); - it('works with function reference', () => { - const message = `explore = { + it('works with function reference', () => { + const message = `explore = { LINK("Wikipedia", title: "Go to Wikipedia") }Read more `; - const res = getSimplePreview(message); - expect(res).toEqual( - '{ LINK("Wikipedia", title: "Go to Wikipedia") }Read more', - ); - }); + const res = getSimplePreview(message); + expect(res).toEqual( + '{ LINK("Wikipedia", title: "Go to Wikipedia") }Read more', + ); + }); - it('works with variable reference', () => { - const message = 'big-green = { $hulk }'; - const res = getSimplePreview(message); - expect(res).toEqual('{ $hulk }'); - }); + it('works with variable reference', () => { + const message = 'big-green = { $hulk }'; + const res = getSimplePreview(message); + expect(res).toEqual('{ $hulk }'); + }); - it('works with message reference', () => { - const message = 'small-white = { banner }'; - const res = getSimplePreview(message); - expect(res).toEqual('{ banner }'); - }); + it('works with message reference', () => { + const message = 'small-white = { banner }'; + const res = getSimplePreview(message); + expect(res).toEqual('{ banner }'); + }); - it('works with message reference with attribute', () => { - const message = 'hero = { ironman.name }'; - const res = getSimplePreview(message); - expect(res).toEqual('{ ironman.name }'); - }); + it('works with message reference with attribute', () => { + const message = 'hero = { ironman.name }'; + const res = getSimplePreview(message); + expect(res).toEqual('{ ironman.name }'); + }); - it('works with term reference', () => { - const message = 'team = { -team-name }'; - const res = getSimplePreview(message); - expect(res).toEqual('{ -team-name }'); - }); + it('works with term reference', () => { + const message = 'team = { -team-name }'; + const res = getSimplePreview(message); + expect(res).toEqual('{ -team-name }'); + }); - it('works with string literals', () => { - const message = 'the-end = { "" }'; // #nospoil - const res = getSimplePreview(message); - expect(res).toEqual('{ "" }'); - }); + it('works with string literals', () => { + const message = 'the-end = { "" }'; // #nospoil + const res = getSimplePreview(message); + expect(res).toEqual('{ "" }'); + }); - it('works with number literals', () => { - const message = 'movies = { 22 }'; - const res = getSimplePreview(message); - expect(res).toEqual('{ 22 }'); - }); + it('works with number literals', () => { + const message = 'movies = { 22 }'; + const res = getSimplePreview(message); + expect(res).toEqual('{ 22 }'); + }); }); diff --git a/translate/src/core/utils/fluent/getSimplePreview.ts b/translate/src/core/utils/fluent/getSimplePreview.ts index 1dbf9599e..b64a49a10 100644 --- a/translate/src/core/utils/fluent/getSimplePreview.ts +++ b/translate/src/core/utils/fluent/getSimplePreview.ts @@ -27,26 +27,26 @@ import serialize from './serialize'; * content if it isn't a valid Fluent message. */ export default function getSimplePreview( - content: string | null | undefined, + content: string | null | undefined, ): string { - if (!content) { - return ''; - } + if (!content) { + return ''; + } - const message = parser.parseEntry(content); + const message = parser.parseEntry(content); - if (message.type !== 'Message' && message.type !== 'Term') { - return content; - } + if (message.type !== 'Message' && message.type !== 'Term') { + return content; + } - let tree; - if (message.value) { - tree = message.value; - } else { - tree = message.attributes[0].value; - } + let tree; + if (message.value) { + tree = message.value; + } else { + tree = message.attributes[0].value; + } - let elements = serialize(tree.elements); + let elements = serialize(tree.elements); - return flattenDeep(elements).join(''); + return flattenDeep(elements).join(''); } diff --git a/translate/src/core/utils/fluent/getSyntaxType.test.js b/translate/src/core/utils/fluent/getSyntaxType.test.js index ea5dc54f4..fa2dfda42 100644 --- a/translate/src/core/utils/fluent/getSyntaxType.test.js +++ b/translate/src/core/utils/fluent/getSyntaxType.test.js @@ -2,112 +2,112 @@ import getSyntaxType from './getSyntaxType'; import parser from './parser'; describe('getSyntaxType', () => { - it('returns "simple" for a string with simple value', () => { - const input = 'my-entry = Hello!'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with simple value', () => { + const input = 'my-entry = Hello!'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with multiline value', () => { - const input = ` + it('returns "simple" for a string with multiline value', () => { + const input = ` my-entry = Multi line value.`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a reference to a built-in function', () => { - const input = - 'my-entry = Today is { DATETIME($date, month: "long", year: "numeric", day: "numeric") }'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a reference to a built-in function', () => { + const input = + 'my-entry = Today is { DATETIME($date, month: "long", year: "numeric", day: "numeric") }'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a Term', () => { - const input = '-my-entry = Hello!'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a Term', () => { + const input = '-my-entry = Hello!'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a TermReference', () => { - const input = 'my-entry = Term { -term } Reference'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a TermReference', () => { + const input = 'my-entry = Term { -term } Reference'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a MessageReference', () => { - const input = 'my-entry = { my_id }'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a MessageReference', () => { + const input = 'my-entry = { my_id }'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a MessageReference with attribute', () => { - const input = 'my-entry = { my_id.title }'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a MessageReference with attribute', () => { + const input = 'my-entry = { my_id.title }'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a StringExpression', () => { - const input = 'my-entry = { "" }'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a StringExpression', () => { + const input = 'my-entry = { "" }'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a NumberExpression', () => { - const input = 'my-entry = { 5 }'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a NumberExpression', () => { + const input = 'my-entry = { 5 }'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "simple" for a string with a single simple attribute', () => { - const input = 'my-entry = \n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns "simple" for a string with a single simple attribute', () => { + const input = 'my-entry = \n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('simple'); - }); + expect(getSyntaxType(message)).toEqual('simple'); + }); - it('returns "rich" for a string with value and attributes', () => { - const input = 'my-entry = World\n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns "rich" for a string with value and attributes', () => { + const input = 'my-entry = World\n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('rich'); - }); + expect(getSyntaxType(message)).toEqual('rich'); + }); - it('returns "rich" for a string with no value and two attributes', () => { - const input = ` + it('returns "rich" for a string with no value and two attributes', () => { + const input = ` my-entry = .an-atribute = Hello! .another-atribute = World!`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('rich'); - }); + expect(getSyntaxType(message)).toEqual('rich'); + }); - it('returns "rich" for a string with a select expression', () => { - const input = ` + it('returns "rich" for a string with a select expression', () => { + const input = ` my-entry = { PLATFORM() -> [variant] Hello! *[another-variant] World! }`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('rich'); - }); + expect(getSyntaxType(message)).toEqual('rich'); + }); - it('returns "rich" for a string with a double select expression in attribute', () => { - const input = ` + it('returns "rich" for a string with a double select expression in attribute', () => { + const input = ` my-entry = .label = { PLATFORM() -> @@ -119,13 +119,13 @@ my-entry = [macos] e *[other] o }`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('rich'); - }); + expect(getSyntaxType(message)).toEqual('rich'); + }); - it('returns "rich" for a string with multiple select expressions and surrounding text', () => { - const input = ` + it('returns "rich" for a string with multiple select expressions and surrounding text', () => { + const input = ` my-entry = There { $num -> [one] is one email @@ -134,13 +134,13 @@ my-entry = *[masculine] him [feminine] her }`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('rich'); - }); + expect(getSyntaxType(message)).toEqual('rich'); + }); - it('returns "complex" for a string with nested select expressions', () => { - const input = ` + it('returns "complex" for a string with nested select expressions', () => { + const input = ` my-entry = { $gender -> *[masculine] @@ -154,8 +154,8 @@ my-entry = *[other] There are many emails for him } }`; - const message = parser.parseEntry(input); + const message = parser.parseEntry(input); - expect(getSyntaxType(message)).toEqual('complex'); - }); + expect(getSyntaxType(message)).toEqual('complex'); + }); }); diff --git a/translate/src/core/utils/fluent/getSyntaxType.ts b/translate/src/core/utils/fluent/getSyntaxType.ts index 0784cb19b..061594e2b 100644 --- a/translate/src/core/utils/fluent/getSyntaxType.ts +++ b/translate/src/core/utils/fluent/getSyntaxType.ts @@ -16,13 +16,13 @@ import type { Entry } from '@fluent/syntax'; * - "complex": can only be shown in a source editor. */ export default function getSyntaxType(message: Entry): SyntaxType { - if (!isSupportedMessage(message)) { - return 'complex'; - } + if (!isSupportedMessage(message)) { + return 'complex'; + } - if (isSimpleMessage(message) || isSimpleSingleAttributeMessage(message)) { - return 'simple'; - } + if (isSimpleMessage(message) || isSimpleSingleAttributeMessage(message)) { + return 'simple'; + } - return 'rich'; + return 'rich'; } diff --git a/translate/src/core/utils/fluent/index.ts b/translate/src/core/utils/fluent/index.ts index 0b8977d21..771e89bb4 100644 --- a/translate/src/core/utils/fluent/index.ts +++ b/translate/src/core/utils/fluent/index.ts @@ -17,21 +17,21 @@ import serialize from './serialize'; import serializer from './serializer'; export default { - areSupportedElements, - convertSyntax, - extractAccessKeyCandidates, - flattenPatternElements, - flattenMessage, - getEmptyMessage, - getReconstructedMessage, - getSimplePreview, - getSyntaxType, - isPluralExpression, - isSimpleElement, - isSimpleMessage, - isSimpleSingleAttributeMessage, - isSupportedMessage, - parser, - serialize, - serializer, + areSupportedElements, + convertSyntax, + extractAccessKeyCandidates, + flattenPatternElements, + flattenMessage, + getEmptyMessage, + getReconstructedMessage, + getSimplePreview, + getSyntaxType, + isPluralExpression, + isSimpleElement, + isSimpleMessage, + isSimpleSingleAttributeMessage, + isSupportedMessage, + parser, + serialize, + serializer, }; diff --git a/translate/src/core/utils/fluent/isPluralExpression.test.js b/translate/src/core/utils/fluent/isPluralExpression.test.js index 3e7ceaa00..27dfa39e7 100644 --- a/translate/src/core/utils/fluent/isPluralExpression.test.js +++ b/translate/src/core/utils/fluent/isPluralExpression.test.js @@ -2,76 +2,76 @@ import isPluralExpression from './isPluralExpression'; import parser from './parser'; describe('isPluralExpression', () => { - it('returns false for elements that are not select expressions', () => { - const input = 'my-entry = Hello!'; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + it('returns false for elements that are not select expressions', () => { + const input = 'my-entry = Hello!'; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeFalsy(); - }); + expect(isPluralExpression(element.expression)).toBeFalsy(); + }); - it('returns true if all variant keys are CLDR plurals', () => { - const input = ` + it('returns true if all variant keys are CLDR plurals', () => { + const input = ` my-entry = { $num -> [one] Hello! *[two] World! }`; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeTruthy(); - }); + expect(isPluralExpression(element.expression)).toBeTruthy(); + }); - it('returns true if all variant keys are numbers', () => { - const input = ` + it('returns true if all variant keys are numbers', () => { + const input = ` my-entry = { $num -> [1] Hello! *[2] World! }`; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeTruthy(); - }); + expect(isPluralExpression(element.expression)).toBeTruthy(); + }); - it('returns true if one variant key is a CLDR plural and the other is a number', () => { - const input = ` + it('returns true if one variant key is a CLDR plural and the other is a number', () => { + const input = ` my-entry = { $num -> [one] Hello! *[1] World! }`; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeTruthy(); - }); + expect(isPluralExpression(element.expression)).toBeTruthy(); + }); - it('returns false if one variant key is a CLDR plural and the other is neither a CLDR plural nor a number', () => { - const input = ` + it('returns false if one variant key is a CLDR plural and the other is neither a CLDR plural nor a number', () => { + const input = ` my-entry = { $num -> [one] Hello! *[variant] World! }`; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeFalsy(); - }); + expect(isPluralExpression(element.expression)).toBeFalsy(); + }); - it('returns false if at least one variant key is neither a CLDR plural nor a number', () => { - const input = ` + it('returns false if at least one variant key is neither a CLDR plural nor a number', () => { + const input = ` my-entry = { $num -> [variant] Hello! *[another-variant] World! }`; - const message = parser.parseEntry(input); - const element = message.value.elements[0]; + const message = parser.parseEntry(input); + const element = message.value.elements[0]; - expect(isPluralExpression(element.expression)).toBeFalsy(); - }); + expect(isPluralExpression(element.expression)).toBeFalsy(); + }); }); diff --git a/translate/src/core/utils/fluent/isPluralExpression.ts b/translate/src/core/utils/fluent/isPluralExpression.ts index 930f8e1c0..035a0eaf9 100644 --- a/translate/src/core/utils/fluent/isPluralExpression.ts +++ b/translate/src/core/utils/fluent/isPluralExpression.ts @@ -8,16 +8,16 @@ import type { SelectExpression } from '@fluent/syntax'; * Keys of all variants of such elements are either CLDR plurals or numbers. */ export default function isPluralExpression( - expression: Readonly, + expression: Readonly, ): boolean { - if (!expression || expression.type !== 'SelectExpression') { - return false; - } + if (!expression || expression.type !== 'SelectExpression') { + return false; + } - return expression.variants.every((variant) => { - return ( - variant.key.type === 'NumberLiteral' || - (variant.key.name && CLDR_PLURALS.indexOf(variant.key.name) !== -1) - ); - }); + return expression.variants.every((variant) => { + return ( + variant.key.type === 'NumberLiteral' || + (variant.key.name && CLDR_PLURALS.indexOf(variant.key.name) !== -1) + ); + }); } diff --git a/translate/src/core/utils/fluent/isSimpleElement.ts b/translate/src/core/utils/fluent/isSimpleElement.ts index 6b11082c2..b3ddf9d57 100644 --- a/translate/src/core/utils/fluent/isSimpleElement.ts +++ b/translate/src/core/utils/fluent/isSimpleElement.ts @@ -7,24 +7,24 @@ import type { PatternElement } from '@fluent/syntax'; * VariableReference, MessageReference, TermReference, FunctionReference */ export default function isSimpleElement(element: PatternElement): boolean { - if (element.type === 'TextElement') { + if (element.type === 'TextElement') { + return true; + } + + // Placeable + if (element.type === 'Placeable') { + switch (element.expression.type) { + case 'FunctionReference': + case 'TermReference': + case 'MessageReference': + case 'VariableReference': + case 'NumberLiteral': + case 'StringLiteral': return true; + default: + return false; } + } - // Placeable - if (element.type === 'Placeable') { - switch (element.expression.type) { - case 'FunctionReference': - case 'TermReference': - case 'MessageReference': - case 'VariableReference': - case 'NumberLiteral': - case 'StringLiteral': - return true; - default: - return false; - } - } - - return false; + return false; } diff --git a/translate/src/core/utils/fluent/isSimpleMessage.test.js b/translate/src/core/utils/fluent/isSimpleMessage.test.js index 64591cbad..8e2a665ff 100644 --- a/translate/src/core/utils/fluent/isSimpleMessage.test.js +++ b/translate/src/core/utils/fluent/isSimpleMessage.test.js @@ -2,24 +2,24 @@ import isSimpleMessage from './isSimpleMessage'; import parser from './parser'; describe('isSimpleMessage', () => { - it('returns true for a string with simple text', () => { - const input = 'my-entry = Hello!'; - const message = parser.parseEntry(input); + it('returns true for a string with simple text', () => { + const input = 'my-entry = Hello!'; + const message = parser.parseEntry(input); - expect(isSimpleMessage(message)).toEqual(true); - }); + expect(isSimpleMessage(message)).toEqual(true); + }); - it('returns false for string with text and an attribute', () => { - const input = 'my-entry = Something\n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns false for string with text and an attribute', () => { + const input = 'my-entry = Something\n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(isSimpleMessage(message)).toEqual(false); - }); + expect(isSimpleMessage(message)).toEqual(false); + }); - it('returns false for string with no text and an attribute', () => { - const input = 'my-entry =\n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns false for string with no text and an attribute', () => { + const input = 'my-entry =\n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(isSimpleMessage(message)).toEqual(false); - }); + expect(isSimpleMessage(message)).toEqual(false); + }); }); diff --git a/translate/src/core/utils/fluent/isSimpleMessage.ts b/translate/src/core/utils/fluent/isSimpleMessage.ts index 23a11d6d1..533d0e23a 100644 --- a/translate/src/core/utils/fluent/isSimpleMessage.ts +++ b/translate/src/core/utils/fluent/isSimpleMessage.ts @@ -8,16 +8,16 @@ import type { Entry } from '@fluent/syntax'; * elements are simple. */ export default function isSimpleMessage(message: Entry): boolean { - if ( - message && - (message.type === 'Message' || message.type === 'Term') && - message.attributes && - !message.attributes.length && - message.value && - message.value.elements.every(isSimpleElement) - ) { - return true; - } + if ( + message && + (message.type === 'Message' || message.type === 'Term') && + message.attributes && + !message.attributes.length && + message.value && + message.value.elements.every(isSimpleElement) + ) { + return true; + } - return false; + return false; } diff --git a/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.test.js b/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.test.js index 845a4fc22..263d01297 100644 --- a/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.test.js +++ b/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.test.js @@ -2,25 +2,25 @@ import isSimpleSingleAttributeMessage from './isSimpleSingleAttributeMessage'; import parser from './parser'; describe('isSimpleSingleAttributeMessage', () => { - it('returns true for a string with a single attribute', () => { - const input = 'my-entry =\n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns true for a string with a single attribute', () => { + const input = 'my-entry =\n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(isSimpleSingleAttributeMessage(message)).toEqual(true); - }); + expect(isSimpleSingleAttributeMessage(message)).toEqual(true); + }); - it('returns false for string with text', () => { - const input = 'my-entry = Something\n .an-atribute = Hello!'; - const message = parser.parseEntry(input); + it('returns false for string with text', () => { + const input = 'my-entry = Something\n .an-atribute = Hello!'; + const message = parser.parseEntry(input); - expect(isSimpleSingleAttributeMessage(message)).toEqual(false); - }); + expect(isSimpleSingleAttributeMessage(message)).toEqual(false); + }); - it('returns false for string with several attributes', () => { - const input = - 'my-entry =\n .an-atribute = Hello!\n .two-attrites = World!'; - const message = parser.parseEntry(input); + it('returns false for string with several attributes', () => { + const input = + 'my-entry =\n .an-atribute = Hello!\n .two-attrites = World!'; + const message = parser.parseEntry(input); - expect(isSimpleSingleAttributeMessage(message)).toEqual(false); - }); + expect(isSimpleSingleAttributeMessage(message)).toEqual(false); + }); }); diff --git a/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.ts b/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.ts index 758a69b6b..4a81bdc45 100644 --- a/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.ts +++ b/translate/src/core/utils/fluent/isSimpleSingleAttributeMessage.ts @@ -6,17 +6,17 @@ import type { Entry } from '@fluent/syntax'; * elements. */ export default function isSimpleSingleAttributeMessage( - message: Entry, + message: Entry, ): boolean { - if ( - message.type === 'Message' && - !message.value && - message.attributes && - message.attributes.length === 1 && - message.attributes[0].value.elements.every(isSimpleElement) - ) { - return true; - } + if ( + message.type === 'Message' && + !message.value && + message.attributes && + message.attributes.length === 1 && + message.attributes[0].value.elements.every(isSimpleElement) + ) { + return true; + } - return false; + return false; } diff --git a/translate/src/core/utils/fluent/isSupportedMessage.ts b/translate/src/core/utils/fluent/isSupportedMessage.ts index 62ad057f5..d95df9147 100644 --- a/translate/src/core/utils/fluent/isSupportedMessage.ts +++ b/translate/src/core/utils/fluent/isSupportedMessage.ts @@ -8,26 +8,24 @@ import type { Entry } from '@fluent/syntax'; * and all attribute elements are supported. */ export default function isSupportedMessage(message: Entry): boolean { - // Parse error - if (message.type === 'Junk') { - return false; - } - // Comments - if ( - message.type === 'Comment' || - message.type === 'GroupComment' || - message.type === 'ResourceComment' - ) { - return false; - } + // Parse error + if (message.type === 'Junk') { + return false; + } + // Comments + if ( + message.type === 'Comment' || + message.type === 'GroupComment' || + message.type === 'ResourceComment' + ) { + return false; + } - if (message.value && !areSupportedElements(message.value.elements)) { - return false; - } + if (message.value && !areSupportedElements(message.value.elements)) { + return false; + } - return message.attributes.every((attribute) => { - return ( - attribute.value && areSupportedElements(attribute.value.elements) - ); - }); + return message.attributes.every((attribute) => { + return attribute.value && areSupportedElements(attribute.value.elements); + }); } diff --git a/translate/src/core/utils/fluent/serialize.ts b/translate/src/core/utils/fluent/serialize.ts index d23d9b360..01b34fbb2 100644 --- a/translate/src/core/utils/fluent/serialize.ts +++ b/translate/src/core/utils/fluent/serialize.ts @@ -9,25 +9,25 @@ type DeepStringArray = Array | string | null; * Walks the given elements' AST and return the most pertinent value for each. */ export default function serialize( - elements: Array, + elements: Array, ): DeepStringArray { - return elements.map((elt): DeepStringArray => { - if (elt.type === 'TextElement') { - return elt.value; - } + return elements.map((elt): DeepStringArray => { + if (elt.type === 'TextElement') { + return elt.value; + } - if (elt.type === 'Placeable') { - if (elt.expression.type === 'SelectExpression') { - const defaultVariants = elt.expression.variants.filter( - (v) => v.default, - ); - return serialize(defaultVariants[0].value.elements); - } else { - const expression = serializeExpression(elt.expression); - return `{ ${expression} }`; - } - } + if (elt.type === 'Placeable') { + if (elt.expression.type === 'SelectExpression') { + const defaultVariants = elt.expression.variants.filter( + (v) => v.default, + ); + return serialize(defaultVariants[0].value.elements); + } else { + const expression = serializeExpression(elt.expression); + return `{ ${expression} }`; + } + } - return null; - }); + return null; + }); } diff --git a/translate/src/core/utils/getOptimizedContent.ts b/translate/src/core/utils/getOptimizedContent.ts index c5d1e0b23..db4100e56 100644 --- a/translate/src/core/utils/getOptimizedContent.ts +++ b/translate/src/core/utils/getOptimizedContent.ts @@ -9,14 +9,14 @@ import fluent from './fluent'; * version of the translation. Otherwise, return the original translation. */ export default function getOptimizedContent( - translation: string | null | undefined, - format: string, + translation: string | null | undefined, + format: string, ): string { - if (!translation) { - return ''; - } - if (format === 'ftl') { - return fluent.getSimplePreview(translation); - } - return translation; + if (!translation) { + return ''; + } + if (format === 'ftl') { + return fluent.getSimplePreview(translation); + } + return translation; } diff --git a/translate/src/core/utils/hooks/useOnDiscard.test.js b/translate/src/core/utils/hooks/useOnDiscard.test.js index cc4322ecf..47b1463f9 100644 --- a/translate/src/core/utils/hooks/useOnDiscard.test.js +++ b/translate/src/core/utils/hooks/useOnDiscard.test.js @@ -6,56 +6,56 @@ import sinon from 'sinon'; import useOnDiscard from './useOnDiscard'; function TestComponent({ onDiscard }) { - const ref = React.useRef(null); - useOnDiscard(ref, onDiscard); - return ( -
    -
    Outside Content
    - {/* Discardable element */} -
    - -
    -
    - ); + const ref = React.useRef(null); + useOnDiscard(ref, onDiscard); + return ( +
    +
    Outside Content
    + {/* Discardable element */} +
    + +
    +
    + ); } describe('useOnDiscard', () => { - let root; + let root; - beforeEach(async () => { - root = document.createElement('div'); - document.body.appendChild(root); + beforeEach(async () => { + root = document.createElement('div'); + document.body.appendChild(root); + }); + + afterEach(() => { + unmountComponentAtNode(root); + root.remove(); + root = null; + }); + + it('runs discard callback upon outside click', () => { + const onDiscardSpy = sinon.spy(); + act(() => { + render(, root); }); - afterEach(() => { - unmountComponentAtNode(root); - root.remove(); - root = null; + const click = new Event('click', { bubbles: true }); + const outside = document.getElementById('js-outside'); + outside.dispatchEvent(click); + + expect(onDiscardSpy.calledOnce).toBe(true); + }); + + it('does not run discard callback upon inside click', () => { + const onDiscardSpy = sinon.spy(); + act(() => { + render(, root); }); - it('runs discard callback upon outside click', () => { - const onDiscardSpy = sinon.spy(); - act(() => { - render(, root); - }); + const click = new Event('click', { bubbles: true }); + const inside = document.getElementById('js-inside'); + inside.dispatchEvent(click); - const click = new Event('click', { bubbles: true }); - const outside = document.getElementById('js-outside'); - outside.dispatchEvent(click); - - expect(onDiscardSpy.calledOnce).toBe(true); - }); - - it('does not run discard callback upon inside click', () => { - const onDiscardSpy = sinon.spy(); - act(() => { - render(, root); - }); - - const click = new Event('click', { bubbles: true }); - const inside = document.getElementById('js-inside'); - inside.dispatchEvent(click); - - expect(onDiscardSpy.callCount).toBe(0); - }); + expect(onDiscardSpy.callCount).toBe(0); + }); }); diff --git a/translate/src/core/utils/hooks/useOnDiscard.ts b/translate/src/core/utils/hooks/useOnDiscard.ts index de971dece..b11c3e0e1 100644 --- a/translate/src/core/utils/hooks/useOnDiscard.ts +++ b/translate/src/core/utils/hooks/useOnDiscard.ts @@ -1,29 +1,29 @@ import * as React from 'react'; export default function useOnDiscard( - ref: { current: null | React.ElementRef }, - onDiscard: () => void, + ref: { current: null | React.ElementRef }, + onDiscard: () => void, ) { - const handleClick = React.useCallback( - (e: MouseEvent) => { - const el = ref.current; - if ( - !(el instanceof HTMLElement) || - !(e.target instanceof Element) || - el.contains(e.target) - ) { - return; - } + const handleClick = React.useCallback( + (e: MouseEvent) => { + const el = ref.current; + if ( + !(el instanceof HTMLElement) || + !(e.target instanceof Element) || + el.contains(e.target) + ) { + return; + } - onDiscard(); - }, - [ref, onDiscard], - ); + onDiscard(); + }, + [ref, onDiscard], + ); - React.useEffect(() => { - window.document.addEventListener('click', handleClick); - return () => { - window.document.removeEventListener('click', handleClick); - }; - }, [handleClick]); + React.useEffect(() => { + window.document.addEventListener('click', handleClick); + return () => { + window.document.removeEventListener('click', handleClick); + }; + }, [handleClick]); } diff --git a/translate/src/hooks.ts b/translate/src/hooks.ts index 1c91c27b1..b0e19420d 100644 --- a/translate/src/hooks.ts +++ b/translate/src/hooks.ts @@ -1,8 +1,8 @@ import { - TypedUseSelectorHook, - useDispatch, - useSelector, - useStore, + TypedUseSelectorHook, + useDispatch, + useSelector, + useStore, } from 'react-redux'; import type { RootState, AppDispatch } from './store'; diff --git a/translate/src/hooks/usePrevious.ts b/translate/src/hooks/usePrevious.ts index c36fa6d55..97e2240c6 100644 --- a/translate/src/hooks/usePrevious.ts +++ b/translate/src/hooks/usePrevious.ts @@ -1,9 +1,9 @@ import { useEffect, useRef } from 'react'; export function usePrevious(value: T): T | undefined { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; } diff --git a/translate/src/index.css b/translate/src/index.css index f2deb0ec9..5028c9635 100644 --- a/translate/src/index.css +++ b/translate/src/index.css @@ -1,7 +1,7 @@ /* Reset HTML5 Search Input in Webkit */ input[type='search'] { - -webkit-appearance: none; - -webkit-border-radius: 0; + -webkit-appearance: none; + -webkit-border-radius: 0; } body, @@ -9,42 +9,42 @@ select, input, textarea, button { - font-family: 'Open Sans', 'Lucida Sans', 'Lucida Grande', - 'Lucida Sans Unicode', Verdana, sans-serif; + font-family: 'Open Sans', 'Lucida Sans', 'Lucida Grande', + 'Lucida Sans Unicode', Verdana, sans-serif; } body { - background: #272a2f; - color: #ebebeb; + background: #272a2f; + color: #ebebeb; } body, html, #root { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } a:link, a:visited { - color: #aaaaaa; - font-weight: 300; - text-decoration: none; + color: #aaaaaa; + font-weight: 300; + text-decoration: none; } a:hover, a:active { - color: #7bc876; - text-decoration: none; + color: #7bc876; + text-decoration: none; } /* Bug 1351813 * Increase font size for the Arabic language to make it readable. */ [data-script='Arabic'] { - font-size: 1.1em; + font-size: 1.1em; } [data-script='Arabic'] .placeable { - font-size: 0.9em; + font-size: 0.9em; } diff --git a/translate/src/index.tsx b/translate/src/index.tsx index 0b83a9694..28756bc78 100644 --- a/translate/src/index.tsx +++ b/translate/src/index.tsx @@ -20,12 +20,12 @@ import App from './App'; TimeAgo.addLocale(en); ReactDOM.render( - - - - - - - , - document.getElementById('root'), + + + + + + + , + document.getElementById('root'), ); diff --git a/translate/src/modules/addonpromotion/components/AddonPromotion.css b/translate/src/modules/addonpromotion/components/AddonPromotion.css index 5d9e27e08..5617219b4 100644 --- a/translate/src/modules/addonpromotion/components/AddonPromotion.css +++ b/translate/src/modules/addonpromotion/components/AddonPromotion.css @@ -1,50 +1,50 @@ .addon-promotion + header { - margin-top: 44px; + margin-top: 44px; } .addon-promotion { - background: #272a2f; - border-bottom: 1px solid #333941; - height: 44px; - position: fixed; - top: 0; - width: 100%; - z-index: 20; + background: #272a2f; + border-bottom: 1px solid #333941; + height: 44px; + position: fixed; + top: 0; + width: 100%; + z-index: 20; } .addon-promotion .container { - align-items: center; - background: #ff3366cc; - display: flex; - height: 100%; - padding: 0 10px; + align-items: center; + background: #ff3366cc; + display: flex; + height: 100%; + padding: 0 10px; } .addon-promotion .dismiss { - background: transparent; - border: none; - color: #ebebeb; - font-size: 28px; - padding: 0; + background: transparent; + border: none; + color: #ebebeb; + font-size: 28px; + padding: 0; } .addon-promotion .dismiss:hover { - color: #272a2f; + color: #272a2f; } .addon-promotion .text { - margin-left: 20px; + margin-left: 20px; } .addon-promotion .get { - background: #272a2f; - border-radius: 3px; - color: #ebebeb; - font-weight: normal; - margin-left: auto; - padding: 7px 12px; + background: #272a2f; + border-radius: 3px; + color: #ebebeb; + font-weight: normal; + margin-left: auto; + padding: 7px 12px; } .addon-promotion .get:hover { - background: #333941; + background: #333941; } diff --git a/translate/src/modules/addonpromotion/components/AddonPromotion.tsx b/translate/src/modules/addonpromotion/components/AddonPromotion.tsx index eb099877e..444924909 100644 --- a/translate/src/modules/addonpromotion/components/AddonPromotion.tsx +++ b/translate/src/modules/addonpromotion/components/AddonPromotion.tsx @@ -10,159 +10,155 @@ import type { UserState } from '~/core/user'; import { AppDispatch, RootState } from '~/store'; type Props = { - user: UserState; + user: UserState; }; type InternalProps = Props & { - dispatch: AppDispatch; + dispatch: AppDispatch; }; type State = { - installed: boolean; + installed: boolean; }; interface PontoonAddonInfo { - installed?: boolean; + installed?: boolean; } interface PontoonAddonInfoMessage { - _type?: 'PontoonAddonInfo'; - value?: PontoonAddonInfo; + _type?: 'PontoonAddonInfo'; + value?: PontoonAddonInfo; } interface WindowWithInfo extends Window { - PontoonAddon?: PontoonAddonInfo; + PontoonAddon?: PontoonAddonInfo; } /** * Renders Pontoon Add-On promotion banner. */ export class AddonPromotionBase extends React.Component { - constructor(props: InternalProps) { - super(props); - this.state = { - installed: false, - }; - } - - componentDidMount() { - window.addEventListener('message', this.handleMessages); - } - - componentWillUnmount() { - window.removeEventListener('message', this.handleMessages); - } - - // Hide Add-On Promotion if Add-On installed while active - handleMessages: (event: MessageEvent) => void = (event: MessageEvent) => { - // only allow messages from authorized senders (extension content script, or Pontoon itself) - if (event.origin !== window.origin || event.source !== window) { - return; - } - let data: PontoonAddonInfoMessage; - switch (typeof event.data) { - case 'object': - data = event.data; - break; - case 'string': - // backward compatibility - // TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released - // and convert this switch into a condition - try { - data = JSON.parse(event.data); - } catch (_) { - return; - } - break; - default: - return; - } - if ( - data?._type === 'PontoonAddonInfo' && - data?.value?.installed === true - ) { - this.setState({ installed: true }); - } + constructor(props: InternalProps) { + super(props); + this.state = { + installed: false, }; + } - handleDismiss: () => void = () => { - this.props.dispatch(user.actions.dismissAddonPromotion()); - }; + componentDidMount() { + window.addEventListener('message', this.handleMessages); + } - render(): null | React.ReactElement<'div'> { - const { user } = this.props; + componentWillUnmount() { + window.removeEventListener('message', this.handleMessages); + } - // User not authenticated or promotion dismissed - if (!user.isAuthenticated || user.hasDismissedAddonPromotion) { - return null; - } - - // Add-On installed - if ( - this.state.installed || - (window as WindowWithInfo).PontoonAddon?.installed === true - ) { - return null; - } - - const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; - const isChrome = navigator.userAgent.indexOf('Chrome') !== -1; - - let downloadHref = ''; - - if (isFirefox) { - downloadHref = - 'https://addons.mozilla.org/firefox/addon/pontoon-tools/'; - } - - if (isChrome) { - downloadHref = - 'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb'; - } - - // Page not loaded in Firefox or Chrome (add-on not available for other browsers) - if (!downloadHref) { - return null; - } - - return ( -
    -
    - - - - - -

    - Take your Pontoon notifications everywhere with the - official Pontoon Add-on. -

    -
    - - - - Get Pontoon Add-On - - -
    -
    - ); + // Hide Add-On Promotion if Add-On installed while active + handleMessages: (event: MessageEvent) => void = (event: MessageEvent) => { + // only allow messages from authorized senders (extension content script, or Pontoon itself) + if (event.origin !== window.origin || event.source !== window) { + return; } + let data: PontoonAddonInfoMessage; + switch (typeof event.data) { + case 'object': + data = event.data; + break; + case 'string': + // backward compatibility + // TODO: remove some reasonable time after https://github.com/MikkCZ/pontoon-addon/pull/155 is released + // and convert this switch into a condition + try { + data = JSON.parse(event.data); + } catch (_) { + return; + } + break; + default: + return; + } + if (data?._type === 'PontoonAddonInfo' && data?.value?.installed === true) { + this.setState({ installed: true }); + } + }; + + handleDismiss: () => void = () => { + this.props.dispatch(user.actions.dismissAddonPromotion()); + }; + + render(): null | React.ReactElement<'div'> { + const { user } = this.props; + + // User not authenticated or promotion dismissed + if (!user.isAuthenticated || user.hasDismissedAddonPromotion) { + return null; + } + + // Add-On installed + if ( + this.state.installed || + (window as WindowWithInfo).PontoonAddon?.installed === true + ) { + return null; + } + + const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; + const isChrome = navigator.userAgent.indexOf('Chrome') !== -1; + + let downloadHref = ''; + + if (isFirefox) { + downloadHref = 'https://addons.mozilla.org/firefox/addon/pontoon-tools/'; + } + + if (isChrome) { + downloadHref = + 'https://chrome.google.com/webstore/detail/pontoon-add-on/gnbfbnpjncpghhjmmhklfhcglbopagbb'; + } + + // Page not loaded in Firefox or Chrome (add-on not available for other browsers) + if (!downloadHref) { + return null; + } + + return ( +
    +
    + + + + + +

    + Take your Pontoon notifications everywhere with the official + Pontoon Add-on. +

    +
    + + + + Get Pontoon Add-On + + +
    +
    + ); + } } const mapStateToProps = (state: RootState): Props => { - return { - user: state[user.NAME], - }; + return { + user: state[user.NAME], + }; }; export default connect(mapStateToProps)(AddonPromotionBase) as any; diff --git a/translate/src/modules/batchactions/actions.ts b/translate/src/modules/batchactions/actions.ts index 24323f121..986293bad 100644 --- a/translate/src/modules/batchactions/actions.ts +++ b/translate/src/modules/batchactions/actions.ts @@ -12,256 +12,248 @@ export const RECEIVE: 'batchactions/RECEIVE' = 'batchactions/RECEIVE'; export const REQUEST: 'batchactions/REQUEST' = 'batchactions/REQUEST'; export const RESET: 'batchactions/RESET' = 'batchactions/RESET'; export const RESET_RESPONSE: 'batchactions/RESET_RESPONSE' = - 'batchactions/RESET_RESPONSE'; + 'batchactions/RESET_RESPONSE'; export const TOGGLE: 'batchactions/TOGGLE' = 'batchactions/TOGGLE'; export const UNCHECK: 'batchactions/UNCHECK' = 'batchactions/UNCHECK'; export type CheckAction = { - type: typeof CHECK; - entities: Array; - lastCheckedEntity: number; + type: typeof CHECK; + entities: Array; + lastCheckedEntity: number; }; export function checkSelection( - entities: Array, - lastCheckedEntity: number, + entities: Array, + lastCheckedEntity: number, ): CheckAction { - return { - type: CHECK, - entities, - lastCheckedEntity, - }; + return { + type: CHECK, + entities, + lastCheckedEntity, + }; } function updateUI( - locale: string, - project: string, - resource: string, - selectedEntity: number, - entities: Array, + locale: string, + project: string, + resource: string, + selectedEntity: number, + entities: Array, ) { - return async (dispatch: AppDispatch) => { - const entitiesData = await api.entity.getEntities( - locale, - project, + return async (dispatch: AppDispatch) => { + const entitiesData = await api.entity.getEntities( + locale, + project, + resource, + entities, + [], + ); + + if (entitiesData.stats) { + // Update stats in progress chart and filter panel. + dispatch(statsActions.update(entitiesData.stats)); + + /* + * Update stats in the resource menu. + * + * TODO: Update stats for all affected resources. ATM that's not possbile, + * since the backend only returns stats for the passed resource. + */ + if (resource !== 'all-resources') { + dispatch( + resourceActions.update( resource, - entities, - [], + entitiesData.stats.approved, + entitiesData.stats.warnings, + ), + ); + } + } + + // Update entity translation data now that it has changed on the server. + for (let entity of entitiesData.entities) { + entity.translation.forEach(function (translation, pluralForm) { + dispatch( + entitiesActions.updateEntityTranslation( + entity.pk, + pluralForm, + translation, + ), ); - if (entitiesData.stats) { - // Update stats in progress chart and filter panel. - dispatch(statsActions.update(entitiesData.stats)); - - /* - * Update stats in the resource menu. - * - * TODO: Update stats for all affected resources. ATM that's not possbile, - * since the backend only returns stats for the passed resource. - */ - if (resource !== 'all-resources') { - dispatch( - resourceActions.update( - resource, - entitiesData.stats.approved, - entitiesData.stats.warnings, - ), - ); - } + if (entity.pk === selectedEntity) { + dispatch(historyActions.request(entity.pk, pluralForm)); + dispatch(historyActions.get(entity.pk, locale, pluralForm)); } - - // Update entity translation data now that it has changed on the server. - for (let entity of entitiesData.entities) { - entity.translation.forEach(function (translation, pluralForm) { - dispatch( - entitiesActions.updateEntityTranslation( - entity.pk, - pluralForm, - translation, - ), - ); - - if (entity.pk === selectedEntity) { - dispatch(historyActions.request(entity.pk, pluralForm)); - dispatch(historyActions.get(entity.pk, locale, pluralForm)); - } - }); - } - }; + }); + } + }; } export function performAction( - action: string, - locale: string, - project: string, - resource: string, - selectedEntity: number, - entities: Array, - find?: string, - replace?: string, + action: string, + locale: string, + project: string, + resource: string, + selectedEntity: number, + entities: Array, + find?: string, + replace?: string, ) { - return async (dispatch: AppDispatch) => { - dispatch(request(action)); + return async (dispatch: AppDispatch) => { + dispatch(request(action)); - const data = await api.entity.batchEdit( - action, - locale, - entities, - find, - replace, - ); + const data = await api.entity.batchEdit( + action, + locale, + entities, + find, + replace, + ); - const response: ResponseType = { - changedCount: 0, - invalidCount: 0, - error: false, - action, - }; - - if ('count' in data) { - response.changedCount = data.count; - response.invalidCount = data.invalid_translation_count; - - if (data.count > 0) { - dispatch( - updateUI( - locale, - project, - resource, - selectedEntity, - entities, - ), - ); - } - } else { - response.error = true; - } - - dispatch(receive(response)); - - setTimeout(() => { - dispatch(reset_response()); - }, 3000); + const response: ResponseType = { + changedCount: 0, + invalidCount: 0, + error: false, + action, }; + + if ('count' in data) { + response.changedCount = data.count; + response.invalidCount = data.invalid_translation_count; + + if (data.count > 0) { + dispatch(updateUI(locale, project, resource, selectedEntity, entities)); + } + } else { + response.error = true; + } + + dispatch(receive(response)); + + setTimeout(() => { + dispatch(reset_response()); + }, 3000); + }; } export type ResponseType = { - action: string; - changedCount: number | null | undefined; - invalidCount: number | null | undefined; - error: boolean | null | undefined; + action: string; + changedCount: number | null | undefined; + invalidCount: number | null | undefined; + error: boolean | null | undefined; }; export type ReceiveAction = { - type: typeof RECEIVE; - response: ResponseType | null | undefined; + type: typeof RECEIVE; + response: ResponseType | null | undefined; }; export function receive( - response?: ResponseType | null | undefined, + response?: ResponseType | null | undefined, ): ReceiveAction { - return { - type: RECEIVE, - response, - }; + return { + type: RECEIVE, + response, + }; } export type RequestAction = { - type: typeof REQUEST; - source: string; + type: typeof REQUEST; + source: string; }; export function request(source: string): RequestAction { - return { - type: REQUEST, - source, - }; + return { + type: REQUEST, + source, + }; } export type ResetResponseAction = { - type: typeof RESET_RESPONSE; + type: typeof RESET_RESPONSE; }; export function reset_response(): ResetResponseAction { - return { - type: RESET_RESPONSE, - }; + return { + type: RESET_RESPONSE, + }; } export type ResetAction = { - type: typeof RESET; + type: typeof RESET; }; export function resetSelection(): ResetAction { - return { - type: RESET, - }; + return { + type: RESET, + }; } export function selectAll( - locale: string, - project: string, - resource: string, - search: string | null | undefined, - status: string | null | undefined, - extra: string | null | undefined, - tag: string | null | undefined, - author: string | null | undefined, - time: string | null | undefined, + locale: string, + project: string, + resource: string, + search: string | null | undefined, + status: string | null | undefined, + extra: string | null | undefined, + tag: string | null | undefined, + author: string | null | undefined, + time: string | null | undefined, ) { - return async (dispatch: AppDispatch) => { - dispatch(request('select-all')); + return async (dispatch: AppDispatch) => { + dispatch(request('select-all')); - const content = await api.entity.getEntities( - locale, - project, - resource, - null, - [], - null, - search, - status, - extra, - tag, - author, - time, - true, - ); + const content = await api.entity.getEntities( + locale, + project, + resource, + null, + [], + null, + search, + status, + extra, + tag, + author, + time, + true, + ); - const entities = content.entity_pks; + const entities = content.entity_pks; - dispatch(receive()); - dispatch(checkSelection(entities, entities[0])); - }; + dispatch(receive()); + dispatch(checkSelection(entities, entities[0])); + }; } export type ToggleAction = { - type: typeof TOGGLE; - entity: number; + type: typeof TOGGLE; + entity: number; }; export function toggleSelection(entity: number): ToggleAction { - return { - type: TOGGLE, - entity, - }; + return { + type: TOGGLE, + entity, + }; } export type UncheckAction = { - type: typeof UNCHECK; - entities: Array; - lastCheckedEntity: number; + type: typeof UNCHECK; + entities: Array; + lastCheckedEntity: number; }; export function uncheckSelection( - entities: Array, - lastCheckedEntity: number, + entities: Array, + lastCheckedEntity: number, ): UncheckAction { - return { - type: UNCHECK, - entities, - lastCheckedEntity, - }; + return { + type: UNCHECK, + entities, + lastCheckedEntity, + }; } export default { - checkSelection, - performAction, - resetSelection, - selectAll, - toggleSelection, - uncheckSelection, + checkSelection, + performAction, + resetSelection, + selectAll, + toggleSelection, + uncheckSelection, }; diff --git a/translate/src/modules/batchactions/components/ApproveAll.test.js b/translate/src/modules/batchactions/components/ApproveAll.test.js index d10734b43..43edd6b6d 100644 --- a/translate/src/modules/batchactions/components/ApproveAll.test.js +++ b/translate/src/modules/batchactions/components/ApproveAll.test.js @@ -5,126 +5,102 @@ import sinon from 'sinon'; import ApproveAll from './ApproveAll'; const DEFAULT_BATCH_ACTIONS = { - entities: [], - lastCheckedEntity: null, - requestInProgress: null, - response: null, + entities: [], + lastCheckedEntity: null, + requestInProgress: null, + response: null, }; describe('', () => { - it('renders default button correctly', () => { - const wrapper = shallow( - , - ); + it('renders default button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.approve-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.approve-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders error button correctly', () => { - const wrapper = shallow( - , - ); + it('renders error button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.approve-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(1); - expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.approve-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.approve-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.approve-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success with invalid button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success with invalid button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.approve-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength( - 1, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.approve-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ApproveAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-ApproveAll--invalid')).toHaveLength(1); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('performs approve all action when Approve All button is clicked', () => { - const mockApproveAll = sinon.spy(); + it('performs approve all action when Approve All button is clicked', () => { + const mockApproveAll = sinon.spy(); - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - expect(mockApproveAll.called).toBeFalsy(); - wrapper.find('.approve-all').simulate('click'); - expect(mockApproveAll.called).toBeTruthy(); - }); + expect(mockApproveAll.called).toBeFalsy(); + wrapper.find('.approve-all').simulate('click'); + expect(mockApproveAll.called).toBeTruthy(); + }); }); diff --git a/translate/src/modules/batchactions/components/ApproveAll.tsx b/translate/src/modules/batchactions/components/ApproveAll.tsx index e8941d682..ecd4279ac 100644 --- a/translate/src/modules/batchactions/components/ApproveAll.tsx +++ b/translate/src/modules/batchactions/components/ApproveAll.tsx @@ -4,95 +4,94 @@ import { Localized } from '@fluent/react'; import type { BatchActionsState } from '~/modules/batchactions'; type Props = { - approveAll: () => void; - batchactions: BatchActionsState; + approveAll: () => void; + batchactions: BatchActionsState; }; /** * Renders Approve All batch action button. */ export default class ApproveAll extends React.Component { - renderDefault(): React.ReactElement { + renderDefault(): React.ReactElement { + return ( + + {'APPROVE ALL'} + + ); + } + + renderError(): React.ReactElement { + return ( + + {'OOPS, SOMETHING WENT WRONG'} + + ); + } + + renderInvalid(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $invalidCount } FAILED'} + + ); + } + + renderSuccess(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $changedCount } STRINGS APPROVED'} + + ); + } + + renderTitle(): null | React.ReactNode { + const { response } = this.props.batchactions; + + if (response && response.action === 'approve') { + if (response.error) { + return this.renderError(); + } else if (response.invalidCount) { return ( - - {'APPROVE ALL'} - + <> + {this.renderSuccess()} + {' · '} + {this.renderInvalid()} + ); + } else { + return this.renderSuccess(); + } + } else { + return this.renderDefault(); } + } - renderError(): React.ReactElement { - return ( - - {'OOPS, SOMETHING WENT WRONG'} - - ); - } - - renderInvalid(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $invalidCount } FAILED'} - - ); - } - - renderSuccess(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $changedCount } STRINGS APPROVED'} - - ); - } - - renderTitle(): null | React.ReactNode { - const { response } = this.props.batchactions; - - if (response && response.action === 'approve') { - if (response.error) { - return this.renderError(); - } else if (response.invalidCount) { - return ( - <> - {this.renderSuccess()} - {' · '} - {this.renderInvalid()} - - ); - } else { - return this.renderSuccess(); - } - } else { - return this.renderDefault(); - } - } - - render(): React.ReactElement<'button'> { - return ( - - ); - } + render(): React.ReactElement<'button'> { + return ( + + ); + } } diff --git a/translate/src/modules/batchactions/components/BatchActions.css b/translate/src/modules/batchactions/components/BatchActions.css index 14efeb738..41018e883 100644 --- a/translate/src/modules/batchactions/components/BatchActions.css +++ b/translate/src/modules/batchactions/components/BatchActions.css @@ -1,133 +1,133 @@ .batch-actions { - height: 100%; - line-height: 22px; + height: 100%; + line-height: 22px; } .batch-actions .topbar { - background: #4d5967; - border-bottom: 1px solid #5e6475; - padding: 12px 10px 13px; + background: #4d5967; + border-bottom: 1px solid #5e6475; + padding: 12px 10px 13px; } .batch-actions .topbar .selecting { - color: #cccccc; - float: right; - margin-top: 2px; + color: #cccccc; + float: right; + margin-top: 2px; } .batch-actions .topbar button { - background: none; - border: none; - color: #cccccc; - font-weight: 300; - padding: 0; + background: none; + border: none; + color: #cccccc; + font-weight: 300; + padding: 0; } .batch-actions .topbar button:hover { - color: #7bc876; + color: #7bc876; } .batch-actions .topbar button.select-all { - float: left; + float: left; } .batch-actions .topbar button.selected-count { - float: right; + float: right; } .batch-actions .topbar button.selected-count .stress { - color: #7bc876; + color: #7bc876; } .batch-actions .topbar button .fa { - padding-right: 5px; + padding-right: 5px; } .batch-actions .topbar .selected-count .fa-times { - display: inline; + display: inline; } .batch-actions .actions-panel { - overflow: auto; - position: absolute; - top: 44px; - left: 0; - right: 0; - bottom: 0; + overflow: auto; + position: absolute; + top: 44px; + left: 0; + right: 0; + bottom: 0; } .batch-actions .actions-panel div { - margin: 0 auto; - min-width: 300px; - padding: 20px; - width: 50%; + margin: 0 auto; + min-width: 300px; + padding: 20px; + width: 50%; } .batch-actions .actions-panel h2 { - color: #cccccc; - font-size: 14px; - font-weight: 300; - padding-bottom: 2px; + color: #cccccc; + font-size: 14px; + font-weight: 300; + padding-bottom: 2px; } .batch-actions .actions-panel .intro p { - color: #aaaaaa; - font-style: italic; + color: #aaaaaa; + font-style: italic; } .batch-actions .actions-panel .intro p .stress { - color: #f36; + color: #f36; } .batch-actions .actions-panel button, .batch-actions .actions-panel input { - display: block; - width: 100%; + display: block; + width: 100%; } .batch-actions .actions-panel button { - background: #7bc876; - border: none; - border-radius: 3px; - color: #272a2f; - font-weight: 600; - margin-top: 10px; - padding: 15px 5px; - position: relative; + background: #7bc876; + border: none; + border-radius: 3px; + color: #272a2f; + font-weight: 600; + margin-top: 10px; + padding: 15px 5px; + position: relative; } .batch-actions .actions-panel button .fa { - position: absolute; - right: 10px; - top: 12px; - opacity: 0.5; + position: absolute; + right: 10px; + top: 12px; + opacity: 0.5; } .batch-actions .actions-panel button.reject-all { - background: #f36; + background: #f36; } .batch-actions .actions-panel input { - background: transparent; - border: none; - border-bottom: 1px solid #5e6475; - color: #ffffff; - float: none; - margin-bottom: 10px; - padding: 15px 0 5px; + background: transparent; + border: none; + border-bottom: 1px solid #5e6475; + color: #ffffff; + float: none; + margin-bottom: 10px; + padding: 15px 0 5px; } /* Remove highlight in Chrome */ .batch-actions .actions-panel input:focus { - outline: none; + outline: none; } /* Remove cancel button in Chrome */ .batch-actions .actions-panel input::-webkit-search-cancel-button { - display: none; + display: none; } /* Style placeholder color in Chrome */ .batch-actions .actions-panel input::-webkit-input-placeholder { - color: #aaaaaa; + color: #aaaaaa; } diff --git a/translate/src/modules/batchactions/components/BatchActions.test.js b/translate/src/modules/batchactions/components/BatchActions.test.js index a64342905..4cc053585 100644 --- a/translate/src/modules/batchactions/components/BatchActions.test.js +++ b/translate/src/modules/batchactions/components/BatchActions.test.js @@ -11,85 +11,81 @@ import ReplaceAll from './ReplaceAll'; import { actions } from '..'; const DEFAULT_BATCH_ACTIONS = { - entities: [], - lastCheckedEntity: null, - requestInProgress: null, - response: null, + entities: [], + lastCheckedEntity: null, + requestInProgress: null, + response: null, }; describe('', () => { - beforeAll(() => { - sinon.stub(actions, 'resetSelection').returns({ type: 'whatever' }); - sinon.stub(actions, 'selectAll').returns({ type: 'whatever' }); - }); + beforeAll(() => { + sinon.stub(actions, 'resetSelection').returns({ type: 'whatever' }); + sinon.stub(actions, 'selectAll').returns({ type: 'whatever' }); + }); - afterEach(() => { - actions.resetSelection.reset(); - actions.selectAll.reset(); - }); + afterEach(() => { + actions.resetSelection.reset(); + actions.selectAll.reset(); + }); - afterAll(() => { - actions.resetSelection.restore(); - actions.selectAll.restore(); - }); + afterAll(() => { + actions.resetSelection.restore(); + actions.selectAll.restore(); + }); - it('renders correctly', () => { - const wrapper = shallow( - , - ); + it('renders correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.batch-actions')).toHaveLength(1); + expect(wrapper.find('.batch-actions')).toHaveLength(1); - expect(wrapper.find('.topbar')).toHaveLength(1); - expect(wrapper.find('.selected-count')).toHaveLength(1); - expect(wrapper.find('.select-all')).toHaveLength(1); + expect(wrapper.find('.topbar')).toHaveLength(1); + expect(wrapper.find('.selected-count')).toHaveLength(1); + expect(wrapper.find('.select-all')).toHaveLength(1); - expect(wrapper.find('.actions-panel')).toHaveLength(1); + expect(wrapper.find('.actions-panel')).toHaveLength(1); - expect( - wrapper.find('#batchactions-BatchActions--warning'), - ).toHaveLength(1); + expect(wrapper.find('#batchactions-BatchActions--warning')).toHaveLength(1); - expect( - wrapper.find('#batchactions-BatchActions--review-heading'), - ).toHaveLength(1); - expect(wrapper.find(ApproveAll)).toHaveLength(1); - expect(wrapper.find(RejectAll)).toHaveLength(1); + expect( + wrapper.find('#batchactions-BatchActions--review-heading'), + ).toHaveLength(1); + expect(wrapper.find(ApproveAll)).toHaveLength(1); + expect(wrapper.find(RejectAll)).toHaveLength(1); - expect( - wrapper.find('#batchactions-BatchActions--find-replace-heading'), - ).toHaveLength(1); - expect(wrapper.find('#batchactions-BatchActions--find')).toHaveLength( - 1, - ); - expect( - wrapper.find('#batchactions-BatchActions--replace-with'), - ).toHaveLength(1); - expect(wrapper.find(ReplaceAll)).toHaveLength(1); - }); + expect( + wrapper.find('#batchactions-BatchActions--find-replace-heading'), + ).toHaveLength(1); + expect(wrapper.find('#batchactions-BatchActions--find')).toHaveLength(1); + expect( + wrapper.find('#batchactions-BatchActions--replace-with'), + ).toHaveLength(1); + expect(wrapper.find(ReplaceAll)).toHaveLength(1); + }); - it('closes batch actions panel when the Close button with selected count is clicked', () => { - const wrapper = shallow( - {}} - />, - ); + it('closes batch actions panel when the Close button with selected count is clicked', () => { + const wrapper = shallow( + {}} + />, + ); - wrapper.find('.selected-count').simulate('click'); - expect(actions.resetSelection.called).toBeTruthy(); - }); + wrapper.find('.selected-count').simulate('click'); + expect(actions.resetSelection.called).toBeTruthy(); + }); - it('selects all entities when the Select All button is clicked', () => { - const wrapper = shallow( - {}} - parameters={{}} - />, - ); + it('selects all entities when the Select All button is clicked', () => { + const wrapper = shallow( + {}} + parameters={{}} + />, + ); - wrapper.find('.select-all').simulate('click'); - expect(actions.selectAll.called).toBeTruthy(); - }); + wrapper.find('.select-all').simulate('click'); + expect(actions.selectAll.called).toBeTruthy(); + }); }); diff --git a/translate/src/modules/batchactions/components/BatchActions.tsx b/translate/src/modules/batchactions/components/BatchActions.tsx index e7db7f470..9993fcbe1 100644 --- a/translate/src/modules/batchactions/components/BatchActions.tsx +++ b/translate/src/modules/batchactions/components/BatchActions.tsx @@ -16,289 +16,285 @@ import type { NavigationParams } from '~/core/navigation'; import { AppDispatch, RootState } from '~/store'; type Props = { - batchactions: BatchActionsState; - parameters: NavigationParams; + batchactions: BatchActionsState; + parameters: NavigationParams; }; type InternalProps = Props & { - dispatch: AppDispatch; + dispatch: AppDispatch; }; /** * Renders batch editor, used for performing mass actions on translations. */ export class BatchActionsBase extends React.Component { - find: { - current: HTMLInputElement | null | undefined; - }; - replace: { - current: HTMLInputElement | null | undefined; - }; + find: { + current: HTMLInputElement | null | undefined; + }; + replace: { + current: HTMLInputElement | null | undefined; + }; - constructor(props: InternalProps) { - super(props); + constructor(props: InternalProps) { + super(props); - this.find = React.createRef(); - this.replace = React.createRef(); + this.find = React.createRef(); + this.replace = React.createRef(); + } + + componentDidMount() { + document.addEventListener('keydown', this.handleShortcuts); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleShortcuts); + } + + handleShortcuts: (event: KeyboardEvent) => void = (event: KeyboardEvent) => { + const key = event.keyCode; + + // On Esc, quit batch actions + if (key === 27) { + this.quitBatchActions(); + } + }; + + quitBatchActions: () => void = () => { + this.props.dispatch(batchactions.actions.resetSelection()); + }; + + selectAllEntities: () => void = () => { + const { + locale, + project, + resource, + search, + status, + extra, + tag, + author, + time, + } = this.props.parameters; + + this.props.dispatch( + batchactions.actions.selectAll( + locale, + project, + resource, + search, + status, + extra, + tag, + author, + time, + ), + ); + }; + + approveAll: () => void = () => { + if (this.props.batchactions.requestInProgress) { + return; } - componentDidMount() { - document.addEventListener('keydown', this.handleShortcuts); + const { entity, locale, project, resource } = this.props.parameters; + + this.props.dispatch( + batchactions.actions.performAction( + 'approve', + locale, + project, + resource, + entity, + this.props.batchactions.entities, + ), + ); + }; + + rejectAll: () => void = () => { + if (this.props.batchactions.requestInProgress) { + return; } - componentWillUnmount() { - document.removeEventListener('keydown', this.handleShortcuts); + const { entity, locale, project, resource } = this.props.parameters; + + this.props.dispatch( + batchactions.actions.performAction( + 'reject', + locale, + project, + resource, + entity, + this.props.batchactions.entities, + ), + ); + }; + + replaceAll: () => void = () => { + if (this.props.batchactions.requestInProgress) { + return; } - handleShortcuts: (event: KeyboardEvent) => void = ( - event: KeyboardEvent, - ) => { - const key = event.keyCode; + const find = this.find.current; + const replace = this.replace.current; - // On Esc, quit batch actions - if (key === 27) { - this.quitBatchActions(); - } - }; - - quitBatchActions: () => void = () => { - this.props.dispatch(batchactions.actions.resetSelection()); - }; - - selectAllEntities: () => void = () => { - const { - locale, - project, - resource, - search, - status, - extra, - tag, - author, - time, - } = this.props.parameters; - - this.props.dispatch( - batchactions.actions.selectAll( - locale, - project, - resource, - search, - status, - extra, - tag, - author, - time, - ), - ); - }; - - approveAll: () => void = () => { - if (this.props.batchactions.requestInProgress) { - return; - } - - const { entity, locale, project, resource } = this.props.parameters; - - this.props.dispatch( - batchactions.actions.performAction( - 'approve', - locale, - project, - resource, - entity, - this.props.batchactions.entities, - ), - ); - }; - - rejectAll: () => void = () => { - if (this.props.batchactions.requestInProgress) { - return; - } - - const { entity, locale, project, resource } = this.props.parameters; - - this.props.dispatch( - batchactions.actions.performAction( - 'reject', - locale, - project, - resource, - entity, - this.props.batchactions.entities, - ), - ); - }; - - replaceAll: () => void = () => { - if (this.props.batchactions.requestInProgress) { - return; - } - - const find = this.find.current; - const replace = this.replace.current; - - if (!find || !replace) { - return; - } - - if (find.value === '') { - find.focus(); - return; - } - - if (find.value === replace.value) { - replace.focus(); - return; - } - - const { entity, locale, project, resource } = this.props.parameters; - - this.props.dispatch( - batchactions.actions.performAction( - 'replace', - locale, - project, - resource, - entity, - this.props.batchactions.entities, - encodeURIComponent(find.value), - encodeURIComponent(replace.value), - ), - ); - }; - - submitReplaceForm: (event: React.SyntheticEvent) => void = - (event: React.SyntheticEvent) => { - event.preventDefault(); - this.replaceAll(); - }; - - render(): React.ReactElement<'div'> { - return ( -
    -
    - }} - > - - - {this.props.batchactions.requestInProgress === - 'select-all' ? ( -
    - ) : ( - , - stress: , - }} - vars={{ - count: this.props.batchactions.entities.length, - }} - > - - - )} -
    - -
    -
    - }} - > -

    - { - 'Warning: These actions will be applied to all selected strings and cannot be undone.' - } -

    -
    -
    - -
    - -

    REVIEW TRANSLATIONS

    -
    - - - - -
    - -
    - -

    FIND & REPLACE IN TRANSLATIONS

    -
    - -
    - - - - - - - - - - -
    -
    -
    - ); + if (!find || !replace) { + return; } + + if (find.value === '') { + find.focus(); + return; + } + + if (find.value === replace.value) { + replace.focus(); + return; + } + + const { entity, locale, project, resource } = this.props.parameters; + + this.props.dispatch( + batchactions.actions.performAction( + 'replace', + locale, + project, + resource, + entity, + this.props.batchactions.entities, + encodeURIComponent(find.value), + encodeURIComponent(replace.value), + ), + ); + }; + + submitReplaceForm: (event: React.SyntheticEvent) => void = ( + event: React.SyntheticEvent, + ) => { + event.preventDefault(); + this.replaceAll(); + }; + + render(): React.ReactElement<'div'> { + return ( +
    +
    + }} + > + + + {this.props.batchactions.requestInProgress === 'select-all' ? ( +
    + ) : ( + , + stress: , + }} + vars={{ + count: this.props.batchactions.entities.length, + }} + > + + + )} +
    + +
    +
    + }} + > +

    + { + 'Warning: These actions will be applied to all selected strings and cannot be undone.' + } +

    +
    +
    + +
    + +

    REVIEW TRANSLATIONS

    +
    + + + + +
    + +
    + +

    FIND & REPLACE IN TRANSLATIONS

    +
    + +
    + + + + + + + + + + +
    +
    +
    + ); + } } const mapStateToProps = (state: RootState): Props => { - return { - batchactions: state[batchactions.NAME], - parameters: navigation.selectors.getNavigationParams(state), - }; + return { + batchactions: state[batchactions.NAME], + parameters: navigation.selectors.getNavigationParams(state), + }; }; export default connect(mapStateToProps)(BatchActionsBase) as any; diff --git a/translate/src/modules/batchactions/components/RejectAll.test.js b/translate/src/modules/batchactions/components/RejectAll.test.js index d2eed2762..dab690be6 100644 --- a/translate/src/modules/batchactions/components/RejectAll.test.js +++ b/translate/src/modules/batchactions/components/RejectAll.test.js @@ -5,146 +5,120 @@ import sinon from 'sinon'; import RejectAll from './RejectAll'; const DEFAULT_BATCH_ACTIONS = { - entities: [], - lastCheckedEntity: null, - requestInProgress: null, - response: null, + entities: [], + lastCheckedEntity: null, + requestInProgress: null, + response: null, }; describe('', () => { - it('renders default button correctly', () => { - const wrapper = shallow( - , - ); + it('renders default button correctly', () => { + const wrapper = shallow(); - expect(wrapper.find('.reject-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.reject-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders error button correctly', () => { - const wrapper = shallow( - , - ); + it('renders error button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.reject-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(1); - expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.reject-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.reject-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.reject-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success with invalid button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success with invalid button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.reject-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength( - 1, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.reject-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-RejectAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-RejectAll--invalid')).toHaveLength(1); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('raise confirmation warning when Reject All button is clicked', () => { - const mockRejectAll = sinon.spy(); + it('raise confirmation warning when Reject All button is clicked', () => { + const mockRejectAll = sinon.spy(); - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - expect(mockRejectAll.called).toBeFalsy(); - wrapper.find('.reject-all').simulate('click'); - expect(mockRejectAll.called).toBeFalsy(); - expect( - wrapper.find('#batchactions-RejectAll--confirmation'), - ).toHaveLength(1); - }); + expect(mockRejectAll.called).toBeFalsy(); + wrapper.find('.reject-all').simulate('click'); + expect(mockRejectAll.called).toBeFalsy(); + expect(wrapper.find('#batchactions-RejectAll--confirmation')).toHaveLength( + 1, + ); + }); - it('performs reject all action when Reject All button is confirmed', () => { - const mockRejectAll = sinon.spy(); + it('performs reject all action when Reject All button is confirmed', () => { + const mockRejectAll = sinon.spy(); - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - wrapper.instance().setState({ isConfirmationVisible: true }); + wrapper.instance().setState({ isConfirmationVisible: true }); - expect(mockRejectAll.called).toBeFalsy(); - wrapper.find('.reject-all').simulate('click'); - expect(mockRejectAll.called).toBeTruthy(); - }); + expect(mockRejectAll.called).toBeFalsy(); + wrapper.find('.reject-all').simulate('click'); + expect(mockRejectAll.called).toBeTruthy(); + }); }); diff --git a/translate/src/modules/batchactions/components/RejectAll.tsx b/translate/src/modules/batchactions/components/RejectAll.tsx index 8ded7cc9f..cb1bc820a 100644 --- a/translate/src/modules/batchactions/components/RejectAll.tsx +++ b/translate/src/modules/batchactions/components/RejectAll.tsx @@ -4,130 +4,129 @@ import { Localized } from '@fluent/react'; import type { BatchActionsState } from '~/modules/batchactions'; type Props = { - rejectAll: () => void; - batchactions: BatchActionsState; + rejectAll: () => void; + batchactions: BatchActionsState; }; type State = { - isConfirmationVisible: boolean; + isConfirmationVisible: boolean; }; /** * Renders Reject All batch action button. */ export default class RejectAll extends React.Component { - constructor(props: Props) { - super(props); + constructor(props: Props) { + super(props); - this.state = { - isConfirmationVisible: false, - }; - } - - rejectAll: () => void = () => { - if (!this.state.isConfirmationVisible) { - this.setState({ - isConfirmationVisible: true, - }); - } else { - this.props.rejectAll(); - this.setState({ - isConfirmationVisible: false, - }); - } + this.state = { + isConfirmationVisible: false, }; + } - renderConfirmation(): React.ReactElement { + rejectAll: () => void = () => { + if (!this.state.isConfirmationVisible) { + this.setState({ + isConfirmationVisible: true, + }); + } else { + this.props.rejectAll(); + this.setState({ + isConfirmationVisible: false, + }); + } + }; + + renderConfirmation(): React.ReactElement { + return ( + + {'ARE YOU SURE?'} + + ); + } + + renderDefault(): React.ReactElement { + return ( + + {'REJECT ALL SUGGESTIONS'} + + ); + } + + renderError(): React.ReactElement { + return ( + + {'OOPS, SOMETHING WENT WRONG'} + + ); + } + + renderInvalid(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $invalidCount } FAILED'} + + ); + } + + renderSuccess(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $changedCount } STRINGS REJECTED'} + + ); + } + + renderTitle(): null | React.ReactNode { + const { response } = this.props.batchactions; + + if (response && response.action === 'reject') { + if (response.error) { + return this.renderError(); + } else if (response.invalidCount) { return ( - - {'ARE YOU SURE?'} - + <> + {this.renderSuccess()} + {' · '} + {this.renderInvalid()} + ); + } else { + return this.renderSuccess(); + } + } else if (this.state.isConfirmationVisible) { + return this.renderConfirmation(); + } else { + return this.renderDefault(); } + } - renderDefault(): React.ReactElement { - return ( - - {'REJECT ALL SUGGESTIONS'} - - ); - } - - renderError(): React.ReactElement { - return ( - - {'OOPS, SOMETHING WENT WRONG'} - - ); - } - - renderInvalid(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $invalidCount } FAILED'} - - ); - } - - renderSuccess(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $changedCount } STRINGS REJECTED'} - - ); - } - - renderTitle(): null | React.ReactNode { - const { response } = this.props.batchactions; - - if (response && response.action === 'reject') { - if (response.error) { - return this.renderError(); - } else if (response.invalidCount) { - return ( - <> - {this.renderSuccess()} - {' · '} - {this.renderInvalid()} - - ); - } else { - return this.renderSuccess(); - } - } else if (this.state.isConfirmationVisible) { - return this.renderConfirmation(); - } else { - return this.renderDefault(); - } - } - - render(): React.ReactElement<'button'> { - return ( - - ); - } + render(): React.ReactElement<'button'> { + return ( + + ); + } } diff --git a/translate/src/modules/batchactions/components/ReplaceAll.test.js b/translate/src/modules/batchactions/components/ReplaceAll.test.js index 143cbc688..0b649858c 100644 --- a/translate/src/modules/batchactions/components/ReplaceAll.test.js +++ b/translate/src/modules/batchactions/components/ReplaceAll.test.js @@ -5,126 +5,102 @@ import sinon from 'sinon'; import ReplaceAll from './ReplaceAll'; const DEFAULT_BATCH_ACTIONS = { - entities: [], - lastCheckedEntity: null, - requestInProgress: null, - response: null, + entities: [], + lastCheckedEntity: null, + requestInProgress: null, + response: null, }; describe('', () => { - it('renders default button correctly', () => { - const wrapper = shallow( - , - ); + it('renders default button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.replace-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.replace-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders error button correctly', () => { - const wrapper = shallow( - , - ); + it('renders error button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.replace-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(1); - expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.replace-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.replace-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength( - 0, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.replace-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength(0); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('renders success with invalid button correctly', () => { - const wrapper = shallow( - , - ); + it('renders success with invalid button correctly', () => { + const wrapper = shallow( + , + ); - expect(wrapper.find('.replace-all')).toHaveLength(1); - expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength( - 0, - ); - expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); - expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength( - 1, - ); - expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength( - 1, - ); - expect(wrapper.find('.fa')).toHaveLength(0); - }); + expect(wrapper.find('.replace-all')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--default')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--error')).toHaveLength(0); + expect(wrapper.find('#batchactions-ReplaceAll--success')).toHaveLength(1); + expect(wrapper.find('#batchactions-ReplaceAll--invalid')).toHaveLength(1); + expect(wrapper.find('.fa')).toHaveLength(0); + }); - it('performs replace all action when Replace All button is clicked', () => { - const mockReplaceAll = sinon.spy(); + it('performs replace all action when Replace All button is clicked', () => { + const mockReplaceAll = sinon.spy(); - const wrapper = shallow( - , - ); + const wrapper = shallow( + , + ); - expect(mockReplaceAll.called).toBeFalsy(); - wrapper.find('.replace-all').simulate('click'); - expect(mockReplaceAll.called).toBeTruthy(); - }); + expect(mockReplaceAll.called).toBeFalsy(); + wrapper.find('.replace-all').simulate('click'); + expect(mockReplaceAll.called).toBeTruthy(); + }); }); diff --git a/translate/src/modules/batchactions/components/ReplaceAll.tsx b/translate/src/modules/batchactions/components/ReplaceAll.tsx index 4a2b58821..8ef0d0ae2 100644 --- a/translate/src/modules/batchactions/components/ReplaceAll.tsx +++ b/translate/src/modules/batchactions/components/ReplaceAll.tsx @@ -4,95 +4,94 @@ import { Localized } from '@fluent/react'; import type { BatchActionsState } from '~/modules/batchactions'; type Props = { - replaceAll: () => void; - batchactions: BatchActionsState; + replaceAll: () => void; + batchactions: BatchActionsState; }; /** * Renders Replace All batch action button. */ export default class ReplaceAll extends React.Component { - renderDefault(): React.ReactElement { + renderDefault(): React.ReactElement { + return ( + + {'REPLACE ALL'} + + ); + } + + renderError(): React.ReactElement { + return ( + + {'OOPS, SOMETHING WENT WRONG'} + + ); + } + + renderInvalid(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $invalidCount } FAILED'} + + ); + } + + renderSuccess(): null | React.ReactElement { + const { response } = this.props.batchactions; + + if (!response) { + return null; + } + + return ( + + {'{ $changedCount } STRINGS REPLACED'} + + ); + } + + renderTitle(): null | React.ReactNode { + const { response } = this.props.batchactions; + + if (response && response.action === 'replace') { + if (response.error) { + return this.renderError(); + } else if (response.invalidCount) { return ( - - {'REPLACE ALL'} - + <> + {this.renderSuccess()} + {' · '} + {this.renderInvalid()} + ); + } else { + return this.renderSuccess(); + } + } else { + return this.renderDefault(); } + } - renderError(): React.ReactElement { - return ( - - {'OOPS, SOMETHING WENT WRONG'} - - ); - } - - renderInvalid(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $invalidCount } FAILED'} - - ); - } - - renderSuccess(): null | React.ReactElement { - const { response } = this.props.batchactions; - - if (!response) { - return null; - } - - return ( - - {'{ $changedCount } STRINGS REPLACED'} - - ); - } - - renderTitle(): null | React.ReactNode { - const { response } = this.props.batchactions; - - if (response && response.action === 'replace') { - if (response.error) { - return this.renderError(); - } else if (response.invalidCount) { - return ( - <> - {this.renderSuccess()} - {' · '} - {this.renderInvalid()} - - ); - } else { - return this.renderSuccess(); - } - } else { - return this.renderDefault(); - } - } - - render(): React.ReactElement<'button'> { - return ( - - ); - } + render(): React.ReactElement<'button'> { + return ( + + ); + } } diff --git a/translate/src/modules/batchactions/reducer.ts b/translate/src/modules/batchactions/reducer.ts index d5f59762b..7f6de5c45 100644 --- a/translate/src/modules/batchactions/reducer.ts +++ b/translate/src/modules/batchactions/reducer.ts @@ -1,119 +1,119 @@ import { - CHECK, - RECEIVE, - REQUEST, - RESET, - RESET_RESPONSE, - TOGGLE, - UNCHECK, + CHECK, + RECEIVE, + REQUEST, + RESET, + RESET_RESPONSE, + TOGGLE, + UNCHECK, } from './actions'; import type { - CheckAction, - ReceiveAction, - RequestAction, - ResetAction, - ResetResponseAction, - ResponseType, - ToggleAction, - UncheckAction, + CheckAction, + ReceiveAction, + RequestAction, + ResetAction, + ResetResponseAction, + ResponseType, + ToggleAction, + UncheckAction, } from './actions'; type Action = - | CheckAction - | ReceiveAction - | RequestAction - | ResetAction - | ResetResponseAction - | ToggleAction - | UncheckAction; + | CheckAction + | ReceiveAction + | RequestAction + | ResetAction + | ResetResponseAction + | ToggleAction + | UncheckAction; export type BatchActionsState = { - readonly entities: Array; - readonly lastCheckedEntity: number | null | undefined; - readonly requestInProgress: string | null | undefined; - readonly response: ResponseType | null | undefined; + readonly entities: Array; + readonly lastCheckedEntity: number | null | undefined; + readonly requestInProgress: string | null | undefined; + readonly response: ResponseType | null | undefined; }; const initial: BatchActionsState = { - entities: [], - lastCheckedEntity: null, - requestInProgress: null, - response: null, + entities: [], + lastCheckedEntity: null, + requestInProgress: null, + response: null, }; function checkEntities( - stateEntities: Array, - actionEntities: Array, + stateEntities: Array, + actionEntities: Array, ) { - // Union with duplicates removed - return stateEntities.concat( - actionEntities.filter((e) => stateEntities.indexOf(e) < 0), - ); + // Union with duplicates removed + return stateEntities.concat( + actionEntities.filter((e) => stateEntities.indexOf(e) < 0), + ); } function toggleEntity(entities: Array, entity: number) { - // Remove entity if present - if (entities.includes(entity)) { - return entities.filter((e) => e !== entity); - } - // Add entity if not present - else { - return entities.concat([entity]); - } + // Remove entity if present + if (entities.includes(entity)) { + return entities.filter((e) => e !== entity); + } + // Add entity if not present + else { + return entities.concat([entity]); + } } function uncheckEntities( - stateEntities: Array, - actionEntities: Array, + stateEntities: Array, + actionEntities: Array, ) { - return stateEntities.filter((e) => actionEntities.indexOf(e) < 0); + return stateEntities.filter((e) => actionEntities.indexOf(e) < 0); } export default function reducer( - state: BatchActionsState = initial, - action: Action, + state: BatchActionsState = initial, + action: Action, ): BatchActionsState { - switch (action.type) { - case CHECK: - return { - ...state, - entities: checkEntities(state.entities, action.entities), - lastCheckedEntity: action.lastCheckedEntity, - }; - case RECEIVE: - return { - ...state, - requestInProgress: null, - response: action.response, - }; - case REQUEST: - return { - ...state, - requestInProgress: action.source, - }; - case RESET: - return { - ...initial, - }; - case RESET_RESPONSE: - return { - ...state, - response: initial.response, - }; - case TOGGLE: - return { - ...state, - entities: toggleEntity(state.entities, action.entity), - lastCheckedEntity: action.entity, - }; - case UNCHECK: - return { - ...state, - entities: uncheckEntities(state.entities, action.entities), - lastCheckedEntity: action.lastCheckedEntity, - }; - default: - return state; - } + switch (action.type) { + case CHECK: + return { + ...state, + entities: checkEntities(state.entities, action.entities), + lastCheckedEntity: action.lastCheckedEntity, + }; + case RECEIVE: + return { + ...state, + requestInProgress: null, + response: action.response, + }; + case REQUEST: + return { + ...state, + requestInProgress: action.source, + }; + case RESET: + return { + ...initial, + }; + case RESET_RESPONSE: + return { + ...state, + response: initial.response, + }; + case TOGGLE: + return { + ...state, + entities: toggleEntity(state.entities, action.entity), + lastCheckedEntity: action.entity, + }; + case UNCHECK: + return { + ...state, + entities: uncheckEntities(state.entities, action.entities), + lastCheckedEntity: action.lastCheckedEntity, + }; + default: + return state; + } } diff --git a/translate/src/modules/entitieslist/components/EntitiesList.css b/translate/src/modules/entitieslist/components/EntitiesList.css index 8aee59e6b..4e343968f 100644 --- a/translate/src/modules/entitieslist/components/EntitiesList.css +++ b/translate/src/modules/entitieslist/components/EntitiesList.css @@ -1,44 +1,44 @@ .entities { - background-color: #3f4752; - height: calc(100% - 53px); - overflow-y: auto; - position: relative; - width: 100%; + background-color: #3f4752; + height: calc(100% - 53px); + overflow-y: auto; + position: relative; + width: 100%; } .entities.unselectable { - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; } .entities ul { - list-style: none; - margin: 0; - padding: 0; - text-align: left; + list-style: none; + margin: 0; + padding: 0; + text-align: left; } .entities > div { - height: 100%; + height: 100%; } .entities .no-results { - color: #ffffff; - font-size: 28px; - font-style: italic; - font-weight: 300; - letter-spacing: -1px; - padding-top: 60px; - text-align: center; + color: #ffffff; + font-size: 28px; + font-style: italic; + font-weight: 300; + letter-spacing: -1px; + padding-top: 60px; + text-align: center; } .entities .no-results div { - color: #7bc876; - display: block; - font-size: 128px; - margin-bottom: 20px; + color: #7bc876; + display: block; + font-size: 128px; + margin-bottom: 20px; } diff --git a/translate/src/modules/entitieslist/components/EntitiesList.test.js b/translate/src/modules/entitieslist/components/EntitiesList.test.js index cfb254de5..21dc1e38e 100644 --- a/translate/src/modules/entitieslist/components/EntitiesList.test.js +++ b/translate/src/modules/entitieslist/components/EntitiesList.test.js @@ -10,124 +10,124 @@ import EntitiesList, { EntitiesListBase } from './EntitiesList'; // Entities shared between tests const ENTITIES = [ - { pk: 1, translation: [{ string: '', errors: [], warnings: [] }] }, - { pk: 2, translation: [{ string: '', errors: [], warnings: [] }] }, + { pk: 1, translation: [{ string: '', errors: [], warnings: [] }] }, + { pk: 2, translation: [{ string: '', errors: [], warnings: [] }] }, ]; describe('', () => { - beforeAll(() => { - sinon - .stub(batchactions.actions, 'resetSelection') - .returns({ type: 'whatever' }); - sinon - .stub(batchactions.actions, 'toggleSelection') - .returns({ type: 'whatever' }); - sinon.stub(entities.actions, 'get').returns({ type: 'whatever' }); - sinon - .stub(navigation.actions, 'updateEntity') - .returns({ type: 'whatever' }); - }); + beforeAll(() => { + sinon + .stub(batchactions.actions, 'resetSelection') + .returns({ type: 'whatever' }); + sinon + .stub(batchactions.actions, 'toggleSelection') + .returns({ type: 'whatever' }); + sinon.stub(entities.actions, 'get').returns({ type: 'whatever' }); + sinon + .stub(navigation.actions, 'updateEntity') + .returns({ type: 'whatever' }); + }); - afterEach(() => { - // Make sure tests do not pollute one another. - batchactions.actions.resetSelection.resetHistory(); - batchactions.actions.toggleSelection.resetHistory(); - entities.actions.get.resetHistory(); - navigation.actions.updateEntity.resetHistory(); - }); + afterEach(() => { + // Make sure tests do not pollute one another. + batchactions.actions.resetSelection.resetHistory(); + batchactions.actions.toggleSelection.resetHistory(); + entities.actions.get.resetHistory(); + navigation.actions.updateEntity.resetHistory(); + }); - afterAll(() => { - batchactions.actions.resetSelection.restore(); - batchactions.actions.toggleSelection.restore(); - entities.actions.get.restore(); - navigation.actions.updateEntity.restore(); - }); + afterAll(() => { + batchactions.actions.resetSelection.restore(); + batchactions.actions.toggleSelection.restore(); + entities.actions.get.restore(); + navigation.actions.updateEntity.restore(); + }); - it('shows a loading animation when there are more entities to load', () => { - const store = createReduxStore(); + it('shows a loading animation when there are more entities to load', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, true)); + store.dispatch(entities.actions.receive(ENTITIES, true)); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); - const scroll = wrapper.find('InfiniteScroll'); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); + const scroll = wrapper.find('InfiniteScroll'); - expect(scroll.find('SkeletonLoader')).toHaveLength(1); - }); + expect(scroll.find('SkeletonLoader')).toHaveLength(1); + }); - it("doesn't display a loading animation when there aren't entities to load", () => { - const store = createReduxStore(); + it("doesn't display a loading animation when there aren't entities to load", () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, false)); + store.dispatch(entities.actions.receive(ENTITIES, false)); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); - const scroll = wrapper.find('InfiniteScroll'); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); + const scroll = wrapper.find('InfiniteScroll'); - expect(scroll.find('SkeletonLoader')).toHaveLength(0); - }); + expect(scroll.find('SkeletonLoader')).toHaveLength(0); + }); - it('shows a loading animation when entities are being fetched from the server', () => { - const store = createReduxStore(); + it('shows a loading animation when entities are being fetched from the server', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.request()); + store.dispatch(entities.actions.request()); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); - const scroll = wrapper.find('InfiniteScroll'); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); + const scroll = wrapper.find('InfiniteScroll'); - expect(scroll.find('SkeletonLoader')).toHaveLength(1); - }); + expect(scroll.find('SkeletonLoader')).toHaveLength(1); + }); - it('shows the correct number of entities', () => { - const store = createReduxStore(); + it('shows the correct number of entities', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, false)); + store.dispatch(entities.actions.receive(ENTITIES, false)); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); - expect(wrapper.find('Entity')).toHaveLength(2); - }); + expect(wrapper.find('Entity')).toHaveLength(2); + }); - it('excludes current entities when requesting new entities', () => { - const store = createReduxStore(); + it('excludes current entities when requesting new entities', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, false)); + store.dispatch(entities.actions.receive(ENTITIES, false)); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); - wrapper.instance().getMoreEntities(); + wrapper.instance().getMoreEntities(); - // Verify the 5th argument of `actions.get` is the list of current entities. - expect(entities.actions.get.args[0][4]).toEqual([1, 2]); - }); + // Verify the 5th argument of `actions.get` is the list of current entities. + expect(entities.actions.get.args[0][4]).toEqual([1, 2]); + }); - it('redirects to the first entity when none is selected', () => { - const store = createReduxStore(); + it('redirects to the first entity when none is selected', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, false)); + store.dispatch(entities.actions.receive(ENTITIES, false)); - mountComponentWithStore(EntitiesList, store); + mountComponentWithStore(EntitiesList, store); - expect(batchactions.actions.resetSelection.calledOnce).toBeTruthy(); - expect(navigation.actions.updateEntity.calledOnce).toBeTruthy(); + expect(batchactions.actions.resetSelection.calledOnce).toBeTruthy(); + expect(navigation.actions.updateEntity.calledOnce).toBeTruthy(); - const call = navigation.actions.updateEntity.firstCall; - expect(call.args[1]).toEqual(ENTITIES[0].pk.toString()); - }); + const call = navigation.actions.updateEntity.firstCall; + expect(call.args[1]).toEqual(ENTITIES[0].pk.toString()); + }); - it('toggles entity for batch editing', () => { - const store = createReduxStore(); + it('toggles entity for batch editing', () => { + const store = createReduxStore(); - store.dispatch(entities.actions.receive(ENTITIES, false)); + store.dispatch(entities.actions.receive(ENTITIES, false)); - const root = mountComponentWithStore(EntitiesList, store); - const wrapper = root.find(EntitiesListBase); + const root = mountComponentWithStore(EntitiesList, store); + const wrapper = root.find(EntitiesListBase); - wrapper.instance().toggleForBatchEditing(ENTITIES[0].pk, false); + wrapper.instance().toggleForBatchEditing(ENTITIES[0].pk, false); - expect(batchactions.actions.toggleSelection.calledOnce).toBeTruthy(); - }); + expect(batchactions.actions.toggleSelection.calledOnce).toBeTruthy(); + }); }); diff --git a/translate/src/modules/entitieslist/components/EntitiesList.tsx b/translate/src/modules/entitieslist/components/EntitiesList.tsx index f045c5a6e..a26f203ef 100644 --- a/translate/src/modules/entitieslist/components/EntitiesList.tsx +++ b/translate/src/modules/entitieslist/components/EntitiesList.tsx @@ -24,18 +24,18 @@ import type { Locale } from '~/core/locale'; import type { NavigationParams } from '~/core/navigation'; type Props = { - batchactions: BatchActionsState; - entities: EntitiesState; - isReadOnlyEditor: boolean; - isTranslator: boolean; - locale: Locale; - parameters: NavigationParams; - router: Record; + batchactions: BatchActionsState; + entities: EntitiesState; + isReadOnlyEditor: boolean; + isTranslator: boolean; + locale: Locale; + parameters: NavigationParams; + router: Record; }; type InternalProps = Props & { - dispatch: AppDispatch; - store: AppStore; + dispatch: AppDispatch; + store: AppStore; }; /** @@ -46,390 +46,359 @@ type InternalProps = Props & { * */ export class EntitiesListBase extends React.Component { - list: { current: any }; + list: { current: any }; - constructor(props: InternalProps) { - super(props); - this.list = React.createRef(); + constructor(props: InternalProps) { + super(props); + this.list = React.createRef(); + } + + componentDidMount() { + document.addEventListener('keydown', this.handleShortcuts); + + this.selectFirstEntityIfNoneSelected(); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleShortcuts); + } + + componentDidUpdate(prevProps: InternalProps) { + this.selectFirstEntityIfNoneSelected(); + + // Whenever the route changes, we want to verify that the user didn't + // change locale, project, resource... If they did, then we'll have + // to reset the current list of entities, in order to start a fresh + // list and hide the previous entities. + // + // Notes: + // * It might seem to be an anti-pattern to change the state after the + // component has rendered, but that's actually the easiest way to + // implement that feature. Note that the first render is not shown + // to the user, so there should be no blinking here. + // Cf. https://reactjs.org/docs/react-component.html#componentdidupdate + // * Other solutions might involve using `connected-react-router`'s + // redux actions (see https://stackoverflow.com/a/37911318/1462501) + // or using `history.listen` to trigger an action on each location + // change. + // * I haven't been able to figure out how to test this feature. It + // is possible that going for another possible solutions will make + // testing easier, which would be very desirable. + const previous = prevProps.parameters; + const current = this.props.parameters; + if ( + previous.locale !== current.locale || + previous.project !== current.project || + previous.resource !== current.resource || + previous.search !== current.search || + previous.status !== current.status || + previous.extra !== current.extra || + previous.tag !== current.tag || + previous.author !== current.author || + previous.time !== current.time + ) { + this.props.dispatch(entities.actions.reset()); } - componentDidMount() { - document.addEventListener('keydown', this.handleShortcuts); + // Scroll to selected entity when entity changes + // and when entity list loads for the first time + if ( + previous.entity !== current.entity || + (!prevProps.entities.entities.length && + this.props.entities.entities.length) + ) { + const list = this.list.current; + const element = list.querySelector('li.selected'); + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const behavior = mediaQuery.matches ? 'auto' : 'smooth'; - this.selectFirstEntityIfNoneSelected(); + if (element) { + element.scrollIntoView({ + behavior: behavior, + block: 'nearest', + }); + } } + } - componentWillUnmount() { - document.removeEventListener('keydown', this.handleShortcuts); + handleShortcuts: (event: KeyboardEvent) => void = (event: KeyboardEvent) => { + const key = event.keyCode; + + // On Ctrl + Shift + A, select all entities for batch editing. + if (key === 65 && !event.altKey && event.ctrlKey && event.shiftKey) { + event.preventDefault(); + + const { + locale, + project, + resource, + search, + status, + extra, + tag, + author, + time, + } = this.props.parameters; + + this.props.dispatch( + batchactions.actions.selectAll( + locale, + project, + resource, + search, + status, + extra, + tag, + author, + time, + ), + ); } + }; - componentDidUpdate(prevProps: InternalProps) { - this.selectFirstEntityIfNoneSelected(); + /* + * If entity not provided through a URL parameter, or if provided entity + * cannot be found, select the first entity in the list. + */ + selectFirstEntityIfNoneSelected() { + const props = this.props; + const selectedEntity = props.parameters.entity; + const firstEntity = props.entities.entities[0]; - // Whenever the route changes, we want to verify that the user didn't - // change locale, project, resource... If they did, then we'll have - // to reset the current list of entities, in order to start a fresh - // list and hide the previous entities. - // - // Notes: - // * It might seem to be an anti-pattern to change the state after the - // component has rendered, but that's actually the easiest way to - // implement that feature. Note that the first render is not shown - // to the user, so there should be no blinking here. - // Cf. https://reactjs.org/docs/react-component.html#componentdidupdate - // * Other solutions might involve using `connected-react-router`'s - // redux actions (see https://stackoverflow.com/a/37911318/1462501) - // or using `history.listen` to trigger an action on each location - // change. - // * I haven't been able to figure out how to test this feature. It - // is possible that going for another possible solutions will make - // testing easier, which would be very desirable. - const previous = prevProps.parameters; - const current = this.props.parameters; - if ( - previous.locale !== current.locale || - previous.project !== current.project || - previous.resource !== current.resource || - previous.search !== current.search || - previous.status !== current.status || - previous.extra !== current.extra || - previous.tag !== current.tag || - previous.author !== current.author || - previous.time !== current.time - ) { - this.props.dispatch(entities.actions.reset()); - } + const entityIds = props.entities.entities.map((entity) => entity.pk); + const isSelectedEntityValid = entityIds.indexOf(selectedEntity) > -1; - // Scroll to selected entity when entity changes - // and when entity list loads for the first time - if ( - previous.entity !== current.entity || - (!prevProps.entities.entities.length && - this.props.entities.entities.length) - ) { - const list = this.list.current; - const element = list.querySelector('li.selected'); - const mediaQuery = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ); - const behavior = mediaQuery.matches ? 'auto' : 'smooth'; - - if (element) { - element.scrollIntoView({ - behavior: behavior, - block: 'nearest', - }); - } - } - } - - handleShortcuts: (event: KeyboardEvent) => void = ( - event: KeyboardEvent, - ) => { - const key = event.keyCode; - - // On Ctrl + Shift + A, select all entities for batch editing. - if (key === 65 && !event.altKey && event.ctrlKey && event.shiftKey) { - event.preventDefault(); - - const { - locale, - project, - resource, - search, - status, - extra, - tag, - author, - time, - } = this.props.parameters; - - this.props.dispatch( - batchactions.actions.selectAll( - locale, - project, - resource, - search, - status, - extra, - tag, - author, - time, - ), - ); - } - }; - - /* - * If entity not provided through a URL parameter, or if provided entity - * cannot be found, select the first entity in the list. - */ - selectFirstEntityIfNoneSelected() { - const props = this.props; - const selectedEntity = props.parameters.entity; - const firstEntity = props.entities.entities[0]; - - const entityIds = props.entities.entities.map((entity) => entity.pk); - const isSelectedEntityValid = entityIds.indexOf(selectedEntity) > -1; - - if ((!selectedEntity || !isSelectedEntityValid) && firstEntity) { - this.selectEntity( - firstEntity, - true, // Replace the last history item instead of pushing a new one. - ); - - // Only do this the very first time entities are loaded. - if ( - props.entities.fetchCount === 1 && - selectedEntity && - !isSelectedEntityValid - ) { - props.dispatch( - notification.actions.add( - notification.messages.ENTITY_NOT_FOUND, - ), - ); - } - } - } - - selectEntity: (entity: EntityType, replaceHistory?: boolean) => void = ( - entity: EntityType, - replaceHistory?: boolean, - ) => { - const { dispatch, parameters, router, store } = this.props; - - // Do not re-select already selected entity - if (entity.pk === parameters.entity) { - return; - } - - const state = store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - dispatch(batchactions.actions.resetSelection()); - dispatch(editor.actions.reset()); - dispatch( - navigation.actions.updateEntity( - router, - entity.pk.toString(), - replaceHistory, - ), - ); - }, - ), - ); - }; - - getSiblingEntities: (entity: number) => void = (entity: number) => { - const { dispatch, locale } = this.props; - dispatch(entities.actions.getSiblingEntities(entity, locale.code)); - }; - - toggleForBatchEditing: (entity: number, shiftKeyPressed: boolean) => void = - (entity: number, shiftKeyPressed: boolean) => { - const props = this.props; - const { dispatch } = props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - // If holding Shift, check all entities in the entity list between the - // lastCheckedEntity and the entity if entity not checked. If entity - // checked, uncheck all entities in-between. - const lastCheckedEntity = - props.batchactions.lastCheckedEntity; - - if (shiftKeyPressed && lastCheckedEntity) { - const entityListIds = props.entities.entities.map( - (e) => e.pk, - ); - const start = entityListIds.indexOf(entity); - const end = - entityListIds.indexOf(lastCheckedEntity); - - const entitySelection = entityListIds.slice( - Math.min(start, end), - Math.max(start, end) + 1, - ); - - if (props.batchactions.entities.includes(entity)) { - dispatch( - batchactions.actions.uncheckSelection( - entitySelection, - entity, - ), - ); - } else { - dispatch( - batchactions.actions.checkSelection( - entitySelection, - entity, - ), - ); - } - } else { - dispatch( - batchactions.actions.toggleSelection(entity), - ); - } - }, - ), - ); - }; - - getMoreEntities: () => void = () => { - const props = this.props; - const { - locale, - project, - resource, - entity, - search, - status, - extra, - tag, - author, - time, - } = props.parameters; - - // Temporary fix for the infinite number of requests from InfiniteScroller - // More info at: - // * https://github.com/CassetteRocks/react-infinite-scroller/issues/149 - // * https://github.com/CassetteRocks/react-infinite-scroller/issues/163 - if (props.entities.fetching) { - return; - } - - // Do not return a specific list of entities defined by their IDs. - const entityIds: number[] = null; - - // Currently shown entities should be excluded from the next results. - const currentEntityIds = props.entities.entities.map( - (entity) => entity.pk, - ); + if ((!selectedEntity || !isSelectedEntityValid) && firstEntity) { + this.selectEntity( + firstEntity, + true, // Replace the last history item instead of pushing a new one. + ); + // Only do this the very first time entities are loaded. + if ( + props.entities.fetchCount === 1 && + selectedEntity && + !isSelectedEntityValid + ) { props.dispatch( - entities.actions.get( - locale, - project, - resource, - entityIds, - currentEntityIds, - entity.toString(), - search, - status, - extra, - tag, - author, - time, - ), - ); - }; - - render(): React.ReactElement<'div'> { - const props = this.props; - const parameters = props.parameters; - - // InfiniteScroll will display information about loading during the request - const hasMore = props.entities.fetching || props.entities.hasMore; - - return ( -
    - - } - useWindow={false} - threshold={600} - > - {hasMore || props.entities.entities.length ? ( -
      - {props.entities.entities.map((entity) => { - const selected = - !props.batchactions.entities.length && - entity.pk === props.parameters.entity; - - return ( - - ); - })} -
    - ) : ( - // When there are no results for the current search. -

    -
    - No results -

    - )} -
    -
    + notification.actions.add(notification.messages.ENTITY_NOT_FOUND), ); + } } + } + + selectEntity: (entity: EntityType, replaceHistory?: boolean) => void = ( + entity: EntityType, + replaceHistory?: boolean, + ) => { + const { dispatch, parameters, router, store } = this.props; + + // Do not re-select already selected entity + if (entity.pk === parameters.entity) { + return; + } + + const state = store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + dispatch(batchactions.actions.resetSelection()); + dispatch(editor.actions.reset()); + dispatch( + navigation.actions.updateEntity( + router, + entity.pk.toString(), + replaceHistory, + ), + ); + }, + ), + ); + }; + + getSiblingEntities: (entity: number) => void = (entity: number) => { + const { dispatch, locale } = this.props; + dispatch(entities.actions.getSiblingEntities(entity, locale.code)); + }; + + toggleForBatchEditing: (entity: number, shiftKeyPressed: boolean) => void = ( + entity: number, + shiftKeyPressed: boolean, + ) => { + const props = this.props; + const { dispatch } = props; + + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + // If holding Shift, check all entities in the entity list between the + // lastCheckedEntity and the entity if entity not checked. If entity + // checked, uncheck all entities in-between. + const lastCheckedEntity = props.batchactions.lastCheckedEntity; + + if (shiftKeyPressed && lastCheckedEntity) { + const entityListIds = props.entities.entities.map((e) => e.pk); + const start = entityListIds.indexOf(entity); + const end = entityListIds.indexOf(lastCheckedEntity); + + const entitySelection = entityListIds.slice( + Math.min(start, end), + Math.max(start, end) + 1, + ); + + if (props.batchactions.entities.includes(entity)) { + dispatch( + batchactions.actions.uncheckSelection(entitySelection, entity), + ); + } else { + dispatch( + batchactions.actions.checkSelection(entitySelection, entity), + ); + } + } else { + dispatch(batchactions.actions.toggleSelection(entity)); + } + }, + ), + ); + }; + + getMoreEntities: () => void = () => { + const props = this.props; + const { + locale, + project, + resource, + entity, + search, + status, + extra, + tag, + author, + time, + } = props.parameters; + + // Temporary fix for the infinite number of requests from InfiniteScroller + // More info at: + // * https://github.com/CassetteRocks/react-infinite-scroller/issues/149 + // * https://github.com/CassetteRocks/react-infinite-scroller/issues/163 + if (props.entities.fetching) { + return; + } + + // Do not return a specific list of entities defined by their IDs. + const entityIds: number[] = null; + + // Currently shown entities should be excluded from the next results. + const currentEntityIds = props.entities.entities.map((entity) => entity.pk); + + props.dispatch( + entities.actions.get( + locale, + project, + resource, + entityIds, + currentEntityIds, + entity.toString(), + search, + status, + extra, + tag, + author, + time, + ), + ); + }; + + render(): React.ReactElement<'div'> { + const props = this.props; + const parameters = props.parameters; + + // InfiniteScroll will display information about loading during the request + const hasMore = props.entities.fetching || props.entities.hasMore; + + return ( +
    + } + useWindow={false} + threshold={600} + > + {hasMore || props.entities.entities.length ? ( +
      + {props.entities.entities.map((entity) => { + const selected = + !props.batchactions.entities.length && + entity.pk === props.parameters.entity; + + return ( + + ); + })} +
    + ) : ( + // When there are no results for the current search. +

    +
    + No results +

    + )} +
    +
    + ); + } } export default function EntitiesList(): React.ReactElement< - typeof EntitiesListBase + typeof EntitiesListBase > { - const state = { - batchactions: useAppSelector((state) => state[batchactions.NAME]), - entities: useAppSelector((state) => state[entities.NAME]), - isReadOnlyEditor: useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), - ), - isTranslator: useAppSelector((state) => - user.selectors.isTranslator(state), - ), - parameters: useAppSelector((state) => - navigation.selectors.getNavigationParams(state), - ), - locale: useAppSelector((state) => state[locale.NAME]), - router: useAppSelector((state) => state.router), - }; + const state = { + batchactions: useAppSelector((state) => state[batchactions.NAME]), + entities: useAppSelector((state) => state[entities.NAME]), + isReadOnlyEditor: useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ), + isTranslator: useAppSelector((state) => user.selectors.isTranslator(state)), + parameters: useAppSelector((state) => + navigation.selectors.getNavigationParams(state), + ), + locale: useAppSelector((state) => state[locale.NAME]), + router: useAppSelector((state) => state.router), + }; - return ( - - ); + return ( + + ); } diff --git a/translate/src/modules/entitieslist/components/Entity.css b/translate/src/modules/entitieslist/components/Entity.css index 0f67375db..499131952 100644 --- a/translate/src/modules/entitieslist/components/Entity.css +++ b/translate/src/modules/entitieslist/components/Entity.css @@ -1,56 +1,56 @@ .entity { - color: white; - cursor: pointer; - line-height: 2rem; - padding: 10px 25px 10px 43px; - position: relative; - vertical-align: baseline; - margin: 0; - border: 0; - outline: 0; - font-size: 100%; + color: white; + cursor: pointer; + line-height: 2rem; + padding: 10px 25px 10px 43px; + position: relative; + vertical-align: baseline; + margin: 0; + border: 0; + outline: 0; + font-size: 100%; } .entity.checked, .entity.selected, .entity.sibling:hover, .entity:hover { - background-color: #333941; + background-color: #333941; } .entity > div { - line-height: 20px; + line-height: 20px; } .entity mark.search { - color: #ffa10f; - font-weight: normal; - font-style: inherit; - background: #676054; - border-radius: 2px; + color: #ffa10f; + font-weight: normal; + font-style: inherit; + background: #676054; + border-radius: 2px; } .entity .source-string, .entity .translation-string { - display: inline-block; - margin: 0; - padding-bottom: 3px; - width: 100%; - vertical-align: top; - word-wrap: break-word; + display: inline-block; + margin: 0; + padding-bottom: 3px; + width: 100%; + vertical-align: top; + word-wrap: break-word; } .entity .translation-string { - color: #aaaaaa; - min-height: 20px; - text-align: start; + color: #aaaaaa; + min-height: 20px; + text-align: start; } .entity .translation-string[data-script='Latin'], .entity .translation-string[data-script='Greek'], .entity .translation-string[data-script='Cyrillic'], .entity .translation-string[data-script='Vietnamese'] { - font-style: italic; + font-style: italic; } /* Bug 1353135 @@ -59,65 +59,65 @@ */ .entity .translation-string[lang='mk'], .entity .translation-string[lang='sr'] { - font-family: 'Ubuntu Regular'; + font-family: 'Ubuntu Regular'; } /* Make selection area bigger and fit the entire row for easier use */ .entity > .status { - margin: -13px -13px -13px -16px; - padding: 13px 13px 13px 16px; - font-size: 16px; + margin: -13px -13px -13px -16px; + padding: 13px 13px 13px 16px; + font-size: 16px; } .entity .status.fa { - left: 16px; - top: 13px; - position: absolute; + left: 16px; + top: 13px; + position: absolute; } .entity .status:before { - color: #5f7285; - content: ''; + color: #5f7285; + content: ''; } .entity.batch-editable .status:hover:before { - content: ''; + content: ''; } .entity.batch-editable.checked > .status:before { - content: ''; + content: ''; } .entity.approved .status:before { - color: #7bc876; + color: #7bc876; } .entity.fuzzy .status:before { - color: #fed271; + color: #fed271; } .entity.errors .status:before { - color: #f36; + color: #f36; } .entity.warnings .status:before { - color: #ffa10f; + color: #ffa10f; } .entity.sibling { - background-color: #4d5967; + background-color: #4d5967; } .entity .sibling-entities-icon { - color: #aaa; - left: 17px; - top: 39px; - position: absolute; - margin: 0 -13px -13px -17px; - padding: 0 13px 13px 17px; - font-size: 14px; + color: #aaa; + left: 17px; + top: 39px; + position: absolute; + margin: 0 -13px -13px -17px; + padding: 0 13px 13px 17px; + font-size: 14px; } .entity .sibling-entities-icon:hover { - color: #7bc876; + color: #7bc876; } diff --git a/translate/src/modules/entitieslist/components/Entity.test.js b/translate/src/modules/entitieslist/components/Entity.test.js index cb9e4ef48..b02cd5ee1 100644 --- a/translate/src/modules/entitieslist/components/Entity.test.js +++ b/translate/src/modules/entitieslist/components/Entity.test.js @@ -5,224 +5,196 @@ import sinon from 'sinon'; import Entity from './Entity'; describe('', () => { - const ENTITY_A = { - original: 'string a', - translation: [ - { - string: 'chaine a', - approved: true, - errors: [], - warnings: [], - }, - ], - }; + const ENTITY_A = { + original: 'string a', + translation: [ + { + string: 'chaine a', + approved: true, + errors: [], + warnings: [], + }, + ], + }; - const ENTITY_B = { - original: 'string b', - translation: [ - { - string: 'chaine b', - fuzzy: true, - errors: [], - warnings: [], - }, - ], - }; + const ENTITY_B = { + original: 'string b', + translation: [ + { + string: 'chaine b', + fuzzy: true, + errors: [], + warnings: [], + }, + ], + }; - const ENTITY_C = { - original: 'string c', - translation: [ - { - string: 'chaine c', - errors: [], - warnings: [], - }, - ], - }; + const ENTITY_C = { + original: 'string c', + translation: [ + { + string: 'chaine c', + errors: [], + warnings: [], + }, + ], + }; - const ENTITY_D = { - original: 'string d', - translation: [ - { - string: 'chaine d', - approved: true, - errors: ['error'], - warnings: [], - }, - ], - }; + const ENTITY_D = { + original: 'string d', + translation: [ + { + string: 'chaine d', + approved: true, + errors: ['error'], + warnings: [], + }, + ], + }; - const ENTITY_E = { - original: 'string e', - translation: [ - { - string: 'chaine e', - fuzzy: true, - errors: [], - warnings: ['warning'], - }, - ], - }; + const ENTITY_E = { + original: 'string e', + translation: [ + { + string: 'chaine e', + fuzzy: true, + errors: [], + warnings: ['warning'], + }, + ], + }; - const ENTITY_F = { - original: 'string f', - translation: [ - { - string: 'chaine f1', - approved: true, - errors: [], - warnings: [], - }, - { - string: 'chaine f2', - fuzzy: true, - errors: [], - warnings: [], - }, - ], - }; + const ENTITY_F = { + original: 'string f', + translation: [ + { + string: 'chaine f1', + approved: true, + errors: [], + warnings: [], + }, + { + string: 'chaine f2', + fuzzy: true, + errors: [], + warnings: [], + }, + ], + }; - const DEFAULT_LOCALE = { - direction: 'ltr', - code: 'kg', - script: 'Latin', - }; + const DEFAULT_LOCALE = { + direction: 'ltr', + code: 'kg', + script: 'Latin', + }; - it('renders the source string and the first translation', () => { - const wrapper = shallow( - , - ); + it('renders the source string and the first translation', () => { + const wrapper = shallow( + , + ); - const contents = wrapper.find('TranslationProxy'); - expect(contents.first().props().content).toContain(ENTITY_A.original); - expect(contents.last().props().content).toContain( - ENTITY_A.translation[0].string, - ); - }); + const contents = wrapper.find('TranslationProxy'); + expect(contents.first().props().content).toContain(ENTITY_A.original); + expect(contents.last().props().content).toContain( + ENTITY_A.translation[0].string, + ); + }); - it('shows the correct status class', () => { - let wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('approved'); + it('shows the correct status class', () => { + let wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('approved'); - wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('fuzzy'); + wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('fuzzy'); - wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('missing'); + wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('missing'); - wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('errors'); + wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('errors'); - wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('warnings'); + wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('warnings'); - wrapper = shallow( - , - ); - expect(wrapper.instance().status).toEqual('partial'); - }); + wrapper = shallow( + , + ); + expect(wrapper.instance().status).toEqual('partial'); + }); - it('calls the selectEntity function on click on li', () => { - const selectEntityFn = sinon.spy(); - const wrapper = mount( - , - ); - wrapper.find('li').simulate('click'); - expect(selectEntityFn.calledOnce).toEqual(true); - }); + it('calls the selectEntity function on click on li', () => { + const selectEntityFn = sinon.spy(); + const wrapper = mount( + , + ); + wrapper.find('li').simulate('click'); + expect(selectEntityFn.calledOnce).toEqual(true); + }); - it('calls the toggleForBatchEditing function on click on .status', () => { - const toggleForBatchEditingFn = sinon.spy(); - const wrapper = mount( - , - ); - wrapper.find('.status').simulate('click'); - expect(toggleForBatchEditingFn.calledOnce).toEqual(true); - }); + it('calls the toggleForBatchEditing function on click on .status', () => { + const toggleForBatchEditingFn = sinon.spy(); + const wrapper = mount( + , + ); + wrapper.find('.status').simulate('click'); + expect(toggleForBatchEditingFn.calledOnce).toEqual(true); + }); - it('does not call the toggleForBatchEditing function if user not translator', () => { - const toggleForBatchEditingFn = sinon.spy(); - const selectEntityFn = sinon.spy(); - const wrapper = mount( - , - ); - wrapper.find('.status').simulate('click'); - expect(toggleForBatchEditingFn.called).toEqual(false); - }); + it('does not call the toggleForBatchEditing function if user not translator', () => { + const toggleForBatchEditingFn = sinon.spy(); + const selectEntityFn = sinon.spy(); + const wrapper = mount( + , + ); + wrapper.find('.status').simulate('click'); + expect(toggleForBatchEditingFn.called).toEqual(false); + }); - it('does not call the toggleForBatchEditing function if read-only editor', () => { - const toggleForBatchEditingFn = sinon.spy(); - const selectEntityFn = sinon.spy(); - const wrapper = mount( - , - ); - wrapper.find('.status').simulate('click'); - expect(toggleForBatchEditingFn.called).toEqual(false); - }); + it('does not call the toggleForBatchEditing function if read-only editor', () => { + const toggleForBatchEditingFn = sinon.spy(); + const selectEntityFn = sinon.spy(); + const wrapper = mount( + , + ); + wrapper.find('.status').simulate('click'); + expect(toggleForBatchEditingFn.called).toEqual(false); + }); }); diff --git a/translate/src/modules/entitieslist/components/Entity.tsx b/translate/src/modules/entitieslist/components/Entity.tsx index ec7c1cfc3..4f49ce248 100644 --- a/translate/src/modules/entitieslist/components/Entity.tsx +++ b/translate/src/modules/entitieslist/components/Entity.tsx @@ -10,20 +10,20 @@ import type { Locale } from '~/core/locale'; import type { NavigationParams } from '~/core/navigation'; type Props = { - checkedForBatchEditing: boolean; - toggleForBatchEditing: (entityPK: number, shiftPressed: boolean) => void; - entity: EntityType; - isReadOnlyEditor: boolean; - isTranslator: boolean; - locale: Locale; - selected: boolean; - selectEntity: (entity: EntityType) => void; - getSiblingEntities: (entityPK: number) => void; - parameters: NavigationParams; + checkedForBatchEditing: boolean; + toggleForBatchEditing: (entityPK: number, shiftPressed: boolean) => void; + entity: EntityType; + isReadOnlyEditor: boolean; + isTranslator: boolean; + locale: Locale; + selected: boolean; + selectEntity: (entity: EntityType) => void; + getSiblingEntities: (entityPK: number) => void; + parameters: NavigationParams; }; type State = { - areSiblingsActive: Boolean; + areSiblingsActive: Boolean; }; /** @@ -45,171 +45,166 @@ type State = { * translation, or the fuzzy translation, or the last suggested translation. */ export default class Entity extends React.Component { - constructor(props) { - super(props); - this.state = { areSiblingsActive: false }; + constructor(props) { + super(props); + this.state = { areSiblingsActive: false }; + } + get status(): string { + const translations = this.props.entity.translation; + let approved = 0; + let fuzzy = 0; + let errors = 0; + let warnings = 0; + + translations.forEach(function (translation) { + if ( + translation.errors.length && + (translation.approved || translation.fuzzy) + ) { + errors++; + } else if ( + translation.warnings.length && + (translation.approved || translation.fuzzy) + ) { + warnings++; + } else if (translation.approved) { + approved++; + } else if (translation.fuzzy) { + fuzzy++; + } + }); + + if (errors) { + return 'errors'; } - get status(): string { - const translations = this.props.entity.translation; - let approved = 0; - let fuzzy = 0; - let errors = 0; - let warnings = 0; - - translations.forEach(function (translation) { - if ( - translation.errors.length && - (translation.approved || translation.fuzzy) - ) { - errors++; - } else if ( - translation.warnings.length && - (translation.approved || translation.fuzzy) - ) { - warnings++; - } else if (translation.approved) { - approved++; - } else if (translation.fuzzy) { - fuzzy++; - } - }); - - if (errors) { - return 'errors'; - } - if (warnings) { - return 'warnings'; - } - if (approved === translations.length) { - return 'approved'; - } - if (fuzzy === translations.length) { - return 'fuzzy'; - } - if (approved > 0 || fuzzy > 0) { - return 'partial'; - } - return 'missing'; + if (warnings) { + return 'warnings'; } - - selectEntity: (e: React.MouseEvent) => null | void = ( - e: React.MouseEvent, - ) => { - if ( - e.target instanceof HTMLElement && - e.target.classList.contains('status') - ) { - return null; - } - this.props.selectEntity(this.props.entity); - }; - - getSiblingEntities: (e: React.MouseEvent) => void = ( - e: React.MouseEvent, - ) => { - e.stopPropagation(); - this.props.getSiblingEntities(this.props.entity.pk); - this.setState({ areSiblingsActive: true }); - }; - - toggleForBatchEditing: (e: React.MouseEvent) => void = ( - e: React.MouseEvent, - ) => { - const { entity, isReadOnlyEditor, isTranslator } = this.props; - - if (isTranslator && !isReadOnlyEditor) { - e.stopPropagation(); - this.props.toggleForBatchEditing(entity.pk, e.shiftKey); - } - }; - - areFiltersApplied: () => boolean = () => { - const parameters = this.props.parameters; - if ( - parameters.status != null || - parameters.extra != null || - parameters.tag != null || - parameters.time != null || - parameters.author != null - ) { - return true; - } - return false; - }; - - showSiblingEntitiesButton: () => boolean = () => { - const isSearched = this.props.parameters.search; - const areFiltersApplied = this.areFiltersApplied(); - const areSiblingsActive = !this.state.areSiblingsActive; - - return (isSearched || areFiltersApplied) && areSiblingsActive; - }; - - render(): React.ReactElement<'li'> { - const { - checkedForBatchEditing, - entity, - isReadOnlyEditor, - isTranslator, - locale, - selected, - parameters, - } = this.props; - - const classSelected = selected ? 'selected' : ''; - - const classBatchEditable = - isTranslator && !isReadOnlyEditor ? 'batch-editable' : ''; - - const classChecked = checkedForBatchEditing ? 'checked' : ''; - - const classSibling = entity.isSibling ? 'sibling' : ''; - return ( -
  • - - {classSelected && !classSibling ? ( -
    - {this.showSiblingEntitiesButton() && ( - - - - )} -
    - ) : null} -
    -

    - -

    -

    - -

    -
    -
  • - ); + if (approved === translations.length) { + return 'approved'; } + if (fuzzy === translations.length) { + return 'fuzzy'; + } + if (approved > 0 || fuzzy > 0) { + return 'partial'; + } + return 'missing'; + } + + selectEntity: (e: React.MouseEvent) => null | void = ( + e: React.MouseEvent, + ) => { + if ( + e.target instanceof HTMLElement && + e.target.classList.contains('status') + ) { + return null; + } + this.props.selectEntity(this.props.entity); + }; + + getSiblingEntities: (e: React.MouseEvent) => void = ( + e: React.MouseEvent, + ) => { + e.stopPropagation(); + this.props.getSiblingEntities(this.props.entity.pk); + this.setState({ areSiblingsActive: true }); + }; + + toggleForBatchEditing: (e: React.MouseEvent) => void = ( + e: React.MouseEvent, + ) => { + const { entity, isReadOnlyEditor, isTranslator } = this.props; + + if (isTranslator && !isReadOnlyEditor) { + e.stopPropagation(); + this.props.toggleForBatchEditing(entity.pk, e.shiftKey); + } + }; + + areFiltersApplied: () => boolean = () => { + const parameters = this.props.parameters; + if ( + parameters.status != null || + parameters.extra != null || + parameters.tag != null || + parameters.time != null || + parameters.author != null + ) { + return true; + } + return false; + }; + + showSiblingEntitiesButton: () => boolean = () => { + const isSearched = this.props.parameters.search; + const areFiltersApplied = this.areFiltersApplied(); + const areSiblingsActive = !this.state.areSiblingsActive; + + return (isSearched || areFiltersApplied) && areSiblingsActive; + }; + + render(): React.ReactElement<'li'> { + const { + checkedForBatchEditing, + entity, + isReadOnlyEditor, + isTranslator, + locale, + selected, + parameters, + } = this.props; + + const classSelected = selected ? 'selected' : ''; + + const classBatchEditable = + isTranslator && !isReadOnlyEditor ? 'batch-editable' : ''; + + const classChecked = checkedForBatchEditing ? 'checked' : ''; + + const classSibling = entity.isSibling ? 'sibling' : ''; + return ( +
  • + + {classSelected && !classSibling ? ( +
    + {this.showSiblingEntitiesButton() && ( + + + + )} +
    + ) : null} +
    +

    + +

    +

    + +

    +
    +
  • + ); + } } diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.css b/translate/src/modules/entitydetails/components/ContextIssueButton.css index 9bd9716c4..7aedd3a7d 100644 --- a/translate/src/modules/entitydetails/components/ContextIssueButton.css +++ b/translate/src/modules/entitydetails/components/ContextIssueButton.css @@ -1,16 +1,16 @@ .metadata .source-string-comment .context-issue-button { - background: #333941; - border: 1px solid #3f4752; - border-radius: 4px; - color: #ccc; - float: right; - font-size: 11px; - font-style: normal; - font-weight: 100; - margin: -1px -1px -1px 4px; /* Compensate for invisible border in default state */ - padding: 2px 4px; + background: #333941; + border: 1px solid #3f4752; + border-radius: 4px; + color: #ccc; + float: right; + font-size: 11px; + font-style: normal; + font-weight: 100; + margin: -1px -1px -1px 4px; /* Compensate for invisible border in default state */ + padding: 2px 4px; } .metadata .source-string-comment .context-issue-button:hover { - border-color: #637283; + border-color: #637283; } diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx index aed638903..1a247cb90 100644 --- a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx +++ b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx @@ -4,22 +4,22 @@ import { Localized } from '@fluent/react'; import './ContextIssueButton.css'; type Props = { - openTeamComments: () => void; + openTeamComments: () => void; }; export default function ContextIssueButton( - props: Props, + props: Props, ): React.ReactElement<'div'> { - return ( -
    - - - -
    - ); + return ( +
    + + + +
    + ); } diff --git a/translate/src/modules/entitydetails/components/EditorSelector.css b/translate/src/modules/entitydetails/components/EditorSelector.css index b01cb048d..21951b7a1 100644 --- a/translate/src/modules/entitydetails/components/EditorSelector.css +++ b/translate/src/modules/entitydetails/components/EditorSelector.css @@ -1,40 +1,40 @@ .editor { - background: rgb(39, 42, 47); - border-bottom: 1px solid #5e6475; - display: flex; - flex-direction: column; - flex-shrink: 0; - min-height: 160px; + background: rgb(39, 42, 47); + border-bottom: 1px solid #5e6475; + display: flex; + flex-direction: column; + flex-shrink: 0; + min-height: 160px; } .editor textarea { - border: none; - box-sizing: border-box; - display: block; - font-size: 14px; - line-height: 22px; - height: calc(100% - 60px); - min-height: 100px; - overflow: auto; - padding: 10px; - width: 100%; - resize: vertical; + border: none; + box-sizing: border-box; + display: block; + font-size: 14px; + line-height: 22px; + height: calc(100% - 60px); + min-height: 100px; + overflow: auto; + padding: 10px; + width: 100%; + resize: vertical; } /* Remove highlight in Chrome */ .editor textarea:focus { - outline: none; + outline: none; } .editor .plural-selector ~ textarea { - min-height: auto; + min-height: auto; } .editor textarea[readonly] { - background: #c7cacf; - cursor: default; + background: #c7cacf; + cursor: default; } .editor textarea[data-script='Arabic'] { - font-size: 15px; + font-size: 15px; } diff --git a/translate/src/modules/entitydetails/components/EditorSelector.tsx b/translate/src/modules/entitydetails/components/EditorSelector.tsx index 649088670..539ee656f 100644 --- a/translate/src/modules/entitydetails/components/EditorSelector.tsx +++ b/translate/src/modules/entitydetails/components/EditorSelector.tsx @@ -6,22 +6,22 @@ import { FluentEditor } from '~/modules/fluenteditor'; import { GenericEditor } from '~/modules/genericeditor'; type Props = { - fileFormat: string; + fileFormat: string; }; export default function EditorSelector( - props: Props, + props: Props, ): React.ReactElement<'div'> { - if (props.fileFormat === 'ftl') { - return ( -
    - -
    - ); - } + if (props.fileFormat === 'ftl') { return ( -
    - -
    +
    + +
    ); + } + return ( +
    + +
    + ); } diff --git a/translate/src/modules/entitydetails/components/EntityDetails.css b/translate/src/modules/entitydetails/components/EntityDetails.css index 95da8f550..3e19ff06a 100644 --- a/translate/src/modules/entitydetails/components/EntityDetails.css +++ b/translate/src/modules/entitydetails/components/EntityDetails.css @@ -1,21 +1,21 @@ .entity-details { - background: #3f4752; - display: flex; - height: 100%; + background: #3f4752; + display: flex; + height: 100%; } .entity-details > section { - height: 100%; + height: 100%; } .entity-details .main-column { - display: flex; - flex-direction: column; - width: 66.67%; + display: flex; + flex-direction: column; + width: 66.67%; } .entity-details .third-column { - width: 33.33%; - border-left: 1px solid #5e6475; - box-sizing: border-box; + width: 33.33%; + border-left: 1px solid #5e6475; + box-sizing: border-box; } diff --git a/translate/src/modules/entitydetails/components/EntityDetails.test.js b/translate/src/modules/entitydetails/components/EntityDetails.test.js index d131b9598..c61c6c575 100644 --- a/translate/src/modules/entitydetails/components/EntityDetails.test.js +++ b/translate/src/modules/entitydetails/components/EntityDetails.test.js @@ -13,187 +13,187 @@ import * as unsavedchanges from '~/modules/unsavedchanges'; import EntityDetails, { EntityDetailsBase } from './EntityDetails'; const ENTITIES = [ - { - pk: 42, - original: 'le test', - translation: [ - { - string: 'test', - errors: [], - warnings: [], - }, - ], - project: { contact: '' }, - comment: '', - }, - { - pk: 1, - original: 'something', - translation: [ - { - string: 'quelque chose', - errors: [], - warnings: [], - }, - ], - project: { contact: '' }, - comment: '', - }, + { + pk: 42, + original: 'le test', + translation: [ + { + string: 'test', + errors: [], + warnings: [], + }, + ], + project: { contact: '' }, + comment: '', + }, + { + pk: 1, + original: 'something', + translation: [ + { + string: 'quelque chose', + errors: [], + warnings: [], + }, + ], + project: { contact: '' }, + comment: '', + }, ]; const TRANSLATION = 'test'; const SELECTED_ENTITY = { - pk: 42, - original: 'le test', - translation: [{ string: TRANSLATION }], + pk: 42, + original: 'le test', + translation: [{ string: TRANSLATION }], }; const NAVIGATION = { - entity: 42, - locale: 'kg', + entity: 42, + locale: 'kg', }; const PARAMETERS = { - pluralForm: 0, + pluralForm: 0, }; const HISTORY = { - translations: [], + translations: [], }; const LOCALES = { - translations: [], + translations: [], }; const USER = { - settings: { - forceSuggestions: true, - }, - username: 'Franck', + settings: { + forceSuggestions: true, + }, + username: 'Franck', }; function createShallowEntityDetails(selectedEntity = SELECTED_ENTITY) { - return shallow( - {}} - user={{ settings: {} }} - />, - ); + return shallow( + {}} + user={{ settings: {} }} + />, + ); } function createEntityDetailsWithStore() { - const initialState = { - entities: { - entities: ENTITIES, - }, - user: USER, - router: { - location: { - pathname: '/kg/pro/all/', - search: '?string=' + ENTITIES[0].pk, - }, - action: 'some-string-to-please-connected-react-router', - }, - locale: { - code: 'kg', - }, - }; - const store = createReduxStore(initialState); - const root = mountComponentWithStore(EntityDetails, store); + const initialState = { + entities: { + entities: ENTITIES, + }, + user: USER, + router: { + location: { + pathname: '/kg/pro/all/', + search: '?string=' + ENTITIES[0].pk, + }, + action: 'some-string-to-please-connected-react-router', + }, + locale: { + code: 'kg', + }, + }; + const store = createReduxStore(initialState); + const root = mountComponentWithStore(EntityDetails, store); - return [root.find(EntityDetailsBase), store]; + return [root.find(EntityDetailsBase), store]; } describe('', () => { - beforeAll(() => { - sinon - .stub(editor.actions, 'updateFailedChecks') - .returns({ type: 'whatever' }); - sinon - .stub(editor.actions, 'resetFailedChecks') - .returns({ type: 'whatever' }); + beforeAll(() => { + sinon + .stub(editor.actions, 'updateFailedChecks') + .returns({ type: 'whatever' }); + sinon + .stub(editor.actions, 'resetFailedChecks') + .returns({ type: 'whatever' }); + }); + + afterEach(() => { + editor.actions.updateFailedChecks.reset(); + editor.actions.resetFailedChecks.reset(); + }); + + afterAll(() => { + editor.actions.updateFailedChecks.restore(); + editor.actions.resetFailedChecks.restore(); + }); + + it('shows an empty section when no entity is selected', () => { + const wrapper = createShallowEntityDetails(null); + expect(wrapper.text()).toContain(''); + }); + + it('loads the correct list of components', () => { + const wrapper = createShallowEntityDetails(); + + expect(wrapper.text()).toContain('EntityNavigation'); + expect(wrapper.text()).toContain('Metadata'); + expect(wrapper.text()).toContain('Editor'); + expect(wrapper.text()).toContain('Helpers'); + }); + + it('shows failed checks for approved (or fuzzy) translations with errors or warnings', () => { + const wrapper = createShallowEntityDetails(); + + // componentDidMount(): reset failed checks + expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); + expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); + + wrapper.setProps({ + pluralForm: -1, + selectedEntity: { + pk: 2, + original: 'something', + translation: [ + { + approved: true, + string: 'quelque chose', + errors: ['Error1'], + warnings: ['Warning1'], + }, + ], + }, }); - afterEach(() => { - editor.actions.updateFailedChecks.reset(); - editor.actions.resetFailedChecks.reset(); + // componentDidUpdate(): update failed checks + expect(editor.actions.updateFailedChecks.calledOnce).toBeTruthy(); + expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); + }); + + it('hides failed checks for approved (or fuzzy) translations without errors or warnings', () => { + const wrapper = createShallowEntityDetails(); + + // componentDidMount(): reset failed checks + expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); + expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); + + wrapper.setProps({ + pluralForm: -1, + selectedEntity: { + pk: 2, + original: 'something', + translation: [ + { + approved: true, + string: 'quelque chose', + errors: [], + warnings: [], + }, + ], + }, }); - afterAll(() => { - editor.actions.updateFailedChecks.restore(); - editor.actions.resetFailedChecks.restore(); - }); - - it('shows an empty section when no entity is selected', () => { - const wrapper = createShallowEntityDetails(null); - expect(wrapper.text()).toContain(''); - }); - - it('loads the correct list of components', () => { - const wrapper = createShallowEntityDetails(); - - expect(wrapper.text()).toContain('EntityNavigation'); - expect(wrapper.text()).toContain('Metadata'); - expect(wrapper.text()).toContain('Editor'); - expect(wrapper.text()).toContain('Helpers'); - }); - - it('shows failed checks for approved (or fuzzy) translations with errors or warnings', () => { - const wrapper = createShallowEntityDetails(); - - // componentDidMount(): reset failed checks - expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); - expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); - - wrapper.setProps({ - pluralForm: -1, - selectedEntity: { - pk: 2, - original: 'something', - translation: [ - { - approved: true, - string: 'quelque chose', - errors: ['Error1'], - warnings: ['Warning1'], - }, - ], - }, - }); - - // componentDidUpdate(): update failed checks - expect(editor.actions.updateFailedChecks.calledOnce).toBeTruthy(); - expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); - }); - - it('hides failed checks for approved (or fuzzy) translations without errors or warnings', () => { - const wrapper = createShallowEntityDetails(); - - // componentDidMount(): reset failed checks - expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); - expect(editor.actions.resetFailedChecks.calledOnce).toBeTruthy(); - - wrapper.setProps({ - pluralForm: -1, - selectedEntity: { - pk: 2, - original: 'something', - translation: [ - { - approved: true, - string: 'quelque chose', - errors: [], - warnings: [], - }, - ], - }, - }); - - // componentDidUpdate(): reset failed checks - expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); - expect(editor.actions.resetFailedChecks.calledTwice).toBeTruthy(); - }); + // componentDidUpdate(): reset failed checks + expect(editor.actions.updateFailedChecks.calledOnce).toBeFalsy(); + expect(editor.actions.resetFailedChecks.calledTwice).toBeTruthy(); + }); }); /** @@ -202,33 +202,30 @@ describe('', () => { * the API calls fail, and the calling code doesn't handle the errors. */ describe.skip('', () => { - const hasFetch = typeof fetch === 'function'; - beforeAll(() => { - if (!hasFetch) - global.fetch = (url) => - Promise.reject(new Error(`Mock fetch: ${url}`)); - sinon.stub(editor.actions, 'update').returns({ type: 'whatever' }); - sinon - .stub(history.actions, 'updateStatus') - .returns({ type: 'whatever' }); - }); + const hasFetch = typeof fetch === 'function'; + beforeAll(() => { + if (!hasFetch) + global.fetch = (url) => Promise.reject(new Error(`Mock fetch: ${url}`)); + sinon.stub(editor.actions, 'update').returns({ type: 'whatever' }); + sinon.stub(history.actions, 'updateStatus').returns({ type: 'whatever' }); + }); - afterEach(() => { - editor.actions.update.resetHistory(); - }); + afterEach(() => { + editor.actions.update.resetHistory(); + }); - afterAll(() => { - if (!hasFetch) delete global.fetch; - editor.actions.update.restore(); - history.actions.updateStatus.restore(); - }); + afterAll(() => { + if (!hasFetch) delete global.fetch; + editor.actions.update.restore(); + history.actions.updateStatus.restore(); + }); - it('dispatches the updateStatus action when updateTranslationStatus is called', () => { - const [wrapper, store] = createEntityDetailsWithStore(); + it('dispatches the updateStatus action when updateTranslationStatus is called', () => { + const [wrapper, store] = createEntityDetailsWithStore(); - wrapper.instance().updateTranslationStatus(42, 'fake translation'); - // Proceed with changes even if unsaved - store.dispatch(unsavedchanges.actions.ignore()); - expect(history.actions.updateStatus.calledOnce).toBeTruthy(); - }); + wrapper.instance().updateTranslationStatus(42, 'fake translation'); + // Proceed with changes even if unsaved + store.dispatch(unsavedchanges.actions.ignore()); + expect(history.actions.updateStatus.calledOnce).toBeTruthy(); + }); }); diff --git a/translate/src/modules/entitydetails/components/EntityDetails.tsx b/translate/src/modules/entitydetails/components/EntityDetails.tsx index 7b06e0942..593523ead 100644 --- a/translate/src/modules/entitydetails/components/EntityDetails.tsx +++ b/translate/src/modules/entitydetails/components/EntityDetails.tsx @@ -38,33 +38,33 @@ import type { TeamCommentState } from '~/modules/teamcomments'; import type { FailedChecks } from '~/core/editor/actions'; type Props = { - activeTranslationString: string; - history: HistoryState; - isReadOnlyEditor: boolean; - isTranslator: boolean; - locale: Locale; - machinery: MachineryState; - nextEntity: Entity; - previousEntity: Entity; - otherlocales: LocalesState; - teamComments: TeamCommentState; - terms: TermState; - parameters: NavigationParams; - pluralForm: number; - router: Record; - selectedEntity: Entity; - user: UserState; + activeTranslationString: string; + history: HistoryState; + isReadOnlyEditor: boolean; + isTranslator: boolean; + locale: Locale; + machinery: MachineryState; + nextEntity: Entity; + previousEntity: Entity; + otherlocales: LocalesState; + teamComments: TeamCommentState; + terms: TermState; + parameters: NavigationParams; + pluralForm: number; + router: Record; + selectedEntity: Entity; + user: UserState; }; type InternalProps = Props & { - dispatch: AppDispatch; - store: AppStore; + dispatch: AppDispatch; + store: AppStore; }; type State = { - translation: string; - commentTabIndex: number; - contactPerson: string; + translation: string; + commentTabIndex: number; + contactPerson: string; }; /** @@ -73,492 +73,466 @@ type State = { * Shows the metadata of the entity and an editor for translations. */ export class EntityDetailsBase extends React.Component { - commentTabRef: { current: Record }; + commentTabRef: { current: Record }; - constructor(props: InternalProps, state: State) { - super(props); - this.state = { - ...state, - commentTabIndex: 0, - contactPerson: '', - }; - this.commentTabRef = React.createRef(); + constructor(props: InternalProps, state: State) { + super(props); + this.state = { + ...state, + commentTabIndex: 0, + contactPerson: '', + }; + this.commentTabRef = React.createRef(); + } + componentDidMount() { + this.updateFailedChecks(); + this.fetchHelpersData(); + } + + componentDidUpdate(prevProps: InternalProps) { + const { activeTranslationString, nextEntity, pluralForm, selectedEntity } = + this.props; + + if ( + pluralForm !== prevProps.pluralForm || + selectedEntity !== prevProps.selectedEntity || + (selectedEntity === nextEntity && + activeTranslationString !== prevProps.activeTranslationString) + ) { + this.updateFailedChecks(); + this.fetchHelpersData(); } - componentDidMount() { - this.updateFailedChecks(); - this.fetchHelpersData(); + } + + /* + * Only fetch helpers data if the entity changes. + * Also fetch history data if the pluralForm changes. + */ + fetchHelpersData() { + const { + dispatch, + locale, + nextEntity, + parameters, + pluralForm, + selectedEntity, + user, + } = this.props; + + if (!parameters.entity || !selectedEntity || !locale) { + return; } - componentDidUpdate(prevProps: InternalProps) { - const { - activeTranslationString, - nextEntity, - pluralForm, - selectedEntity, - } = this.props; + dispatch(editor.actions.resetHelperElementIndex()); - if ( - pluralForm !== prevProps.pluralForm || - selectedEntity !== prevProps.selectedEntity || - (selectedEntity === nextEntity && - activeTranslationString !== prevProps.activeTranslationString) - ) { - this.updateFailedChecks(); - this.fetchHelpersData(); - } + if ( + selectedEntity.pk !== this.props.history.entity || + pluralForm !== this.props.history.pluralForm || + selectedEntity === nextEntity + ) { + dispatch(history.actions.request(parameters.entity, pluralForm)); + dispatch( + history.actions.get(parameters.entity, parameters.locale, pluralForm), + ); } - /* - * Only fetch helpers data if the entity changes. - * Also fetch history data if the pluralForm changes. - */ - fetchHelpersData() { - const { - dispatch, - locale, - nextEntity, - parameters, - pluralForm, - selectedEntity, - user, - } = this.props; + const source = utils.getOptimizedContent( + selectedEntity.machinery_original, + selectedEntity.format, + ); - if (!parameters.entity || !selectedEntity || !locale) { - return; - } + if ( + source !== this.props.terms.sourceString && + parameters.project !== 'terminology' + ) { + dispatch(terms.actions.get(source, parameters.locale)); + } - dispatch(editor.actions.resetHelperElementIndex()); + if (selectedEntity.pk !== this.props.machinery.entity) { + dispatch(machinery.actions.resetSearch('')); + dispatch(machinery.actions.setEntity(selectedEntity.pk, source)); + dispatch( + machinery.actions.get( + source, + locale, + user.isAuthenticated, + selectedEntity.pk, + ), + ); + } - if ( - selectedEntity.pk !== this.props.history.entity || - pluralForm !== this.props.history.pluralForm || - selectedEntity === nextEntity - ) { - dispatch(history.actions.request(parameters.entity, pluralForm)); - dispatch( - history.actions.get( - parameters.entity, - parameters.locale, - pluralForm, - ), - ); - } + if (selectedEntity.pk !== this.props.otherlocales.entity) { + dispatch(otherlocales.actions.get(parameters.entity, parameters.locale)); + } + if (selectedEntity.pk !== this.props.teamComments.entity) { + dispatch(teamcomments.actions.request(parameters.entity)); + dispatch(teamcomments.actions.get(parameters.entity, parameters.locale)); + } + } + + updateFailedChecks() { + const { dispatch, pluralForm, selectedEntity } = this.props; + + if (!selectedEntity) { + return; + } + + const plural = pluralForm === -1 ? 0 : pluralForm; + const translation = selectedEntity.translation[plural]; + + // Only show failed checks for active translations that are approved or fuzzy, + // i.e. when their status icon is colored as error/warning in the string list + if ( + translation && + (translation.errors.length || translation.warnings.length) && + (translation.approved || translation.fuzzy) + ) { + const failedChecks: FailedChecks = { + clErrors: translation.errors, + clWarnings: translation.warnings, + pErrors: [], + pndbWarnings: [], + ttWarnings: [], + }; + dispatch(editor.actions.updateFailedChecks(failedChecks, 'stored')); + } else { + dispatch(editor.actions.resetFailedChecks()); + } + } + + searchMachinery: (query: string, page?: number) => void = ( + query: string, + page?: number, + ) => { + const { dispatch, locale, selectedEntity, user } = this.props; + const { get, getConcordanceSearchResults, resetSearch } = machinery.actions; + + if (query) { + if (page) { + dispatch(getConcordanceSearchResults(query, locale, page)); + } else { + dispatch(resetSearch(query)); + dispatch(getConcordanceSearchResults(query, locale)); + dispatch(get(query, locale, user.isAuthenticated, null)); + } + } else { + dispatch(resetSearch('')); + if (selectedEntity) { + // On empty query, use source string as input const source = utils.getOptimizedContent( - selectedEntity.machinery_original, - selectedEntity.format, + selectedEntity.machinery_original, + selectedEntity.format, ); + const pk = selectedEntity.pk; + dispatch(get(source, locale, user.isAuthenticated, pk)); + } + } + }; - if ( - source !== this.props.terms.sourceString && - parameters.project !== 'terminology' - ) { - dispatch(terms.actions.get(source, parameters.locale)); - } + copyLinkToClipboard: () => void = () => { + const { locale, project, resource, entity } = this.props.parameters; + const { protocol, host } = window.location; - if (selectedEntity.pk !== this.props.machinery.entity) { - dispatch(machinery.actions.resetSearch('')); - dispatch(machinery.actions.setEntity(selectedEntity.pk, source)); - dispatch( - machinery.actions.get( - source, - locale, - user.isAuthenticated, - selectedEntity.pk, - ), - ); - } + const string_link = `${protocol}//${host}/${locale}/${project}/${resource}/?string=${entity}`; + navigator.clipboard.writeText(string_link).then(() => { + // Notify the user of the change that happened. + const notif = notification.messages.STRING_LINK_COPIED; + this.props.dispatch(notification.actions.add(notif)); + }); + }; - if (selectedEntity.pk !== this.props.otherlocales.entity) { - dispatch( - otherlocales.actions.get(parameters.entity, parameters.locale), - ); - } + goToNextEntity: () => void = () => { + const { dispatch, nextEntity, router } = this.props; - if (selectedEntity.pk !== this.props.teamComments.entity) { - dispatch(teamcomments.actions.request(parameters.entity)); - dispatch( - teamcomments.actions.get(parameters.entity, parameters.locale), - ); - } + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + dispatch( + navigation.actions.updateEntity(router, nextEntity.pk.toString()), + ); + dispatch(editor.actions.reset()); + }, + ), + ); + }; + + goToPreviousEntity: () => void = () => { + const { dispatch, previousEntity, router } = this.props; + + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + dispatch( + navigation.actions.updateEntity( + router, + previousEntity.pk.toString(), + ), + ); + dispatch(editor.actions.reset()); + }, + ), + ); + }; + + navigateToPath: (path: string) => void = (path: string) => { + const { dispatch } = this.props; + + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + dispatch(push(path)); + }, + ), + ); + }; + + updateEditorTranslation: (translation: string, changeSource: string) => void = + (translation: string, changeSource: string) => { + this.props.dispatch(editor.actions.update(translation, changeSource)); + }; + + addTextToEditorTranslation: (content: string, changeSource?: string) => void = + (content: string, changeSource?: string) => { + this.props.dispatch( + editor.actions.updateSelection(content, changeSource), + ); + }; + + deleteTranslation: (translationId: number) => void = ( + translationId: number, + ) => { + const { parameters, pluralForm, dispatch } = this.props; + dispatch( + history.actions.deleteTranslation( + parameters.entity, + parameters.locale, + pluralForm, + translationId, + ), + ); + }; + + setCommentTabIndex: (tab: number) => void = (tab: number) => { + this.setState({ commentTabIndex: tab }); + }; + + setContactPerson: (contact: string) => void = (contact: string) => { + this.setState({ contactPerson: contact }); + }; + + resetContactPerson: () => void = () => { + this.setState({ contactPerson: '' }); + }; + + addComment: ( + comment: string, + translation: number | null | undefined, + ) => void = (comment: string, translation: number | null | undefined) => { + const { parameters, pluralForm, dispatch } = this.props; + dispatch( + comments.actions.addComment( + parameters.entity, + parameters.locale, + pluralForm, + translation, + comment, + ), + ); + }; + + togglePinnedStatus: (pinned: boolean, commentId: number) => void = ( + pinned: boolean, + commentId: number, + ) => { + this.props.dispatch( + teamcomments.actions.togglePinnedStatus(pinned, commentId), + ); + }; + + /* + * This is a copy of EditorBase.updateTranslationStatus(). + * When changing this function, you probably want to change both. + * We might want to refactor to keep the logic in one place only. + */ + updateTranslationStatus: ( + translationId: number, + change: ChangeOperation, + ) => void = (translationId: number, change: ChangeOperation) => { + const { + locale, + nextEntity, + parameters, + pluralForm, + router, + selectedEntity, + dispatch, + } = this.props; + + const state = this.props.store.getState(); + const unsavedChangesExist = state[unsavedchanges.NAME].exist; + const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; + + // No need to check for unsaved changes in `EditorBase.updateTranslationStatus()`, + // because it cannot be triggered for the use case of bug 1508474. + dispatch( + unsavedchanges.actions.check( + unsavedChangesExist, + unsavedChangesIgnored, + () => { + dispatch( + history.actions.updateStatus( + change, + selectedEntity, + locale, + parameters.resource, + pluralForm, + translationId, + nextEntity, + router, + ), + ); + }, + ), + ); + }; + + render(): null | React.ReactElement<'section'> { + const state = this.props; + + if (!state.locale) { + return null; } - updateFailedChecks() { - const { dispatch, pluralForm, selectedEntity } = this.props; - - if (!selectedEntity) { - return; - } - - const plural = pluralForm === -1 ? 0 : pluralForm; - const translation = selectedEntity.translation[plural]; - - // Only show failed checks for active translations that are approved or fuzzy, - // i.e. when their status icon is colored as error/warning in the string list - if ( - translation && - (translation.errors.length || translation.warnings.length) && - (translation.approved || translation.fuzzy) - ) { - const failedChecks: FailedChecks = { - clErrors: translation.errors, - clWarnings: translation.warnings, - pErrors: [], - pndbWarnings: [], - ttWarnings: [], - }; - dispatch(editor.actions.updateFailedChecks(failedChecks, 'stored')); - } else { - dispatch(editor.actions.resetFailedChecks()); - } + if (!state.selectedEntity) { + return
    ; } - searchMachinery: (query: string, page?: number) => void = ( - query: string, - page?: number, - ) => { - const { dispatch, locale, selectedEntity, user } = this.props; - const { get, getConcordanceSearchResults, resetSearch } = - machinery.actions; - - if (query) { - if (page) { - dispatch(getConcordanceSearchResults(query, locale, page)); - } else { - dispatch(resetSearch(query)); - dispatch(getConcordanceSearchResults(query, locale)); - dispatch(get(query, locale, user.isAuthenticated, null)); - } - } else { - dispatch(resetSearch('')); - if (selectedEntity) { - // On empty query, use source string as input - const source = utils.getOptimizedContent( - selectedEntity.machinery_original, - selectedEntity.format, - ); - const pk = selectedEntity.pk; - dispatch(get(source, locale, user.isAuthenticated, pk)); - } - } - }; - - copyLinkToClipboard: () => void = () => { - const { locale, project, resource, entity } = this.props.parameters; - const { protocol, host } = window.location; - - const string_link = `${protocol}//${host}/${locale}/${project}/${resource}/?string=${entity}`; - navigator.clipboard.writeText(string_link).then(() => { - // Notify the user of the change that happened. - const notif = notification.messages.STRING_LINK_COPIED; - this.props.dispatch(notification.actions.add(notif)); - }); - }; - - goToNextEntity: () => void = () => { - const { dispatch, nextEntity, router } = this.props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - dispatch( - navigation.actions.updateEntity( - router, - nextEntity.pk.toString(), - ), - ); - dispatch(editor.actions.reset()); - }, - ), - ); - }; - - goToPreviousEntity: () => void = () => { - const { dispatch, previousEntity, router } = this.props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - dispatch( - navigation.actions.updateEntity( - router, - previousEntity.pk.toString(), - ), - ); - dispatch(editor.actions.reset()); - }, - ), - ); - }; - - navigateToPath: (path: string) => void = (path: string) => { - const { dispatch } = this.props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - dispatch(push(path)); - }, - ), - ); - }; - - updateEditorTranslation: ( - translation: string, - changeSource: string, - ) => void = (translation: string, changeSource: string) => { - this.props.dispatch(editor.actions.update(translation, changeSource)); - }; - - addTextToEditorTranslation: ( - content: string, - changeSource?: string, - ) => void = (content: string, changeSource?: string) => { - this.props.dispatch( - editor.actions.updateSelection(content, changeSource), - ); - }; - - deleteTranslation: (translationId: number) => void = ( - translationId: number, - ) => { - const { parameters, pluralForm, dispatch } = this.props; - dispatch( - history.actions.deleteTranslation( - parameters.entity, - parameters.locale, - pluralForm, - translationId, - ), - ); - }; - - setCommentTabIndex: (tab: number) => void = (tab: number) => { - this.setState({ commentTabIndex: tab }); - }; - - setContactPerson: (contact: string) => void = (contact: string) => { - this.setState({ contactPerson: contact }); - }; - - resetContactPerson: () => void = () => { - this.setState({ contactPerson: '' }); - }; - - addComment: ( - comment: string, - translation: number | null | undefined, - ) => void = (comment: string, translation: number | null | undefined) => { - const { parameters, pluralForm, dispatch } = this.props; - dispatch( - comments.actions.addComment( - parameters.entity, - parameters.locale, - pluralForm, - translation, - comment, - ), - ); - }; - - togglePinnedStatus: (pinned: boolean, commentId: number) => void = ( - pinned: boolean, - commentId: number, - ) => { - this.props.dispatch( - teamcomments.actions.togglePinnedStatus(pinned, commentId), - ); - }; - - /* - * This is a copy of EditorBase.updateTranslationStatus(). - * When changing this function, you probably want to change both. - * We might want to refactor to keep the logic in one place only. - */ - updateTranslationStatus: ( - translationId: number, - change: ChangeOperation, - ) => void = (translationId: number, change: ChangeOperation) => { - const { - locale, - nextEntity, - parameters, - pluralForm, - router, - selectedEntity, - dispatch, - } = this.props; - - const state = this.props.store.getState(); - const unsavedChangesExist = state[unsavedchanges.NAME].exist; - const unsavedChangesIgnored = state[unsavedchanges.NAME].ignored; - - // No need to check for unsaved changes in `EditorBase.updateTranslationStatus()`, - // because it cannot be triggered for the use case of bug 1508474. - dispatch( - unsavedchanges.actions.check( - unsavedChangesExist, - unsavedChangesIgnored, - () => { - dispatch( - history.actions.updateStatus( - change, - selectedEntity, - locale, - parameters.resource, - pluralForm, - translationId, - nextEntity, - router, - ), - ); - }, - ), - ); - }; - - render(): null | React.ReactElement<'section'> { - const state = this.props; - - if (!state.locale) { - return null; - } - - if (!state.selectedEntity) { - return
    ; - } - - return ( -
    -
    - - - - -
    -
    - -
    -
    - ); - } + return ( +
    +
    + + + + +
    +
    + +
    +
    + ); + } } export default function EntityDetails(): React.ReactElement< - typeof EntityDetailsBase + typeof EntityDetailsBase > { - const state = { - activeTranslationString: useAppSelector((state) => - plural.selectors.getTranslationStringForSelectedEntity(state), - ), - history: useAppSelector((state) => state[history.NAME]), - isReadOnlyEditor: useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), - ), - isTranslator: useAppSelector((state) => - user.selectors.isTranslator(state), - ), - locale: useAppSelector((state) => state[locale.NAME]), - machinery: useAppSelector((state) => state[machinery.NAME]), - nextEntity: useAppSelector((state) => - entities.selectors.getNextEntity(state), - ), - previousEntity: useAppSelector((state) => - entities.selectors.getPreviousEntity(state), - ), - otherlocales: useAppSelector((state) => state[otherlocales.NAME]), - teamComments: useAppSelector((state) => state[teamcomments.NAME]), - terms: useAppSelector((state) => state[terms.NAME]), - parameters: useAppSelector((state) => - navigation.selectors.getNavigationParams(state), - ), - pluralForm: useAppSelector((state) => - plural.selectors.getPluralForm(state), - ), - router: useAppSelector((state) => state.router), - selectedEntity: useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ), - user: useAppSelector((state) => state[user.NAME]), - }; - return ( - - ); + const state = { + activeTranslationString: useAppSelector((state) => + plural.selectors.getTranslationStringForSelectedEntity(state), + ), + history: useAppSelector((state) => state[history.NAME]), + isReadOnlyEditor: useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ), + isTranslator: useAppSelector((state) => user.selectors.isTranslator(state)), + locale: useAppSelector((state) => state[locale.NAME]), + machinery: useAppSelector((state) => state[machinery.NAME]), + nextEntity: useAppSelector((state) => + entities.selectors.getNextEntity(state), + ), + previousEntity: useAppSelector((state) => + entities.selectors.getPreviousEntity(state), + ), + otherlocales: useAppSelector((state) => state[otherlocales.NAME]), + teamComments: useAppSelector((state) => state[teamcomments.NAME]), + terms: useAppSelector((state) => state[terms.NAME]), + parameters: useAppSelector((state) => + navigation.selectors.getNavigationParams(state), + ), + pluralForm: useAppSelector((state) => + plural.selectors.getPluralForm(state), + ), + router: useAppSelector((state) => state.router), + selectedEntity: useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ), + user: useAppSelector((state) => state[user.NAME]), + }; + return ( + + ); } diff --git a/translate/src/modules/entitydetails/components/EntityNavigation.css b/translate/src/modules/entitydetails/components/EntityNavigation.css index d8df191b3..fb1a5f309 100644 --- a/translate/src/modules/entitydetails/components/EntityNavigation.css +++ b/translate/src/modules/entitydetails/components/EntityNavigation.css @@ -1,30 +1,30 @@ .entity-navigation { - background: #4d5967; - border-bottom: 1px solid #5e6475; - padding: 12px 10px 13px; + background: #4d5967; + border-bottom: 1px solid #5e6475; + padding: 12px 10px 13px; } .entity-navigation button { - background: none; - border: none; - color: #cccccc; - float: right; - font-weight: 300; - padding: 0; + background: none; + border: none; + color: #cccccc; + float: right; + font-weight: 300; + padding: 0; } .entity-navigation button.link { - float: left; + float: left; } .entity-navigation button.previous { - margin-right: 30px; + margin-right: 30px; } .entity-navigation button:hover { - color: #7bc876; + color: #7bc876; } .entity-navigation button .fa { - padding-right: 5px; + padding-right: 5px; } diff --git a/translate/src/modules/entitydetails/components/EntityNavigation.test.js b/translate/src/modules/entitydetails/components/EntityNavigation.test.js index 727d709bd..d4ad6c25b 100644 --- a/translate/src/modules/entitydetails/components/EntityNavigation.test.js +++ b/translate/src/modules/entitydetails/components/EntityNavigation.test.js @@ -5,91 +5,91 @@ import sinon from 'sinon'; import EntityNavigation from './EntityNavigation'; describe('', () => { - function getEntityNav({ create = shallow } = {}) { - const copyMock = sinon.stub(); - const nextMock = sinon.stub(); - const prevMock = sinon.stub(); - const wrapper = create( - , - ); + function getEntityNav({ create = shallow } = {}) { + const copyMock = sinon.stub(); + const nextMock = sinon.stub(); + const prevMock = sinon.stub(); + const wrapper = create( + , + ); - return { - wrapper, - copyMock, - nextMock, - prevMock, - }; - } + return { + wrapper, + copyMock, + nextMock, + prevMock, + }; + } - it('puts a copy of string link on clipboard', () => { - const { wrapper, copyMock } = getEntityNav(); + it('puts a copy of string link on clipboard', () => { + const { wrapper, copyMock } = getEntityNav(); - expect(copyMock.calledOnce).toBeFalsy(); - wrapper.find('button.link').simulate('click'); - expect(copyMock.calledOnce).toBeTruthy(); + expect(copyMock.calledOnce).toBeFalsy(); + wrapper.find('button.link').simulate('click'); + expect(copyMock.calledOnce).toBeTruthy(); + }); + + it('goes to the next entity on click on the Next button', () => { + const { wrapper, nextMock } = getEntityNav(); + + expect(nextMock.calledOnce).toBeFalsy(); + wrapper.find('button.next').simulate('click'); + expect(nextMock.calledOnce).toBeTruthy(); + }); + + it('goes to the next entity on Alt + Down', () => { + // Simulating the key presses on `document`. + // See https://github.com/airbnb/enzyme/issues/426 + const eventsMap = {}; + document.addEventListener = sinon.spy((event, cb) => { + eventsMap[event] = cb; }); - it('goes to the next entity on click on the Next button', () => { - const { wrapper, nextMock } = getEntityNav(); + const { nextMock } = getEntityNav(mount); - expect(nextMock.calledOnce).toBeFalsy(); - wrapper.find('button.next').simulate('click'); - expect(nextMock.calledOnce).toBeTruthy(); + expect(nextMock.calledOnce).toBeFalsy(); + const event = { + preventDefault: sinon.spy(), + keyCode: 40, // Down + altKey: true, + ctrlKey: false, + shiftKey: false, + }; + eventsMap.keydown(event); + expect(nextMock.calledOnce).toBeTruthy(); + }); + + it('goes to the previous entity on click on the Previous button', () => { + const { wrapper, prevMock } = getEntityNav(); + + expect(prevMock.calledOnce).toBeFalsy(); + wrapper.find('button.previous').simulate('click'); + expect(prevMock.calledOnce).toBeTruthy(); + }); + + it('goes to the previous entity on Alt + Up', () => { + // Simulating the key presses on `document`. + // See https://github.com/airbnb/enzyme/issues/426 + const eventsMap = {}; + document.addEventListener = sinon.spy((event, cb) => { + eventsMap[event] = cb; }); - it('goes to the next entity on Alt + Down', () => { - // Simulating the key presses on `document`. - // See https://github.com/airbnb/enzyme/issues/426 - const eventsMap = {}; - document.addEventListener = sinon.spy((event, cb) => { - eventsMap[event] = cb; - }); + const { prevMock } = getEntityNav(mount); - const { nextMock } = getEntityNav(mount); - - expect(nextMock.calledOnce).toBeFalsy(); - const event = { - preventDefault: sinon.spy(), - keyCode: 40, // Down - altKey: true, - ctrlKey: false, - shiftKey: false, - }; - eventsMap.keydown(event); - expect(nextMock.calledOnce).toBeTruthy(); - }); - - it('goes to the previous entity on click on the Previous button', () => { - const { wrapper, prevMock } = getEntityNav(); - - expect(prevMock.calledOnce).toBeFalsy(); - wrapper.find('button.previous').simulate('click'); - expect(prevMock.calledOnce).toBeTruthy(); - }); - - it('goes to the previous entity on Alt + Up', () => { - // Simulating the key presses on `document`. - // See https://github.com/airbnb/enzyme/issues/426 - const eventsMap = {}; - document.addEventListener = sinon.spy((event, cb) => { - eventsMap[event] = cb; - }); - - const { prevMock } = getEntityNav(mount); - - expect(prevMock.calledOnce).toBeFalsy(); - const event = { - preventDefault: sinon.spy(), - keyCode: 38, // Up - altKey: true, - ctrlKey: false, - shiftKey: false, - }; - eventsMap.keydown(event); - expect(prevMock.calledOnce).toBeTruthy(); - }); + expect(prevMock.calledOnce).toBeFalsy(); + const event = { + preventDefault: sinon.spy(), + keyCode: 38, // Up + altKey: true, + ctrlKey: false, + shiftKey: false, + }; + eventsMap.keydown(event); + expect(prevMock.calledOnce).toBeTruthy(); + }); }); diff --git a/translate/src/modules/entitydetails/components/EntityNavigation.tsx b/translate/src/modules/entitydetails/components/EntityNavigation.tsx index 12b67fb52..9b53c20cf 100644 --- a/translate/src/modules/entitydetails/components/EntityNavigation.tsx +++ b/translate/src/modules/entitydetails/components/EntityNavigation.tsx @@ -4,9 +4,9 @@ import { Localized } from '@fluent/react'; import './EntityNavigation.css'; type Props = { - readonly copyLinkToClipboard: () => void; - readonly goToNextEntity: () => void; - readonly goToPreviousEntity: () => void; + readonly copyLinkToClipboard: () => void; + readonly goToNextEntity: () => void; + readonly goToPreviousEntity: () => void; }; /** @@ -15,77 +15,75 @@ type Props = { * Shows copy link and next/previous buttons. */ export default class EntityNavigation extends React.Component { - componentDidMount() { - document.addEventListener('keydown', this.handleShortcuts); + componentDidMount() { + document.addEventListener('keydown', this.handleShortcuts); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleShortcuts); + } + + handleShortcuts: (event: KeyboardEvent) => void = (event: KeyboardEvent) => { + const key = event.keyCode; + + // On Alt + Up, move to the previous entity. + if (key === 38 && event.altKey && !event.ctrlKey && !event.shiftKey) { + event.preventDefault(); + this.props.goToPreviousEntity(); } - componentWillUnmount() { - document.removeEventListener('keydown', this.handleShortcuts); + // On Alt + Down, move to the next entity. + if (key === 40 && event.altKey && !event.ctrlKey && !event.shiftKey) { + event.preventDefault(); + this.props.goToNextEntity(); } + }; - handleShortcuts: (event: KeyboardEvent) => void = ( - event: KeyboardEvent, - ) => { - const key = event.keyCode; - - // On Alt + Up, move to the previous entity. - if (key === 38 && event.altKey && !event.ctrlKey && !event.shiftKey) { - event.preventDefault(); - this.props.goToPreviousEntity(); - } - - // On Alt + Down, move to the next entity. - if (key === 40 && event.altKey && !event.ctrlKey && !event.shiftKey) { - event.preventDefault(); - this.props.goToNextEntity(); - } - }; - - render(): React.ReactNode { - return ( -
    - }} - > - - - , - }} - > - - - }} - > - - -
    - ); - } + render(): React.ReactNode { + return ( +
    + }} + > + + + , + }} + > + + + }} + > + + +
    + ); + } } diff --git a/translate/src/modules/entitydetails/components/FluentAttribute.tsx b/translate/src/modules/entitydetails/components/FluentAttribute.tsx index 0cc7c2094..73b6552ef 100644 --- a/translate/src/modules/entitydetails/components/FluentAttribute.tsx +++ b/translate/src/modules/entitydetails/components/FluentAttribute.tsx @@ -8,38 +8,35 @@ import Property from './Property'; import type { Entity } from '~/core/api'; type Props = { - readonly entity: Entity; + readonly entity: Entity; }; /** * Get attribute of a simple single-attribute Fluent message. */ export default function FluentAttribute( - props: Props, + props: Props, ): null | React.ReactElement { - const { entity } = props; + const { entity } = props; - if (entity.format !== 'ftl') { - return null; - } + if (entity.format !== 'ftl') { + return null; + } - const message = fluent.parser.parseEntry(entity.original); + const message = fluent.parser.parseEntry(entity.original); - if ( - message.type !== 'Message' || - !fluent.isSimpleSingleAttributeMessage(message) - ) { - return null; - } + if ( + message.type !== 'Message' || + !fluent.isSimpleSingleAttributeMessage(message) + ) { + return null; + } - return ( - - - {message.attributes[0].id.name} - - - ); + return ( + + + {message.attributes[0].id.name} + + + ); } diff --git a/translate/src/modules/entitydetails/components/GenericOriginalString.test.js b/translate/src/modules/entitydetails/components/GenericOriginalString.test.js index 13bd6c349..f3a78c0b7 100644 --- a/translate/src/modules/entitydetails/components/GenericOriginalString.test.js +++ b/translate/src/modules/entitydetails/components/GenericOriginalString.test.js @@ -4,54 +4,54 @@ import { shallow } from 'enzyme'; import GenericOriginalString from './GenericOriginalString'; const ENTITY = { - pk: 42, - original: 'le test', - original_plural: 'les tests', + pk: 42, + original: 'le test', + original_plural: 'les tests', }; const LOCALE = { - code: 'kg', - cldrPlurals: [1, 3, 5], + code: 'kg', + cldrPlurals: [1, 3, 5], }; function createGenericOriginalString(pluralForm = -1) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders correctly', () => { - const wrapper = createGenericOriginalString(); + it('renders correctly', () => { + const wrapper = createGenericOriginalString(); - const originalContent = wrapper.find('ContentMarker').props().children; - expect(originalContent).toContain(ENTITY.original); - }); + const originalContent = wrapper.find('ContentMarker').props().children; + expect(originalContent).toContain(ENTITY.original); + }); - it('renders the selected plural form as original string', () => { - const wrapper = createGenericOriginalString(2); + it('renders the selected plural form as original string', () => { + const wrapper = createGenericOriginalString(2); - expect( - wrapper.find('#entitydetails-GenericOriginalString--plural'), - ).toHaveLength(1); + expect( + wrapper.find('#entitydetails-GenericOriginalString--plural'), + ).toHaveLength(1); - const originalContent = wrapper.find('ContentMarker').props().children; - expect(originalContent).toContain(ENTITY.original_plural); - }); + const originalContent = wrapper.find('ContentMarker').props().children; + expect(originalContent).toContain(ENTITY.original_plural); + }); - it('renders the selected singular form as original string', () => { - const wrapper = createGenericOriginalString(0); + it('renders the selected singular form as original string', () => { + const wrapper = createGenericOriginalString(0); - expect( - wrapper.find('#entitydetails-GenericOriginalString--singular'), - ).toHaveLength(1); + expect( + wrapper.find('#entitydetails-GenericOriginalString--singular'), + ).toHaveLength(1); - const originalContent = wrapper.find('ContentMarker').props().children; - expect(originalContent).toContain(ENTITY.original); - }); + const originalContent = wrapper.find('ContentMarker').props().children; + expect(originalContent).toContain(ENTITY.original); + }); }); diff --git a/translate/src/modules/entitydetails/components/GenericOriginalString.tsx b/translate/src/modules/entitydetails/components/GenericOriginalString.tsx index 328223950..097cee7f3 100644 --- a/translate/src/modules/entitydetails/components/GenericOriginalString.tsx +++ b/translate/src/modules/entitydetails/components/GenericOriginalString.tsx @@ -8,44 +8,44 @@ import type { Locale } from '~/core/locale'; import type { TermState } from '~/core/term'; type Props = { - readonly entity: Entity; - readonly locale: Locale; - readonly pluralForm: number; - readonly terms: TermState; - readonly handleClickOnPlaceable: ( - event: React.MouseEvent, - ) => void; + readonly entity: Entity; + readonly locale: Locale; + readonly pluralForm: number; + readonly terms: TermState; + readonly handleClickOnPlaceable: ( + event: React.MouseEvent, + ) => void; }; function getOriginalContent(props: Props) { - const { entity, locale, pluralForm } = props; - - if (pluralForm === -1) { - return { - title: null, - original: entity.original, - }; - } - - if (locale.cldrPlurals[pluralForm] === 1) { - return { - title: ( - -

    SINGULAR

    -
    - ), - original: entity.original, - }; - } + const { entity, locale, pluralForm } = props; + if (pluralForm === -1) { return { - title: ( - -

    PLURAL

    -
    - ), - original: entity.original_plural, + title: null, + original: entity.original, }; + } + + if (locale.cldrPlurals[pluralForm] === 1) { + return { + title: ( + +

    SINGULAR

    +
    + ), + original: entity.original, + }; + } + + return { + title: ( + +

    PLURAL

    +
    + ), + original: entity.original_plural, + }; } /** @@ -55,18 +55,18 @@ function getOriginalContent(props: Props) { * string, and also display which form is being rendered. */ export default function GenericOriginalString( - props: Props, + props: Props, ): React.ReactElement { - const { title, original } = getOriginalContent(props); + const { title, original } = getOriginalContent(props); - const TermsAndPlaceablesMarker = getMarker(props.terms); + const TermsAndPlaceablesMarker = getMarker(props.terms); - return ( - <> - {title} -

    - {original} -

    - - ); + return ( + <> + {title} +

    + {original} +

    + + ); } diff --git a/translate/src/modules/entitydetails/components/Helpers.css b/translate/src/modules/entitydetails/components/Helpers.css index e96f5e484..028f1a27d 100644 --- a/translate/src/modules/entitydetails/components/Helpers.css +++ b/translate/src/modules/entitydetails/components/Helpers.css @@ -1,79 +1,79 @@ .third-column .top { - border-bottom: 1px solid #5e6475; - box-sizing: border-box; - height: calc(40% + 21px); - min-height: 200px; + border-bottom: 1px solid #5e6475; + box-sizing: border-box; + height: calc(40% + 21px); + min-height: 200px; } .third-column .bottom { - box-sizing: border-box; - height: calc(60% - 21px); + box-sizing: border-box; + height: calc(60% - 21px); } /* Styling the react-tabs library */ .react-tabs { - overflow: hidden; - -webkit-tap-highlight-color: transparent; - height: 100%; + overflow: hidden; + -webkit-tap-highlight-color: transparent; + height: 100%; } .react-tabs__tab-list { - display: table; - table-layout: fixed; - margin: 0; - width: 100%; + display: table; + table-layout: fixed; + margin: 0; + width: 100%; } .react-tabs__tab { - background: #4d5967; - border-bottom: 1px solid #5e6475; - border-right: 1px solid #5e6475; - box-sizing: border-box; - color: #cccccc; - cursor: pointer; - display: table-cell; - font-weight: 300; - height: 44px; - line-height: 18px; - list-style: none; - padding: 12px 5px 13px; - text-align: center; + background: #4d5967; + border-bottom: 1px solid #5e6475; + border-right: 1px solid #5e6475; + box-sizing: border-box; + color: #cccccc; + cursor: pointer; + display: table-cell; + font-weight: 300; + height: 44px; + line-height: 18px; + list-style: none; + padding: 12px 5px 13px; + text-align: center; } .react-tabs__tab:last-child { - border-right: none; + border-right: none; } .react-tabs__tab--selected { - background: #3f4752; - border-bottom: none; + background: #3f4752; + border-bottom: none; } .react-tabs__tab-panel { - display: none; - height: calc(100% - 44px); - overflow-y: auto; + display: none; + height: calc(100% - 44px); + overflow-y: auto; } .react-tabs__tab-panel--selected { - display: block; + display: block; } /* Other tools styles */ .react-tabs span.count { - background: #3f4752; - border-radius: 3px; - color: #cccccc; - font-weight: 300; - margin-left: 3px; - padding: 0 5px; + background: #3f4752; + border-radius: 3px; + color: #cccccc; + font-weight: 300; + margin-left: 3px; + padding: 0 5px; } .react-tabs .react-tabs__tab--selected span.count { - background: #4d5967; + background: #4d5967; } .react-tabs span.count .preferred, .react-tabs span.count .pinned { - color: #7bc876; + color: #7bc876; } diff --git a/translate/src/modules/entitydetails/components/Helpers.tsx b/translate/src/modules/entitydetails/components/Helpers.tsx index 343088b3e..d4fe8edd9 100644 --- a/translate/src/modules/entitydetails/components/Helpers.tsx +++ b/translate/src/modules/entitydetails/components/Helpers.tsx @@ -21,25 +21,25 @@ import type { MachineryState } from '~/modules/machinery'; import type { LocalesState } from '~/modules/otherlocales'; type Props = { - entity: Entity; - isReadOnlyEditor: boolean; - locale: Locale; - machinery: MachineryState; - otherlocales: LocalesState; - teamComments: TeamCommentState; - terms: TermState; - parameters: NavigationParams; - user: UserState; - commentTabRef: Record; - commentTabIndex: number; - contactPerson: string; - searchMachinery: (source: string) => void; - addComment: (comment: string, id: number | null | undefined) => void; - togglePinnedStatus: (status: boolean, id: number) => void; - addTextToEditorTranslation: (text: string) => void; - navigateToPath: (path: string) => void; - setCommentTabIndex: (index: number) => void; - resetContactPerson: () => void; + entity: Entity; + isReadOnlyEditor: boolean; + locale: Locale; + machinery: MachineryState; + otherlocales: LocalesState; + teamComments: TeamCommentState; + terms: TermState; + parameters: NavigationParams; + user: UserState; + commentTabRef: Record; + commentTabIndex: number; + contactPerson: string; + searchMachinery: (source: string) => void; + addComment: (comment: string, id: number | null | undefined) => void; + togglePinnedStatus: (status: boolean, id: number) => void; + addTextToEditorTranslation: (text: string) => void; + navigateToPath: (path: string) => void; + setCommentTabIndex: (index: number) => void; + resetContactPerson: () => void; }; /** @@ -48,119 +48,117 @@ type Props = { * Shows the metadata of the entity and an editor for translations. */ export default function Helpers(props: Props): React.ReactElement { - const { - entity, - isReadOnlyEditor, - locale, - machinery, - otherlocales, - teamComments, - terms, - parameters, - user, - commentTabRef, - commentTabIndex, - contactPerson, - searchMachinery, - addComment, - togglePinnedStatus, - addTextToEditorTranslation, - navigateToPath, - setCommentTabIndex, - resetContactPerson, - } = props; - const dispatch = useAppDispatch(); + const { + entity, + isReadOnlyEditor, + locale, + machinery, + otherlocales, + teamComments, + terms, + parameters, + user, + commentTabRef, + commentTabIndex, + contactPerson, + searchMachinery, + addComment, + togglePinnedStatus, + addTextToEditorTranslation, + navigateToPath, + setCommentTabIndex, + resetContactPerson, + } = props; + const dispatch = useAppDispatch(); - return ( - <> -
    - setCommentTabIndex(tab)} - > - - {parameters.project === 'terminology' ? null : ( - - - {'TERMS'} - - - - )} - - - {'COMMENTS'} - - - - - {parameters.project === 'terminology' ? null : ( - - - - )} - - - - -
    -
    - { - if (index === lastIndex) { - return false; - } - dispatch(editor.actions.selectHelperTabIndex(index)); - dispatch(editor.actions.resetHelperElementIndex()); - }} - > - - - - {'MACHINERY'} - - - - - - {'LOCALES'} - - - - - - - - - - - -
    - - ); + return ( + <> +
    + setCommentTabIndex(tab)} + > + + {parameters.project === 'terminology' ? null : ( + + + {'TERMS'} + + + + )} + + + {'COMMENTS'} + + + + + {parameters.project === 'terminology' ? null : ( + + + + )} + + + + +
    +
    + { + if (index === lastIndex) { + return false; + } + dispatch(editor.actions.selectHelperTabIndex(index)); + dispatch(editor.actions.resetHelperElementIndex()); + }} + > + + + + {'MACHINERY'} + + + + + + {'LOCALES'} + + + + + + + + + + + +
    + + ); } diff --git a/translate/src/modules/entitydetails/components/Metadata.css b/translate/src/modules/entitydetails/components/Metadata.css index 2f0b495f3..15bf2330e 100644 --- a/translate/src/modules/entitydetails/components/Metadata.css +++ b/translate/src/modules/entitydetails/components/Metadata.css @@ -1,96 +1,96 @@ .metadata { - background-color: #3f4752; - color: #aaa; - flex-shrink: 0; - font-size: 12px; - font-style: italic; - height: 20%; - min-height: 100px; - line-height: 22px; - overflow: auto; - padding: 10px; + background-color: #3f4752; + color: #aaa; + flex-shrink: 0; + font-size: 12px; + font-style: italic; + height: 20%; + min-height: 100px; + line-height: 22px; + overflow: auto; + padding: 10px; } .metadata h2 { - font-style: normal; - font-size: 11px; - font-weight: 300; - line-height: 11px; - margin-top: 6px; - padding-bottom: 2px; + font-style: normal; + font-size: 11px; + font-weight: 300; + line-height: 11px; + margin-top: 6px; + padding-bottom: 2px; } .metadata .title { - color: #888; + color: #888; } .metadata div a { - color: #7bc876; - text-decoration: none; + color: #7bc876; + text-decoration: none; } .metadata .comment .content p { - display: inline; + display: inline; } .metadata div .divider { - font-style: normal; - font-weight: 100; - margin: 0 3px; + font-style: normal; + font-weight: 100; + margin: 0 3px; } .metadata .original { - color: white; - font-size: 14px; - font-style: normal; - line-height: 22px; - margin-top: -2px; /* Align with the source string comment button */ - padding-bottom: 6px; - text-align: start; - white-space: pre-wrap; + color: white; + font-size: 14px; + font-style: normal; + line-height: 22px; + margin-top: -2px; /* Align with the source string comment button */ + padding-bottom: 6px; + text-align: start; + white-space: pre-wrap; } .metadata .original .placeable { - cursor: pointer; + cursor: pointer; } .metadata .original .term { - background: inherit; - border-bottom: 1px solid #7bc876; - color: inherit; - cursor: pointer; - font-weight: normal; - font-style: inherit; + background: inherit; + border-bottom: 1px solid #7bc876; + color: inherit; + cursor: pointer; + font-weight: normal; + font-style: inherit; } .metadata ul { - list-style: none; - margin: 0; - padding: 0; + list-style: none; + margin: 0; + padding: 0; } .metadata button { - background: none; - border: none; - color: #7bc876; - font-style: italic; - padding: 0 0 0 2px; + background: none; + border: none; + color: #7bc876; + font-style: italic; + padding: 0 0 0 2px; } .metadata .resource-comment { - display: flex; + display: flex; } .metadata .resource-comment .comment { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .metadata .resource-comment .comment.expanded { - white-space: normal; + white-space: normal; } .metadata .resource-comment button { - white-space: nowrap; + white-space: nowrap; } diff --git a/translate/src/modules/entitydetails/components/Metadata.test.js b/translate/src/modules/entitydetails/components/Metadata.test.js index c3c455844..d32fe3836 100644 --- a/translate/src/modules/entitydetails/components/Metadata.test.js +++ b/translate/src/modules/entitydetails/components/Metadata.test.js @@ -4,127 +4,125 @@ import { shallow } from 'enzyme'; import Metadata from './Metadata'; const ENTITY = { - pk: 42, - original: 'le test', - original_plural: 'les tests', - comment: 'my comment', - path: 'path/to/RESOURCE', - source: [['file_source.rs', '31']], - translation: [{ string: 'the test' }, { string: 'plural' }], - project: { - slug: 'callme', - name: 'CallMe', - }, + pk: 42, + original: 'le test', + original_plural: 'les tests', + comment: 'my comment', + path: 'path/to/RESOURCE', + source: [['file_source.rs', '31']], + translation: [{ string: 'the test' }, { string: 'plural' }], + project: { + slug: 'callme', + name: 'CallMe', + }, }; const LOCALE = { - code: 'kg', - cldrPlurals: [1, 3, 5], + code: 'kg', + cldrPlurals: [1, 3, 5], }; const USER = { - user: 'A_Ludgate', + user: 'A_Ludgate', }; function createShallowMetadata(entity = ENTITY, pluralForm = -1) { - return shallow( - , - ); + return shallow( + , + ); } describe('', () => { - it('renders correctly', () => { - const wrapper = createShallowMetadata(); + it('renders correctly', () => { + const wrapper = createShallowMetadata(); - expect(wrapper.text()).toContain(ENTITY.source[0][0]); + expect(wrapper.text()).toContain(ENTITY.source[0][0]); - // Comments are hidden in a Linkify component. - const content = wrapper - .find('Linkify') - .map((item) => item.props().children); - expect(content).toContain(ENTITY.comment); + // Comments are hidden in a Linkify component. + const content = wrapper + .find('Linkify') + .map((item) => item.props().children); + expect(content).toContain(ENTITY.comment); - expect( - wrapper - .find('#entitydetails-Metadata--resource a.resource-path') - .text(), - ).toContain(ENTITY.path); + expect( + wrapper.find('#entitydetails-Metadata--resource a.resource-path').text(), + ).toContain(ENTITY.path); + }); + + it('does not require a comment', () => { + const wrapper = createShallowMetadata({ + ...ENTITY, + ...{ comment: '' }, }); - it('does not require a comment', () => { - const wrapper = createShallowMetadata({ - ...ENTITY, - ...{ comment: '' }, - }); + expect(wrapper.text()).toContain(ENTITY.source[0][0]); - expect(wrapper.text()).toContain(ENTITY.source[0][0]); + // Comments are hidden in a Linkify component. + const content = wrapper + .find('Linkify') + .map((item) => item.props().children); + expect(content).not.toContain(ENTITY.comment); + }); - // Comments are hidden in a Linkify component. - const content = wrapper - .find('Linkify') - .map((item) => item.props().children); - expect(content).not.toContain(ENTITY.comment); + it('does not require a source', () => { + const wrapper = createShallowMetadata({ ...ENTITY, ...{ source: [] } }); + + expect(wrapper.text()).not.toContain(ENTITY.source[0][0]); + + // Comments are hidden in a Linkify component. + const content = wrapper + .find('Linkify') + .map((item) => item.props().children); + expect(content).toContain(ENTITY.comment); + }); + + it('handles sources as an object with examples', () => { + const withSourceAsObject = { + source: { + arg1: { + content: '', + example: 'example_1', + }, + arg2: { + content: '', + example: 'example_2', + }, + }, + }; + const wrapper = createShallowMetadata({ + ...ENTITY, + ...withSourceAsObject, }); - it('does not require a source', () => { - const wrapper = createShallowMetadata({ ...ENTITY, ...{ source: [] } }); + const sourceContent = wrapper + .find('Property[title="PLACEHOLDER EXAMPLES"] Linkify') + .props().children; + expect(sourceContent).toContain('$ARG1$: example_1'); + expect(sourceContent).toContain('$ARG2$: example_2'); + }); - expect(wrapper.text()).not.toContain(ENTITY.source[0][0]); - - // Comments are hidden in a Linkify component. - const content = wrapper - .find('Linkify') - .map((item) => item.props().children); - expect(content).toContain(ENTITY.comment); + it('handles sources as an object without examples', () => { + const withSourceAsObject = { + source: { + arg1: { + content: '', + }, + arg2: { + content: '', + }, + }, + }; + const wrapper = createShallowMetadata({ + ...ENTITY, + ...withSourceAsObject, }); - it('handles sources as an object with examples', () => { - const withSourceAsObject = { - source: { - arg1: { - content: '', - example: 'example_1', - }, - arg2: { - content: '', - example: 'example_2', - }, - }, - }; - const wrapper = createShallowMetadata({ - ...ENTITY, - ...withSourceAsObject, - }); - - const sourceContent = wrapper - .find('Property[title="PLACEHOLDER EXAMPLES"] Linkify') - .props().children; - expect(sourceContent).toContain('$ARG1$: example_1'); - expect(sourceContent).toContain('$ARG2$: example_2'); - }); - - it('handles sources as an object without examples', () => { - const withSourceAsObject = { - source: { - arg1: { - content: '', - }, - arg2: { - content: '', - }, - }, - }; - const wrapper = createShallowMetadata({ - ...ENTITY, - ...withSourceAsObject, - }); - - expect( - wrapper.find('Property[title="PLACEHOLDER EXAMPLES"]').exists(), - ).toBeFalsy(); - }); + expect( + wrapper.find('Property[title="PLACEHOLDER EXAMPLES"]').exists(), + ).toBeFalsy(); + }); }); diff --git a/translate/src/modules/entitydetails/components/Metadata.tsx b/translate/src/modules/entitydetails/components/Metadata.tsx index d76e423cd..97d877da8 100644 --- a/translate/src/modules/entitydetails/components/Metadata.tsx +++ b/translate/src/modules/entitydetails/components/Metadata.tsx @@ -19,61 +19,61 @@ import type { TeamCommentState } from '~/modules/teamcomments'; import type { UserState } from '~/core/user'; type Props = { - readonly entity: Entity; - readonly isReadOnlyEditor: boolean; - readonly locale: Locale; - readonly pluralForm: number; - readonly terms: TermState; - readonly teamComments: TeamCommentState; - readonly user: UserState; - readonly commentTabRef: Record; - readonly addTextToEditorTranslation: (text: string) => void; - readonly navigateToPath: (path: string) => void; - setCommentTabIndex: (id: number) => void; - setContactPerson: (contact: string) => void; + readonly entity: Entity; + readonly isReadOnlyEditor: boolean; + readonly locale: Locale; + readonly pluralForm: number; + readonly terms: TermState; + readonly teamComments: TeamCommentState; + readonly user: UserState; + readonly commentTabRef: Record; + readonly addTextToEditorTranslation: (text: string) => void; + readonly navigateToPath: (path: string) => void; + setCommentTabIndex: (id: number) => void; + setContactPerson: (contact: string) => void; }; type State = { - popupTerms: Array; + popupTerms: Array; }; function ResourceComment({ comment }: { comment: string }) { - const ref = React.useRef(null); - const [overflow, setOverflow] = React.useState(false); - const [expand, setExpand] = React.useState(false); + const ref = React.useRef(null); + const [overflow, setOverflow] = React.useState(false); + const [expand, setExpand] = React.useState(false); - React.useLayoutEffect(() => { - const body = ref.current?.querySelector('.comment'); - if (body && body.scrollWidth > body.offsetWidth) setOverflow(true); - }, []); + React.useLayoutEffect(() => { + const body = ref.current?.querySelector('.comment'); + if (body && body.scrollWidth > body.offsetWidth) setOverflow(true); + }, []); - return !comment ? null : ( -
    - - - - {comment} - - - - {overflow && !expand && ( - - - - )} -
    - ); + return !comment ? null : ( +
    + + + + {comment} + + + + {overflow && !expand && ( + + + + )} +
    + ); } /** @@ -90,324 +90,307 @@ function ResourceComment({ comment }: { comment: string }) { * - a link to the project */ export default class Metadata extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - popupTerms: [], - }; + constructor(props: Props) { + super(props); + this.state = { + popupTerms: [], + }; + } + + componentDidUpdate(prevProps: Props) { + if (this.props.entity !== prevProps.entity) { + this.setState({ + popupTerms: [], + }); } + } - componentDidUpdate(prevProps: Props) { - if (this.props.entity !== prevProps.entity) { - this.setState({ - popupTerms: [], - }); + handleClickOnPlaceable: (e: React.MouseEvent) => void = + (e: React.MouseEvent) => { + const target = e.target; + if (!(target instanceof HTMLElement)) { + return; + } + if (target && target.classList.contains('placeable')) { + if (this.props.isReadOnlyEditor) { + return; } - } + if (target.dataset['match']) { + this.props.addTextToEditorTranslation(target.dataset['match']); + } else if (target.firstChild) { + const child = target.firstChild; + if (child instanceof Text) { + this.props.addTextToEditorTranslation(child.data); + } + } + } - handleClickOnPlaceable: ( - e: React.MouseEvent, - ) => void = (e: React.MouseEvent) => { - const target = e.target; - if (!(target instanceof HTMLElement)) { - return; - } - if (target && target.classList.contains('placeable')) { - if (this.props.isReadOnlyEditor) { - return; - } - if (target.dataset['match']) { - this.props.addTextToEditorTranslation(target.dataset['match']); - } else if (target.firstChild) { - const child = target.firstChild; - if (child instanceof Text) { - this.props.addTextToEditorTranslation(child.data); - } - } - } - - // Handle click on Term - const markedTerm = target.dataset['term']; - if (target && markedTerm) { - const popupTerms = this.props.terms.terms.filter( - (t) => t.text === markedTerm, - ); - this.setState({ popupTerms: popupTerms }); - } + // Handle click on Term + const markedTerm = target.dataset['term']; + if (target && markedTerm) { + const popupTerms = this.props.terms.terms.filter( + (t) => t.text === markedTerm, + ); + this.setState({ popupTerms: popupTerms }); + } }; - hidePopupTerms: () => void = () => { - this.setState({ popupTerms: [] }); - }; + hidePopupTerms: () => void = () => { + this.setState({ popupTerms: [] }); + }; - renderComment(entity: Entity): React.ReactNode { - if (!entity.comment) { - return null; - } - - let comment = entity.comment; - - const parts = entity.comment.split('\n'); - if (parts[0].startsWith('MAX_LENGTH')) { - // This comment contains a max length instruction. Remove that part. - parts.shift(); - comment = parts.join('\n'); - } - - return ( - - - - {comment} - - - - ); + renderComment(entity: Entity): React.ReactNode { + if (!entity.comment) { + return null; } - renderGroupComment(entity: Entity): React.ReactNode { - if (!entity.group_comment) { - return null; - } + let comment = entity.comment; - return ( - - - - {entity.group_comment} - - - - ); + const parts = entity.comment.split('\n'); + if (parts[0].startsWith('MAX_LENGTH')) { + // This comment contains a max length instruction. Remove that part. + parts.shift(); + comment = parts.join('\n'); } - renderPinnedComments(teamComments: TeamCommentState): React.ReactNode { - if (!teamComments) { - return null; - } + return ( + + + + {comment} + + + + ); + } - const pinnedComments = teamComments.comments.filter((comment) => { - return comment.pinned === true; - }); - - return ( - <> - {!pinnedComments - ? null - : pinnedComments.map((pinnedComment) => { - return ( - - - - {/* We can safely use parse with pinnedComment.content as it is - * sanitized when coming from the DB. See: - * - pontoon.base.forms.AddCommentForm(} - * - pontoon.base.forms.HtmlField() - */} - {parse(pinnedComment.content)} - - - - ); - })} - - ); + renderGroupComment(entity: Entity): React.ReactNode { + if (!entity.group_comment) { + return null; } - renderContext(entity: Entity): React.ReactNode { - if (!entity.context) { - return null; - } + return ( + + + + {entity.group_comment} + + + + ); + } - return ( - - - {entity.context} - - - ); + renderPinnedComments(teamComments: TeamCommentState): React.ReactNode { + if (!teamComments) { + return null; } - renderSourceArray(source: Array>): React.ReactNode { - if (!source.length || (source.length === 1 && !source[0])) { - return null; - } + const pinnedComments = teamComments.comments.filter((comment) => { + return comment.pinned === true; + }); - return ( -
      - {source.map((value, key) => { - return ( -
    • - #: - {value.join(':')} -
    • - ); - })} -
    - ); - } - - renderSourceObject(source: Record): React.ReactNode { - const examples: string[] = []; - for (const [value, { example }] of Object.entries(source)) { - // Only placeholders with examples - if (example) { - examples.push(`$${value.toUpperCase()}$: ${example}`); - } - } - - if (examples.length === 0) { - return null; - } - - return ( - - - - {examples.join(', ')} - - - - ); - } - - renderSources(entity: Entity): React.ReactNode { - if (!entity.source) { - return null; - } - - if (Array.isArray(entity.source)) { - return this.renderSourceArray(entity.source); - } - - return this.renderSourceObject(entity.source); - } - - navigateToPath: (event: React.MouseEvent) => void = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - - const path = event.currentTarget.pathname; - this.props.navigateToPath(path); - }; - - openTeamComments: () => void = () => { - const teamCommentsTab = this.props.commentTabRef.current; - const index = teamCommentsTab._reactInternalFiber.index; - this.props.setCommentTabIndex(index); - this.props.setContactPerson(this.props.entity.project.contact.name); - }; - - render(): React.ReactNode { - const { - entity, - isReadOnlyEditor, - locale, - pluralForm, - terms, - user, - teamComments, - } = this.props; - const { popupTerms } = this.state; - const contactPerson = entity.project.contact; - const showContextIssueButton = user.isAuthenticated && contactPerson; - - return ( -
    - {!showContextIssueButton ? null : ( - - )} - - - {popupTerms.length > 0 && ( - - )} - {this.renderPinnedComments(teamComments)} - {this.renderComment(entity)} - {this.renderGroupComment(entity)} - - - {this.renderContext(entity)} - {this.renderSources(entity)} + return ( + <> + {!pinnedComments + ? null + : pinnedComments.map((pinnedComment) => { + return ( - - - {entity.project.name} - - - - {entity.path} - - + + + {/* We can safely use parse with pinnedComment.content as it is + * sanitized when coming from the DB. See: + * - pontoon.base.forms.AddCommentForm(} + * - pontoon.base.forms.HtmlField() + */} + {parse(pinnedComment.content)} + + -
    - ); + ); + })} + + ); + } + + renderContext(entity: Entity): React.ReactNode { + if (!entity.context) { + return null; } + + return ( + + + {entity.context} + + + ); + } + + renderSourceArray(source: Array>): React.ReactNode { + if (!source.length || (source.length === 1 && !source[0])) { + return null; + } + + return ( +
      + {source.map((value, key) => { + return ( +
    • + #: + {value.join(':')} +
    • + ); + })} +
    + ); + } + + renderSourceObject(source: Record): React.ReactNode { + const examples: string[] = []; + for (const [value, { example }] of Object.entries(source)) { + // Only placeholders with examples + if (example) { + examples.push(`$${value.toUpperCase()}$: ${example}`); + } + } + + if (examples.length === 0) { + return null; + } + + return ( + + + + {examples.join(', ')} + + + + ); + } + + renderSources(entity: Entity): React.ReactNode { + if (!entity.source) { + return null; + } + + if (Array.isArray(entity.source)) { + return this.renderSourceArray(entity.source); + } + + return this.renderSourceObject(entity.source); + } + + navigateToPath: (event: React.MouseEvent) => void = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + + const path = event.currentTarget.pathname; + this.props.navigateToPath(path); + }; + + openTeamComments: () => void = () => { + const teamCommentsTab = this.props.commentTabRef.current; + const index = teamCommentsTab._reactInternalFiber.index; + this.props.setCommentTabIndex(index); + this.props.setContactPerson(this.props.entity.project.contact.name); + }; + + render(): React.ReactNode { + const { + entity, + isReadOnlyEditor, + locale, + pluralForm, + terms, + user, + teamComments, + } = this.props; + const { popupTerms } = this.state; + const contactPerson = entity.project.contact; + const showContextIssueButton = user.isAuthenticated && contactPerson; + + return ( +
    + {!showContextIssueButton ? null : ( + + )} + + + {popupTerms.length > 0 && ( + + )} + {this.renderPinnedComments(teamComments)} + {this.renderComment(entity)} + {this.renderGroupComment(entity)} + + + {this.renderContext(entity)} + {this.renderSources(entity)} + + + + {entity.project.name} + + + + {entity.path} + + + +
    + ); + } } diff --git a/translate/src/modules/entitydetails/components/OriginalStringProxy.tsx b/translate/src/modules/entitydetails/components/OriginalStringProxy.tsx index f3639f749..4f090dda0 100644 --- a/translate/src/modules/entitydetails/components/OriginalStringProxy.tsx +++ b/translate/src/modules/entitydetails/components/OriginalStringProxy.tsx @@ -9,13 +9,13 @@ import type { Locale } from '~/core/locale'; import type { TermState } from '~/core/term'; type Props = { - readonly entity: Entity; - readonly locale: Locale; - readonly pluralForm: number; - readonly terms: TermState; - readonly handleClickOnPlaceable: ( - event: React.MouseEvent, - ) => void; + readonly entity: Entity; + readonly locale: Locale; + readonly pluralForm: number; + readonly terms: TermState; + readonly handleClickOnPlaceable: ( + event: React.MouseEvent, + ) => void; }; /** @@ -25,25 +25,25 @@ type Props = { * component. For everything else, return a generic OriginalString component. */ export default function OriginalStringProxy( - props: Props, + props: Props, ): React.ReactElement { - if (props.entity.format === 'ftl') { - return ( - - ); - } - + if (props.entity.format === 'ftl') { return ( - + ); + } + + return ( + + ); } diff --git a/translate/src/modules/entitydetails/components/Property.tsx b/translate/src/modules/entitydetails/components/Property.tsx index 530666741..b2942e937 100644 --- a/translate/src/modules/entitydetails/components/Property.tsx +++ b/translate/src/modules/entitydetails/components/Property.tsx @@ -1,23 +1,23 @@ import * as React from 'react'; type Props = { - readonly title: string; - readonly className: string; - readonly children: React.ReactNode; + readonly title: string; + readonly className: string; + readonly children: React.ReactNode; }; /** * Component to dislay a property of an entity in the Metadata component. */ export default function Property(props: Props): React.ReactElement<'div'> { - const { children, className, title } = props; + const { children, className, title } = props; - return ( -
    - {title} - {/* Extra space between elements prevents cross + return ( +
    + {title} + {/* Extra space between elements prevents cross element selection on double click (bug 1228873) */}{' '} - {children} -
    - ); + {children} +
    + ); } diff --git a/translate/src/modules/entitydetails/components/Screenshots.css b/translate/src/modules/entitydetails/components/Screenshots.css index d358c0cc7..d39b61cb4 100644 --- a/translate/src/modules/entitydetails/components/Screenshots.css +++ b/translate/src/modules/entitydetails/components/Screenshots.css @@ -1,16 +1,16 @@ .screenshots { - float: right; - text-align: right; - width: 150px; + float: right; + text-align: right; + width: 150px; } .screenshots img { - border: 1px solid #5e6475; - cursor: zoom-in; - max-height: 100px; - max-width: 150px; + border: 1px solid #5e6475; + cursor: zoom-in; + max-height: 100px; + max-width: 150px; } .screenshots img:hover { - opacity: 0.5; + opacity: 0.5; } diff --git a/translate/src/modules/entitydetails/components/Screenshots.test.js b/translate/src/modules/entitydetails/components/Screenshots.test.js index 489a36fc0..044d1711b 100644 --- a/translate/src/modules/entitydetails/components/Screenshots.test.js +++ b/translate/src/modules/entitydetails/components/Screenshots.test.js @@ -5,20 +5,18 @@ import { createReduxStore, mountComponentWithStore } from '~/test/store'; import sinon from 'sinon'; describe('', () => { - it('shows a Lightbox on image click', () => { - const store = createReduxStore(); - const stub = sinon.stub(store, 'dispatch'); - const source = 'That is an image URL: http://link.to/image.png'; - const wrapper = mountComponentWithStore(Screenshots, store, { - locale: 'kg', - source, - }); - wrapper.find('img').simulate('click'); - expect( - stub.calledOnceWith( - lightbox.actions.open('http://link.to/image.png'), - ), - ).toBeTruthy(); - store.dispatch.restore(); + it('shows a Lightbox on image click', () => { + const store = createReduxStore(); + const stub = sinon.stub(store, 'dispatch'); + const source = 'That is an image URL: http://link.to/image.png'; + const wrapper = mountComponentWithStore(Screenshots, store, { + locale: 'kg', + source, }); + wrapper.find('img').simulate('click'); + expect( + stub.calledOnceWith(lightbox.actions.open('http://link.to/image.png')), + ).toBeTruthy(); + store.dispatch.restore(); + }); }); diff --git a/translate/src/modules/entitydetails/components/Screenshots.tsx b/translate/src/modules/entitydetails/components/Screenshots.tsx index c8498cc33..4458d2903 100644 --- a/translate/src/modules/entitydetails/components/Screenshots.tsx +++ b/translate/src/modules/entitydetails/components/Screenshots.tsx @@ -6,8 +6,8 @@ import { actions } from '~/core/lightbox'; import { useAppDispatch } from '~/hooks'; type Props = { - locale: string; - source: string; + locale: string; + source: string; }; /** @@ -17,25 +17,25 @@ type Props = { * source string and then shows a miniature of those images. */ export default function Screenshots(props: Props) { - const { locale, source } = props; - const dispatch = useAppDispatch(); + const { locale, source } = props; + const dispatch = useAppDispatch(); - const images = getImageURLs(source, locale); + const images = getImageURLs(source, locale); - if (images.length === 0) { - return null; - } + if (images.length === 0) { + return null; + } - return ( -
    - {images.map((urlWithLocale) => ( - dispatch(actions.open(urlWithLocale))} - /> - ))} -
    - ); + return ( +
    + {images.map((urlWithLocale) => ( + dispatch(actions.open(urlWithLocale))} + /> + ))} +
    + ); } diff --git a/translate/src/modules/entitydetails/components/TermsPopup.css b/translate/src/modules/entitydetails/components/TermsPopup.css index 2cf6addfd..89f3cd449 100644 --- a/translate/src/modules/entitydetails/components/TermsPopup.css +++ b/translate/src/modules/entitydetails/components/TermsPopup.css @@ -1,16 +1,16 @@ .terms-popup { - background-color: #272a2f; - border: 1px solid #333941; - font-style: normal; - max-width: 500px; - position: absolute; - z-index: 20; + background-color: #272a2f; + border: 1px solid #333941; + font-style: normal; + max-width: 500px; + position: absolute; + z-index: 20; } .terms-popup .terms-list .term:not(.cannot-copy):hover { - background-color: #3f4752; + background-color: #3f4752; } .terms-popup li.term:last-child { - border-bottom: none; + border-bottom: none; } diff --git a/translate/src/modules/entitydetails/components/TermsPopup.tsx b/translate/src/modules/entitydetails/components/TermsPopup.tsx index 82a2d5640..f9dba0653 100644 --- a/translate/src/modules/entitydetails/components/TermsPopup.tsx +++ b/translate/src/modules/entitydetails/components/TermsPopup.tsx @@ -8,30 +8,30 @@ import { useOnDiscard } from '~/core/utils'; import type { TermType } from '~/core/api'; type Props = { - readonly isReadOnlyEditor: boolean; - readonly locale: string; - readonly terms: Array; - readonly addTextToEditorTranslation: (text: string) => void; - readonly hide: () => void; - readonly navigateToPath: (path: string) => void; + readonly isReadOnlyEditor: boolean; + readonly locale: string; + readonly terms: Array; + readonly addTextToEditorTranslation: (text: string) => void; + readonly hide: () => void; + readonly navigateToPath: (path: string) => void; }; /** * Shows a popup with a list of all terms belonging to the highlighted one. */ export default function TermsPopup(props: Props): React.ReactElement<'div'> { - const ref = React.useRef(null); - useOnDiscard(ref, props.hide); + const ref = React.useRef(null); + useOnDiscard(ref, props.hide); - return ( -
    - -
    - ); + return ( +
    + +
    + ); } diff --git a/translate/src/modules/fluenteditor/components/FluentEditor.css b/translate/src/modules/fluenteditor/components/FluentEditor.css index 120343035..bf42f3eb2 100644 --- a/translate/src/modules/fluenteditor/components/FluentEditor.css +++ b/translate/src/modules/fluenteditor/components/FluentEditor.css @@ -1,17 +1,17 @@ .editor .ftl { - background: transparent; - border: 0; - color: #aaa; - float: left; - font-size: 18px; - font-weight: bold; - height: 40px; - line-height: 0; - padding: 10px 5px 10px 0; - text-transform: uppercase; + background: transparent; + border: 0; + color: #aaa; + float: left; + font-size: 18px; + font-weight: bold; + height: 40px; + line-height: 0; + padding: 10px 5px 10px 0; + text-transform: uppercase; } .editor .ftl.active, .editor .ftl:hover { - color: #7bc876; + color: #7bc876; } diff --git a/translate/src/modules/fluenteditor/components/FluentEditor.test.js b/translate/src/modules/fluenteditor/components/FluentEditor.test.js index 664331d30..aed295376 100644 --- a/translate/src/modules/fluenteditor/components/FluentEditor.test.js +++ b/translate/src/modules/fluenteditor/components/FluentEditor.test.js @@ -3,9 +3,9 @@ import * as entities from '~/core/entities'; import * as navigation from '~/core/navigation'; import { - createDefaultUser, - createReduxStore, - mountComponentWithStore, + createDefaultUser, + createReduxStore, + mountComponentWithStore, } from '~/test/store'; import FluentEditor from './FluentEditor'; @@ -26,113 +26,113 @@ const RICH_MESSAGE_STRING = `my-message = `; const ENTITIES = [ - { - pk: 1, - original: 'my-message = Hello', - translation: [ - { - string: 'my-message = Salut', - }, - ], - }, - { - pk: 2, - original: 'my-message =\n .my-attr = Something guud', - translation: [ - { string: 'my-message =\n .my-attr = Quelque chose de bien' }, - ], - }, - { - pk: 3, - original: NESTED_SELECTORS_STRING, - translation: [{ string: NESTED_SELECTORS_STRING }], - }, - { - pk: 4, - original: 'my-message = Hello', - translation: [{ string: '' }], - }, - { - pk: 5, - original: RICH_MESSAGE_STRING, - translation: [], - }, + { + pk: 1, + original: 'my-message = Hello', + translation: [ + { + string: 'my-message = Salut', + }, + ], + }, + { + pk: 2, + original: 'my-message =\n .my-attr = Something guud', + translation: [ + { string: 'my-message =\n .my-attr = Quelque chose de bien' }, + ], + }, + { + pk: 3, + original: NESTED_SELECTORS_STRING, + translation: [{ string: NESTED_SELECTORS_STRING }], + }, + { + pk: 4, + original: 'my-message = Hello', + translation: [{ string: '' }], + }, + { + pk: 5, + original: RICH_MESSAGE_STRING, + translation: [], + }, ]; async function createComponent(entityPk = 1) { - const store = createReduxStore(); - createDefaultUser(store); + const store = createReduxStore(); + createDefaultUser(store); - const wrapper = mountComponentWithStore(FluentEditor, store); + const wrapper = mountComponentWithStore(FluentEditor, store); - store.dispatch(entities.actions.receive(ENTITIES)); - await store.dispatch( - navigation.actions.updateEntity(store.getState().router, entityPk), - ); - store.dispatch(editor.actions.reset()); + store.dispatch(entities.actions.receive(ENTITIES)); + await store.dispatch( + navigation.actions.updateEntity(store.getState().router, entityPk), + ); + store.dispatch(editor.actions.reset()); - wrapper.update(); + wrapper.update(); - return [wrapper, store]; + return [wrapper, store]; } describe('', () => { - it('renders the simple form when passing a simple string', async () => { - const [wrapper] = await createComponent(1); + it('renders the simple form when passing a simple string', async () => { + const [wrapper] = await createComponent(1); - expect(wrapper.find('SourceEditor').exists()).toBeFalsy(); - expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); - }); + expect(wrapper.find('SourceEditor').exists()).toBeFalsy(); + expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); + }); - it('renders the simple form when passing a simple string with one attribute', async () => { - const [wrapper] = await createComponent(2); + it('renders the simple form when passing a simple string with one attribute', async () => { + const [wrapper] = await createComponent(2); - expect(wrapper.find('SourceEditor').exists()).toBeFalsy(); - expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); - }); + expect(wrapper.find('SourceEditor').exists()).toBeFalsy(); + expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); + }); - it('renders the rich form when passing a supported rich message', async () => { - const [wrapper] = await createComponent(5); + it('renders the rich form when passing a supported rich message', async () => { + const [wrapper] = await createComponent(5); - expect(wrapper.find('RichEditor').exists()).toBeTruthy(); - }); + expect(wrapper.find('RichEditor').exists()).toBeTruthy(); + }); - it('renders the source form when passing a complex string', async () => { - const [wrapper] = await createComponent(3); + it('renders the source form when passing a complex string', async () => { + const [wrapper] = await createComponent(3); - expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); - expect(wrapper.find('SimpleEditor').exists()).toBeFalsy(); - }); + expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); + expect(wrapper.find('SimpleEditor').exists()).toBeFalsy(); + }); - it('converts translation when switching source mode', async () => { - const [wrapper] = await createComponent(1); - expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); + it('converts translation when switching source mode', async () => { + const [wrapper] = await createComponent(1); + expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); - // Force source mode. - wrapper.find('button.ftl').simulate('click'); + // Force source mode. + wrapper.find('button.ftl').simulate('click'); - expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); - expect(wrapper.find('textarea').text()).toEqual('my-message = Salut\n'); - }); + expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); + expect(wrapper.find('textarea').text()).toEqual('my-message = Salut\n'); + }); - it('sets empty initial translation in source mode when untranslated', async () => { - const [wrapper] = await createComponent(4); + it('sets empty initial translation in source mode when untranslated', async () => { + const [wrapper] = await createComponent(4); - // Force source mode. - wrapper.find('button.ftl').simulate('click'); + // Force source mode. + wrapper.find('button.ftl').simulate('click'); - expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); - expect(wrapper.find('textarea').text()).toEqual('my-message = '); - }); + expect(wrapper.find('SourceEditor').exists()).toBeTruthy(); + expect(wrapper.find('textarea').text()).toEqual('my-message = '); + }); - it('changes editor implementation when changing translation syntax', async () => { - const [wrapper, store] = await createComponent(1); - expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); + it('changes editor implementation when changing translation syntax', async () => { + const [wrapper, store] = await createComponent(1); + expect(wrapper.find('SimpleEditor').exists()).toBeTruthy(); - // Change translation to a rich string. - store.dispatch(editor.actions.update(RICH_MESSAGE_STRING, 'external')); - wrapper.update(); + // Change translation to a rich string. + store.dispatch(editor.actions.update(RICH_MESSAGE_STRING, 'external')); + wrapper.update(); - expect(wrapper.find('RichEditor').exists()).toBeTruthy(); - }); + expect(wrapper.find('RichEditor').exists()).toBeTruthy(); + }); }); diff --git a/translate/src/modules/fluenteditor/components/FluentEditor.tsx b/translate/src/modules/fluenteditor/components/FluentEditor.tsx index aeac73bf3..79583ee06 100644 --- a/translate/src/modules/fluenteditor/components/FluentEditor.tsx +++ b/translate/src/modules/fluenteditor/components/FluentEditor.tsx @@ -24,19 +24,19 @@ import RichEditor from './rich/RichEditor'; * - "complex" otherwise */ function getSyntaxType(source): SyntaxType { - if (source && typeof source !== 'string') { - return fluent.getSyntaxType(source); - } + if (source && typeof source !== 'string') { + return fluent.getSyntaxType(source); + } - const message = fluent.parser.parseEntry(source); + const message = fluent.parser.parseEntry(source); - // In case a simple message gets analyzed again. - if (message.type === 'Junk') { - return 'simple'; - } + // In case a simple message gets analyzed again. + if (message.type === 'Junk') { + return 'simple'; + } - // Figure out and set the syntax type. - return fluent.getSyntaxType(message); + // Figure out and set the syntax type. + return fluent.getSyntaxType(message); } /** @@ -47,72 +47,68 @@ function getSyntaxType(source): SyntaxType { * as that is dealt with by using the `useForceSource` hook. */ function useLoadTranslation(forceSource) { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const updateTranslation = editor.useUpdateTranslation(); - const changeSource = useAppSelector((state) => state.editor.changeSource); + const updateTranslation = editor.useUpdateTranslation(); + const changeSource = useAppSelector((state) => state.editor.changeSource); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const locale = useAppSelector((state) => state.locale); - const activeTranslationString = useAppSelector((state) => - plural.selectors.getTranslationStringForSelectedEntity(state), - ); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const locale = useAppSelector((state) => state.locale); + const activeTranslationString = useAppSelector((state) => + plural.selectors.getTranslationStringForSelectedEntity(state), + ); - // We do not want to perform any formatting when the user switches "force source", - // as that is handled in the `useForceSource` hook. We thus keep track of that variable's - // value and only update when it didn't change since the last render. - const prevForceSource = React.useRef(forceSource); + // We do not want to perform any formatting when the user switches "force source", + // as that is handled in the `useForceSource` hook. We thus keep track of that variable's + // value and only update when it didn't change since the last render. + const prevForceSource = React.useRef(forceSource); - React.useLayoutEffect(() => { - if ( - prevForceSource.current !== forceSource || - !entity || - // We want to run this only when the editor state has been reset. - changeSource !== 'reset' - ) { - prevForceSource.current = forceSource; - return; - } + React.useLayoutEffect(() => { + if ( + prevForceSource.current !== forceSource || + !entity || + // We want to run this only when the editor state has been reset. + changeSource !== 'reset' + ) { + prevForceSource.current = forceSource; + return; + } - const syntax = getSyntaxType( - activeTranslationString || entity.original, + const syntax = getSyntaxType(activeTranslationString || entity.original); + + let translationContent: string | Entry = ''; + if (syntax === 'complex') { + // Use the actual content that we get from the server: a Fluent message as a string. + translationContent = activeTranslationString; + } else if (syntax === 'simple') { + // Use a simplified preview of the Fluent message. + translationContent = fluent.getSimplePreview(activeTranslationString); + } else if (syntax === 'rich') { + // Use a Fluent Message object. + if (activeTranslationString) { + translationContent = fluent.flattenMessage( + fluent.parser.parseEntry(activeTranslationString), ); - - let translationContent: string | Entry = ''; - if (syntax === 'complex') { - // Use the actual content that we get from the server: a Fluent message as a string. - translationContent = activeTranslationString; - } else if (syntax === 'simple') { - // Use a simplified preview of the Fluent message. - translationContent = fluent.getSimplePreview( - activeTranslationString, - ); - } else if (syntax === 'rich') { - // Use a Fluent Message object. - if (activeTranslationString) { - translationContent = fluent.flattenMessage( - fluent.parser.parseEntry(activeTranslationString), - ); - } else { - translationContent = fluent.getEmptyMessage( - fluent.parser.parseEntry(entity.original), - locale, - ); - } - } - dispatch(editor.actions.setInitialTranslation(translationContent)); - updateTranslation(translationContent, 'initial'); - }, [ - changeSource, - forceSource, - entity, - activeTranslationString, - locale, - updateTranslation, - dispatch, - ]); + } else { + translationContent = fluent.getEmptyMessage( + fluent.parser.parseEntry(entity.original), + locale, + ); + } + } + dispatch(editor.actions.setInitialTranslation(translationContent)); + updateTranslation(translationContent, 'initial'); + }, [ + changeSource, + forceSource, + entity, + activeTranslationString, + locale, + updateTranslation, + dispatch, + ]); } /** @@ -123,50 +119,50 @@ function useLoadTranslation(forceSource) { * - a function to toggle the source mode. */ function useForceSource(): [boolean, () => void] { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const translation = useAppSelector((state) => state.editor.translation); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const activeTranslationString = useAppSelector((state) => - plural.selectors.getTranslationStringForSelectedEntity(state), - ); - const locale = useAppSelector((state) => state.locale); + const translation = useAppSelector((state) => state.editor.translation); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const activeTranslationString = useAppSelector((state) => + plural.selectors.getTranslationStringForSelectedEntity(state), + ); + const locale = useAppSelector((state) => state.locale); - // Force using the source editor. - const [forceSource, setForceSource] = React.useState(false); + // Force using the source editor. + const [forceSource, setForceSource] = React.useState(false); - // When the entity changes, reset the `forceSource` setting. Never show the source - // editor by default. - React.useEffect(() => { - setForceSource(false); - }, [entity]); + // When the entity changes, reset the `forceSource` setting. Never show the source + // editor by default. + React.useEffect(() => { + setForceSource(false); + }, [entity]); - // When a user wants to force (or unforce) the source editor, we need to convert - // the existing translation to a format appropriate for the next editor type. - function changeForceSource() { - const syntax = getSyntaxType(translation); + // When a user wants to force (or unforce) the source editor, we need to convert + // the existing translation to a format appropriate for the next editor type. + function changeForceSource() { + const syntax = getSyntaxType(translation); - if (syntax === 'complex') { - return; - } - const fromSyntax = forceSource ? 'complex' : syntax; - const toSyntax = forceSource ? syntax : 'complex'; - const [translationContent, initialContent] = fluent.convertSyntax( - fromSyntax, - toSyntax, - translation, - entity.original, - activeTranslationString, - locale, - ); - dispatch(editor.actions.setInitialTranslation(initialContent)); - dispatch(editor.actions.update(translationContent)); - setForceSource(!forceSource); + if (syntax === 'complex') { + return; } + const fromSyntax = forceSource ? 'complex' : syntax; + const toSyntax = forceSource ? syntax : 'complex'; + const [translationContent, initialContent] = fluent.convertSyntax( + fromSyntax, + toSyntax, + translation, + entity.original, + activeTranslationString, + locale, + ); + dispatch(editor.actions.setInitialTranslation(initialContent)); + dispatch(editor.actions.update(translationContent)); + setForceSource(!forceSource); + } - return [forceSource, changeForceSource]; + return [forceSource, changeForceSource]; } /** @@ -175,78 +171,78 @@ function useForceSource(): [boolean, () => void] { * Renders the most appropriate type of editor for the current translation. */ export default function FluentEditor(): null | React.ReactElement { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const translation = useAppSelector((state) => state.editor.translation); - const isReadOnlyEditor = useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), + const translation = useAppSelector((state) => state.editor.translation); + const isReadOnlyEditor = useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const activeTranslationString = useAppSelector((state) => + plural.selectors.getTranslationStringForSelectedEntity(state), + ); + const user = useAppSelector((state) => state.user); + + const [forceSource, changeForceSource] = useForceSource(); + useLoadTranslation(forceSource); + + if (!entity) { + return null; + } + + const syntax = getSyntaxType( + translation || activeTranslationString || entity.original, + ); + + // Choose which editor implementation to render. + let EditorImplementation = RichEditor; + if (forceSource || syntax === 'complex') { + EditorImplementation = SourceEditor; + } else if (syntax === 'simple') { + EditorImplementation = SimpleEditor; + } + + // When the syntax is complex, the editor is blocked in source mode, and it + // becomes impossible to switch to a different editor type. Thus we show a + // notification to the user if they try to use the "FTL" switch button. + function showUnsupportedMessage() { + dispatch( + notification.actions.add( + notification.messages.FTL_NOT_SUPPORTED_RICH_EDITOR, + ), ); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const activeTranslationString = useAppSelector((state) => - plural.selectors.getTranslationStringForSelectedEntity(state), - ); - const user = useAppSelector((state) => state.user); + } - const [forceSource, changeForceSource] = useForceSource(); - useLoadTranslation(forceSource); - - if (!entity) { - return null; + // Show a button to allow switching to the source editor. + let ftlSwitch = null; + // But only if the user is logged in and the string is not read-only. + if (user.isAuthenticated && !isReadOnlyEditor) { + if (syntax === 'complex') { + // TODO: To Localize + ftlSwitch = ( + + ); + } else { + // TODO: To Localize + ftlSwitch = ( + + ); } + } - const syntax = getSyntaxType( - translation || activeTranslationString || entity.original, - ); - - // Choose which editor implementation to render. - let EditorImplementation = RichEditor; - if (forceSource || syntax === 'complex') { - EditorImplementation = SourceEditor; - } else if (syntax === 'simple') { - EditorImplementation = SimpleEditor; - } - - // When the syntax is complex, the editor is blocked in source mode, and it - // becomes impossible to switch to a different editor type. Thus we show a - // notification to the user if they try to use the "FTL" switch button. - function showUnsupportedMessage() { - dispatch( - notification.actions.add( - notification.messages.FTL_NOT_SUPPORTED_RICH_EDITOR, - ), - ); - } - - // Show a button to allow switching to the source editor. - let ftlSwitch = null; - // But only if the user is logged in and the string is not read-only. - if (user.isAuthenticated && !isReadOnlyEditor) { - if (syntax === 'complex') { - // TODO: To Localize - ftlSwitch = ( - - ); - } else { - // TODO: To Localize - ftlSwitch = ( - - ); - } - } - - return ; + return ; } diff --git a/translate/src/modules/fluenteditor/components/rich/RichEditor.tsx b/translate/src/modules/fluenteditor/components/rich/RichEditor.tsx index a2222b2ba..9e6d6c189 100644 --- a/translate/src/modules/fluenteditor/components/rich/RichEditor.tsx +++ b/translate/src/modules/fluenteditor/components/rich/RichEditor.tsx @@ -8,7 +8,7 @@ import { fluent } from '~/core/utils'; import RichTranslationForm from './RichTranslationForm'; type Props = { - ftlSwitch: React.ReactNode; + ftlSwitch: React.ReactNode; }; /** @@ -20,74 +20,74 @@ type Props = { * overwritten, to handle the conversion from AST to string and back. */ export default function RichEditor(props: Props): React.ReactElement { - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const sendTranslation = editor.useSendTranslation(); - const updateTranslation = editor.useUpdateTranslation(); + const sendTranslation = editor.useSendTranslation(); + const updateTranslation = editor.useUpdateTranslation(); - const translation = useAppSelector((state) => state.editor.translation); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const changeSource = useAppSelector((state) => state.editor.changeSource); - const locale = useAppSelector((state) => state.locale); + const translation = useAppSelector((state) => state.editor.translation); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const changeSource = useAppSelector((state) => state.editor.changeSource); + const locale = useAppSelector((state) => state.locale); - /** - * Hook that makes sure the translation is a Fluent message. - */ - React.useLayoutEffect(() => { - if (typeof translation === 'string') { - const message = fluent.parser.parseEntry(translation); - // We need to check the syntax, because it can happen that a - // translation changes syntax, for example if loading a new one - // from history. In such cases, this RichEditor will render with - // the new translation, but must not re-format it. We thus make sure - // that the syntax of the translation is "rich" before we update it. - const syntax = fluent.getSyntaxType(message); - if (syntax !== 'rich') { - return; - } - updateTranslation(fluent.flattenMessage(message), changeSource); - } - }, [translation, changeSource, updateTranslation, dispatch]); - - function clearEditor() { - if (entity) { - updateTranslation( - fluent.getEmptyMessage( - fluent.parser.parseEntry(entity.original), - locale, - ), - ); - } + /** + * Hook that makes sure the translation is a Fluent message. + */ + React.useLayoutEffect(() => { + if (typeof translation === 'string') { + const message = fluent.parser.parseEntry(translation); + // We need to check the syntax, because it can happen that a + // translation changes syntax, for example if loading a new one + // from history. In such cases, this RichEditor will render with + // the new translation, but must not re-format it. We thus make sure + // that the syntax of the translation is "rich" before we update it. + const syntax = fluent.getSyntaxType(message); + if (syntax !== 'rich') { + return; + } + updateTranslation(fluent.flattenMessage(message), changeSource); } + }, [translation, changeSource, updateTranslation, dispatch]); - function copyOriginalIntoEditor() { - if (entity) { - const origMsg = fluent.parser.parseEntry(entity.original); - updateTranslation(fluent.flattenMessage(origMsg)); - } + function clearEditor() { + if (entity) { + updateTranslation( + fluent.getEmptyMessage( + fluent.parser.parseEntry(entity.original), + locale, + ), + ); } + } - function sendFluentTranslation(ignoreWarnings?: boolean) { - const fluentString = fluent.serializer.serializeEntry(translation); - sendTranslation(ignoreWarnings, fluentString); + function copyOriginalIntoEditor() { + if (entity) { + const origMsg = fluent.parser.parseEntry(entity.original); + updateTranslation(fluent.flattenMessage(origMsg)); } + } - return ( - <> - - - - ); + function sendFluentTranslation(ignoreWarnings?: boolean) { + const fluentString = fluent.serializer.serializeEntry(translation); + sendTranslation(ignoreWarnings, fluentString); + } + + return ( + <> + + + + ); } diff --git a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.css b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.css index ffdd5d828..a2b054bcb 100644 --- a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.css +++ b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.css @@ -1,82 +1,82 @@ .fluent-rich-translation-form { - height: 100%; - line-height: 22px; - padding: 10px; - overflow: auto; + height: 100%; + line-height: 22px; + padding: 10px; + overflow: auto; } .fluent-rich-translation-form table { - width: 100%; + width: 100%; } .fluent-rich-translation-form table tr > td:first { - width: calc(25% - 5px); + width: calc(25% - 5px); } .fluent-rich-translation-form table tr > td:last-child { - width: 75%; + width: 75%; } .fluent-rich-translation-form table tr > td > label { - background: transparent; - border: 1px solid #5e6475; - box-sizing: border-box; - color: #aaaaaa; - display: block; - font-weight: normal; - margin: 2px 5px 2px 0; - padding: 0 4px; + background: transparent; + border: 1px solid #5e6475; + box-sizing: border-box; + color: #aaaaaa; + display: block; + font-weight: normal; + margin: 2px 5px 2px 0; + padding: 0 4px; } .fluent-rich-translation-form table tr.indented > td > label { - margin-left: 10px; + margin-left: 10px; } .fluent-rich-translation-form table tr > td > label .divider { - color: #7bc876; - padding: 0 3px; + color: #7bc876; + padding: 0 3px; } .fluent-rich-translation-form table tr > td > label .stress { - color: #7bc876; + color: #7bc876; } .fluent-rich-translation-form table tr > td > textarea { - min-height: 0; - height: 24px; - margin: 2px 0; - padding: 0 4px; + min-height: 0; + height: 24px; + margin: 2px 0; + padding: 0 4px; } .fluent-rich-translation-form table tr > td > textarea[maxlength='1'] { - float: left; - margin-right: 3px; - resize: none; - text-align: center; - width: 24px; + float: left; + margin-right: 3px; + resize: none; + text-align: center; + width: 24px; } .fluent-rich-translation-form table tr > td > .accesskeys { - float: right; - margin: 2px 0; - width: calc(100% - 27px); + float: right; + margin: 2px 0; + width: calc(100% - 27px); } .fluent-rich-translation-form table tr > td > .accesskeys .key { - border: 1px solid #5e6475; - background: #4d5967; - color: #ebebeb; - cursor: pointer; - display: inline-block; - line-height: 22px; - margin-right: 3px; - padding: 0; - text-align: center; - vertical-align: top; - width: 24px; + border: 1px solid #5e6475; + background: #4d5967; + color: #ebebeb; + cursor: pointer; + display: inline-block; + line-height: 22px; + margin-right: 3px; + padding: 0; + text-align: center; + vertical-align: top; + width: 24px; } .fluent-rich-translation-form table tr > td > .accesskeys .key.active, .fluent-rich-translation-form table tr > td > .accesskeys .key:hover { - border-color: #7bc876; + border-color: #7bc876; } diff --git a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.test.js b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.test.js index be7d748fc..02b94531b 100644 --- a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.test.js +++ b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.test.js @@ -9,70 +9,70 @@ import { createReduxStore, mountComponentWithStore } from '~/test/store'; import RichTranslationForm from './RichTranslationForm'; const DEFAULT_LOCALE = { - direction: 'ltr', - code: 'kg', - script: 'Latin', - cldrPlurals: [1, 5], + direction: 'ltr', + code: 'kg', + script: 'Latin', + cldrPlurals: [1, 5], }; function createComponent(entityString, updateTranslation) { - if (!updateTranslation) { - updateTranslation = sinon.fake(); - } + if (!updateTranslation) { + updateTranslation = sinon.fake(); + } - const store = createReduxStore(); - store.dispatch(locale.actions.receive(DEFAULT_LOCALE)); + const store = createReduxStore(); + store.dispatch(locale.actions.receive(DEFAULT_LOCALE)); - const message = fluent.parser.parseEntry(entityString); - store.dispatch(editor.actions.update(message)); - store.dispatch(editor.actions.setInitialTranslation(message)); + const message = fluent.parser.parseEntry(entityString); + store.dispatch(editor.actions.update(message)); + store.dispatch(editor.actions.setInitialTranslation(message)); - const wrapper = mountComponentWithStore(RichTranslationForm, store, { - updateTranslation, - }); + const wrapper = mountComponentWithStore(RichTranslationForm, store, { + updateTranslation, + }); - wrapper.update(); + wrapper.update(); - return [wrapper, store]; + return [wrapper, store]; } describe('', () => { - it('renders textarea for a value and each attribute', () => { - const [wrapper] = createComponent( - `message = Value + it('renders textarea for a value and each attribute', () => { + const [wrapper] = createComponent( + `message = Value .attr-1 = And .attr-2 = Attributes `, - ); + ); - expect(wrapper.find('textarea')).toHaveLength(3); - expect(wrapper.find('textarea').at(0).html()).toContain('Value'); - expect(wrapper.find('textarea').at(1).html()).toContain('And'); - expect(wrapper.find('textarea').at(2).html()).toContain('Attributes'); - }); + expect(wrapper.find('textarea')).toHaveLength(3); + expect(wrapper.find('textarea').at(0).html()).toContain('Value'); + expect(wrapper.find('textarea').at(1).html()).toContain('And'); + expect(wrapper.find('textarea').at(2).html()).toContain('Attributes'); + }); - it('renders select expression properly', () => { - const [wrapper] = createComponent( - `my-entry = + it('renders select expression properly', () => { + const [wrapper] = createComponent( + `my-entry = { PLATFORM() -> [variant] Hello! *[another-variant] World! } `, - ); + ); - expect(wrapper.find('textarea')).toHaveLength(2); + expect(wrapper.find('textarea')).toHaveLength(2); - expect(wrapper.find('label').at(0).html()).toContain('variant'); - expect(wrapper.find('textarea').at(0).html()).toContain('Hello!'); + expect(wrapper.find('label').at(0).html()).toContain('variant'); + expect(wrapper.find('textarea').at(0).html()).toContain('Hello!'); - expect(wrapper.find('label').at(1).html()).toContain('another-variant'); - expect(wrapper.find('textarea').at(1).html()).toContain('World!'); - }); + expect(wrapper.find('label').at(1).html()).toContain('another-variant'); + expect(wrapper.find('textarea').at(1).html()).toContain('World!'); + }); - it('renders select expression in attributes properly', () => { - const [wrapper] = createComponent( - `my-entry = + it('renders select expression in attributes properly', () => { + const [wrapper] = createComponent( + `my-entry = .label = { PLATFORM() -> [macosx] Preferences @@ -84,139 +84,137 @@ describe('', () => { *[other] s } `, - ); + ); - expect(wrapper.find('textarea')).toHaveLength(4); + expect(wrapper.find('textarea')).toHaveLength(4); - expect(wrapper.find('label .attribute-label').at(0).html()).toContain( - 'label', - ); - expect(wrapper.find('label .label').at(0).html()).toContain('macosx'); - expect(wrapper.find('textarea').at(0).html()).toContain('Preferences'); + expect(wrapper.find('label .attribute-label').at(0).html()).toContain( + 'label', + ); + expect(wrapper.find('label .label').at(0).html()).toContain('macosx'); + expect(wrapper.find('textarea').at(0).html()).toContain('Preferences'); - expect(wrapper.find('label .attribute-label').at(1).html()).toContain( - 'label', - ); - expect(wrapper.find('label').at(1).html()).toContain('other'); - expect(wrapper.find('textarea').at(1).html()).toContain('Options'); + expect(wrapper.find('label .attribute-label').at(1).html()).toContain( + 'label', + ); + expect(wrapper.find('label').at(1).html()).toContain('other'); + expect(wrapper.find('textarea').at(1).html()).toContain('Options'); - expect(wrapper.find('label .attribute-label').at(2).html()).toContain( - 'accesskey', - ); - expect(wrapper.find('label').at(2).html()).toContain('macosx'); - expect(wrapper.find('textarea').at(2).html()).toContain('e'); + expect(wrapper.find('label .attribute-label').at(2).html()).toContain( + 'accesskey', + ); + expect(wrapper.find('label').at(2).html()).toContain('macosx'); + expect(wrapper.find('textarea').at(2).html()).toContain('e'); - expect(wrapper.find('label .attribute-label').at(3).html()).toContain( - 'accesskey', - ); - expect(wrapper.find('label').at(3).html()).toContain('other'); - expect(wrapper.find('textarea').at(3).html()).toContain('s'); - }); + expect(wrapper.find('label .attribute-label').at(3).html()).toContain( + 'accesskey', + ); + expect(wrapper.find('label').at(3).html()).toContain('other'); + expect(wrapper.find('textarea').at(3).html()).toContain('s'); + }); - it('renders plural string properly', () => { - const [wrapper] = createComponent( - `my-entry = + it('renders plural string properly', () => { + const [wrapper] = createComponent( + `my-entry = { $num -> [one] Hello! *[other] World! } `, - ); + ); - expect(wrapper.find('textarea')).toHaveLength(2); + expect(wrapper.find('textarea')).toHaveLength(2); - expect(wrapper.find('textarea').at(0).html()).toContain('Hello!'); + expect(wrapper.find('textarea').at(0).html()).toContain('Hello!'); - const varsSingular = wrapper - .find('#fluenteditor-RichTranslationForm--plural-example') - .at(0) - .prop('vars'); - expect(varsSingular.plural).toEqual('one'); - expect(varsSingular.example).toEqual(1); + const varsSingular = wrapper + .find('#fluenteditor-RichTranslationForm--plural-example') + .at(0) + .prop('vars'); + expect(varsSingular.plural).toEqual('one'); + expect(varsSingular.example).toEqual(1); - expect(wrapper.find('textarea').at(1).html()).toContain('World!'); + expect(wrapper.find('textarea').at(1).html()).toContain('World!'); - const varsPlural = wrapper - .find('#fluenteditor-RichTranslationForm--plural-example') - .at(1) - .prop('vars'); - expect(varsPlural.plural).toEqual('other'); - expect(varsPlural.example).toEqual(2); - }); + const varsPlural = wrapper + .find('#fluenteditor-RichTranslationForm--plural-example') + .at(1) + .prop('vars'); + expect(varsPlural.plural).toEqual('other'); + expect(varsPlural.example).toEqual(2); + }); - it('renders access keys properly', () => { - const [wrapper] = createComponent( - `title = Title + it('renders access keys properly', () => { + const [wrapper] = createComponent( + `title = Title .label = Candidates .accesskey = C `, - ); + ); - expect(wrapper.find('textarea')).toHaveLength(3); + expect(wrapper.find('textarea')).toHaveLength(3); - expect(wrapper.find('label').at(1).html()).toContain('label'); - expect(wrapper.find('textarea').at(1).prop('value')).toEqual( - 'Candidates', - ); + expect(wrapper.find('label').at(1).html()).toContain('label'); + expect(wrapper.find('textarea').at(1).prop('value')).toEqual('Candidates'); - expect(wrapper.find('label').at(2).html()).toContain('accesskey'); - expect(wrapper.find('textarea').at(2).prop('value')).toEqual('C'); - expect(wrapper.find('textarea').at(2).prop('maxLength')).toEqual(1); + expect(wrapper.find('label').at(2).html()).toContain('accesskey'); + expect(wrapper.find('textarea').at(2).prop('value')).toEqual('C'); + expect(wrapper.find('textarea').at(2).prop('maxLength')).toEqual(1); - expect(wrapper.find('.accesskeys')).toHaveLength(1); - expect(wrapper.find('.accesskeys button')).toHaveLength(8); - expect(wrapper.find('.accesskeys button').at(0).text()).toEqual('C'); - expect(wrapper.find('.accesskeys button').at(1).text()).toEqual('a'); - expect(wrapper.find('.accesskeys button').at(2).text()).toEqual('n'); - expect(wrapper.find('.accesskeys button').at(3).text()).toEqual('d'); - expect(wrapper.find('.accesskeys button').at(4).text()).toEqual('i'); - expect(wrapper.find('.accesskeys button').at(5).text()).toEqual('t'); - expect(wrapper.find('.accesskeys button').at(6).text()).toEqual('e'); - expect(wrapper.find('.accesskeys button').at(7).text()).toEqual('s'); - }); + expect(wrapper.find('.accesskeys')).toHaveLength(1); + expect(wrapper.find('.accesskeys button')).toHaveLength(8); + expect(wrapper.find('.accesskeys button').at(0).text()).toEqual('C'); + expect(wrapper.find('.accesskeys button').at(1).text()).toEqual('a'); + expect(wrapper.find('.accesskeys button').at(2).text()).toEqual('n'); + expect(wrapper.find('.accesskeys button').at(3).text()).toEqual('d'); + expect(wrapper.find('.accesskeys button').at(4).text()).toEqual('i'); + expect(wrapper.find('.accesskeys button').at(5).text()).toEqual('t'); + expect(wrapper.find('.accesskeys button').at(6).text()).toEqual('e'); + expect(wrapper.find('.accesskeys button').at(7).text()).toEqual('s'); + }); - it('does not render the access key UI if no candidates can be generated', () => { - const [wrapper] = createComponent( - `title = + it('does not render the access key UI if no candidates can be generated', () => { + const [wrapper] = createComponent( + `title = .label = { reference } .accesskey = C `, - ); + ); - expect(wrapper.find('.accesskeys')).toHaveLength(0); - }); + expect(wrapper.find('.accesskeys')).toHaveLength(0); + }); - it('does not render the access key UI if access key is longer than 1 character', () => { - const [wrapper] = createComponent( - `title = + it('does not render the access key UI if access key is longer than 1 character', () => { + const [wrapper] = createComponent( + `title = .label = Candidates .accesskey = { reference } `, - ); + ); - expect(wrapper.find('.accesskeys')).toHaveLength(0); - }); + expect(wrapper.find('.accesskeys')).toHaveLength(0); + }); - it('updates the translation when selectionReplacementContent is passed', async () => { - const updateMock = sinon.spy(); - const [wrapper, store] = createComponent( - `title = Value + it('updates the translation when selectionReplacementContent is passed', async () => { + const updateMock = sinon.spy(); + const [wrapper, store] = createComponent( + `title = Value .label = Something `, - updateMock, - ); + updateMock, + ); - await store.dispatch(editor.actions.updateSelection('Add')); + await store.dispatch(editor.actions.updateSelection('Add')); - // Force a re-render -- see https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/update.html - wrapper.setProps({}); + // Force a re-render -- see https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/update.html + wrapper.setProps({}); - expect(updateMock.called).toBeTruthy(); - const replaceContent = fluent.parser.parseEntry( - `title = AddValue + expect(updateMock.called).toBeTruthy(); + const replaceContent = fluent.parser.parseEntry( + `title = AddValue .label = Something `, - ); - expect(updateMock.calledWith(replaceContent)).toBeTruthy(); - }); + ); + expect(updateMock.calledWith(replaceContent)).toBeTruthy(); + }); }); diff --git a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.tsx b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.tsx index c76e6cf87..32c899bca 100644 --- a/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.tsx +++ b/translate/src/modules/fluenteditor/components/rich/RichTranslationForm.tsx @@ -14,10 +14,10 @@ import { fluent } from '~/core/utils'; import type { Translation } from '~/core/editor'; import type { Attribute, Entry } from '@fluent/syntax'; import type { - Pattern, - PatternElement, - Variant, - SyntaxNode, + Pattern, + PatternElement, + Variant, + SyntaxNode, } from '@fluent/syntax'; type MessagePath = Array; @@ -28,471 +28,469 @@ type Child = SyntaxNode | Array; * value. */ function getUpdatedTranslation( - translation: Entry, - value: string, - path: MessagePath, + translation: Entry, + value: string, + path: MessagePath, ) { - // Never mutate state. - const source = translation.clone(); - // Safeguard against all the entry types, keep cloning, though. - if (source.type !== 'Message' && source.type !== 'Term') { - return source; - } - let dest: Child = source; - // Walk the path until the next to last item. - for (let i = 0, ln = path.length; i < ln - 1; i++) { - dest = dest[path[i]]; - } - // Assign the new value to the last element in the path, so that - // it is actually assigned to the message object reference and - // to the extracted value. - dest[path[path.length - 1]] = value; - + // Never mutate state. + const source = translation.clone(); + // Safeguard against all the entry types, keep cloning, though. + if (source.type !== 'Message' && source.type !== 'Term') { return source; + } + let dest: Child = source; + // Walk the path until the next to last item. + for (let i = 0, ln = path.length; i < ln - 1; i++) { + dest = dest[path[i]]; + } + // Assign the new value to the last element in the path, so that + // it is actually assigned to the message object reference and + // to the extracted value. + dest[path[path.length - 1]] = value; + + return source; } type Props = { - clearEditor: () => void; - copyOriginalIntoEditor: () => void; - sendTranslation: (ignoreWarnings?: boolean) => void; - updateTranslation: (translation: Translation) => void; + clearEditor: () => void; + copyOriginalIntoEditor: () => void; + sendTranslation: (ignoreWarnings?: boolean) => void; + updateTranslation: (translation: Translation) => void; }; /** * Render a Rich editor for Fluent string editing. */ export default function RichTranslationForm( - props: Props, + props: Props, ): null | React.ReactElement<'div'> { - const { - clearEditor, - copyOriginalIntoEditor, - sendTranslation, - updateTranslation, - } = props; + const { + clearEditor, + copyOriginalIntoEditor, + sendTranslation, + updateTranslation, + } = props; - const accessKeyElementIdRef = React.useRef(null); - const focusedElementIdRef = React.useRef(null); + const accessKeyElementIdRef = React.useRef(null); + const focusedElementIdRef = React.useRef(null); - const dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); - const message = useAppSelector((state) => state.editor.translation); - const changeSource = useAppSelector((state) => state.editor.changeSource); - const localeState = useAppSelector((state) => state.locale); - const isReadOnlyEditor = useAppSelector((state) => - entities.selectors.isReadOnlyEditor(state), + const message = useAppSelector((state) => state.editor.translation); + const changeSource = useAppSelector((state) => state.editor.changeSource); + const localeState = useAppSelector((state) => state.locale); + const isReadOnlyEditor = useAppSelector((state) => + entities.selectors.isReadOnlyEditor(state), + ); + const searchInputFocused = useAppSelector( + (state) => state.search.searchInputFocused, + ); + const entity = useAppSelector((state) => + entities.selectors.getSelectedEntity(state), + ); + const unsavedChangesExist = useAppSelector( + (state) => state.unsavedchanges.exist, + ); + + const tableBodyRef: { current: any } = React.useRef(); + + const handleShortcutsFn = editor.useHandleShortcuts(); + + const updateRichTranslation = React.useCallback( + (value: string, path: MessagePath) => { + if (typeof message === 'string') { + return; + } + + const source = getUpdatedTranslation(message, value, path); + updateTranslation(source); + }, + [message, updateTranslation], + ); + + const getFirstInput = React.useCallback(() => { + if (tableBodyRef.current) { + return tableBodyRef.current.querySelector('textarea:first-of-type'); + } + return null; + }, []); + + const getFocusedElement = React.useCallback(() => { + if (focusedElementIdRef.current && tableBodyRef.current) { + return tableBodyRef.current.querySelector( + 'textarea#' + focusedElementIdRef.current, + ); + } + return null; + }, []); + + // Replace selected content on external actions (for example, when a user clicks + // on a placeable). + editor.useReplaceSelectionContent((content: string, source: string) => { + // If there is no explicitely focused element, find the first input. + const target = getFocusedElement() || getFirstInput(); + + if (!target) { + return; + } + + if (source === 'machinery') { + // Replace the whole content instead of just what was selected. + target.select(); + } + + const newSelectionPos = target.selectionStart + content.length; + + // Update content in the textarea. + target.setRangeText(content); + + // Put the cursor right after the newly inserted content. + target.setSelectionRange(newSelectionPos, newSelectionPos); + + // Update the state to show the new content in the Editor. + updateRichTranslation(target.value, target.id.split('-')); + }); + + // Reset the currently focused element when the entity changes or when + // the translation changes from an external source. + React.useEffect(() => { + if (changeSource === 'internal') { + return; + } + focusedElementIdRef.current = null; + }, [entity, changeSource]); + + // Reset checks when content of the editor changes and some changes have been made. + React.useEffect(() => { + if (unsavedChangesExist) { + dispatch(editor.actions.resetFailedChecks()); + } + }, [message, dispatch, unsavedChangesExist]); + + // When content of the translation changes, update unsaved changes. + editor.useUpdateUnsavedChanges(false); + + // Put focus on input. + React.useEffect(() => { + const input = getFocusedElement() || getFirstInput(); + + if (!input || searchInputFocused) { + return; + } + + input.focus(); + + const putCursorToStart = changeSource !== 'internal'; + if (putCursorToStart) { + input.setSelectionRange(0, 0); + } + }, [ + message, + changeSource, + searchInputFocused, + getFocusedElement, + getFirstInput, + ]); + + if ( + typeof message === 'string' || + !(message instanceof Message || message instanceof Term) + ) { + // This is a transitional state, and this editor is not able to handle a + // non-Fluent message translation. Thus we abort this render and wait for the + // next one. + return null; + } + + function handleShortcuts(event: React.KeyboardEvent) { + handleShortcutsFn( + event, + sendTranslation, + clearEditor, + copyOriginalIntoEditor, ); - const searchInputFocused = useAppSelector( - (state) => state.search.searchInputFocused, + } + + function createHandleChange(path: MessagePath) { + return (event: React.SyntheticEvent) => { + updateRichTranslation(event.currentTarget.value, path); + }; + } + + function handleAccessKeyClick(event: React.MouseEvent) { + if (isReadOnlyEditor) { + return null; + } + + updateRichTranslation( + event.currentTarget.textContent, + accessKeyElementIdRef.current.split('-'), ); - const entity = useAppSelector((state) => - entities.selectors.getSelectedEntity(state), - ); - const unsavedChangesExist = useAppSelector( - (state) => state.unsavedchanges.exist, + } + + function setFocusedInput(event: React.FocusEvent) { + focusedElementIdRef.current = event.currentTarget.id; + } + + function renderTextarea( + value: string, + path: MessagePath, + maxlength?: number | null | undefined, + ) { + return ( +