From bd811865eb2f6f91bd2c9bcac4b6aee0f9bca81b Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 7 Nov 2025 10:22:38 +0000 Subject: [PATCH 01/73] Create HelpDropdown --- .../Sidebar/SidebarNav/HelpDropdown.tsx | 82 +++++++++++++++++++ .../Sidebar/SidebarNav/SecondaryItems.tsx | 10 ++- 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx new file mode 100644 index 00000000000..ac8daa28f7a --- /dev/null +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -0,0 +1,82 @@ +import type { FC } from 'react'; +import { memo } from 'react'; + +import { useTranslation } from 'next-i18next'; +import { + UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, +} from 'reactstrap'; + +import { useGrowiVersion } from '~/stores-universal/context'; + +import { SkeletonItem } from './SkeletonItem'; + + +export const HelpDropdown: FC = memo(() => { + const { t } = useTranslation('commons'); + const { data: growiVersion } = useGrowiVersion(); + + if (growiVersion == null) { + return ; + } + + return ( + + + help + + + + + {t('Help')} + + + + + + + GROWI Docs + open_in_new + + + + + + GROWI.cloud {t('Help')} + open_in_new + + + + + + + + GROWI + {' '} + {growiVersion} + + + + + ); +}); diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx index 7ffd1c292de..300c4a35963 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx @@ -4,7 +4,7 @@ import { memo } from 'react'; import dynamic from 'next/dynamic'; import Link from 'next/link'; -import { useIsGuestUser, useGrowiCloudUri, useIsAdmin } from '~/stores-universal/context'; +import { useIsGuestUser, useIsAdmin } from '~/stores-universal/context'; import { SkeletonItem } from './SkeletonItem'; @@ -16,6 +16,11 @@ const PersonalDropdown = dynamic(() => import('./PersonalDropdown').then(mod => loading: () => , }); +const HelpDropdown = dynamic(() => import('./HelpDropdown').then(mod => mod.HelpDropdown), { + ssr: false, + loading: () => , +}); + type SecondaryItemProps = { label: string, @@ -42,12 +47,11 @@ const SecondaryItem: FC = (props: SecondaryItemProps) => { export const SecondaryItems: FC = memo(() => { const { data: isAdmin } = useIsAdmin(); - const { data: growiCloudUri } = useGrowiCloudUri(); const { data: isGuestUser } = useIsGuestUser(); return (
- + {isAdmin && } {!isGuestUser && } From fabb2a9f97a7b783d6ac6defa4530e3fea95a9c9 Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 7 Nov 2025 11:02:11 +0000 Subject: [PATCH 02/73] Add shortcut menu --- .../SidebarNav/HelpDropdown.module.scss | 9 +++++++ .../Sidebar/SidebarNav/HelpDropdown.tsx | 25 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss new file mode 100644 index 00000000000..94de9024aa9 --- /dev/null +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss @@ -0,0 +1,9 @@ +@use '@growi/core-styles/scss/helpers/modifier-keys'; + +.help-dropdown :global { + @include modifier-keys.modifier-key; +} + +.help-dropdown-menu :global { + @include modifier-keys.modifier-key; +} diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index ac8daa28f7a..9660f22af1b 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -7,20 +7,29 @@ import { } from 'reactstrap'; import { useGrowiVersion } from '~/stores-universal/context'; +import { useShortcutsModal } from '~/stores/modal'; import { SkeletonItem } from './SkeletonItem'; +import styles from './HelpDropdown.module.scss'; + export const HelpDropdown: FC = memo(() => { const { t } = useTranslation('commons'); const { data: growiVersion } = useGrowiVersion(); + const { open: openShortcutsModal } = useShortcutsModal(); if (growiVersion == null) { return ; } + // add classes to cmd-key by OS + const platform = window.navigator.platform.toLowerCase(); + const isMac = (platform.indexOf('mac') > -1); + const os = isMac ? 'mac' : 'win'; + return ( - + { container="body" data-testid="help-dropdown-menu" style={{ minWidth: '280px', fontSize: '14px' }} + className={styles['help-dropdown-menu']} > {t('Help')} @@ -69,6 +79,19 @@ export const HelpDropdown: FC = memo(() => { + openShortcutsModal()} + > + + keyboard + {t('Shortcuts')} +  -/ + + + + + GROWI From a1a4f3aa628f560d552d4640caa4947b62c8ff14 Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 28 Nov 2025 08:18:51 +0000 Subject: [PATCH 03/73] Apply jotai --- .../client/components/Sidebar/SidebarNav/HelpDropdown.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index 9660f22af1b..8d011c0ddc9 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -6,8 +6,8 @@ import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, } from 'reactstrap'; -import { useGrowiVersion } from '~/stores-universal/context'; -import { useShortcutsModal } from '~/stores/modal'; +import { useGrowiVersion } from '~/states/global'; +import { useShortcutsModalActions } from '~/states/ui/modal/shortcuts'; import { SkeletonItem } from './SkeletonItem'; @@ -16,8 +16,8 @@ import styles from './HelpDropdown.module.scss'; export const HelpDropdown: FC = memo(() => { const { t } = useTranslation('commons'); - const { data: growiVersion } = useGrowiVersion(); - const { open: openShortcutsModal } = useShortcutsModal(); + const growiVersion = useGrowiVersion(); + const { open: openShortcutsModal } = useShortcutsModalActions(); if (growiVersion == null) { return ; From 571764714b1b5fc3e53aec97c22814698b100226 Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 28 Nov 2025 08:24:35 +0000 Subject: [PATCH 04/73] Delete SystemVersion layout --- .../components/SystemVersion.module.scss | 10 ----- .../src/client/components/SystemVersion.tsx | 41 ------------------- .../app/src/components/Layout/AdminLayout.tsx | 5 --- .../app/src/components/Layout/BasicLayout.tsx | 5 --- .../src/components/Layout/ShareLinkLayout.tsx | 5 --- 5 files changed, 66 deletions(-) delete mode 100644 apps/app/src/client/components/SystemVersion.module.scss delete mode 100644 apps/app/src/client/components/SystemVersion.tsx diff --git a/apps/app/src/client/components/SystemVersion.module.scss b/apps/app/src/client/components/SystemVersion.module.scss deleted file mode 100644 index b845e5c5f4e..00000000000 --- a/apps/app/src/client/components/SystemVersion.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -@use '@growi/core-styles/scss/helpers/modifier-keys'; - -.system-version :global { - position: fixed; - right: 0.5em; - bottom: 0; - opacity: 0.6; - - @include modifier-keys.modifier-key; -} diff --git a/apps/app/src/client/components/SystemVersion.tsx b/apps/app/src/client/components/SystemVersion.tsx deleted file mode 100644 index 5110d1b98d4..00000000000 --- a/apps/app/src/client/components/SystemVersion.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { type JSX } from 'react'; - -import { useGrowiVersion } from '~/states/global'; -import { useShortcutsModalActions } from '~/states/ui/modal/shortcuts'; - -import styles from './SystemVersion.module.scss'; - - -type Props = { - showShortcutsButton?: boolean, -} - -const SystemVersion = (props: Props): JSX.Element => { - const { showShortcutsButton } = props; - - const { open: openShortcutsModal } = useShortcutsModalActions(); - - const growiVersion = useGrowiVersion(); - // add classes to cmd-key by OS - const platform = window.navigator.platform.toLowerCase(); - const isMac = (platform.indexOf('mac') > -1); - const os = isMac ? 'mac' : 'win'; - - return ( - <> -
- - GROWI {growiVersion} - - { showShortcutsButton && ( - - ) } -
- - - ); -}; - -export default SystemVersion; diff --git a/apps/app/src/components/Layout/AdminLayout.tsx b/apps/app/src/components/Layout/AdminLayout.tsx index 4b882d9d6db..97ab767c22f 100644 --- a/apps/app/src/components/Layout/AdminLayout.tsx +++ b/apps/app/src/components/Layout/AdminLayout.tsx @@ -20,11 +20,6 @@ const PageCreateModal = dynamic( () => import('~/client/components/PageCreateModal'), { ssr: false }, ); -const SystemVersion = dynamic( - // biome-ignore lint/style/noRestrictedImports: no-problem dynamic import - () => import('~/client/components/SystemVersion'), - { ssr: false }, -); const HotkeysManager = dynamic( // biome-ignore lint/style/noRestrictedImports: no-problem dynamic import () => import('~/client/components/Hotkeys/HotkeysManager'), diff --git a/apps/app/src/components/Layout/BasicLayout.tsx b/apps/app/src/components/Layout/BasicLayout.tsx index 2a12d541eab..ec7aa33ea40 100644 --- a/apps/app/src/components/Layout/BasicLayout.tsx +++ b/apps/app/src/components/Layout/BasicLayout.tsx @@ -43,10 +43,6 @@ const GrowiNavbarBottom = dynamic( ), { ssr: false }, ); -const SystemVersion = dynamic( - () => import('~/client/components/SystemVersion'), - { ssr: false }, -); // Page modals const PageCreateModal = dynamic( () => import('~/client/components/PageCreateModal'), @@ -100,7 +96,6 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => { - ); }; diff --git a/apps/app/src/components/Layout/ShareLinkLayout.tsx b/apps/app/src/components/Layout/ShareLinkLayout.tsx index 02bef313065..8067c257f0c 100644 --- a/apps/app/src/components/Layout/ShareLinkLayout.tsx +++ b/apps/app/src/components/Layout/ShareLinkLayout.tsx @@ -18,10 +18,6 @@ const GrowiNavbarBottom = dynamic( ), { ssr: false }, ); -const SystemVersion = dynamic( - () => import('~/client/components/SystemVersion'), - { ssr: false }, -); // biome-ignore-end lint/style/noRestrictedImports: no-problem dynamic import type Props = { @@ -37,7 +33,6 @@ export const ShareLinkLayout = ({ children }: Props): JSX.Element => { - ); }; From 0da23a5d1fdb30efa4372749ecb2c01790442b24 Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 28 Nov 2025 09:14:34 +0000 Subject: [PATCH 05/73] Adjust style --- .../SidebarNav/HelpDropdown.module.scss | 5 ++++ .../Sidebar/SidebarNav/HelpDropdown.tsx | 28 ++++++++----------- .../app/src/components/Layout/AdminLayout.tsx | 1 - 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss index 94de9024aa9..540796f8746 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.module.scss @@ -7,3 +7,8 @@ .help-dropdown-menu :global { @include modifier-keys.modifier-key; } + +.help-dropdown-menu :global { + --bs-dropdown-font-size: 14px; + min-width: 240px; +} diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index 8d011c0ddc9..047f8f2c42c 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -40,15 +40,11 @@ export const HelpDropdown: FC = memo(() => { - + {t('Help')} - - - { data-testid="growi-docs-link" > - GROWI Docs - open_in_new + GROWI Docs + open_in_new @@ -72,29 +68,27 @@ export const HelpDropdown: FC = memo(() => { data-testid="growi-cloud-help-link" > - GROWI.cloud {t('Help')} - open_in_new + GROWI.cloud {t('Help')} + open_in_new - - - openShortcutsModal()} > - keyboard {t('Shortcuts')} -  -/ + +  + / + - - - GROWI + + + GROWI Version {' '} {growiVersion} diff --git a/apps/app/src/components/Layout/AdminLayout.tsx b/apps/app/src/components/Layout/AdminLayout.tsx index 97ab767c22f..744f736b38d 100644 --- a/apps/app/src/components/Layout/AdminLayout.tsx +++ b/apps/app/src/components/Layout/AdminLayout.tsx @@ -55,7 +55,6 @@ const AdminLayout = ({ children, componentTitle }: Props): JSX.Element => {
- From 55bc9e6db6528759be9237d96bde6ec92285e1db Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 28 Nov 2025 09:31:10 +0000 Subject: [PATCH 06/73] Remove growi.org link --- .../client/components/Sidebar/SidebarNav/HelpDropdown.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index 047f8f2c42c..ecee11680d0 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -87,9 +87,8 @@ export const HelpDropdown: FC = memo(() => { - - GROWI Version - {' '} + + GROWI Version {growiVersion} From 07d739e5ece0ebfc7d25d269851830228e5c3d49 Mon Sep 17 00:00:00 2001 From: satof3 Date: Fri, 28 Nov 2025 10:28:42 +0000 Subject: [PATCH 07/73] Translation --- apps/app/public/static/locales/en_US/translation.json | 5 +++++ apps/app/public/static/locales/fr_FR/translation.json | 5 +++++ apps/app/public/static/locales/ja_JP/translation.json | 5 +++++ apps/app/public/static/locales/ko_KR/translation.json | 5 +++++ apps/app/public/static/locales/zh_CN/translation.json | 5 +++++ .../client/components/Sidebar/SidebarNav/HelpDropdown.tsx | 8 ++++---- 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/app/public/static/locales/en_US/translation.json b/apps/app/public/static/locales/en_US/translation.json index 64dcaa9db8c..a64c7561b33 100644 --- a/apps/app/public/static/locales/en_US/translation.json +++ b/apps/app/public/static/locales/en_US/translation.json @@ -786,6 +786,11 @@ "updatedAt": "Last update date" } }, + "help_dropdown": { + "show_shortcuts": "Show shortcuts", + "growi_cloud_help": "GROWI.cloud Help", + "growi_version": "GROWI version" + }, "private_legacy_pages": { "title": "Private Legacy Pages", "bulk_operation": "Bulk operation", diff --git a/apps/app/public/static/locales/fr_FR/translation.json b/apps/app/public/static/locales/fr_FR/translation.json index 3a6f2792703..6313bcebda0 100644 --- a/apps/app/public/static/locales/fr_FR/translation.json +++ b/apps/app/public/static/locales/fr_FR/translation.json @@ -780,6 +780,11 @@ "updatedAt": "Dernière modification" } }, + "help_dropdown": { + "show_shortcuts": "Afficher les raccourcis", + "growi_cloud_help": "Aide GROWI.cloud", + "growi_version": "Version GROWI" + }, "private_legacy_pages": { "title": "Anciennes pages privées", "bulk_operation": "Opération de masse", diff --git a/apps/app/public/static/locales/ja_JP/translation.json b/apps/app/public/static/locales/ja_JP/translation.json index 3afef6febc5..d214e7c9eae 100644 --- a/apps/app/public/static/locales/ja_JP/translation.json +++ b/apps/app/public/static/locales/ja_JP/translation.json @@ -819,6 +819,11 @@ "updatedAt": "更新日時" } }, + "help_dropdown": { + "show_shortcuts": "ショートカットを表示", + "growi_cloud_help": "GROWI.cloud ヘルプ", + "growi_version": "GROWI バージョン" + }, "private_legacy_pages": { "title": "旧形式のプライベートページ", "bulk_operation": "一括操作", diff --git a/apps/app/public/static/locales/ko_KR/translation.json b/apps/app/public/static/locales/ko_KR/translation.json index 6951b1f0b41..619aac42b99 100644 --- a/apps/app/public/static/locales/ko_KR/translation.json +++ b/apps/app/public/static/locales/ko_KR/translation.json @@ -746,6 +746,11 @@ "updatedAt": "마지막 업데이트일" } }, + "help_dropdown": { + "show_shortcuts": "단축키 표시", + "growi_cloud_help": "GROWI.cloud 도움말", + "growi_version": "GROWI 버전" + }, "private_legacy_pages": { "title": "비공개 레거시 페이지", "bulk_operation": "대량 작업", diff --git a/apps/app/public/static/locales/zh_CN/translation.json b/apps/app/public/static/locales/zh_CN/translation.json index fa2d3ef01ca..6e481c19623 100644 --- a/apps/app/public/static/locales/zh_CN/translation.json +++ b/apps/app/public/static/locales/zh_CN/translation.json @@ -791,6 +791,11 @@ "updatedAt": "按更新日期排序" } }, + "help_dropdown": { + "show_shortcuts": "显示快捷键", + "growi_cloud_help": "GROWI.cloud 帮助", + "growi_version": "GROWI 版本" + }, "private_legacy_pages": { "title": "私人遗留页面", "bulk_operation": "批量操作", diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index ecee11680d0..69d95ffb62a 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -15,7 +15,7 @@ import styles from './HelpDropdown.module.scss'; export const HelpDropdown: FC = memo(() => { - const { t } = useTranslation('commons'); + const { t } = useTranslation(); const growiVersion = useGrowiVersion(); const { open: openShortcutsModal } = useShortcutsModalActions(); @@ -68,7 +68,7 @@ export const HelpDropdown: FC = memo(() => { data-testid="growi-cloud-help-link" > - GROWI.cloud {t('Help')} + {t('help_dropdown.growi_cloud_help')} open_in_new @@ -77,7 +77,7 @@ export const HelpDropdown: FC = memo(() => { onClick={() => openShortcutsModal()} > - {t('Shortcuts')} + {t('help_dropdown.show_shortcuts')}  + / @@ -88,7 +88,7 @@ export const HelpDropdown: FC = memo(() => { - GROWI Version + {t('help_dropdown.growi_version')} {growiVersion} From d4a35835e2975c2db6d1a96f462b24c61ca54bc4 Mon Sep 17 00:00:00 2001 From: satof3 Date: Tue, 2 Dec 2025 07:52:30 +0000 Subject: [PATCH 08/73] Apply FB --- .../Sidebar/SidebarNav/HelpDropdown.tsx | 29 +++++++------------ .../Sidebar/SidebarNav/SecondaryItems.tsx | 8 +---- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx index 69d95ffb62a..f67dd27ece2 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/HelpDropdown.tsx @@ -6,7 +6,7 @@ import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, } from 'reactstrap'; -import { useGrowiVersion } from '~/states/global'; +import { useGrowiVersion, useGrowiCloudUri } from '~/states/global'; import { useShortcutsModalActions } from '~/states/ui/modal/shortcuts'; import { SkeletonItem } from './SkeletonItem'; @@ -18,6 +18,7 @@ export const HelpDropdown: FC = memo(() => { const { t } = useTranslation(); const growiVersion = useGrowiVersion(); const { open: openShortcutsModal } = useShortcutsModalActions(); + const growiCloudUri = useGrowiCloudUri(); if (growiVersion == null) { return ; @@ -28,6 +29,11 @@ export const HelpDropdown: FC = memo(() => { const isMac = (platform.indexOf('mac') > -1); const os = isMac ? 'mac' : 'win'; + // Cloud users see Help, others see Docs + const isCloudUser = growiCloudUri != null; + const helpUrl = isCloudUser ? 'https://growi.cloud/help/' : 'https://docs.growi.org'; + const helpLabel = isCloudUser ? t('Help') : 'GROWI Docs'; + return ( { - GROWI Docs - open_in_new + {helpLabel} + external_link - - - {t('help_dropdown.growi_cloud_help')} - open_in_new - - openShortcutsModal()} diff --git a/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx b/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx index 33297654e34..9f61a0640d5 100644 --- a/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx +++ b/apps/app/src/client/components/Sidebar/SidebarNav/SecondaryItems.tsx @@ -5,8 +5,8 @@ import dynamic from 'next/dynamic'; import Link from 'next/link'; import { useIsAdmin, useIsGuestUser } from '~/states/context'; -import { useGrowiCloudUri } from '~/states/global'; +import { HelpDropdown } from './HelpDropdown'; import { SkeletonItem } from './SkeletonItem'; import styles from './SecondaryItems.module.scss'; @@ -17,11 +17,6 @@ const PersonalDropdown = dynamic(() => import('./PersonalDropdown').then(mod => loading: () => , }); -const HelpDropdown = dynamic(() => import('./HelpDropdown').then(mod => mod.HelpDropdown), { - ssr: false, - loading: () => , -}); - type SecondaryItemProps = { label: string, @@ -48,7 +43,6 @@ const SecondaryItem: FC = (props: SecondaryItemProps) => { export const SecondaryItems: FC = memo(() => { const isAdmin = useIsAdmin(); - const growiCloudUri = useGrowiCloudUri(); const isGuestUser = useIsGuestUser(); return ( From 958225166d8b81a8850cc0a5f7645f14c6043465 Mon Sep 17 00:00:00 2001 From: Futa Arai Date: Sun, 21 Dec 2025 13:43:22 +0900 Subject: [PATCH 09/73] configure biome for PageEditor Hotkeys Navbar PageHeader --- apps/app/.eslintrc.js | 4 + .../components/Hotkeys/HotkeysDetector.jsx | 49 +- .../components/Hotkeys/HotkeysManager.jsx | 7 +- .../Hotkeys/Subscribers/CreatePage.jsx | 2 - .../Hotkeys/Subscribers/EditPage.tsx | 25 +- .../Subscribers/FocusToGlobalSearch.jsx | 7 +- .../Subscribers/ShowShortcutsModal.tsx | 12 +- .../Hotkeys/Subscribers/ShowStaffCredit.jsx | 17 +- .../Subscribers/SwitchToMirrorMode.jsx | 2 - .../Navbar/GrowiContextualSubNavigation.tsx | 333 ++++++++----- .../components/Navbar/GrowiNavbarBottom.tsx | 44 +- .../Navbar/PageEditorModeManager.tsx | 61 ++- .../components/PageEditor/Cheatsheet.tsx | 38 +- .../ConflictDiffModal/ConflictDiffModal.tsx | 203 +++++--- .../PageEditor/ConflictDiffModal/dynamic.tsx | 5 +- .../DrawioModal/DrawioCommunicationHelper.ts | 44 +- .../PageEditor/DrawioModal/DrawioModal.tsx | 116 +++-- .../PageEditor/DrawioModal/dynamic.tsx | 4 +- .../EditorNavbar/EditingUserList.tsx | 25 +- .../PageEditor/EditorNavbar/EditorNavbar.tsx | 18 +- .../EditorAssistantToggleButton.tsx | 6 +- .../EditorNavbarBottom/EditorNavbarBottom.tsx | 22 +- .../EditorNavbarBottom/GrantSelector.tsx | 278 +++++++---- .../EditorNavbarBottom/OptionsSelector.tsx | 458 ++++++++++-------- .../EditorNavbarBottom/SavePageControls.tsx | 369 ++++++++------ .../components/PageEditor/GridEditModal.jsx | 90 ++-- .../components/PageEditor/GridEditorUtil.js | 40 +- .../HandsontableModal/HandsontableModal.tsx | 298 ++++++++---- .../PageEditor/HandsontableModal/dynamic.tsx | 6 +- .../LinkEditModal/LinkEditModal.tsx | 266 ++++++---- .../PageEditor/LinkEditModal/dynamic.tsx | 4 +- .../components/PageEditor/MarkdownListUtil.js | 17 +- .../MarkdownTableDataImportForm.tsx | 59 ++- .../components/PageEditor/PageEditor.tsx | 371 ++++++++------ .../PageEditor/PageEditorReadOnly.tsx | 97 ++-- .../client/components/PageEditor/Preview.tsx | 42 +- .../PageEditor/ScrollSyncHelper.tsx | 134 +++-- .../PageEditor/SimpleCheatsheet.jsx | 32 +- .../client/components/PageEditor/conflict.tsx | 195 +++++--- .../markdown-drawio-util-for-editor.ts | 17 +- .../markdown-table-util-for-editor.ts | 71 +-- .../PageEditor/page-path-rename-utils.ts | 89 ++-- .../components/PageHeader/PageHeader.tsx | 8 +- .../components/PageHeader/PagePathHeader.tsx | 111 +++-- .../PageHeader/PageTitleHeader.spec.tsx | 17 +- .../components/PageHeader/PageTitleHeader.tsx | 87 ++-- biome.json | 4 - 47 files changed, 2577 insertions(+), 1627 deletions(-) diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js index 275aa360004..0c361a72f3c 100644 --- a/apps/app/.eslintrc.js +++ b/apps/app/.eslintrc.js @@ -41,6 +41,10 @@ module.exports = { 'src/client/components/*.jsx', 'src/client/components/*.ts', 'src/client/components/*.js', + 'src/client/components/PageEditor/**', + 'src/client/components/Hotkeys/**', + 'src/client/components/Navbar/**', + 'src/client/components/PageHeader/**', 'src/services/**', 'src/states/**', 'src/stores/**', diff --git a/apps/app/src/client/components/Hotkeys/HotkeysDetector.jsx b/apps/app/src/client/components/Hotkeys/HotkeysDetector.jsx index fd35754df1e..6e9b3e65460 100644 --- a/apps/app/src/client/components/Hotkeys/HotkeysDetector.jsx +++ b/apps/app/src/client/components/Hotkeys/HotkeysDetector.jsx @@ -1,22 +1,17 @@ -import React, { useMemo, useCallback } from 'react'; - +import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import { GlobalHotKeys } from 'react-hotkeys'; import HotkeyStroke from '~/client/models/HotkeyStroke'; const HotkeysDetector = (props) => { - const { keySet, strokeSet, onDetected } = props; // memorize HotkeyStroke instances - const hotkeyStrokes = useMemo( - () => { - const strokes = Array.from(strokeSet); - return strokes.map(stroke => new HotkeyStroke(stroke)); - }, - [strokeSet], - ); + const hotkeyStrokes = useMemo(() => { + const strokes = Array.from(strokeSet); + return strokes.map((stroke) => new HotkeyStroke(stroke)); + }, [strokeSet]); /** * return key expression string includes modifier @@ -43,19 +38,22 @@ const HotkeysDetector = (props) => { /** * evaluate the key user pressed and trigger onDetected */ - const checkHandler = useCallback((event) => { - const eventKey = getKeyExpression(event); - - hotkeyStrokes.forEach((hotkeyStroke) => { - // if any stroke is completed - if (hotkeyStroke.evaluate(eventKey)) { - // cancel the key event - event.preventDefault(); - // invoke detected handler - onDetected(hotkeyStroke.stroke); - } - }); - }, [hotkeyStrokes, getKeyExpression, onDetected]); + const checkHandler = useCallback( + (event) => { + const eventKey = getKeyExpression(event); + + hotkeyStrokes.forEach((hotkeyStroke) => { + // if any stroke is completed + if (hotkeyStroke.evaluate(eventKey)) { + // cancel the key event + event.preventDefault(); + // invoke detected handler + onDetected(hotkeyStroke.stroke); + } + }); + }, + [hotkeyStrokes, getKeyExpression, onDetected], + ); // memorize keyMap for GlobalHotKeys const keyMap = useMemo(() => { @@ -67,10 +65,7 @@ const HotkeysDetector = (props) => { return { check: checkHandler }; }, [checkHandler]); - return ( - - ); - + return ; }; HotkeysDetector.propTypes = { diff --git a/apps/app/src/client/components/Hotkeys/HotkeysManager.jsx b/apps/app/src/client/components/Hotkeys/HotkeysManager.jsx index e3609ab3ec4..b1944a0530b 100644 --- a/apps/app/src/client/components/Hotkeys/HotkeysManager.jsx +++ b/apps/app/src/client/components/Hotkeys/HotkeysManager.jsx @@ -27,7 +27,7 @@ SUPPORTED_COMPONENTS.forEach((comp) => { strokes.forEach((stroke) => { // register key - stroke.forEach(key => KEY_SET.add(key)); + stroke.forEach((key) => KEY_SET.add(key)); // register stroke STROKE_SET.add(stroke); // register component @@ -58,7 +58,7 @@ const HotkeysManager = (props) => { const key = (Math.random() * 1000).toString(); const components = STROKE_TO_COMPONENT_MAP[strokeDetermined.toString()]; - const newViews = components.map(Component => ( + const newViews = components.map((Component) => ( )); setView(view.concat(newViews).flat()); @@ -67,14 +67,13 @@ const HotkeysManager = (props) => { return ( <> onDetected(stroke)} + onDetected={(stroke) => onDetected(stroke)} keySet={KEY_SET} strokeSet={STROKE_SET} /> {view} ); - }; export default HotkeysManager; diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/CreatePage.jsx b/apps/app/src/client/components/Hotkeys/Subscribers/CreatePage.jsx index 3a82f61cef6..ab850b063fe 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/CreatePage.jsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/CreatePage.jsx @@ -1,12 +1,10 @@ import React, { useEffect } from 'react'; - import PropTypes from 'prop-types'; import { useCurrentPagePath } from '~/states/page'; import { usePageCreateModalActions } from '~/states/ui/modal/page-create'; const CreatePage = React.memo((props) => { - const { open: openCreateModal } = usePageCreateModalActions(); const currentPath = useCurrentPagePath(); diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/EditPage.tsx b/apps/app/src/client/components/Hotkeys/Subscribers/EditPage.tsx index 8c62117363c..1f3980f2ebf 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/EditPage.tsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/EditPage.tsx @@ -1,22 +1,21 @@ import { useCallback, useEffect, useRef } from 'react'; - import { useTranslation } from 'next-i18next'; import { useStartEditing } from '~/client/services/use-start-editing'; import { toastError } from '~/client/util/toastr'; import { useCurrentPathname } from '~/states/global'; -import { useIsEditable, useCurrentPagePath } from '~/states/page'; +import { useCurrentPagePath, useIsEditable } from '~/states/page'; type Props = { - onDeleteRender: () => void, -} + onDeleteRender: () => void; +}; /** * Custom hook for edit page logic */ const useEditPage = ( - onCompleted: () => void, - onError?: (path: string) => void, + onCompleted: () => void, + onError?: (path: string) => void, ): void => { const isEditable = useIsEditable(); const startEditing = useStartEditing(); @@ -26,7 +25,7 @@ const useEditPage = ( const isExecutedRef = useRef(false); useEffect(() => { - (async() => { + (async () => { // Prevent multiple executions if (isExecutedRef.current) return; isExecutedRef.current = true; @@ -42,8 +41,7 @@ const useEditPage = ( try { await startEditing(path); - } - catch (err) { + } catch (err) { onError?.(path); } @@ -58,9 +56,12 @@ const useEditPage = ( const EditPage = (props: Props): null => { const { t } = useTranslation('commons'); - const handleError = useCallback((path: string) => { - toastError(t('toaster.create_failed', { target: path })); - }, [t]); + const handleError = useCallback( + (path: string) => { + toastError(t('toaster.create_failed', { target: path })); + }, + [t], + ); useEditPage(props.onDeleteRender, handleError); diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx b/apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx index d87bc6bdcee..d7e967f1870 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx @@ -1,9 +1,11 @@ import { useEffect } from 'react'; -import { useSearchModalStatus, useSearchModalActions } from '~/features/search/client/states/modal/search'; +import { + useSearchModalActions, + useSearchModalStatus, +} from '~/features/search/client/states/modal/search'; import { useIsEditable } from '~/states/page'; - const FocusToGlobalSearch = (props) => { const isEditable = useIsEditable(); const searchModalData = useSearchModalStatus(); @@ -20,7 +22,6 @@ const FocusToGlobalSearch = (props) => { // remove this props.onDeleteRender(); } - }, [isEditable, openSearchModal, props, searchModalData.isOpened]); return null; diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/ShowShortcutsModal.tsx b/apps/app/src/client/components/Hotkeys/Subscribers/ShowShortcutsModal.tsx index ef03e602911..05bc6d3f8a3 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/ShowShortcutsModal.tsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/ShowShortcutsModal.tsx @@ -1,12 +1,14 @@ -import React, { useEffect, type JSX } from 'react'; +import React, { type JSX, useEffect } from 'react'; -import { useShortcutsModalStatus, useShortcutsModalActions } from '~/states/ui/modal/shortcuts'; +import { + useShortcutsModalActions, + useShortcutsModalStatus, +} from '~/states/ui/modal/shortcuts'; type Props = { - onDeleteRender: () => void, -} + onDeleteRender: () => void; +}; const ShowShortcutsModal = (props: Props): JSX.Element => { - const status = useShortcutsModalStatus(); const { open } = useShortcutsModalActions(); diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/ShowStaffCredit.jsx b/apps/app/src/client/components/Hotkeys/Subscribers/ShowStaffCredit.jsx index 1a1656379c8..e7fe93b8b5a 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/ShowStaffCredit.jsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/ShowStaffCredit.jsx @@ -3,9 +3,7 @@ import PropTypes from 'prop-types'; import StaffCredit from '../../StaffCredit/StaffCredit'; const ShowStaffCredit = (props) => { - return props.onDeleteRender(this)} />; - }; ShowStaffCredit.propTypes = { @@ -13,7 +11,20 @@ ShowStaffCredit.propTypes = { }; ShowStaffCredit.getHotkeyStrokes = () => { - return [['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']]; + return [ + [ + 'ArrowUp', + 'ArrowUp', + 'ArrowDown', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'ArrowLeft', + 'ArrowRight', + 'b', + 'a', + ], + ]; }; export default ShowStaffCredit; diff --git a/apps/app/src/client/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx b/apps/app/src/client/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx index b41455f4071..c075040649d 100644 --- a/apps/app/src/client/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx +++ b/apps/app/src/client/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx @@ -1,9 +1,7 @@ import React, { useEffect } from 'react'; - import PropTypes from 'prop-types'; const SwitchToMirrorMode = (props) => { - // setup effect useEffect(() => { document.body.classList.add('mirror'); diff --git a/apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx b/apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx index aa5e6d2784a..521ffd649a9 100644 --- a/apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx +++ b/apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx @@ -1,32 +1,42 @@ -import React, { - useState, useCallback, useMemo, type JSX, -} from 'react'; - - -import { isPopulated } from '@growi/core'; +import React, { type JSX, useCallback, useMemo, useState } from 'react'; +import dynamic from 'next/dynamic'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; import type { + IPageInfoForEntity, IPagePopulatedToShowRevision, - IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity, + IPageToRenameWithMeta, + IPageWithMeta, } from '@growi/core'; +import { isPopulated } from '@growi/core'; import { pagePathUtils } from '@growi/core/dist/utils'; import { GlobalCodeMirrorEditorKey } from '@growi/editor'; import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'next-i18next'; -import dynamic from 'next/dynamic'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; import Sticky from 'react-stickynode'; -import { DropdownItem, UncontrolledTooltip, Tooltip } from 'reactstrap'; +import { DropdownItem, Tooltip, UncontrolledTooltip } from 'reactstrap'; -import { exportAsMarkdown, updateContentWidth, syncLatestRevisionBody } from '~/client/services/page-operation'; +import { + exportAsMarkdown, + syncLatestRevisionBody, + updateContentWidth, +} from '~/client/services/page-operation'; import { usePrintMode } from '~/client/services/use-print-mode'; -import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr'; +import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr'; import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar'; import { usePageBulkExportSelectModalActions } from '~/features/page-bulk-export/client/states/modal'; -import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui'; +import type { + OnDeletedFunction, + OnDuplicatedFunction, + OnRenamedFunction, +} from '~/interfaces/ui'; import { useShouldExpandContent } from '~/services/layout/use-should-expand-content'; -import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context'; +import { + useIsGuestUser, + useIsReadOnlyUser, + useIsSharedUser, +} from '~/states/context'; import { useCurrentPathname, useCurrentUser } from '~/states/global'; import { useCurrentPageId, useFetchCurrentPage } from '~/states/page'; import { useShareLinkId } from '~/states/page/hooks'; @@ -38,18 +48,22 @@ import { } from '~/states/server-configurations'; import { useDeviceLargerThanMd } from '~/states/ui/device'; import { useEditorMode } from '~/states/ui/editor'; -import { PageAccessoriesModalContents, usePageAccessoriesModalActions } from '~/states/ui/modal/page-accessories'; +import { + PageAccessoriesModalContents, + usePageAccessoriesModalActions, +} from '~/states/ui/modal/page-accessories'; import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete'; -import { usePageDuplicateModalActions, type IPageForPageDuplicateModal } from '~/states/ui/modal/page-duplicate'; +import { + type IPageForPageDuplicateModal, + usePageDuplicateModalActions, +} from '~/states/ui/modal/page-duplicate'; import { usePresentationModalActions } from '~/states/ui/modal/page-presentation'; import { usePageRenameModalActions } from '~/states/ui/modal/page-rename'; import { - useIsAbleToShowPageManagement, useIsAbleToChangeEditorMode, + useIsAbleToShowPageManagement, } from '~/states/ui/page-abilities'; -import { - useSWRxPageInfo, -} from '~/stores/page'; +import { useSWRxPageInfo } from '~/stores/page'; import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing'; import { CreateTemplateModalLazyLoaded } from '../CreateTemplateModal'; @@ -59,29 +73,35 @@ import { Skeleton } from '../Skeleton'; import styles from './GrowiContextualSubNavigation.module.scss'; import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss'; - const PageEditorModeManager = dynamic( - () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager), - { ssr: false, loading: () => }, + () => + import('./PageEditorModeManager').then((mod) => mod.PageEditorModeManager), + { + ssr: false, + loading: () => ( + + ), + }, ); const PageControls = dynamic( - () => import('../PageControls').then(mod => mod.PageControls), + () => import('../PageControls').then((mod) => mod.PageControls), { ssr: false, loading: () => <> }, ); - type PageOperationMenuItemsProps = { - pageId: string, - revisionId: string, - isLinkSharingDisabled?: boolean, -} + pageId: string; + revisionId: string; + isLinkSharingDisabled?: boolean; +}; -const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element => { +const PageOperationMenuItems = ( + props: PageOperationMenuItemsProps, +): JSX.Element => { const { t } = useTranslation(); - const { - pageId, revisionId, isLinkSharingDisabled, - } = props; + const { pageId, revisionId, isLinkSharingDisabled } = props; const isGuestUser = useIsGuestUser(); const isReadOnlyUser = useIsReadOnlyUser(); @@ -91,9 +111,12 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element const { open: openPresentationModal } = usePresentationModalActions(); const { open: openAccessoriesModal } = usePageAccessoriesModalActions(); - const { open: openPageBulkExportSelectModal } = usePageBulkExportSelectModalActions(); + const { open: openPageBulkExportSelectModal } = + usePageBulkExportSelectModalActions(); - const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN); + const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated( + GlobalCodeMirrorEditorKey.MAIN, + ); const [isBulkExportTooltipOpen, setIsBulkExportTooltipOpen] = useState(false); @@ -117,8 +140,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element } toastSuccess(t('sync-latest-revision-body.success-toaster')); - } - catch { + } catch { toastError(t('sync-latest-revision-body.error-toaster')); } } @@ -130,7 +152,9 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element onClick={() => syncLatestRevisionBodyHandler()} className="grw-page-control-dropdown-item" > - sync + + sync + {t('sync-latest-revision-body.menuitem')} @@ -140,7 +164,9 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element data-testid="open-presentation-modal-btn" className="grw-page-control-dropdown-item" > - jamboard_kiosk + + jamboard_kiosk + {t('Presentation Mode')} @@ -149,7 +175,9 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element onClick={() => exportAsMarkdown(pageId, revisionId, 'md')} className="grw-page-control-dropdown-item" > - cloud_download + + cloud_download + {t('page_export.export_page_markdown')} @@ -162,7 +190,9 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element className="grw-page-control-dropdown-item" disabled={!isUploadEnabled ?? true} > - cloud_download + + cloud_download + {t('page_export.bulk_export')} @@ -185,32 +215,47 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element refs: PageAccessoriesModalControl */} openAccessoriesModal(PageAccessoriesModalContents.PageHistory)} + onClick={() => + openAccessoriesModal(PageAccessoriesModalContents.PageHistory) + } disabled={!!isGuestUser || !!isSharedUser} data-testid="open-page-accessories-modal-btn-with-history-tab" className="grw-page-control-dropdown-item" > - history + + history + {t('History')} openAccessoriesModal(PageAccessoriesModalContents.Attachment)} + onClick={() => + openAccessoriesModal(PageAccessoriesModalContents.Attachment) + } data-testid="open-page-accessories-modal-btn-with-attachment-data-tab" className="grw-page-control-dropdown-item" > - attachment + + attachment + {t('attachment_data')} {!isGuestUser && !isReadOnlyUser && !isSharedUser && ( - + openAccessoriesModal(PageAccessoriesModalContents.ShareLink)} + onClick={() => + openAccessoriesModal(PageAccessoriesModalContents.ShareLink) + } data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab" className="grw-page-control-dropdown-item" > - share + + share + {t('share_links.share_link_management')} @@ -220,10 +265,12 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element }; type CreateTemplateMenuItemsProps = { - onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void, -} + onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void; +}; -const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Element => { +const CreateTemplateMenuItems = ( + props: CreateTemplateMenuItemsProps, +): JSX.Element => { const { t } = useTranslation(); const { onClickTemplateMenuItem } = props; @@ -240,7 +287,9 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme className="grw-page-control-dropdown-item" data-testid="open-page-template-modal-btn" > - contract_edit + + contract_edit + {t('template.option_label.create/edit')} @@ -248,11 +297,12 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme }; type GrowiContextualSubNavigationProps = { - currentPage?: IPagePopulatedToShowRevision | null, + currentPage?: IPagePopulatedToShowRevision | null; }; -const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => { - +const GrowiContextualSubNavigation = ( + props: GrowiContextualSubNavigationProps, +): JSX.Element => { const { currentPage } = props; const { t } = useTranslation(); @@ -267,14 +317,17 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? ''); const revision = currentPage?.revision; - const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined; + const revisionId = + revision != null && isPopulated(revision) ? revision._id : undefined; const { editorMode } = useEditorMode(); const pageId = useCurrentPageId(true); const currentUser = useCurrentUser(); const isGuestUser = useIsGuestUser(); const isReadOnlyUser = useIsReadOnlyUser(); - const isLocalAccountRegistrationEnabled = useAtomValue(isLocalAccountRegistrationEnabledAtom); + const isLocalAccountRegistrationEnabled = useAtomValue( + isLocalAccountRegistrationEnabledAtom, + ); const isLinkSharingDisabled = useAtomValue(disableLinkSharingAtom); const isSharedUser = useIsSharedUser(); @@ -293,67 +346,87 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): const path = currentPage?.path ?? currentPathname; - const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false); - - const duplicateItemClickedHandler = useCallback(async (page: IPageForPageDuplicateModal) => { - const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => { - router.push(toPath); - }; - openDuplicateModal(page, { onDuplicated: duplicatedHandler }); - }, [openDuplicateModal, router]); - - const renameItemClickedHandler = useCallback(async (page: IPageToRenameWithMeta) => { - const renamedHandler: OnRenamedFunction = () => { - fetchCurrentPage({ force: true }); - mutatePageInfo(); - mutatePageTree(); - mutateRecentlyUpdated(); - }; - openRenameModal(page, { onRenamed: renamedHandler }); - }, [fetchCurrentPage, mutatePageInfo, openRenameModal]); - - const deleteItemClickedHandler = useCallback((pageWithMeta: IPageWithMeta) => { - const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => { - if (typeof pathOrPathsToDelete !== 'string') { - return; - } + const [isPageTemplateModalShown, setIsPageTempleteModalShown] = + useState(false); + + const duplicateItemClickedHandler = useCallback( + async (page: IPageForPageDuplicateModal) => { + const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => { + router.push(toPath); + }; + openDuplicateModal(page, { onDuplicated: duplicatedHandler }); + }, + [openDuplicateModal, router], + ); - const path = pathOrPathsToDelete; + const renameItemClickedHandler = useCallback( + async (page: IPageToRenameWithMeta) => { + const renamedHandler: OnRenamedFunction = () => { + fetchCurrentPage({ force: true }); + mutatePageInfo(); + mutatePageTree(); + mutateRecentlyUpdated(); + }; + openRenameModal(page, { onRenamed: renamedHandler }); + }, + [fetchCurrentPage, mutatePageInfo, openRenameModal], + ); - if (isCompletely) { - // redirect to NotFound Page - router.push(path); - } - else if (currentPathname != null) { - router.push(currentPathname); - } + const deleteItemClickedHandler = useCallback( + (pageWithMeta: IPageWithMeta) => { + const deletedHandler: OnDeletedFunction = ( + pathOrPathsToDelete, + isRecursively, + isCompletely, + ) => { + if (typeof pathOrPathsToDelete !== 'string') { + return; + } - fetchCurrentPage({ force: true }); - mutatePageInfo(); - mutatePageTree(); - mutateRecentlyUpdated(); - }; - openDeleteModal([pageWithMeta], { onDeleted: deletedHandler }); - }, [currentPathname, fetchCurrentPage, openDeleteModal, router, mutatePageInfo]); - - const switchContentWidthHandler = useCallback(async (pageId: string, value: boolean) => { - if (!isSharedPage) { - await updateContentWidth(pageId, value); - fetchCurrentPage({ force: true }); - } - }, [isSharedPage, fetchCurrentPage]); + const path = pathOrPathsToDelete; + + if (isCompletely) { + // redirect to NotFound Page + router.push(path); + } else if (currentPathname != null) { + router.push(currentPathname); + } + + fetchCurrentPage({ force: true }); + mutatePageInfo(); + mutatePageTree(); + mutateRecentlyUpdated(); + }; + openDeleteModal([pageWithMeta], { onDeleted: deletedHandler }); + }, + [ + currentPathname, + fetchCurrentPage, + openDeleteModal, + router, + mutatePageInfo, + ], + ); + + const switchContentWidthHandler = useCallback( + async (pageId: string, value: boolean) => { + if (!isSharedPage) { + await updateContentWidth(pageId, value); + fetchCurrentPage({ force: true }); + } + }, + [isSharedPage, fetchCurrentPage], + ); const additionalMenuItemsRenderer = useCallback(() => { if (revisionId == null || pageId == null) { return ( <> - {!isReadOnlyUser - && ( - setIsPageTempleteModalShown(true)} - /> - ) - } + {!isReadOnlyUser && ( + setIsPageTempleteModalShown(true)} + /> + )} ); } @@ -371,8 +444,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): onClickTemplateMenuItem={() => setIsPageTempleteModalShown(true)} /> - ) - } + )} ); }, [isLinkSharingDisabled, pageId, revisionId, isReadOnlyUser]); @@ -389,11 +461,12 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): setStickyActive(status.status === Sticky.STATUS_FIXED)} + onStateChange={(status) => + setStickyActive(status.status === Sticky.STATUS_FIXED) + } innerActiveClass="w-100 end-0" > - - @@ -464,8 +551,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): )} ); - }; - export default GrowiContextualSubNavigation; diff --git a/apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx b/apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx index 181fb635dd2..808efd625e1 100644 --- a/apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx +++ b/apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, type JSX } from 'react'; +import React, { type JSX, useCallback } from 'react'; import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar'; import { useSearchModalActions } from '~/features/search/client/states/modal/search'; @@ -9,9 +9,7 @@ import { useDrawerOpened } from '~/states/ui/sidebar'; import styles from './GrowiNavbarBottom.module.scss'; - export const GrowiNavbarBottom = (): JSX.Element => { - const [isDrawerOpened, setIsDrawerOpened] = useDrawerOpened(); const { open: openCreateModal } = usePageCreateModalActions(); const currentPagePath = useCurrentPagePath(); @@ -23,13 +21,13 @@ export const GrowiNavbarBottom = (): JSX.Element => { }, [openSearchModal]); return ( -
-
-
); }; diff --git a/apps/app/src/client/components/Navbar/PageEditorModeManager.tsx b/apps/app/src/client/components/Navbar/PageEditorModeManager.tsx index 8f427563ce9..025008477cc 100644 --- a/apps/app/src/client/components/Navbar/PageEditorModeManager.tsx +++ b/apps/app/src/client/components/Navbar/PageEditorModeManager.tsx @@ -1,7 +1,4 @@ -import React, { - type ReactNode, useCallback, useMemo, type JSX, -} from 'react'; - +import React, { type JSX, type ReactNode, useCallback, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; import { useCreatePage } from '~/client/services/create-page'; @@ -9,24 +6,24 @@ import { useStartEditing } from '~/client/services/use-start-editing'; import { toastError } from '~/client/util/toastr'; import { useCurrentPageYjsData } from '~/features/collaborative-editor/states'; import { useDeviceLargerThanMd } from '~/states/ui/device'; -import { useEditorMode, EditorMode } from '~/states/ui/editor'; +import { EditorMode, useEditorMode } from '~/states/ui/editor'; import styles from './PageEditorModeManager.module.scss'; - type PageEditorModeButtonProps = { - currentEditorMode: EditorMode, - editorMode: EditorMode, - children?: ReactNode, - isBtnDisabled?: boolean, - onClick?: () => void, -} + currentEditorMode: EditorMode; + editorMode: EditorMode; + children?: ReactNode; + isBtnDisabled?: boolean; + onClick?: () => void; +}; const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => { - const { - currentEditorMode, isBtnDisabled, editorMode, children, onClick, - } = props; + const { currentEditorMode, isBtnDisabled, editorMode, children, onClick } = + props; - const classNames = ['btn py-1 px-2 d-flex align-items-center justify-content-center']; + const classNames = [ + 'btn py-1 px-2 d-flex align-items-center justify-content-center', + ]; if (currentEditorMode === editorMode) { classNames.push('active'); } @@ -47,17 +44,13 @@ const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => { }); type Props = { - editorMode: EditorMode | undefined, - isBtnDisabled: boolean, - path?: string, -} + editorMode: EditorMode | undefined; + isBtnDisabled: boolean; + path?: string; +}; export const PageEditorModeManager = (props: Props): JSX.Element => { - const { - editorMode = EditorMode.View, - isBtnDisabled, - path, - } = props; + const { editorMode = EditorMode.View, isBtnDisabled, path } = props; const { t } = useTranslation('commons'); @@ -71,8 +64,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => { const editButtonClickedHandler = useCallback(async () => { try { await startEditing(path); - } - catch (err) { + } catch (err) { toastError(t('toaster.create_failed', { target: path })); } }, [startEditing, path, t]); @@ -105,7 +97,8 @@ export const PageEditorModeManager = (props: Props): JSX.Element => { isBtnDisabled={_isBtnDisabled} onClick={() => setEditorMode(EditorMode.View)} > - play_arrow{t('View')} + play_arrow + {t('View')} )} {(isDeviceLargerThanMd || editorMode === EditorMode.View) && ( @@ -115,12 +108,18 @@ export const PageEditorModeManager = (props: Props): JSX.Element => { isBtnDisabled={_isBtnDisabled} onClick={editButtonClickedHandler} > - edit_square{t('Edit')} - {circleColor != null && } + + edit_square + + {t('Edit')} + {circleColor != null && ( + + )} )} ); - }; diff --git a/apps/app/src/client/components/PageEditor/Cheatsheet.tsx b/apps/app/src/client/components/PageEditor/Cheatsheet.tsx index a22f46fa4db..a492f3e47c5 100644 --- a/apps/app/src/client/components/PageEditor/Cheatsheet.tsx +++ b/apps/app/src/client/components/PageEditor/Cheatsheet.tsx @@ -1,7 +1,6 @@ /* eslint-disable max-len */ import React, { type JSX } from 'react'; - import { useTranslation } from 'next-i18next'; import { PrismAsyncLight } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; @@ -10,8 +9,8 @@ export const Cheatsheet = (): JSX.Element => { const { t } = useTranslation(); /* - * Each Element - */ + * Each Element + */ // Left Side const codeStr = `# ${t('sandbox.header_x', { index: '1' })}\n## ${t('sandbox.header_x', { index: '2' })}\n### ${t('sandbox.header_x', { index: '3' })}`; const codeBlockStr = 'text\n\ntext'; @@ -28,10 +27,10 @@ export const Cheatsheet = (): JSX.Element => { const taskStr = `- [ ] ${t('sandbox.task')}(${t('sandbox.task_unchecked')})\n- [x] ${t('sandbox.task')}(${t('sandbox.task_checked')})`; const quoteStr = `> ${t('sandbox.quote1')}\n> ${t('sandbox.quote2')}`; const nestedQuoteStr = `>> ${t('sandbox.quote_nested')}\n>>> ${t('sandbox.quote_nested')}\n>>>> ${t('sandbox.quote_nested')}`; - const tableStr = '|Left | Mid | Right|\n|:----------|:---------:|----------:|\n|col 1 | col 2 | col 3|\n|col 1 | col 2 | col 3|'; + const tableStr = + '|Left | Mid | Right|\n|:----------|:---------:|----------:|\n|col 1 | col 2 | col 3|\n|col 1 | col 2 | col 3|'; const imageStr = '![ex](https://example.com/image.png)'; - const renderCheetSheetElm = (CheetSheetElm: string) => { return ( { ); }; - return (
- {/* Header */}

{t('sandbox.header')}

{renderCheetSheetElm(codeStr)} {/* Block */}

{t('sandbox.block')}

-

[{t('sandbox.empty_line')}]{t('sandbox.block_detail')}

+

+ [{t('sandbox.empty_line')}] + {t('sandbox.block_detail')} +

{renderCheetSheetElm(codeBlockStr)} {/* Line Break */}

{t('sandbox.line_break')}

-

[ ][ ] {t('sandbox.line_break_detail')}

+

+ [ ][ ] {t('sandbox.line_break_detail')} +

{renderCheetSheetElm(lineBlockStr)} - {/* Typography */}

{t('sandbox.typography')}

{renderCheetSheetElm(typographyStr)} @@ -93,22 +94,29 @@ export const Cheatsheet = (): JSX.Element => { {renderCheetSheetElm(nestedQuoteStr)} - {/* Table */}

{t('sandbox.table')}

{renderCheetSheetElm(tableStr)} {/* Image */}

{t('sandbox.image')}

-

![{t('sandbox.alt_text')}](URL) {t('sandbox.insert_image')}

+

+ ![{t('sandbox.alt_text')}](URL){' '} + {t('sandbox.insert_image')} +

{renderCheetSheetElm(imageStr)}
- - external_link {t('sandbox.open_sandbox')} + + external_link{' '} + {t('sandbox.open_sandbox')}
); - }; diff --git a/apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.tsx b/apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.tsx index 05142aae759..7be88748eaf 100644 --- a/apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.tsx +++ b/apps/app/src/client/components/PageEditor/ConflictDiffModal/ConflictDiffModal.tsx @@ -1,7 +1,5 @@ -import React, { - useState, useEffect, useCallback, useMemo, -} from 'react'; - +import type React from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { IUser } from '@growi/core'; import { GlobalCodeMirrorEditorKey } from '@growi/editor'; import { CodeMirrorEditorDiff } from '@growi/editor/dist/client/components/diff/CodeMirrorEditorDiff'; @@ -10,10 +8,7 @@ import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/co import { UserPicture } from '@growi/ui/dist/components'; import { format } from 'date-fns/format'; import { useTranslation } from 'next-i18next'; -import { - Modal, ModalHeader, ModalBody, ModalFooter, -} from 'reactstrap'; - +import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { useCurrentUser } from '~/states/global'; import { @@ -22,58 +17,73 @@ import { useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, } from '~/states/page'; -import { useConflictDiffModalActions, useConflictDiffModalStatus } from '~/states/ui/modal/conflict-diff'; +import { + useConflictDiffModalActions, + useConflictDiffModalStatus, +} from '~/states/ui/modal/conflict-diff'; import styles from './ConflictDiffModal.module.scss'; type IRevisionOnConflict = { - revisionBody: string - createdAt: Date - user: IUser -} + revisionBody: string; + createdAt: Date; + user: IUser; +}; /** * ConflictDiffModalSubstance - Presentation component (heavy logic, rendered only when isOpen) */ type ConflictDiffModalSubstanceProps = { - request: IRevisionOnConflict - latest: IRevisionOnConflict - isModalExpanded: boolean - setIsModalExpanded: React.Dispatch> + request: IRevisionOnConflict; + latest: IRevisionOnConflict; + isModalExpanded: boolean; + setIsModalExpanded: React.Dispatch>; }; const formatedDate = (date: Date): string => { return format(date, 'yyyy/MM/dd HH:mm:ss'); }; -const ConflictDiffModalSubstance = (props: ConflictDiffModalSubstanceProps): React.JSX.Element => { - const { - request, latest, isModalExpanded, setIsModalExpanded, - } = props; +const ConflictDiffModalSubstance = ( + props: ConflictDiffModalSubstanceProps, +): React.JSX.Element => { + const { request, latest, isModalExpanded, setIsModalExpanded } = props; const [resolvedRevision, setResolvedRevision] = useState(''); const [isRevisionselected, setIsRevisionSelected] = useState(false); - const [revisionSelectedToggler, setRevisionSelectedToggler] = useState(false); + const [revisionSelectedToggler, setRevisionSelectedToggler] = + useState(false); const { t } = useTranslation(); const conflictDiffModalStatus = useConflictDiffModalStatus(); const { close: closeConflictDiffModal } = useConflictDiffModalActions(); - const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.DIFF); + const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated( + GlobalCodeMirrorEditorKey.DIFF, + ); // Memoize formatted dates - const requestFormattedDate = useMemo(() => formatedDate(request.createdAt), [request.createdAt]); - const latestFormattedDate = useMemo(() => formatedDate(latest.createdAt), [latest.createdAt]); + const requestFormattedDate = useMemo( + () => formatedDate(request.createdAt), + [request.createdAt], + ); + const latestFormattedDate = useMemo( + () => formatedDate(latest.createdAt), + [latest.createdAt], + ); - const selectRevisionHandler = useCallback((selectedRevision: string) => { - setResolvedRevision(selectedRevision); - setRevisionSelectedToggler(prev => !prev); + const selectRevisionHandler = useCallback( + (selectedRevision: string) => { + setResolvedRevision(selectedRevision); + setRevisionSelectedToggler((prev) => !prev); - if (!isRevisionselected) { - setIsRevisionSelected(true); - } - }, [isRevisionselected]); + if (!isRevisionselected) { + setIsRevisionSelected(true); + } + }, + [isRevisionselected], + ); - const resolveConflictHandler = useCallback(async() => { + const resolveConflictHandler = useCallback(async () => { const newBody = codeMirrorEditor?.getDocString(); if (newBody == null) { return; @@ -87,51 +97,76 @@ const ConflictDiffModalSubstance = (props: ConflictDiffModalSubstanceProps): Rea // Enable selecting the same revision after editing by including revisionSelectedToggler in the dependency array of useEffect }, [codeMirrorEditor, resolvedRevision, revisionSelectedToggler]); - const headerButtons = useMemo(() => ( -
- - -
- ), [closeConflictDiffModal, isModalExpanded, setIsModalExpanded]); + const headerButtons = useMemo( + () => ( +
+ + +
+ ), + [closeConflictDiffModal, isModalExpanded, setIsModalExpanded], + ); return ( <> - - error{t('modal_resolve_conflict.resolve_conflict')} + + error + {t('modal_resolve_conflict.resolve_conflict')}
-

{t('modal_resolve_conflict.resolve_conflict_message')}

+

+ {t('modal_resolve_conflict.resolve_conflict_message')} +

-

{t('modal_resolve_conflict.requested_revision')}

+

+ {t('modal_resolve_conflict.requested_revision')} +

updated by {request.user.username}

-

{ requestFormattedDate }

+

{requestFormattedDate}

-

{t('modal_resolve_conflict.latest_revision')}

+

+ {t('modal_resolve_conflict.latest_revision')} +

updated by {latest.user.username}

-

{ latestFormattedDate }

+

{latestFormattedDate}

@@ -146,10 +181,16 @@ const ConflictDiffModalSubstance = (props: ConflictDiffModalSubstanceProps): Rea
@@ -159,17 +200,25 @@ const ConflictDiffModalSubstance = (props: ConflictDiffModalSubstanceProps): Rea
-

{t('modal_resolve_conflict.selected_editable_revision')}

+

+ {t('modal_resolve_conflict.selected_editable_revision')} +

@@ -210,29 +259,37 @@ export const ConflictDiffModal = (): React.JSX.Element => { const remoteRevisionLastUpdateUser = useRemoteRevisionLastUpdateUser(); const remoteRevisionLastUpdatedAt = useRemoteRevisionLastUpdatedAt(); - const isRemotePageDataInappropriate = remoteRevisionBody == null || remoteRevisionLastUpdateUser == null; + const isRemotePageDataInappropriate = + remoteRevisionBody == null || remoteRevisionLastUpdateUser == null; const [isModalExpanded, setIsModalExpanded] = useState(false); // Check if all required data is available - const isDataReady = conflictDiffModalStatus?.isOpened - && currentUser != null - && currentPage != null - && !isRemotePageDataInappropriate; + const isDataReady = + conflictDiffModalStatus?.isOpened && + currentUser != null && + currentPage != null && + !isRemotePageDataInappropriate; // Prepare data for Substance const currentTime: Date = new Date(); - const request: IRevisionOnConflict | null = isDataReady ? { - revisionBody: conflictDiffModalStatus.requestRevisionBody ?? '', - createdAt: currentTime, - user: currentUser, - } : null; - - const latest: IRevisionOnConflict | null = isDataReady ? { - revisionBody: remoteRevisionBody, - createdAt: new Date(remoteRevisionLastUpdatedAt ?? currentTime.toString()), - user: remoteRevisionLastUpdateUser, - } : null; + const request: IRevisionOnConflict | null = isDataReady + ? { + revisionBody: conflictDiffModalStatus.requestRevisionBody ?? '', + createdAt: currentTime, + user: currentUser, + } + : null; + + const latest: IRevisionOnConflict | null = isDataReady + ? { + revisionBody: remoteRevisionBody, + createdAt: new Date( + remoteRevisionLastUpdatedAt ?? currentTime.toString(), + ), + user: remoteRevisionLastUpdateUser, + } + : null; return ( { const ConflictDiffModal = useLazyLoader( 'conflict-diff-modal', - () => import('./ConflictDiffModal').then(mod => ({ default: mod.ConflictDiffModal })), + () => + import('./ConflictDiffModal').then((mod) => ({ + default: mod.ConflictDiffModal, + })), status?.isOpened ?? false, ); diff --git a/apps/app/src/client/components/PageEditor/DrawioModal/DrawioCommunicationHelper.ts b/apps/app/src/client/components/PageEditor/DrawioModal/DrawioCommunicationHelper.ts index 685ac76937a..4e3fa7b5481 100644 --- a/apps/app/src/client/components/PageEditor/DrawioModal/DrawioCommunicationHelper.ts +++ b/apps/app/src/client/components/PageEditor/DrawioModal/DrawioCommunicationHelper.ts @@ -1,43 +1,45 @@ import loggerFactory from '~/utils/logger'; - const logger = loggerFactory('growi:cli:DrawioCommunicationHelper'); export type DrawioConfig = { - css: string, - customFonts: string[], - compressXml: boolean, -} + css: string; + customFonts: string[]; + compressXml: boolean; +}; export type DrawioCommunicationCallbackOptions = { onClose?: () => void; onSave?: (drawioData: string) => void; -} +}; export class DrawioCommunicationHelper { - drawioUri: string; drawioConfig: DrawioConfig; callbackOpts?: DrawioCommunicationCallbackOptions; - - constructor(drawioUri: string, drawioConfig: DrawioConfig, callbackOpts?: DrawioCommunicationCallbackOptions) { + constructor( + drawioUri: string, + drawioConfig: DrawioConfig, + callbackOpts?: DrawioCommunicationCallbackOptions, + ) { this.drawioUri = drawioUri; this.drawioConfig = drawioConfig; this.callbackOpts = callbackOpts; } onReceiveMessage(event: MessageEvent, drawioMxFile: string | null): void { - // check origin if (event.origin != null && this.drawioUri != null) { const originUrl = new URL(event.origin); const drawioUrl = new URL(this.drawioUri); if (originUrl.origin !== drawioUrl.origin) { - logger.debug(`Skipping the event because the origins are mismatched. expected: '${drawioUrl.origin}', actual: '${originUrl.origin}'`); + logger.debug( + `Skipping the event because the origins are mismatched. expected: '${drawioUrl.origin}', actual: '${originUrl.origin}'`, + ); return; } } @@ -52,10 +54,13 @@ export class DrawioCommunicationHelper { // * https://desk.draw.io/support/solutions/articles/16000103852-how-to-customise-the-draw-io-interface // * https://desk.draw.io/support/solutions/articles/16000042544-how-does-embed-mode-work- // * https://desk.draw.io/support/solutions/articles/16000058316-how-to-configure-draw-io- - event.source.postMessage(JSON.stringify({ - action: 'configure', - config: this.drawioConfig, - }), { targetOrigin: '*' }); + event.source.postMessage( + JSON.stringify({ + action: 'configure', + config: this.drawioConfig, + }), + { targetOrigin: '*' }, + ); return; } @@ -73,10 +78,10 @@ export class DrawioCommunicationHelper { const drawioData = dom.getElementsByTagName('diagram')[0].innerHTML; /* - * Saving Drawio will be implemented by the following tasks - * https://redmine.weseek.co.jp/issues/100845 - * https://redmine.weseek.co.jp/issues/104507 - */ + * Saving Drawio will be implemented by the following tasks + * https://redmine.weseek.co.jp/issues/100845 + * https://redmine.weseek.co.jp/issues/104507 + */ this.callbackOpts?.onSave?.(drawioData); } @@ -93,5 +98,4 @@ export class DrawioCommunicationHelper { // NOTHING DONE. (Receive unknown iframe message.) } - } diff --git a/apps/app/src/client/components/PageEditor/DrawioModal/DrawioModal.tsx b/apps/app/src/client/components/PageEditor/DrawioModal/DrawioModal.tsx index ad26c32f30c..cd64f6601fe 100644 --- a/apps/app/src/client/components/PageEditor/DrawioModal/DrawioModal.tsx +++ b/apps/app/src/client/components/PageEditor/DrawioModal/DrawioModal.tsx @@ -1,27 +1,32 @@ -import React, { - useCallback, useEffect, useMemo, type JSX, -} from 'react'; - +import React, { type JSX, useCallback, useEffect, useMemo } from 'react'; import { Lang } from '@growi/core'; import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor'; -import { useDrawioModalForEditorStatus, useDrawioModalForEditorActions } from '@growi/editor/dist/states/modal/drawio-for-editor'; -import { LoadingSpinner } from '@growi/ui/dist/components'; import { - Modal, - ModalBody, -} from 'reactstrap'; + useDrawioModalForEditorActions, + useDrawioModalForEditorStatus, +} from '@growi/editor/dist/states/modal/drawio-for-editor'; +import { LoadingSpinner } from '@growi/ui/dist/components'; +import { Modal, ModalBody } from 'reactstrap'; -import { replaceFocusedDrawioWithEditor, getMarkdownDrawioMxfile } from '~/client/components/PageEditor/markdown-drawio-util-for-editor'; +import { + getMarkdownDrawioMxfile, + replaceFocusedDrawioWithEditor, +} from '~/client/components/PageEditor/markdown-drawio-util-for-editor'; import { useRendererConfig } from '~/states/server-configurations'; -import { useDrawioModalActions, useDrawioModalStatus } from '~/states/ui/modal/drawio'; +import { + useDrawioModalActions, + useDrawioModalStatus, +} from '~/states/ui/modal/drawio'; import { useSWRxPersonalSettings } from '~/stores/personal-settings'; import loggerFactory from '~/utils/logger'; -import { type DrawioConfig, DrawioCommunicationHelper } from './DrawioCommunicationHelper'; +import { + DrawioCommunicationHelper, + type DrawioConfig, +} from './DrawioCommunicationHelper'; const logger = loggerFactory('growi:components:DrawioModal'); - // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ const DIAGRAMS_NET_LANG_MAP = { en_US: 'en', @@ -34,9 +39,9 @@ export const getDiagramsNetLangCode = (lang: Lang): string => { return DIAGRAMS_NET_LANG_MAP[lang]; }; - const headerColor = '#334455'; -const fontFamily = "-apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif"; +const fontFamily = + "-apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif"; const drawioConfig: DrawioConfig = { css: ` @@ -52,7 +57,6 @@ const drawioConfig: DrawioConfig = { compressXml: true, }; - const DrawioModalSubstance = (): JSX.Element => { const { drawioUri } = useRendererConfig(); const { data: personalSettingsInfo } = useSWRxPersonalSettings({ @@ -69,7 +73,8 @@ const DrawioModalSubstance = (): JSX.Element => { const editorKey = drawioModalDataInEditor?.editorKey ?? null; const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey); const editor = codeMirrorEditor?.view; - const isOpenedInEditor = (drawioModalDataInEditor?.isOpened ?? false) && (editor != null); + const isOpenedInEditor = + (drawioModalDataInEditor?.isOpened ?? false) && editor != null; const isOpened = drawioModalData?.isOpened ?? false; // Memoize URI with parameters calculation @@ -82,8 +87,7 @@ const DrawioModalSubstance = (): JSX.Element => { let url; try { url = new URL(drawioUri); - } - catch (err) { + } catch (err) { logger.debug(err); return undefined; } @@ -91,7 +95,10 @@ const DrawioModalSubstance = (): JSX.Element => { // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported- url.searchParams.append('spin', '1'); url.searchParams.append('embed', '1'); - url.searchParams.append('lang', getDiagramsNetLangCode(personalSettingsInfo?.lang ?? Lang.en_US)); + url.searchParams.append( + 'lang', + getDiagramsNetLangCode(personalSettingsInfo?.lang ?? Lang.en_US), + ); url.searchParams.append('ui', 'atlas'); url.searchParams.append('configure', '1'); @@ -104,34 +111,47 @@ const DrawioModalSubstance = (): JSX.Element => { return undefined; } - const saveHandler = editor != null - ? (drawioMxFile: string) => replaceFocusedDrawioWithEditor(editor, drawioMxFile) - : drawioModalData?.onSave; + const saveHandler = + editor != null + ? (drawioMxFile: string) => + replaceFocusedDrawioWithEditor(editor, drawioMxFile) + : drawioModalData?.onSave; const closeHandler = isOpened ? closeDrawioModal : closeDrawioModalInEditor; - return new DrawioCommunicationHelper( - drawioUri, - drawioConfig, - { onClose: closeHandler, onSave: saveHandler }, - ); - }, [drawioUri, editor, drawioModalData?.onSave, isOpened, closeDrawioModal, closeDrawioModalInEditor]); - - const receiveMessageHandler = useCallback((event: MessageEvent) => { - if (drawioModalData == null || drawioCommunicationHelper == null) { - return; - } - - const drawioMxFile = editor != null ? getMarkdownDrawioMxfile(editor) : drawioModalData.drawioMxFile; - drawioCommunicationHelper.onReceiveMessage(event, drawioMxFile ?? null); - }, [drawioCommunicationHelper, drawioModalData, editor]); + return new DrawioCommunicationHelper(drawioUri, drawioConfig, { + onClose: closeHandler, + onSave: saveHandler, + }); + }, [ + drawioUri, + editor, + drawioModalData?.onSave, + isOpened, + closeDrawioModal, + closeDrawioModalInEditor, + ]); + + const receiveMessageHandler = useCallback( + (event: MessageEvent) => { + if (drawioModalData == null || drawioCommunicationHelper == null) { + return; + } + + const drawioMxFile = + editor != null + ? getMarkdownDrawioMxfile(editor) + : drawioModalData.drawioMxFile; + drawioCommunicationHelper.onReceiveMessage(event, drawioMxFile ?? null); + }, + [drawioCommunicationHelper, drawioModalData, editor], + ); // Memoize toggle handler const toggleHandler = useCallback(() => { if (isOpened) { closeDrawioModal(); - } - else { + } else { closeDrawioModalInEditor(); } }, [isOpened, closeDrawioModal, closeDrawioModalInEditor]); @@ -139,13 +159,12 @@ const DrawioModalSubstance = (): JSX.Element => { useEffect(() => { if (isOpened || isOpenedInEditor) { window.addEventListener('message', receiveMessageHandler); - } - else { + } else { window.removeEventListener('message', receiveMessageHandler); } // clean up - return function() { + return () => { window.removeEventListener('message', receiveMessageHandler); }; }, [isOpened, isOpenedInEditor, receiveMessageHandler]); @@ -167,17 +186,16 @@ const DrawioModalSubstance = (): JSX.Element => { {/* iframe */} - { drawioUriWithParams != null && ( + {drawioUriWithParams != null && (
- { (isOpened || isOpenedInEditor) && ( + {(isOpened || isOpenedInEditor) && ( - ) } + > + )}
- ) } + )}
); diff --git a/apps/app/src/client/components/PageEditor/DrawioModal/dynamic.tsx b/apps/app/src/client/components/PageEditor/DrawioModal/dynamic.tsx index fba4e4ba900..5c8343b0098 100644 --- a/apps/app/src/client/components/PageEditor/DrawioModal/dynamic.tsx +++ b/apps/app/src/client/components/PageEditor/DrawioModal/dynamic.tsx @@ -1,11 +1,9 @@ import type { JSX } from 'react'; - import { useDrawioModalForEditorStatus } from '@growi/editor/dist/states/modal/drawio-for-editor'; import { useLazyLoader } from '~/components/utils/use-lazy-loader'; import { useDrawioModalStatus } from '~/states/ui/modal/drawio'; - type DrawioModalProps = Record; export const DrawioModalLazyLoaded = (): JSX.Element => { @@ -17,7 +15,7 @@ export const DrawioModalLazyLoaded = (): JSX.Element => { const DrawioModal = useLazyLoader( 'drawio-modal', - () => import('./DrawioModal').then(mod => ({ default: mod.DrawioModal })), + () => import('./DrawioModal').then((mod) => ({ default: mod.DrawioModal })), isOpened || isOpenedInEditor, ); diff --git a/apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx b/apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx index 4e516311335..141d780217c 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx @@ -1,5 +1,4 @@ import { type FC, useState } from 'react'; - import type { EditingClient } from '@growi/editor'; import { UserPicture } from '@growi/ui/dist/components'; import { Popover, PopoverBody } from 'reactstrap'; @@ -11,8 +10,8 @@ import styles from './EditingUserList.module.scss'; const userListPopoverClass = styles['user-list-popover'] ?? ''; type Props = { - clientList: EditingClient[] -} + clientList: EditingClient[]; +}; export const EditingUserList: FC = ({ clientList }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -29,7 +28,7 @@ export const EditingUserList: FC = ({ clientList }) => { return (
- {firstFourUsers.map(editingClient => ( + {firstFourUsers.map((editingClient) => (
= ({ clientList }) => { {remainingUsers.length > 0 && (
- - + diff --git a/apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx b/apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx index 20873060c0d..c4bdc042368 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx @@ -11,18 +11,20 @@ const moduleClass = styles['editor-navbar'] ?? ''; const EditingUsers = (): JSX.Element => { const editingClients = useEditingClients(); - return ( - - ); + return ; }; export const EditorNavbar = (): JSX.Element => { return ( -
-
-
+
+
+ +
+
+ +
); }; diff --git a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorAssistantToggleButton.tsx b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorAssistantToggleButton.tsx index 9fc1f0b76d4..65b28e7e80d 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorAssistantToggleButton.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorAssistantToggleButton.tsx @@ -1,8 +1,10 @@ import { useCallback } from 'react'; - import { useTranslation } from 'next-i18next'; -import { useAiAssistantSidebarStatus, useAiAssistantSidebarActions } from '~/features/openai/client/states'; +import { + useAiAssistantSidebarActions, + useAiAssistantSidebarStatus, +} from '~/features/openai/client/states'; export const EditorAssistantToggleButton = (): JSX.Element => { const { t } = useTranslation(); diff --git a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.tsx b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.tsx index cae247ae0fa..77ce49ca0e9 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/EditorNavbarBottom.tsx @@ -1,7 +1,6 @@ import type { JSX } from 'react'; - -import { useAtomValue } from 'jotai'; import dynamic from 'next/dynamic'; +import { useAtomValue } from 'jotai'; import { aiEnabledAtom } from '~/states/server-configurations'; import { useDrawerOpened } from '~/states/ui/sidebar'; @@ -10,11 +9,16 @@ import { EditorAssistantToggleButton } from './EditorAssistantToggleButton'; import styles from './EditorNavbarBottom.module.scss'; - const moduleClass = styles['grw-editor-navbar-bottom']; -const SavePageControls = dynamic(() => import('./SavePageControls').then(mod => mod.SavePageControls), { ssr: false }); -const OptionsSelector = dynamic(() => import('./OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false }); +const SavePageControls = dynamic( + () => import('./SavePageControls').then((mod) => mod.SavePageControls), + { ssr: false }, +); +const OptionsSelector = dynamic( + () => import('./OptionsSelector').then((mod) => mod.OptionsSelector), + { ssr: false }, +); export const EditorNavbarBottom = (): JSX.Element => { const isAiEnabled = useAtomValue(aiEnabledAtom); @@ -22,7 +26,9 @@ export const EditorNavbarBottom = (): JSX.Element => { return (
-
+
{
- {isAiEnabled && ( - - )} + {isAiEnabled && }
diff --git a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/GrantSelector.tsx b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/GrantSelector.tsx index 09734344399..1c1f9fc0bad 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/GrantSelector.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/GrantSelector.tsx @@ -1,17 +1,15 @@ -import React, { - useCallback, useEffect, useState, type JSX, -} from 'react'; - -import { - PageGrant, GroupType, getIdForRef, -} from '@growi/core'; +import React, { type JSX, useCallback, useEffect, useState } from 'react'; +import { GroupType, getIdForRef, PageGrant } from '@growi/core'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useTranslation } from 'next-i18next'; import { + DropdownItem, + DropdownMenu, + DropdownToggle, + Modal, + ModalBody, + ModalHeader, UncontrolledDropdown, - DropdownToggle, DropdownMenu, DropdownItem, - - Modal, ModalHeader, ModalBody, } from 'reactstrap'; import type { UserRelatedGroupsData } from '~/interfaces/page'; @@ -21,17 +19,25 @@ import { useCurrentPageId } from '~/states/page'; import { useSelectedGrant } from '~/states/ui/editor'; import { useSWRxCurrentGrantData } from '~/stores/page'; - const AVAILABLE_GRANTS = [ { - grant: PageGrant.GRANT_PUBLIC, iconName: 'group', btnStyleClass: 'outline-info', label: 'Public', + grant: PageGrant.GRANT_PUBLIC, + iconName: 'group', + btnStyleClass: 'outline-info', + label: 'Public', }, { - grant: PageGrant.GRANT_RESTRICTED, iconName: 'link', btnStyleClass: 'outline-success', label: 'Anyone with the link', + grant: PageGrant.GRANT_RESTRICTED, + iconName: 'link', + btnStyleClass: 'outline-success', + label: 'Anyone with the link', }, // { grant: 3, iconClass: '', label: 'Specified users only' }, { - grant: PageGrant.GRANT_OWNER, iconName: 'lock', btnStyleClass: 'outline-danger', label: 'Only me', + grant: PageGrant.GRANT_OWNER, + iconName: 'lock', + btnStyleClass: 'outline-danger', + label: 'Only me', }, { grant: PageGrant.GRANT_USER_GROUP, @@ -42,11 +48,10 @@ const AVAILABLE_GRANTS = [ }, ]; - type Props = { - disabled?: boolean, - openInModal?: boolean, -} + disabled?: boolean; + openInModal?: boolean; +}; /** * Page grant select component @@ -54,11 +59,7 @@ type Props = { export const GrantSelector = (props: Props): JSX.Element => { const { t } = useTranslation(); - const { - disabled, - openInModal, - } = props; - + const { disabled, openInModal } = props; const [isSelectGroupModalShown, setIsSelectGroupModalShown] = useState(false); @@ -76,10 +77,12 @@ export const GrantSelector = (props: Props): JSX.Element => { const currentPageGrant = grantData?.grantData.currentPageGrant; if (currentPageGrant == null) return; - const userRelatedGrantedGroups = currentPageGrant.groupGrantData - ?.userRelatedGroups.filter(group => group.status === UserGroupPageGrantStatus.isGranted)?.map((group) => { - return { item: group.id, type: group.type }; - }) ?? []; + const userRelatedGrantedGroups = + currentPageGrant.groupGrantData?.userRelatedGroups + .filter((group) => group.status === UserGroupPageGrantStatus.isGranted) + ?.map((group) => { + return { item: group.id, type: group.type }; + }) ?? []; setSelectedGrant({ grant: currentPageGrant.grant, userRelatedGrantedGroups, @@ -98,48 +101,77 @@ export const GrantSelector = (props: Props): JSX.Element => { /** * change event handler for grant selector */ - const changeGrantHandler = useCallback((grant: PageGrant) => { - // select group - if (grant === 5) { - if (selectedGrant?.grant !== 5) applyCurrentPageGrantToSelectedGrant(); - showSelectGroupModal(); - return; - } - - setSelectedGrant({ grant, userRelatedGrantedGroups: undefined }); - }, [setSelectedGrant, showSelectGroupModal, applyCurrentPageGrantToSelectedGrant, selectedGrant?.grant]); + const changeGrantHandler = useCallback( + (grant: PageGrant) => { + // select group + if (grant === 5) { + if (selectedGrant?.grant !== 5) applyCurrentPageGrantToSelectedGrant(); + showSelectGroupModal(); + return; + } - const groupListItemClickHandler = useCallback((clickedGroup: UserRelatedGroupsData) => { - const userRelatedGrantedGroups = selectedGrant?.userRelatedGrantedGroups ?? []; + setSelectedGrant({ grant, userRelatedGrantedGroups: undefined }); + }, + [ + setSelectedGrant, + showSelectGroupModal, + applyCurrentPageGrantToSelectedGrant, + selectedGrant?.grant, + ], + ); - let userRelatedGrantedGroupsCopy = [...userRelatedGrantedGroups]; - if (userRelatedGrantedGroupsCopy.find(group => getIdForRef(group.item) === clickedGroup.id) == null) { - const grantGroupInfo = { item: clickedGroup.id, type: clickedGroup.type }; - userRelatedGrantedGroupsCopy.push(grantGroupInfo); - } - else { - userRelatedGrantedGroupsCopy = userRelatedGrantedGroupsCopy.filter(group => getIdForRef(group.item) !== clickedGroup.id); - } - setSelectedGrant({ grant: 5, userRelatedGrantedGroups: userRelatedGrantedGroupsCopy }); - }, [setSelectedGrant, selectedGrant?.userRelatedGrantedGroups]); + const groupListItemClickHandler = useCallback( + (clickedGroup: UserRelatedGroupsData) => { + const userRelatedGrantedGroups = + selectedGrant?.userRelatedGrantedGroups ?? []; + + let userRelatedGrantedGroupsCopy = [...userRelatedGrantedGroups]; + if ( + userRelatedGrantedGroupsCopy.find( + (group) => getIdForRef(group.item) === clickedGroup.id, + ) == null + ) { + const grantGroupInfo = { + item: clickedGroup.id, + type: clickedGroup.type, + }; + userRelatedGrantedGroupsCopy.push(grantGroupInfo); + } else { + userRelatedGrantedGroupsCopy = userRelatedGrantedGroupsCopy.filter( + (group) => getIdForRef(group.item) !== clickedGroup.id, + ); + } + setSelectedGrant({ + grant: 5, + userRelatedGrantedGroups: userRelatedGrantedGroupsCopy, + }); + }, + [setSelectedGrant, selectedGrant?.userRelatedGrantedGroups], + ); /** * Render grant selector DOM. */ const renderGrantSelector = useCallback(() => { - let dropdownToggleBtnColor; let dropdownToggleLabelElm; - const userRelatedGrantedGroups = groupGrantData?.userRelatedGroups.filter((group) => { - return selectedGrant?.userRelatedGrantedGroups?.some(grantedGroup => getIdForRef(grantedGroup.item) === group.id); - }) ?? []; - const nonUserRelatedGrantedGroups = groupGrantData?.nonUserRelatedGrantedGroups ?? []; + const userRelatedGrantedGroups = + groupGrantData?.userRelatedGroups.filter((group) => { + return selectedGrant?.userRelatedGrantedGroups?.some( + (grantedGroup) => getIdForRef(grantedGroup.item) === group.id, + ); + }) ?? []; + const nonUserRelatedGrantedGroups = + groupGrantData?.nonUserRelatedGrantedGroups ?? []; const dropdownMenuElems = AVAILABLE_GRANTS.map((opt) => { - const label = ((opt.grant === 5 && opt.reselectLabel != null) && userRelatedGrantedGroups.length > 0) - ? opt.reselectLabel // when grantGroup is selected - : opt.label; + const label = + opt.grant === 5 && + opt.reselectLabel != null && + userRelatedGrantedGroups.length > 0 + ? opt.reselectLabel // when grantGroup is selected + : opt.label; const labelElm = ( @@ -154,24 +186,41 @@ export const GrantSelector = (props: Props): JSX.Element => { dropdownToggleLabelElm = labelElm; } - return changeGrantHandler(opt.grant)}>{labelElm}; + return ( + changeGrantHandler(opt.grant)} + > + {labelElm} + + ); }); // add specified group option - if (selectedGrant?.grant === PageGrant.GRANT_USER_GROUP && (userRelatedGrantedGroups.length > 0 || nonUserRelatedGrantedGroups.length > 0)) { - const grantedGroupNames = [...userRelatedGrantedGroups.map(group => group.name), ...nonUserRelatedGrantedGroups.map(group => group.name)]; + if ( + selectedGrant?.grant === PageGrant.GRANT_USER_GROUP && + (userRelatedGrantedGroups.length > 0 || + nonUserRelatedGrantedGroups.length > 0) + ) { + const grantedGroupNames = [ + ...userRelatedGrantedGroups.map((group) => group.name), + ...nonUserRelatedGrantedGroups.map((group) => group.name), + ]; const labelElm = ( account_tree - {grantedGroupNames.length > 1 + {grantedGroupNames.length > 1 ? ( // substring for group name truncate - ? ( - - {`${grantedGroupNames[0].substring(0, 30)}, ... `} - +{grantedGroupNames.length - 1} + + {`${grantedGroupNames[0].substring(0, 30)}, ... `} + + +{grantedGroupNames.length - 1} - ) : grantedGroupNames[0].substring(0, 30)} + + ) : ( + grantedGroupNames[0].substring(0, 30) + )} ); @@ -179,7 +228,9 @@ export const GrantSelector = (props: Props): JSX.Element => { // set dropdownToggleLabelElm dropdownToggleLabelElm = labelElm; - dropdownMenuElems.push({labelElm}); + dropdownMenuElems.push( + {labelElm}, + ); } return ( @@ -193,13 +244,23 @@ export const GrantSelector = (props: Props): JSX.Element => { > {dropdownToggleLabelElm} - + {dropdownMenuElems}
); - }, [changeGrantHandler, disabled, groupGrantData, selectedGrant, t, openInModal]); + }, [ + changeGrantHandler, + disabled, + groupGrantData, + selectedGrant, + t, + openInModal, + ]); /** * Render select grantgroup modal. @@ -224,18 +285,26 @@ export const GrantSelector = (props: Props): JSX.Element => { return (

{t('user_group.belonging_to_no_group')}

- { currentUser?.admin && ( -

login{t('user_group.manage_user_groups')}

- ) } + {currentUser?.admin && ( +

+ + login + {t('user_group.manage_user_groups')} + +

+ )}
); } return (
- { userRelatedGroups.map((group) => { - const isGroupGranted = selectedGrant?.userRelatedGrantedGroups?.some(grantedGroup => getIdForRef(grantedGroup.item) === group.id); - const cannotGrantGroup = group.status === UserGroupPageGrantStatus.cannotGrant; + {userRelatedGroups.map((group) => { + const isGroupGranted = selectedGrant?.userRelatedGrantedGroups?.some( + (grantedGroup) => getIdForRef(grantedGroup.item) === group.id, + ); + const cannotGrantGroup = + group.status === UserGroupPageGrantStatus.cannotGrant; const activeClass = isGroupGranted ? 'active' : ''; return ( @@ -246,14 +315,22 @@ export const GrantSelector = (props: Props): JSX.Element => { onClick={() => groupListItemClickHandler(group)} disabled={cannotGrantGroup} > - +

{group.name}

- {group.type === GroupType.externalUserGroup && {group.provider}} + {group.type === GroupType.externalUserGroup && ( + + {group.provider} + + )} {/* TODO: Replace
(TBD) List group members
*/} ); - }) } - { nonUserRelatedGrantedGroups.map((group) => { + })} + {nonUserRelatedGrantedGroups.map((group) => { return ( ); - }) } - + })} +
); - - }, [currentUser?.admin, groupListItemClickHandler, shouldFetch, t, groupGrantData, selectedGrant?.userRelatedGrantedGroups]); + }, [ + currentUser?.admin, + groupListItemClickHandler, + shouldFetch, + t, + groupGrantData, + selectedGrant?.userRelatedGrantedGroups, + ]); const renderModalCloseButton = useCallback(() => { return ( @@ -288,23 +381,26 @@ export const GrantSelector = (props: Props): JSX.Element => { return ( <> - { renderGrantSelector() } + {renderGrantSelector()} {/* render modal */} - { !disabled && currentUser != null && ( + {!disabled && currentUser != null && ( setIsSelectGroupModalShown(false)} centered > - setIsSelectGroupModalShown(false)} className="fs-5 text-muted fw-bold pb-2" close={renderModalCloseButton()}> + setIsSelectGroupModalShown(false)} + className="fs-5 text-muted fw-bold pb-2" + close={renderModalCloseButton()} + > {t('user_group.select_group')} - - {renderSelectGroupModalContent()} - + {renderSelectGroupModalContent()} - ) } + )} ); }; diff --git a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/OptionsSelector.tsx b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/OptionsSelector.tsx index 2139724f132..4bffe90caa8 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/OptionsSelector.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/OptionsSelector.tsx @@ -1,33 +1,41 @@ -import React, { - memo, useCallback, useMemo, useState, type JSX, -} from 'react'; - +import React, { type JSX, memo, useCallback, useMemo, useState } from 'react'; +import Image from 'next/image'; import { - type EditorTheme, type KeyMapMode, PasteMode, AllPasteMode, DEFAULT_KEYMAP, DEFAULT_PASTE_MODE, DEFAULT_THEME, + AllPasteMode, + DEFAULT_KEYMAP, + DEFAULT_PASTE_MODE, + DEFAULT_THEME, + type EditorTheme, + type KeyMapMode, + PasteMode, } from '@growi/editor'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'next-i18next'; -import Image from 'next/image'; import { - Dropdown, DropdownToggle, DropdownMenu, Input, FormGroup, + Dropdown, + DropdownMenu, + DropdownToggle, + FormGroup, + Input, } from 'reactstrap'; import { isIndentSizeForcedAtom } from '~/states/server-configurations'; import { useDeviceLargerThanMd } from '~/states/ui/device'; -import { useCurrentIndentSize, useCurrentIndentSizeActions } from '~/states/ui/editor'; +import { + useCurrentIndentSize, + useCurrentIndentSizeActions, +} from '~/states/ui/editor'; import { useEditorSettings } from '~/stores/editor'; type RadioListItemProps = { - onClick: () => void, - icon?: React.ReactNode, - text: string, - checked?: boolean -} + onClick: () => void; + icon?: React.ReactNode; + text: string; + checked?: boolean; +}; const RadioListItem = (props: RadioListItemProps): JSX.Element => { - const { - onClick, icon, text, checked, - } = props; + const { onClick, icon, text, checked } = props; return (
  • { checked={checked} /> {icon} - +
  • ); }; - type SelectorProps = { - header: string, - onClickBefore: () => void, - items: JSX.Element, -} + header: string; + onClickBefore: () => void; + items: JSX.Element; +}; const Selector = (props: SelectorProps): JSX.Element => { - const { header, onClickBefore, items } = props; return (
    -
    -
      - {items} -
    +
      {items}
    ); - }; - type EditorThemeToLabel = { [key in EditorTheme]: string; -} +}; const EDITORTHEME_LABEL_MAP: EditorThemeToLabel = { defaultlight: 'DefaultLight', @@ -87,33 +100,46 @@ const EDITORTHEME_LABEL_MAP: EditorThemeToLabel = { kimbie: 'Kimbie', }; -const ThemeSelector = memo(({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { - - const { t } = useTranslation(); - const { data: editorSettings, update } = useEditorSettings(); - const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME; - - const listItems = useMemo(() => ( - <> - {(Object.keys(EDITORTHEME_LABEL_MAP) as EditorTheme[]).map((theme) => { - const themeLabel = EDITORTHEME_LABEL_MAP[theme]; - return ( - update({ theme })} text={themeLabel} checked={theme === selectedTheme} /> - ); - })} - - ), [update, selectedTheme]); +const ThemeSelector = memo( + ({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { + const { t } = useTranslation(); + const { data: editorSettings, update } = useEditorSettings(); + const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME; + + const listItems = useMemo( + () => ( + <> + {(Object.keys(EDITORTHEME_LABEL_MAP) as EditorTheme[]).map( + (theme) => { + const themeLabel = EDITORTHEME_LABEL_MAP[theme]; + return ( + update({ theme })} + text={themeLabel} + checked={theme === selectedTheme} + /> + ); + }, + )} + + ), + [update, selectedTheme], + ); - return ( - - ); -}); + return ( + + ); + }, +); ThemeSelector.displayName = 'ThemeSelector'; - type KeyMapModeToLabel = { [key in KeyMapMode]: string; -} +}; const KEYMAP_LABEL_MAP: KeyMapModeToLabel = { default: 'Default', @@ -122,98 +148,135 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = { vscode: 'Visual Studio Code', }; -const KeymapSelector = memo(({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { - - const { t } = useTranslation(); - const { data: editorSettings, update } = useEditorSettings(); - const selectedKeymapMode = editorSettings?.keymapMode ?? DEFAULT_KEYMAP; - - const listItems = useMemo(() => ( - <> - {(Object.keys(KEYMAP_LABEL_MAP) as KeyMapMode[]).map((keymapMode) => { - const keymapLabel = KEYMAP_LABEL_MAP[keymapMode]; - const icon = (keymapMode !== 'default') - ? {keymapMode} - : null; - return ( - update({ keymapMode })} icon={icon} text={keymapLabel} checked={keymapMode === selectedKeymapMode} /> - ); - })} - - ), [update, selectedKeymapMode]); - +const KeymapSelector = memo( + ({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { + const { t } = useTranslation(); + const { data: editorSettings, update } = useEditorSettings(); + const selectedKeymapMode = editorSettings?.keymapMode ?? DEFAULT_KEYMAP; + + const listItems = useMemo( + () => ( + <> + {(Object.keys(KEYMAP_LABEL_MAP) as KeyMapMode[]).map((keymapMode) => { + const keymapLabel = KEYMAP_LABEL_MAP[keymapMode]; + const icon = + keymapMode !== 'default' ? ( + {keymapMode} + ) : null; + return ( + update({ keymapMode })} + icon={icon} + text={keymapLabel} + checked={keymapMode === selectedKeymapMode} + /> + ); + })} + + ), + [update, selectedKeymapMode], + ); - return ( - - ); -}); + return ( + + ); + }, +); KeymapSelector.displayName = 'KeymapSelector'; - const TYPICAL_INDENT_SIZE = [2, 4]; -const IndentSizeSelector = memo(({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { - - const { t } = useTranslation(); - const currentIndentSize = useCurrentIndentSize(); - const { mutate: mutateCurrentIndentSize } = useCurrentIndentSizeActions(); - - const listItems = useMemo(() => ( - <> - {TYPICAL_INDENT_SIZE.map((indent) => { - return ( - mutateCurrentIndentSize(indent)} text={indent.toString()} checked={indent === currentIndentSize} /> - ); - })} - - ), [currentIndentSize, mutateCurrentIndentSize]); +const IndentSizeSelector = memo( + ({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { + const { t } = useTranslation(); + const currentIndentSize = useCurrentIndentSize(); + const { mutate: mutateCurrentIndentSize } = useCurrentIndentSizeActions(); + + const listItems = useMemo( + () => ( + <> + {TYPICAL_INDENT_SIZE.map((indent) => { + return ( + mutateCurrentIndentSize(indent)} + text={indent.toString()} + checked={indent === currentIndentSize} + /> + ); + })} + + ), + [currentIndentSize, mutateCurrentIndentSize], + ); - return ( - - ); -}); + return ( + + ); + }, +); IndentSizeSelector.displayName = 'IndentSizeSelector'; +const PasteSelector = memo( + ({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { + const { t } = useTranslation(); + const { data: editorSettings, update } = useEditorSettings(); + const selectedPasteMode = editorSettings?.pasteMode ?? DEFAULT_PASTE_MODE; + + const listItems = useMemo( + () => ( + <> + {AllPasteMode.map((pasteMode) => { + return ( + update({ pasteMode })} + text={t(`page_edit.paste.${pasteMode}`) ?? ''} + checked={pasteMode === selectedPasteMode} + /> + ); + })} + + ), + [update, t, selectedPasteMode], + ); -const PasteSelector = memo(({ onClickBefore }: { onClickBefore: () => void }): JSX.Element => { - - const { t } = useTranslation(); - const { data: editorSettings, update } = useEditorSettings(); - const selectedPasteMode = editorSettings?.pasteMode ?? DEFAULT_PASTE_MODE; - - const listItems = useMemo(() => ( - <> - {(AllPasteMode).map((pasteMode) => { - return ( - update({ pasteMode })} text={t(`page_edit.paste.${pasteMode}`) ?? ''} checked={pasteMode === selectedPasteMode} /> - ); - })} - - ), [update, t, selectedPasteMode]); - - return ( - - ); -}); + return ( + + ); + }, +); PasteSelector.displayName = 'PasteSelector'; - type SwitchItemProps = { - inputId: string, - onChange: () => void, - checked: boolean, - text: string, + inputId: string; + onChange: () => void; + checked: boolean; + text: string; }; const SwitchItem = memo((props: SwitchItemProps): JSX.Element => { - const { - inputId, onChange, checked, text, - } = props; + const { inputId, onChange, checked, text } = props; return ( - ); }); @@ -265,29 +328,32 @@ const ConfigurationSelector = memo((): JSX.Element => { }); ConfigurationSelector.displayName = 'ConfigurationSelector'; - type ChangeStateButtonProps = { - onClick: () => void, - header: string, - data: string, - disabled?: boolean, -} + onClick: () => void; + header: string; + data: string; + disabled?: boolean; +}; const ChangeStateButton = memo((props: ChangeStateButtonProps): JSX.Element => { - const { - onClick, header, data, disabled, - } = props; + const { onClick, header, data, disabled } = props; return ( - ); }); - const OptionsStatus = { Home: 'Home', Theme: 'Theme', @@ -295,10 +361,9 @@ const OptionsStatus = { Indent: 'Indent', Paste: 'Paste', } as const; -type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus]; +type OptionStatus = (typeof OptionsStatus)[keyof typeof OptionsStatus]; export const OptionsSelector = (): JSX.Element => { - const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -309,12 +374,24 @@ export const OptionsSelector = (): JSX.Element => { const isIndentSizeForced = useAtomValue(isIndentSizeForcedAtom); const [isDeviceLargerThanMd] = useDeviceLargerThanMd(); - if (editorSettings == null || currentIndentSize == null || isIndentSizeForced == null) { + if ( + editorSettings == null || + currentIndentSize == null || + isIndentSizeForced == null + ) { return <>; } return ( - { setStatus(OptionsStatus.Home); setDropdownOpen(!dropdownOpen) }} direction="up" className=""> + { + setStatus(OptionsStatus.Home); + setDropdownOpen(!dropdownOpen); + }} + direction="up" + className="" + > { `} > settings - { - isDeviceLargerThanMd - ? - : <> - } + {isDeviceLargerThanMd ? ( + + ) : ( + <> + )} - { - status === OptionsStatus.Home && ( -
    - -
    - setStatus(OptionsStatus.Theme)} - header={t('page_edit.theme')} - data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''} - /> -
    - setStatus(OptionsStatus.Keymap)} - header={t('page_edit.keymap')} - data={KEYMAP_LABEL_MAP[editorSettings.keymapMode ?? ''] ?? ''} - /> -
    - setStatus(OptionsStatus.Indent)} - header={t('page_edit.indent')} - data={currentIndentSize.toString() ?? ''} - /> -
    - setStatus(OptionsStatus.Paste)} - header={t('page_edit.paste.title')} - data={t(`page_edit.paste.${editorSettings.pasteMode ?? PasteMode.both}`) ?? ''} - /> -
    - -
    - ) - } + {status === OptionsStatus.Home && ( +
    + +
    + setStatus(OptionsStatus.Theme)} + header={t('page_edit.theme')} + data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''} + /> +
    + setStatus(OptionsStatus.Keymap)} + header={t('page_edit.keymap')} + data={KEYMAP_LABEL_MAP[editorSettings.keymapMode ?? ''] ?? ''} + /> +
    + setStatus(OptionsStatus.Indent)} + header={t('page_edit.indent')} + data={currentIndentSize.toString() ?? ''} + /> +
    + setStatus(OptionsStatus.Paste)} + header={t('page_edit.paste.title')} + data={ + t( + `page_edit.paste.${editorSettings.pasteMode ?? PasteMode.both}`, + ) ?? '' + } + /> +
    + +
    + )} {status === OptionsStatus.Theme && ( setStatus(OptionsStatus.Home)} /> - ) - } + )} {status === OptionsStatus.Keymap && ( setStatus(OptionsStatus.Home)} /> - ) - } + )} {status === OptionsStatus.Indent && ( - setStatus(OptionsStatus.Home)} /> - ) - } + setStatus(OptionsStatus.Home)} + /> + )} {status === OptionsStatus.Paste && ( setStatus(OptionsStatus.Home)} /> )} diff --git a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx index c66e6886280..3213978f952 100644 --- a/apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx +++ b/apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx @@ -1,26 +1,38 @@ -import React, { - useCallback, useState, useEffect, type JSX, -} from 'react'; - +import React, { type JSX, useCallback, useEffect, useState } from 'react'; import { PageGrant } from '@growi/core'; import { globalEventTarget } from '@growi/core/dist/utils'; -import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils'; +import { + isTopPage, + isUsersProtectedPages, +} from '@growi/core/dist/utils/page-path-utils'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'next-i18next'; import { - UncontrolledButtonDropdown, Button, - DropdownToggle, DropdownMenu, DropdownItem, Modal, + Button, + DropdownItem, + DropdownMenu, + DropdownToggle, + Modal, + UncontrolledButtonDropdown, } from 'reactstrap'; -import { useIsEditable, useCurrentPageData, useCurrentPagePath } from '~/states/page'; +import { + useCurrentPageData, + useCurrentPagePath, + useIsEditable, +} from '~/states/page'; import { isAclEnabledAtom, isSlackConfiguredAtom, } from '~/states/server-configurations'; import { useDeviceLargerThanMd } from '~/states/ui/device'; import { - useEditorMode, useSelectedGrant, useWaitingSaveProcessing, useIsSlackEnabled, EditorMode, + EditorMode, + useEditorMode, + useIsSlackEnabled, + useSelectedGrant, + useWaitingSaveProcessing, } from '~/states/ui/editor'; import { useSWRxSlackChannels } from '~/stores/editor'; import loggerFactory from '~/utils/logger'; @@ -28,18 +40,19 @@ import loggerFactory from '~/utils/logger'; import { NotAvailable } from '../../NotAvailable'; import { SlackNotification } from '../../SlackNotification'; import type { SaveOptions } from '../PageEditor'; - import { GrantSelector } from './GrantSelector'; - const logger = loggerFactory('growi:SavePageControls'); - -const SavePageButton = (props: { slackChannels: string, isSlackEnabled?: boolean, isDeviceLargerThanMd?: boolean }) => { - +const SavePageButton = (props: { + slackChannels: string; + isSlackEnabled?: boolean; + isDeviceLargerThanMd?: boolean; +}) => { const { t } = useTranslation(); const _isWaitingSaveProcessing = useWaitingSaveProcessing(); - const [isSavePageModalShown, setIsSavePageModalShown] = useState(false); + const [isSavePageModalShown, setIsSavePageModalShown] = + useState(false); const [selectedGrant] = useSelectedGrant(); const { slackChannels, isSlackEnabled = false, isDeviceLargerThanMd } = props; @@ -48,35 +61,52 @@ const SavePageButton = (props: { slackChannels: string, isSlackEnabled?: boolean const save = useCallback(async (): Promise => { // save - globalEventTarget.dispatchEvent(new CustomEvent('saveAndReturnToView', { - detail: { - wip: false, slackChannels, isSlackEnabled, - }, - })); + globalEventTarget.dispatchEvent( + new CustomEvent('saveAndReturnToView', { + detail: { + wip: false, + slackChannels, + isSlackEnabled, + }, + }), + ); }, [isSlackEnabled, slackChannels]); const saveAndOverwriteScopesOfDescendants = useCallback(() => { // save - globalEventTarget.dispatchEvent(new CustomEvent('saveAndReturnToView', { - detail: { - wip: false, overwriteScopesOfDescendants: true, slackChannels, isSlackEnabled, - }, - })); + globalEventTarget.dispatchEvent( + new CustomEvent('saveAndReturnToView', { + detail: { + wip: false, + overwriteScopesOfDescendants: true, + slackChannels, + isSlackEnabled, + }, + }), + ); }, [isSlackEnabled, slackChannels]); const saveAndMakeWip = useCallback(() => { // save - globalEventTarget.dispatchEvent(new CustomEvent('saveAndReturnToView', { - detail: { - wip: true, slackChannels, isSlackEnabled, - }, - })); + globalEventTarget.dispatchEvent( + new CustomEvent('saveAndReturnToView', { + detail: { + wip: true, + slackChannels, + isSlackEnabled, + }, + }), + ); }, [isSlackEnabled, slackChannels]); const labelSubmitButton = t('Update'); - const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton }); + const labelOverwriteScopes = t('page_edit.overwrite_scopes', { + operation: labelSubmitButton, + }); const labelUnpublishPage = t('wip_page.save_as_wip'); - const restrictedGrantOverrideErrorTitle = t('Not available when "anyone with the link" is selected'); + const restrictedGrantOverrideErrorTitle = t( + 'Not available when "anyone with the link" is selected', + ); return ( <> @@ -89,67 +119,89 @@ const SavePageButton = (props: { slackChannels: string, isSlackEnabled?: boolean onClick={save} disabled={isWaitingSaveProcessing} > - {isWaitingSaveProcessing && ( - - )} + {isWaitingSaveProcessing && } {labelSubmitButton} - { - isDeviceLargerThanMd ? ( - <> - - + {isDeviceLargerThanMd ? ( + <> + + + + + {labelOverwriteScopes} + + + + {labelUnpublishPage} + + + + ) : ( + <> + setIsSavePageModalShown(true)} + /> + setIsSavePageModalShown(false)} + > +
    - + - + - - - -
    -
    - - ) - } + + +
    + + + )} ); }; - export const SavePageControls = (): JSX.Element | null => { const { t } = useTranslation('commons'); const currentPage = useCurrentPageData(); @@ -164,7 +216,8 @@ export const SavePageControls = (): JSX.Element | null => { const [isDeviceLargerThanMd] = useDeviceLargerThanMd(); const [slackChannels, setSlackChannels] = useState(''); - const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = useState(false); + const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = + useState(false); // DO NOT dependent on slackChannelsData directly: https://github.com/growilabs/growi/pull/7332 const slackChannelsDataString = slackChannelsData?.toString(); @@ -175,7 +228,6 @@ export const SavePageControls = (): JSX.Element | null => { } }, [editorMode, setIsSlackEnabled, slackChannelsDataString]); - const slackChannelsChangedHandler = useCallback((slackChannels: string) => { setSlackChannels(slackChannels); }, []); @@ -188,89 +240,92 @@ export const SavePageControls = (): JSX.Element | null => { return null; } - const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? ''); + const isGrantSelectorDisabledPage = + isTopPage(currentPage?.path ?? '') || + isUsersProtectedPages(currentPage?.path ?? ''); return (
    - { - isDeviceLargerThanMd ? ( - <> - { - isSlackConfigured && ( -
    - {isSlackEnabled != null && ( - - )} -
    - ) - } + {isDeviceLargerThanMd ? ( + <> + {isSlackConfigured && ( +
    + {isSlackEnabled != null && ( + + )} +
    + )} - { - isAclEnabled && ( -
    - -
    - ) - } + {isAclEnabled && ( +
    + +
    + )} - - - ) : ( - <> - - - -
    - { - isAclEnabled && ( - <> - - - ) - } + + + ) : ( + <> + + + +
    + {isAclEnabled && ( + <> + + + )} - { - isSlackConfigured && isSlackEnabled != null && ( - <> - - - ) - } -
    - -
    + {isSlackConfigured && isSlackEnabled != null && ( + <> + + + )} +
    +
    - - - ) - } +
    +
    + + )}
    ); }; diff --git a/apps/app/src/client/components/PageEditor/GridEditModal.jsx b/apps/app/src/client/components/PageEditor/GridEditModal.jsx index 3c3cdea6081..87c2d58c834 100644 --- a/apps/app/src/client/components/PageEditor/GridEditModal.jsx +++ b/apps/app/src/client/components/PageEditor/GridEditModal.jsx @@ -1,10 +1,7 @@ import React from 'react'; - import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; -import { - Modal, ModalHeader, ModalBody, ModalFooter, -} from 'reactstrap'; +import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import BootstrapGrid from '~/client/models/BootstrapGrid'; @@ -19,7 +16,6 @@ const resSizeObj = { [resSizes.MD_SIZE]: { displayText: 'desktop' }, }; class GridEditModal extends React.Component { - constructor(props) { super(props); @@ -73,7 +69,10 @@ class GridEditModal extends React.Component { pasteCodedGrid() { const { colsRatios, responsiveSize } = this.state; - const convertedHTML = geu.convertRatiosAndSizeToHTML(colsRatios, responsiveSize); + const convertedHTML = geu.convertRatiosAndSizeToHTML( + colsRatios, + responsiveSize, + ); const spaceTab = ' '; const pastedGridData = `::: editable-row\n
    \n${spaceTab}
    \n${convertedHTML}\n${spaceTab}
    \n
    \n:::`; @@ -92,16 +91,22 @@ class GridEditModal extends React.Component { const { t } = this.props; const output = Object.entries(resSizeObj).map((responsiveSizeForMap) => { return ( -
    +
    this.checkResposiveSize(responsiveSizeForMap[0])} + onChange={(e) => this.checkResposiveSize(responsiveSizeForMap[0])} /> -
    @@ -119,17 +124,33 @@ class GridEditModal extends React.Component { {gridDivisions.map((gridDivision) => { const numOfDivisions = gridDivision.numberOfGridDivisions; return ( -
    -
    {numOfDivisions} {t('grid_edit.division')}
    +
    +
    + {numOfDivisions} {t('grid_edit.division')} +
    {gridDivision.mapping.map((gridOneDivision) => { const keyOfRow = `${numOfDivisions}-divisions-${gridOneDivision.join('-')}`; return ( - @@ -145,8 +166,10 @@ class GridEditModal extends React.Component { renderPreview() { const { t } = this.props; - const isMdSelected = this.state.responsiveSize === BootstrapGrid.ResponsiveSize.MD_SIZE; - const isXsSelected = this.state.responsiveSize === BootstrapGrid.ResponsiveSize.XS_SIZE; + const isMdSelected = + this.state.responsiveSize === BootstrapGrid.ResponsiveSize.MD_SIZE; + const isXsSelected = + this.state.responsiveSize === BootstrapGrid.ResponsiveSize.XS_SIZE; return (
    @@ -178,19 +201,20 @@ class GridEditModal extends React.Component { const ratio = isBreakEnabled ? 12 : colsRatio; const key = `grid-preview-col-${i}`; const className = `col-${ratio} grid-edit-border-for-each-cols`; - return ( -
    - ); + return
    ; }); - return ( -
    {convertedHTML}
    - ); + return
    {convertedHTML}
    ; } render() { const { t } = this.props; return ( - + {t('grid_edit.create_bootstrap_4_grid')} @@ -214,7 +238,10 @@ class GridEditModal extends React.Component { > {this.renderSelectedGridPattern()} -
    +
    {this.renderGridDivisionMenu()}
    @@ -231,16 +258,22 @@ class GridEditModal extends React.Component {

    {t('preview')}

    -
    - {this.renderPreview()} -
    +
    {this.renderPreview()}
    - -
    @@ -248,7 +281,6 @@ class GridEditModal extends React.Component { ); } - } const GridEditModalFc = React.forwardRef((props, ref) => { diff --git a/apps/app/src/client/components/PageEditor/GridEditorUtil.js b/apps/app/src/client/components/PageEditor/GridEditorUtil.js index dd744ccfcde..b126f7d786c 100644 --- a/apps/app/src/client/components/PageEditor/GridEditorUtil.js +++ b/apps/app/src/client/components/PageEditor/GridEditorUtil.js @@ -2,7 +2,6 @@ * Utility for grid editor */ class GridEditorUtil { - constructor() { // https://regex101.com/r/7BN2fR/11 this.lineBeginPartOfGridRE = /^:::(\s.*)editable-row$/; @@ -10,19 +9,42 @@ class GridEditorUtil { this.mappingAllGridDivisionPatterns = [ { numberOfGridDivisions: 2, - mapping: [[2, 10], [4, 8], [6, 6], [8, 4], [10, 2]], + mapping: [ + [2, 10], + [4, 8], + [6, 6], + [8, 4], + [10, 2], + ], }, { numberOfGridDivisions: 3, - mapping: [[2, 5, 5], [5, 2, 5], [5, 5, 2], [4, 4, 4], [3, 3, 6], [3, 6, 3], [6, 3, 3]], + mapping: [ + [2, 5, 5], + [5, 2, 5], + [5, 5, 2], + [4, 4, 4], + [3, 3, 6], + [3, 6, 3], + [6, 3, 3], + ], }, { numberOfGridDivisions: 4, - mapping: [[2, 2, 4, 4], [4, 4, 2, 2], [2, 4, 2, 4], [4, 2, 4, 2], [3, 3, 3, 3], [2, 2, 2, 6], [6, 2, 2, 2]], + mapping: [ + [2, 2, 4, 4], + [4, 4, 2, 2], + [2, 4, 2, 4], + [4, 2, 4, 2], + [3, 3, 3, 3], + [2, 2, 2, 6], + [6, 2, 2, 2], + ], }, ]; this.isInGridBlock = this.isInGridBlock.bind(this); - this.replaceGridWithHtmlWithEditor = this.replaceGridWithHtmlWithEditor.bind(this); + this.replaceGridWithHtmlWithEditor = + this.replaceGridWithHtmlWithEditor.bind(this); } /** @@ -34,7 +56,7 @@ class GridEditorUtil { if (bog === null || eog === null) { return false; } - return (JSON.stringify(bog) !== JSON.stringify(eog)); + return JSON.stringify(bog) !== JSON.stringify(eog); } /** @@ -98,7 +120,10 @@ class GridEditorUtil { const lastLine = editor.getDoc().lastLine(); if (this.lineEndPartOfGridRE.test(editor.getDoc().getLine(curPos.line))) { - return { line: curPos.line, ch: editor.getDoc().getLine(curPos.line).length }; + return { + line: curPos.line, + ch: editor.getDoc().getLine(curPos.line).length, + }; } let line = curPos.line + 1; @@ -147,7 +172,6 @@ class GridEditorUtil { }); return cols.join('\n'); } - } // singleton pattern diff --git a/apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx b/apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx index 1a5bc6e6aec..eb5ff74a2aa 100644 --- a/apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx +++ b/apps/app/src/client/components/PageEditor/HandsontableModal/HandsontableModal.tsx @@ -1,19 +1,35 @@ import React, { - useState, useCallback, useMemo, useEffect, type JSX, + type JSX, + useCallback, + useEffect, + useMemo, + useState, } from 'react'; - -import { MarkdownTable, useHandsontableModalForEditorStatus, useHandsontableModalForEditorActions } from '@growi/editor'; +import { + MarkdownTable, + useHandsontableModalForEditorActions, + useHandsontableModalForEditorStatus, +} from '@growi/editor'; import { HotTable } from '@handsontable/react'; import type Handsontable from 'handsontable'; import { useTranslation } from 'next-i18next'; import { Collapse, - Modal, ModalHeader, ModalBody, ModalFooter, + Modal, + ModalBody, + ModalFooter, + ModalHeader, } from 'reactstrap'; import { debounce } from 'throttle-debounce'; -import { replaceFocusedMarkdownTableWithEditor, getMarkdownTable } from '~/client/components/PageEditor/markdown-table-util-for-editor'; -import { useHandsontableModalActions, useHandsontableModalStatus } from '~/states/ui/modal/handsontable'; +import { + getMarkdownTable, + replaceFocusedMarkdownTableWithEditor, +} from '~/client/components/PageEditor/markdown-table-util-for-editor'; +import { + useHandsontableModalActions, + useHandsontableModalStatus, +} from '~/states/ui/modal/handsontable'; import ExpandOrContractButton from '../../ExpandOrContractButton'; import { MarkdownTableDataImportForm } from '../MarkdownTableDataImportForm'; @@ -42,7 +58,9 @@ type HandsontableModalSubstanceProps = { /** * HandsontableModalSubstance - Presentation component (heavy logic, rendered only when isOpen) */ -const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX.Element => { +const HandsontableModalSubstance = ( + props: HandsontableModalSubstanceProps, +): JSX.Element => { const { initialTable, autoFormatMarkdownTable, @@ -99,11 +117,16 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX * However, all operations are reflected in the data to be saved because the HotTable data is used when the save method is called. */ const [hotTable, setHotTable] = useState(); - const [hotTableContainer, setHotTableContainer] = useState(); - const [isDataImportAreaExpanded, setIsDataImportAreaExpanded] = useState(false); - const [markdownTable, setMarkdownTable] = useState(defaultMarkdownTable); - const [markdownTableOnInit, setMarkdownTableOnInit] = useState(defaultMarkdownTable); - const [handsontableHeight, setHandsontableHeight] = useState(DEFAULT_HOT_HEIGHT); + const [hotTableContainer, setHotTableContainer] = + useState(); + const [isDataImportAreaExpanded, setIsDataImportAreaExpanded] = + useState(false); + const [markdownTable, setMarkdownTable] = + useState(defaultMarkdownTable); + const [markdownTableOnInit, setMarkdownTableOnInit] = + useState(defaultMarkdownTable); + const [handsontableHeight, setHandsontableHeight] = + useState(DEFAULT_HOT_HEIGHT); const [handsontableWidth, setHandsontableWidth] = useState(0); // Memoize window resize handler @@ -117,13 +140,15 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX }, [hotTableContainer]); // Memoize debounced handler - const debouncedHandleWindowExpandedChange = useMemo(() => ( - debounce(100, handleWindowExpandedChange) - ), [handleWindowExpandedChange]); + const debouncedHandleWindowExpandedChange = useMemo( + () => debounce(100, handleWindowExpandedChange), + [handleWindowExpandedChange], + ); // Initialize table data when component mounts (modal opens) useEffect(() => { - const initTableInstance = initialTable == null ? defaultMarkdownTable : initialTable.clone(); + const initTableInstance = + initialTable == null ? defaultMarkdownTable : initialTable.clone(); setMarkdownTable(initialTable ?? defaultMarkdownTable); setMarkdownTableOnInit(initTableInstance); debouncedHandleWindowExpandedChange(); @@ -172,7 +197,13 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX onSave(newMarkdownTable); cancel(); - }, [hotTable, markdownTable.options.align, autoFormatMarkdownTable, onSave, cancel]); + }, [ + hotTable, + markdownTable.options.align, + autoFormatMarkdownTable, + onSave, + cancel, + ]); const beforeColumnResizeHandler = (currentColumn) => { /* @@ -181,7 +212,6 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX * * At the moment, using 'afterColumnResizeHandler' instead. */ - // store column index // this.manuallyResizedColumnIndicesSet.add(currentColumn); }; @@ -236,7 +266,12 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX for (let i = 0; i < align.length; i++) { for (let j = 0; j < hotInstance.countRows(); j++) { - hotInstance.setCellMeta(j, i, 'className', MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING[align[i]]); + hotInstance.setCellMeta( + j, + i, + 'className', + MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING[align[i]], + ); } } hotInstance.render(); @@ -282,49 +317,48 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX const removed = align.splice(columns[0], columns.length); /* - * The following is a description of the algorithm for the alignment synchronization. - * - * Consider the case where the target is X and the columns are [2,3] and data is as follows. - * - * 0 1 2 3 4 5 (insert position number) - * +-+-+-+-+-+ - * | | | | | | - * +-+-+-+-+-+ - * 0 1 2 3 4 (column index number) - * - * At first, remove columns by the splice. - * - * 0 1 2 4 5 - * +-+-+ +-+ - * | | | | | - * +-+-+ +-+ - * 0 1 4 - * - * Next, insert those columns into a new position. - * However the target number is a insert position number before deletion, it may be changed. - * These are changed as follows. - * - * Before: - * 0 1 2 4 5 - * +-+-+ +-+ - * | | | | | - * +-+-+ +-+ - * - * After: - * 0 1 2 2 3 - * +-+-+ +-+ - * | | | | | - * +-+-+ +-+ - * - * If X is 0, 1 or 2, that is, lower than columns[0], the target number is not changed. - * If X is 4 or 5, that is, higher than columns[columns.length - 1], the target number is modified to the original value minus columns.length. - * - */ + * The following is a description of the algorithm for the alignment synchronization. + * + * Consider the case where the target is X and the columns are [2,3] and data is as follows. + * + * 0 1 2 3 4 5 (insert position number) + * +-+-+-+-+-+ + * | | | | | | + * +-+-+-+-+-+ + * 0 1 2 3 4 (column index number) + * + * At first, remove columns by the splice. + * + * 0 1 2 4 5 + * +-+-+ +-+ + * | | | | | + * +-+-+ +-+ + * 0 1 4 + * + * Next, insert those columns into a new position. + * However the target number is a insert position number before deletion, it may be changed. + * These are changed as follows. + * + * Before: + * 0 1 2 4 5 + * +-+-+ +-+ + * | | | | | + * +-+-+ +-+ + * + * After: + * 0 1 2 2 3 + * +-+-+ +-+ + * | | | | | + * +-+-+ +-+ + * + * If X is 0, 1 or 2, that is, lower than columns[0], the target number is not changed. + * If X is 4 or 5, that is, higher than columns[columns.length - 1], the target number is modified to the original value minus columns.length. + * + */ let insertPosition = 0; if (target <= columns[0]) { insertPosition = target; - } - else if (columns[columns.length - 1] < target) { + } else if (columns[columns.length - 1] < target) { insertPosition = target - columns.length; } @@ -334,7 +368,9 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX setMarkdownTable((prevMarkdownTable) => { // change only align info, so share table data to avoid redundant copy - const newMarkdownTable = new MarkdownTable(prevMarkdownTable.table, { align }); + const newMarkdownTable = new MarkdownTable(prevMarkdownTable.table, { + align, + }); return newMarkdownTable; }); @@ -347,7 +383,9 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX const align = (direction: string, startCol: number, endCol: number) => { setMarkdownTable((prevMarkdownTable) => { // change only align info, so share table data to avoid redundant copy - const newMarkdownTable = new MarkdownTable(prevMarkdownTable.table, { align: [].concat(prevMarkdownTable.options.align) }); + const newMarkdownTable = new MarkdownTable(prevMarkdownTable.table, { + align: [].concat(prevMarkdownTable.options.align), + }); for (let i = startCol; i <= endCol; i++) { newMarkdownTable.options.align[i] = direction; } @@ -365,8 +403,14 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX const selectedRange = hotTable.hotInstance.getSelectedRange(); if (selectedRange == null) return; - const startCol = selectedRange[0].from.col < selectedRange[0].to.col ? selectedRange[0].from.col : selectedRange[0].to.col; - const endCol = selectedRange[0].from.col < selectedRange[0].to.col ? selectedRange[0].to.col : selectedRange[0].from.col; + const startCol = + selectedRange[0].from.col < selectedRange[0].to.col + ? selectedRange[0].from.col + : selectedRange[0].to.col; + const endCol = + selectedRange[0].from.col < selectedRange[0].to.col + ? selectedRange[0].to.col + : selectedRange[0].from.col; align(direction, startCol, endCol); }; @@ -413,15 +457,23 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX { name: 'Left', key: 'align_columns:1', - callback: (key, selection) => { align('l', selection[0].start.col, selection[0].end.col) }, - }, { + callback: (key, selection) => { + align('l', selection[0].start.col, selection[0].end.col); + }, + }, + { name: 'Center', key: 'align_columns:2', - callback: (key, selection) => { align('c', selection[0].start.col, selection[0].end.col) }, - }, { + callback: (key, selection) => { + align('c', selection[0].start.col, selection[0].end.col); + }, + }, + { name: 'Right', key: 'align_columns:3', - callback: (key, selection) => { align('r', selection[0].start.col, selection[0].end.col) }, + callback: (key, selection) => { + align('r', selection[0].start.col, selection[0].end.col); + }, }, ], }, @@ -442,7 +494,12 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX contractWindow={contractWindow} expandWindow={expandWindow} /> - + ); @@ -462,22 +519,51 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX onClick={toggleDataImportArea} > {t('handsontable_modal.data_import')} - {isDataImportAreaExpanded ? 'expand_less' : 'expand_more'} + + {isDataImportAreaExpanded ? 'expand_less' : 'expand_more'} +
    - - -
    - +
    @@ -505,10 +591,24 @@ const HandsontableModalSubstance = (props: HandsontableModalSubstanceProps): JSX
    - +
    - - + +
    @@ -526,7 +626,8 @@ export const HandsontableModal = (): JSX.Element => { // for Editor const handsontableModalForEditorData = useHandsontableModalForEditorStatus(); - const { close: closeHandsontableModalForEditor } = useHandsontableModalForEditorActions(); + const { close: closeHandsontableModalForEditor } = + useHandsontableModalForEditorActions(); const isOpenedForView = handsontableModalData.isOpened; const isOpenedForEditor = handsontableModalForEditorData.isOpened; @@ -541,10 +642,15 @@ export const HandsontableModal = (): JSX.Element => { return editor != null ? getMarkdownTable(editor) : undefined; } return handsontableModalData.table; - }, [isOpenedForEditor, handsontableModalForEditorData.editor, handsontableModalData.table]); + }, [ + isOpenedForEditor, + handsontableModalForEditorData.editor, + handsontableModalData.table, + ]); // Determine autoFormatMarkdownTable based on mode - const autoFormatMarkdownTable = handsontableModalData.autoFormatMarkdownTable ?? false; + const autoFormatMarkdownTable = + handsontableModalData.autoFormatMarkdownTable ?? false; const toggle = useCallback(() => { closeHandsontableModal(); @@ -561,17 +667,23 @@ export const HandsontableModal = (): JSX.Element => { }, []); // Create save handler based on mode - const handleSave = useCallback((newMarkdownTable: MarkdownTable) => { - if (isOpenedForEditor) { - const editor = handsontableModalForEditorData.editor; - if (editor != null) { - replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable); + const handleSave = useCallback( + (newMarkdownTable: MarkdownTable) => { + if (isOpenedForEditor) { + const editor = handsontableModalForEditorData.editor; + if (editor != null) { + replaceFocusedMarkdownTableWithEditor(editor, newMarkdownTable); + } + } else { + handsontableModalData.onSave?.(newMarkdownTable); } - } - else { - handsontableModalData.onSave?.(newMarkdownTable); - } - }, [isOpenedForEditor, handsontableModalForEditorData.editor, handsontableModalData]); + }, + [ + isOpenedForEditor, + handsontableModalForEditorData.editor, + handsontableModalData, + ], + ); return ( { const HandsontableModal = useLazyLoader( 'handsontable-modal', - () => import('./HandsontableModal').then(mod => ({ default: mod.HandsontableModal })), + () => + import('./HandsontableModal').then((mod) => ({ + default: mod.HandsontableModal, + })), status?.isOpened || statusForEditor?.isOpened || false, ); diff --git a/apps/app/src/client/components/PageEditor/LinkEditModal/LinkEditModal.tsx b/apps/app/src/client/components/PageEditor/LinkEditModal/LinkEditModal.tsx index f6871809ea0..5dddf211c9a 100644 --- a/apps/app/src/client/components/PageEditor/LinkEditModal/LinkEditModal.tsx +++ b/apps/app/src/client/components/PageEditor/LinkEditModal/LinkEditModal.tsx @@ -1,17 +1,17 @@ -import React, { - useEffect, useState, useCallback, -} from 'react'; - -import path from 'path'; - +import type React from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Linker } from '@growi/editor/dist/models/linker'; -import { useLinkEditModalStatus, useLinkEditModalActions } from '@growi/editor/dist/states/modal/link-edit'; +import { + useLinkEditModalActions, + useLinkEditModalStatus, +} from '@growi/editor/dist/states/modal/link-edit'; import { useTranslation } from 'next-i18next'; +import path from 'path'; import { Modal, - ModalHeader, ModalBody, ModalFooter, + ModalHeader, Popover, PopoverBody, } from 'reactstrap'; @@ -25,10 +25,8 @@ import loggerFactory from '~/utils/logger'; import SearchTypeahead from '../../SearchTypeahead'; import Preview from '../Preview'; - import styles from './LinkEditPreview.module.scss'; - const logger = loggerFactory('growi:components:LinkEditModal'); /** @@ -52,11 +50,16 @@ const LinkEditModalSubstance: React.FC = () => { const [permalink, setPermalink] = useState(''); const [isPreviewOpen, setIsPreviewOpen] = useState(false); - const getRootPath = useCallback((type: string) => { - // rootPaths of md link and pukiwiki link are different - if (currentPath == null) return ''; - return type === Linker.types.markdownLink ? path.dirname(currentPath) : currentPath; - }, [currentPath]); + const getRootPath = useCallback( + (type: string) => { + // rootPaths of md link and pukiwiki link are different + if (currentPath == null) return ''; + return type === Linker.types.markdownLink + ? path.dirname(currentPath) + : currentPath; + }, + [currentPath], + ); // parse link, link is ... // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga') @@ -64,52 +67,61 @@ const LinkEditModalSubstance: React.FC = () => { // case-3. relative path of this growi's page (ex. '../fuga', 'hoge') // case-4. external link (ex. 'https://growi.org') // case-5. the others (ex. '') - const parseLinkAndSetState = useCallback((link: string, type: string) => { - // create url from link, add dummy origin if link is not valid url. - // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4) - // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5) - let isFqcn = false; - let isUseRelativePath = false; - let url; - try { - const url = new URL(link, 'http://example.com'); - isFqcn = url.origin !== 'http://example.com'; - } - catch (err) { - logger.debug(err); - } - - // case-1: when link is this growi's page url, return pathname only - let reshapedLink = url != null && url.origin === window.location.origin - ? decodeURIComponent(url.pathname) - : link; + const parseLinkAndSetState = useCallback( + (link: string, type: string) => { + // create url from link, add dummy origin if link is not valid url. + // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4) + // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5) + let isFqcn = false; + let isUseRelativePath = false; + let url; + try { + const url = new URL(link, 'http://example.com'); + isFqcn = url.origin !== 'http://example.com'; + } catch (err) { + logger.debug(err); + } - // case-3 - if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') { - isUseRelativePath = true; - const rootPath = getRootPath(type); - reshapedLink = path.resolve(rootPath, reshapedLink); - } + // case-1: when link is this growi's page url, return pathname only + let reshapedLink = + url != null && url.origin === window.location.origin + ? decodeURIComponent(url.pathname) + : link; + + // case-3 + if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') { + isUseRelativePath = true; + const rootPath = getRootPath(type); + reshapedLink = path.resolve(rootPath, reshapedLink); + } - setLinkInputValue(reshapedLink); - setIsUseRelativePath(isUseRelativePath); - }, [getRootPath]); + setLinkInputValue(reshapedLink); + setIsUseRelativePath(isUseRelativePath); + }, + [getRootPath], + ); useEffect(() => { - if (linkEditModalStatus == null) { return } - const { label = '', link = '' } = linkEditModalStatus.defaultMarkdownLink ?? {}; - const { type = Linker.types.markdownLink } = linkEditModalStatus.defaultMarkdownLink ?? {}; + if (linkEditModalStatus == null) { + return; + } + const { label = '', link = '' } = + linkEditModalStatus.defaultMarkdownLink ?? {}; + const { type = Linker.types.markdownLink } = + linkEditModalStatus.defaultMarkdownLink ?? {}; parseLinkAndSetState(link, type); setLabelInputValue(label); setIsUsePermanentLink(false); setPermalink(''); setLinkerType(type); - }, [linkEditModalStatus, parseLinkAndSetState]); const toggleIsUseRelativePath = useCallback(() => { - if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) { + if ( + !linkInputValue.startsWith('/') || + linkerType === Linker.types.growiLink + ) { return; } @@ -128,7 +140,7 @@ const LinkEditModalSubstance: React.FC = () => { setIsUseRelativePath(false); }, [permalink, linkerType, isUsePermanentLink]); - const setMarkdownHandler = useCallback(async() => { + const setMarkdownHandler = useCallback(async () => { const path = linkInputValue; let markdown = ''; let pagePath = ''; @@ -137,20 +149,23 @@ const LinkEditModalSubstance: React.FC = () => { if (path.startsWith('/')) { try { const pathWithoutFragment = new URL(path, 'http://dummy').pathname; - const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1)); + const isPermanentLink = validator.isMongoId( + pathWithoutFragment.slice(1), + ); const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null; - const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId }); + const { data } = await apiv3Get('/page', { + path: pathWithoutFragment, + page_id: pageId, + }); const { page } = data; markdown = page.revision.body; pagePath = page.path; permalink = page.id; - } - catch (err) { + } catch (err) { setPreviewError(err.message); } - } - else { + } else { setPreviewError(t('link_edit.page_not_found_in_preview', { path })); } @@ -160,11 +175,13 @@ const LinkEditModalSubstance: React.FC = () => { }, [linkInputValue, t]); const generateLink = useCallback(() => { - let reshapedLink = linkInputValue; if (isUseRelativePath) { const rootPath = getRootPath(linkerType); - reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue); + reshapedLink = + rootPath === linkInputValue + ? '.' + : path.relative(rootPath, linkInputValue); } if (isUsePermanentLink && permalink != null) { @@ -172,7 +189,15 @@ const LinkEditModalSubstance: React.FC = () => { } return new Linker(linkerType, labelInputValue, reshapedLink); - }, [linkInputValue, isUseRelativePath, getRootPath, linkerType, isUsePermanentLink, permalink, labelInputValue]); + }, [ + linkInputValue, + isUseRelativePath, + getRootPath, + linkerType, + isUsePermanentLink, + permalink, + labelInputValue, + ]); const renderLinkPreview = (): React.JSX.Element => { const linker = generateLink(); @@ -180,12 +205,18 @@ const LinkEditModalSubstance: React.FC = () => {

    Markdown

    -

    {linker.generateMarkdownText()}

    +

    + {linker.generateMarkdownText()} +

    - arrow_right - arrow_drop_down + + arrow_right + + + arrow_drop_down +
    @@ -212,16 +243,22 @@ const LinkEditModalSubstance: React.FC = () => { setLabelInputValue(label); }, []); - const handleChangeLinkInput = useCallback((link) => { - let useRelativePath = isUseRelativePath; - if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) { - useRelativePath = false; - } - setLinkInputValue(link); - setIsUseRelativePath(useRelativePath); - setIsUsePermanentLink(false); - setPermalink(''); - }, [linkInputValue, isUseRelativePath, linkerType]); + const handleChangeLinkInput = useCallback( + (link) => { + let useRelativePath = isUseRelativePath; + if ( + !linkInputValue.startsWith('/') || + linkerType === Linker.types.growiLink + ) { + useRelativePath = false; + } + setLinkInputValue(link); + setIsUseRelativePath(useRelativePath); + setIsUsePermanentLink(false); + setPermalink(''); + }, + [linkInputValue, isUseRelativePath, linkerType], + ); const save = useCallback(() => { const linker = generateLink(); @@ -233,7 +270,7 @@ const LinkEditModalSubstance: React.FC = () => { close(); }, [generateLink, linkEditModalStatus, close]); - const toggleIsPreviewOpen = useCallback(async() => { + const toggleIsPreviewOpen = useCallback(async () => { // open popover if (!isPreviewOpen) { setMarkdownHandler(); @@ -259,18 +296,36 @@ const LinkEditModalSubstance: React.FC = () => { autoFocus />
    - - + - {markdown != null && pagePath != null && rendererOptions != null - && ( -
    - -
    - ) - } + {markdown != null && + pagePath != null && + rendererOptions != null && ( +
    + +
    + )}
    @@ -286,7 +341,7 @@ const LinkEditModalSubstance: React.FC = () => { className="form-control" id="label" value={labelInputValue} - onChange={e => handleChangeLabelInput(e.target.value)} + onChange={(e) => handleChangeLabelInput(e.target.value)} disabled={linkerType === Linker.types.growiLink} placeholder={linkInputValue} /> @@ -302,7 +357,9 @@ const LinkEditModalSubstance: React.FC = () => {
    - +
    { type="checkbox" checked={isUseRelativePath} onChange={toggleIsUseRelativePath} - disabled={!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink} + disabled={ + !linkInputValue.startsWith('/') || + linkerType === Linker.types.growiLink + } /> -
    @@ -324,9 +387,14 @@ const LinkEditModalSubstance: React.FC = () => { type="checkbox" checked={isUsePermanentLink} onChange={toggleIsUsePamanentLink} - disabled={permalink === '' || linkerType === Linker.types.growiLink} + disabled={ + permalink === '' || linkerType === Linker.types.growiLink + } /> -
    @@ -358,11 +426,19 @@ const LinkEditModalSubstance: React.FC = () => {
    - { previewError && {previewError}} - - @@ -380,7 +456,13 @@ export const LinkEditModal = (): React.JSX.Element => { const isOpened = linkEditModalStatus?.isOpened ?? false; return ( - + {isOpened && } ); diff --git a/apps/app/src/client/components/PageEditor/LinkEditModal/dynamic.tsx b/apps/app/src/client/components/PageEditor/LinkEditModal/dynamic.tsx index 3613e4f926f..106543b3404 100644 --- a/apps/app/src/client/components/PageEditor/LinkEditModal/dynamic.tsx +++ b/apps/app/src/client/components/PageEditor/LinkEditModal/dynamic.tsx @@ -1,5 +1,4 @@ import type { JSX } from 'react'; - import { useLinkEditModalStatus } from '@growi/editor/dist/states/modal/link-edit'; import { useLazyLoader } from '~/components/utils/use-lazy-loader'; @@ -11,7 +10,8 @@ export const LinkEditModalLazyLoaded = (): JSX.Element => { const LinkEditModal = useLazyLoader( 'link-edit-modal', - () => import('./LinkEditModal').then(mod => ({ default: mod.LinkEditModal })), + () => + import('./LinkEditModal').then((mod) => ({ default: mod.LinkEditModal })), status?.isOpened ?? false, ); diff --git a/apps/app/src/client/components/PageEditor/MarkdownListUtil.js b/apps/app/src/client/components/PageEditor/MarkdownListUtil.js index aea9ccff9a4..dbdc40dad8b 100644 --- a/apps/app/src/client/components/PageEditor/MarkdownListUtil.js +++ b/apps/app/src/client/components/PageEditor/MarkdownListUtil.js @@ -2,14 +2,16 @@ * Utility for markdown list */ class MarkdownListUtil { - constructor() { // https://github.com/codemirror/CodeMirror/blob/c7853a989c77bb9f520c9c530cbe1497856e96fc/addon/edit/continuelist.js#L14 // https://regex101.com/r/7BN2fR/5 - this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/; - this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/; + this.indentAndMarkRE = + /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/; + this.indentAndMarkOnlyRE = + /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/; - this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this); + this.newlineAndIndentContinueMarkdownList = + this.newlineAndIndentContinueMarkdownList.bind(this); this.pasteText = this.pasteText.bind(this); } @@ -23,13 +25,11 @@ class MarkdownListUtil { if (this.indentAndMarkOnlyRE.test(strFromBol)) { // clear current line and end list editor.replaceBolToCurrentPos('\n'); - } - else if (this.indentAndMarkRE.test(strFromBol)) { + } else if (this.indentAndMarkRE.test(strFromBol)) { // continue list const indentAndMark = strFromBol.match(this.indentAndMarkRE)[0]; editor.insertText(`\n${indentAndMark}`); - } - else { + } else { editor.insertLinebreak(); } } @@ -124,7 +124,6 @@ class MarkdownListUtil { return isListful; } - } // singleton pattern diff --git a/apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx b/apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx index 11f29d53868..f4886ba3a57 100644 --- a/apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx +++ b/apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx @@ -1,23 +1,21 @@ -import React, { useState, type JSX } from 'react'; - +import React, { type JSX, useState } from 'react'; import { MarkdownTable } from '@growi/editor'; import { useTranslation } from 'next-i18next'; -import { - Button, - Collapse, -} from 'reactstrap'; - +import { Button, Collapse } from 'reactstrap'; type MarkdownTableDataImportFormProps = { - onCancel: () => void, - onImport: (table: MarkdownTable) => void, -} - -export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormProps): JSX.Element => { + onCancel: () => void; + onImport: (table: MarkdownTable) => void; +}; +export const MarkdownTableDataImportForm = ( + props: MarkdownTableDataImportFormProps, +): JSX.Element => { const { onCancel, onImport } = props; - const { t } = useTranslation('commons', { keyPrefix: 'handsontable_modal.data_import_form' }); + const { t } = useTranslation('commons', { + keyPrefix: 'handsontable_modal.data_import_form', + }); const [dataFormat, setDataFormat] = useState('csv'); const [data, setData] = useState(''); @@ -44,8 +42,7 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr const markdownTable = convertFormDataToMarkdownTable(); onImport(markdownTable); setParserErrorMessage(null); - } - catch (e) { + } catch (e) { setParserErrorMessage(e.message); } }; @@ -53,12 +50,16 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr return (
    - +
    - + + ); diff --git a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx index dc28c4c8d83..80332907b43 100644 --- a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx +++ b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx @@ -1,23 +1,15 @@ /* eslint-disable react/no-danger */ import React from 'react'; - import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; -import { - Modal, - ModalHeader, - ModalBody, - ModalFooter, -} from 'reactstrap'; +import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option'; // import { toastSuccess, toastError } from '~/client/util/toastr'; - class ImportCollectionConfigurationModal extends React.Component { - constructor(props) { super(props); @@ -46,9 +38,7 @@ class ImportCollectionConfigurationModal extends React.Component { } updateOption() { - const { - collectionName, onOptionChange, onClose, - } = this.props; + const { collectionName, onOptionChange, onClose } = this.props; if (onOptionChange != null) { onOptionChange(collectionName, this.state.option); @@ -61,7 +51,8 @@ class ImportCollectionConfigurationModal extends React.Component { const { t } = this.props; const { option } = this.state; - const translationBase = 'admin:importer_management.growi_settings.configuration.pages'; + const translationBase = + 'admin:importer_management.growi_settings.configuration.pages'; /* eslint-disable react/no-unescaped-entities */ return ( @@ -72,11 +63,21 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })} + onChange={() => + this.changeHandler({ + isOverwriteAuthorWithCurrentUser: + !option.isOverwriteAuthorWithCurrentUser, + }) + } />
    @@ -85,13 +86,23 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })} + onChange={() => + this.changeHandler({ + makePublicForGrant2: !option.makePublicForGrant2, + }) + } />
    @@ -101,13 +112,23 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })} + onChange={() => + this.changeHandler({ + makePublicForGrant4: !option.makePublicForGrant4, + }) + } />
    @@ -117,13 +138,23 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })} + onChange={() => + this.changeHandler({ + makePublicForGrant5: !option.makePublicForGrant5, + }) + } />
    @@ -133,11 +164,20 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })} + onChange={() => + this.changeHandler({ + initPageMetadatas: !option.initPageMetadatas, + }) + } />
    @@ -149,7 +189,8 @@ class ImportCollectionConfigurationModal extends React.Component { const { t } = this.props; const { option } = this.state; - const translationBase = 'admin:importer_management.growi_settings.configuration.revisions'; + const translationBase = + 'admin:importer_management.growi_settings.configuration.revisions'; /* eslint-disable react/no-unescaped-entities */ return ( @@ -160,11 +201,21 @@ class ImportCollectionConfigurationModal extends React.Component { type="checkbox" className="form-check-input" checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning - onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })} + onChange={() => + this.changeHandler({ + isOverwriteAuthorWithCurrentUser: + !option.isOverwriteAuthorWithCurrentUser, + }) + } />
    @@ -189,23 +240,36 @@ class ImportCollectionConfigurationModal extends React.Component { } return ( - + {`'${collectionName}'`} Configuration - - {contents} - + {contents} - - + + ); } - } ImportCollectionConfigurationModal.propTypes = { diff --git a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx index be5a48b3e36..bfc0106f4c5 100644 --- a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx +++ b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx @@ -1,17 +1,23 @@ import React from 'react'; - import PropTypes from 'prop-types'; import { - Progress, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, + DropdownItem, + DropdownMenu, + DropdownToggle, + Progress, + UncontrolledDropdown, } from 'reactstrap'; import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option'; - const MODE_ATTR_MAP = { insert: { color: 'info', icon: 'add_circle', label: 'Insert' }, upsert: { color: 'success', icon: 'add_circle', label: 'Upsert' }, - flushAndInsert: { color: 'danger', icon: 'autorenew', label: 'Flush and Insert' }, + flushAndInsert: { + color: 'danger', + icon: 'autorenew', + label: 'Flush and Insert', + }, }; export const DEFAULT_MODE = 'insert'; @@ -23,13 +29,13 @@ export const MODE_RESTRICTED_COLLECTION = { }; export default class ImportCollectionItem extends React.Component { - constructor(props) { super(props); this.changeHandler = this.changeHandler.bind(this); this.modeSelectedHandler = this.modeSelectedHandler.bind(this); - this.configButtonClickedHandler = this.configButtonClickedHandler.bind(this); + this.configButtonClickedHandler = + this.configButtonClickedHandler.bind(this); this.errorLinkClickedHandler = this.errorLinkClickedHandler.bind(this); } @@ -76,13 +82,16 @@ export default class ImportCollectionItem extends React.Component { renderModeLabel(mode, isColorized = false) { const attrMap = MODE_ATTR_MAP[mode]; const className = isColorized ? `text-${attrMap.color}` : ''; - return {attrMap.icon} {attrMap.label}; + return ( + + {attrMap.icon}{' '} + {attrMap.label} + + ); } renderCheckbox() { - const { - collectionName, isSelected, isImporting, - } = this.props; + const { collectionName, isSelected, isImporting } = this.props; return (
    @@ -96,7 +105,10 @@ export default class ImportCollectionItem extends React.Component { disabled={isImporting} onChange={this.changeHandler} /> -
    @@ -104,22 +116,26 @@ export default class ImportCollectionItem extends React.Component { } renderModeSelector() { - const { - collectionName, option, isImporting, - } = this.props; + const { collectionName, option, isImporting } = this.props; const currentMode = option?.mode || 'insert'; const attrMap = MODE_ATTR_MAP[currentMode]; - const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP); + const modes = + MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP); return ( Mode:  - + {this.renderModeLabel(currentMode)} - {modes.map(mode => ( + {modes.map((mode) => ( this.modeSelectedHandler(mode)} @@ -141,7 +157,9 @@ export default class ImportCollectionItem extends React.Component { type="button" className="btn btn-outline-secondary btn-sm p-1 ms-2" disabled={isImporting || !isConfigButtonAvailable} - onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null} + onClick={ + isConfigButtonAvailable ? this.configButtonClickedHandler : null + } > settings @@ -149,17 +167,37 @@ export default class ImportCollectionItem extends React.Component { } renderProgressBar() { - const { - isImporting, insertedCount, modifiedCount, errorsCount, - } = this.props; + const { isImporting, insertedCount, modifiedCount, errorsCount } = + this.props; const total = insertedCount + modifiedCount + errorsCount; return ( - - - + + + ); } @@ -174,20 +212,35 @@ export default class ImportCollectionItem extends React.Component { const { insertedCount, modifiedCount, errorsCount } = this.props; return (
    - {insertedCount} Inserted,  - {modifiedCount} Modified,  - { errorsCount > 0 - ? {errorsCount} Failed - : 0 Failed - } + + {insertedCount} Inserted + + ,  + + {modifiedCount} Modified + + ,  + {errorsCount > 0 ? ( + + + {errorsCount} Failed + + + ) : ( + + 0 Failed + + )}
    ); } render() { - const { - isSelected, isHideProgress, - } = this.props; + const { isSelected, isHideProgress } = this.props; return (
    @@ -211,7 +264,6 @@ export default class ImportCollectionItem extends React.Component {
    ); } - } ImportCollectionItem.propTypes = { diff --git a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx index db074fd678c..e7f6329cc7d 100644 --- a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx +++ b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx @@ -1,31 +1,31 @@ import React from 'react'; - import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; import { apiv3Post } from '~/client/util/apiv3-client'; -import { toastSuccess, toastError } from '~/client/util/toastr'; +import { toastError, toastSuccess } from '~/client/util/toastr'; import { useAdminSocket } from '~/features/admin/states/socket-io'; import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option'; import { ImportOptionForPages } from '~/models/admin/import-option-for-pages'; import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions'; - import ErrorViewer from './ErrorViewer'; import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal'; -import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem'; +import ImportCollectionItem, { + DEFAULT_MODE, + MODE_RESTRICTED_COLLECTION, +} from './ImportCollectionItem'; - -const GROUPS_PAGE = [ - 'pages', 'revisions', 'tags', 'pagetagrelations', -]; +const GROUPS_PAGE = ['pages', 'revisions', 'tags', 'pagetagrelations']; const GROUPS_USER = [ - 'users', 'externalaccounts', 'usergroups', 'usergrouprelations', + 'users', + 'externalaccounts', + 'usergroups', + 'usergrouprelations', ]; -const GROUPS_CONFIG = [ - 'configs', 'updateposts', 'globalnotificationsettings', -]; -const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG); +const GROUPS_CONFIG = ['configs', 'updateposts', 'globalnotificationsettings']; +const ALL_GROUPED_COLLECTIONS = + GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG); /** @type Record */ const IMPORT_OPTION_CLASS_MAPPING = { @@ -34,7 +34,6 @@ const IMPORT_OPTION_CLASS_MAPPING = { }; class ImportForm extends React.Component { - constructor(props) { super(props); @@ -69,12 +68,17 @@ class ImportForm extends React.Component { this.initialState.collectionNameToFileNameMap[collectionName] = fileName; // determine initial mode - const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null) - ? MODE_RESTRICTED_COLLECTION[collectionName][0] - : DEFAULT_MODE; + const initialMode = + MODE_RESTRICTED_COLLECTION[collectionName] != null + ? MODE_RESTRICTED_COLLECTION[collectionName][0] + : DEFAULT_MODE; // create GrowiArchiveImportOption instance - const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption; - this.initialState.optionsMap[collectionName] = new ImportOption(collectionName, initialMode); + const ImportOption = + IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption; + this.initialState.optionsMap[collectionName] = new ImportOption( + collectionName, + initialMode, + ); }); this.state = this.initialState; @@ -106,21 +110,24 @@ class ImportForm extends React.Component { // websocket event // eslint-disable-next-line object-curly-newline - socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => { - const { progressMap, errorsMap } = this.state; - progressMap[collectionName] = collectionProgress; - - if (appendedErrors != null) { - const errors = errorsMap[collectionName] || []; - errorsMap[collectionName] = errors.concat(appendedErrors); - } - - this.setState({ - isImporting: true, - progressMap, - errorsMap, - }); - }); + socket.on( + 'admin:onProgressForImport', + ({ collectionName, collectionProgress, appendedErrors }) => { + const { progressMap, errorsMap } = this.state; + progressMap[collectionName] = collectionProgress; + + if (appendedErrors != null) { + const errors = errorsMap[collectionName] || []; + errorsMap[collectionName] = errors.concat(appendedErrors); + } + + this.setState({ + isImporting: true, + progressMap, + errorsMap, + }); + }, + ); // websocket event socket.on('admin:onTerminateForImport', () => { @@ -154,8 +161,7 @@ class ImportForm extends React.Component { const selectedCollections = new Set(this.state.selectedCollections); if (bool) { selectedCollections.add(collectionName); - } - else { + } else { selectedCollections.delete(collectionName); } @@ -165,7 +171,9 @@ class ImportForm extends React.Component { } async checkAll() { - await this.setState({ selectedCollections: new Set(this.allCollectionNames) }); + await this.setState({ + selectedCollections: new Set(this.allCollectionNames), + }); this.validate(); } @@ -186,11 +194,17 @@ class ImportForm extends React.Component { } openConfigurationModal(collectionName) { - this.setState({ isConfigurationModalOpen: true, collectionNameForConfiguration: collectionName }); + this.setState({ + isConfigurationModalOpen: true, + collectionNameForConfiguration: collectionName, + }); } showErrorsViewer(collectionName) { - this.setState({ isErrorsViewerOpen: true, collectionNameForErrorsViewer: collectionName }); + this.setState({ + isErrorsViewerOpen: true, + collectionNameForErrorsViewer: collectionName, + }); } async validate() { @@ -224,7 +238,9 @@ class ImportForm extends React.Component { const { warnForOtherGroups, selectedCollections } = this.state; if (selectedCollections.size === 0) { - warnForOtherGroups.push(t('admin:importer_management.growi_settings.errors.at_least_one')); + warnForOtherGroups.push( + t('admin:importer_management.growi_settings.errors.at_least_one'), + ); } this.setState({ warnForOtherGroups }); @@ -234,13 +250,20 @@ class ImportForm extends React.Component { const { t } = this.props; const { warnForPageGroups, selectedCollections } = this.state; - const pageRelatedCollectionsLength = ['pages', 'revisions'].filter((collectionName) => { - return selectedCollections.has(collectionName); - }).length; + const pageRelatedCollectionsLength = ['pages', 'revisions'].filter( + (collectionName) => { + return selectedCollections.has(collectionName); + }, + ).length; // MUST be included both or neither when importing - if (pageRelatedCollectionsLength !== 0 && pageRelatedCollectionsLength !== 2) { - warnForPageGroups.push(t('admin:importer_management.growi_settings.errors.page_and_revision')); + if ( + pageRelatedCollectionsLength !== 0 && + pageRelatedCollectionsLength !== 2 + ) { + warnForPageGroups.push( + t('admin:importer_management.growi_settings.errors.page_and_revision'), + ); } this.setState({ warnForPageGroups }); @@ -253,7 +276,12 @@ class ImportForm extends React.Component { // MUST include also 'users' if 'externalaccounts' is selected if (selectedCollections.has('externalaccounts')) { if (!selectedCollections.has('users')) { - warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' })); + warnForUserGroups.push( + t('admin:importer_management.growi_settings.errors.depends', { + target: 'Users', + condition: 'Externalaccounts', + }), + ); } } @@ -267,7 +295,12 @@ class ImportForm extends React.Component { // MUST include also 'users' if 'usergroups' is selected if (selectedCollections.has('usergroups')) { if (!selectedCollections.has('users')) { - warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' })); + warnForUserGroups.push( + t('admin:importer_management.growi_settings.errors.depends', { + target: 'Users', + condition: 'Usergroups', + }), + ); } } @@ -281,7 +314,12 @@ class ImportForm extends React.Component { // MUST include also 'usergroups' if 'usergrouprelations' is selected if (selectedCollections.has('usergrouprelations')) { if (!selectedCollections.has('usergroups')) { - warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' })); + warnForUserGroups.push( + t('admin:importer_management.growi_settings.errors.depends', { + target: 'Usergroups', + condition: 'Usergrouprelations', + }), + ); } } @@ -289,9 +327,7 @@ class ImportForm extends React.Component { } async import() { - const { - fileName, onPostImport, t, - } = this.props; + const { fileName, onPostImport, t } = this.props; const { selectedCollections, optionsMap } = this.state; // init progress data @@ -314,8 +350,7 @@ class ImportForm extends React.Component { } toastSuccess(undefined, 'Import process has requested.'); - } - catch (err) { + } catch (err) { if (err.code === 'only_upsert_available') { toastError(t('admin:importer_management.error.only_upsert_available')); } @@ -363,7 +398,11 @@ class ImportForm extends React.Component { return !ALL_GROUPED_COLLECTIONS.includes(collectionName); }); - return this.renderGroups(collectionNames, 'Other', this.state.warnForOtherGroups); + return this.renderGroups( + collectionNames, + 'Other', + this.state.warnForOtherGroups, + ); } renderImportItems(collectionNames) { @@ -382,15 +421,21 @@ class ImportForm extends React.Component { {collectionNames.map((collectionName) => { const collectionProgress = progressMap[collectionName]; const errorsCount = errorsMap[collectionName]?.length ?? 0; - const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName); + const isConfigButtonAvailable = Object.keys( + IMPORT_OPTION_CLASS_MAPPING, + ).includes(collectionName); return (
    -
    -
      -
    • {t('admin:importer_management.growi_settings.description_of_import_mode.about')}
    • +
    • + {t( + 'admin:importer_management.growi_settings.description_of_import_mode.about', + )} +
      • -
      • {t('admin:importer_management.growi_settings.description_of_import_mode.insert')}
      • -
      • {t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}
      • -
      • {t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}
      • +
      • + {t( + 'admin:importer_management.growi_settings.description_of_import_mode.insert', + )} +
      • +
      • + {t( + 'admin:importer_management.growi_settings.description_of_import_mode.upsert', + )} +
      • +
      • + {t( + 'admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert', + )} +
    @@ -479,10 +560,19 @@ class ImportForm extends React.Component { {this.renderOthers()}
    - -
    @@ -492,7 +582,6 @@ class ImportForm extends React.Component { ); } - } ImportForm.propTypes = { @@ -516,5 +605,4 @@ const ImportFormWrapperFc = (props) => { return ; }; - export default ImportFormWrapperFc; diff --git a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx index f511d931f5f..aa47f3a11ed 100644 --- a/apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx +++ b/apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx @@ -1,5 +1,4 @@ import React from 'react'; - import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; @@ -7,7 +6,6 @@ import { apiv3PostForm } from '~/client/util/apiv3-client'; import { toastError } from '~/client/util/toastr'; class UploadForm extends React.Component { - constructor(props) { super(props); @@ -33,14 +31,12 @@ class UploadForm extends React.Component { try { const { data } = await apiv3PostForm('/import/upload', formData); this.props.onUpload(data); - } - catch (err) { + } catch (err) { if (err[0].code === 'versions-are-not-met') { if (this.props.onVersionMismatch !== null) { this.props.onVersionMismatch(err[0].code); } - } - else { + } else { toastError(err); } } @@ -48,9 +44,9 @@ class UploadForm extends React.Component { validateForm() { return ( - this.inputRef.current // null check - && this.inputRef.current.files[0] // null check - && /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension + this.inputRef.current && // null check + this.inputRef.current.files[0] && // null check + /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension ); } @@ -61,7 +57,10 @@ class UploadForm extends React.Component {
    -
    - {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? ( + {adminSlackIntegrationLegacyContainer.state.selectSlackOption === + 'Incoming Webhooks' ? ( -

    {t('notification_settings.slack_incoming_configuration')}

    +

    + {t('notification_settings.slack_incoming_configuration')} +

    - +
    { type="checkbox" className="form-check-input" id="cbPrioritizeIWH" - checked={adminSlackIntegrationLegacyContainer.state.isIncomingWebhookPrioritized || false} - onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }} + checked={ + adminSlackIntegrationLegacyContainer.state + .isIncomingWebhookPrioritized || false + } + onChange={() => { + adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized(); + }} /> -
    @@ -100,40 +140,54 @@ const SlackConfiguration = (props) => {
    - ) - : ( - -

    {t('notification_settings.slack_app_configuration')}

    - - + ) : ( + +

    + {t('notification_settings.slack_app_configuration')} +

    -
    - -
    - -
    -
    + -
    - ) - } +
    + +
    + +
    +
    +
    + )} {

    - {' '} - {t('notification_settings.how_to.header')} + {' '} + + {t('notification_settings.how_to.header')} +

    -
      +
      1. {t('notification_settings.how_to.workspace')}
          {/* eslint-disable-next-line react/no-danger */} -
        1. +
        2. {t('notification_settings.how_to.workspace_desc2')}
        3. {t('notification_settings.how_to.workspace_desc3')}
        @@ -161,11 +226,14 @@ const SlackConfiguration = (props) => { {t('notification_settings.how_to.at_growi')}
          {/* eslint-disable-next-line react/no-danger */} -
        1. +
      - ); @@ -173,7 +241,9 @@ const SlackConfiguration = (props) => { SlackConfiguration.propTypes = { t: PropTypes.func.isRequired, // i18next - adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired, + adminSlackIntegrationLegacyContainer: PropTypes.instanceOf( + AdminSlackIntegrationLegacyContainer, + ).isRequired, }; const SlackConfigurationWrapperFc = (props) => { @@ -182,6 +252,9 @@ const SlackConfigurationWrapperFc = (props) => { return ; }; -const SlackConfigurationWrapper = withUnstatedContainers(SlackConfigurationWrapperFc, [AdminSlackIntegrationLegacyContainer]); +const SlackConfigurationWrapper = withUnstatedContainers( + SlackConfigurationWrapperFc, + [AdminSlackIntegrationLegacyContainer], +); export default SlackConfigurationWrapper; diff --git a/apps/app/src/client/components/Admin/ManageExternalAccount.tsx b/apps/app/src/client/components/Admin/ManageExternalAccount.tsx index 6cc0fa70e99..2ba15176c5f 100644 --- a/apps/app/src/client/components/Admin/ManageExternalAccount.tsx +++ b/apps/app/src/client/components/Admin/ManageExternalAccount.tsx @@ -1,34 +1,38 @@ -import React, { useCallback, useEffect, type JSX } from 'react'; - -import { useTranslation } from 'next-i18next'; +import React, { type JSX, useCallback, useEffect } from 'react'; import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer'; import { toastError } from '~/client/util/toastr'; import PaginationWrapper from '../PaginationWrapper'; import { withUnstatedContainers } from '../UnstatedUtils'; - import ExternalAccountTable from './Users/ExternalAccountTable'; type ManageExternalAccountProps = { - adminExternalAccountsContainer: AdminExternalAccountsContainer, -} - -const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element => { + adminExternalAccountsContainer: AdminExternalAccountsContainer; +}; +const ManageExternalAccount = ( + props: ManageExternalAccountProps, +): JSX.Element => { const { t } = useTranslation(); const { adminExternalAccountsContainer } = props; - const { activePage, totalAccounts, pagingLimit } = adminExternalAccountsContainer.state; + const { activePage, totalAccounts, pagingLimit } = + adminExternalAccountsContainer.state; - const externalAccountPageHandler = useCallback(async(selectedPage) => { - try { - await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(selectedPage); - } - catch (err) { - toastError(err); - } - }, [adminExternalAccountsContainer]); + const externalAccountPageHandler = useCallback( + async (selectedPage) => { + try { + await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum( + selectedPage, + ); + } catch (err) { + toastError(err); + } + }, + [adminExternalAccountsContainer], + ); // for Next routing useEffect(() => { @@ -54,28 +58,29 @@ const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element = prefetch={false} className="btn btn-outline-secondary" > - + {t('admin:user_management.back_to_user_management')}

      {t('admin:user_management.external_account_list')}

      - {(totalAccounts !== 0) ? ( + {totalAccounts !== 0 ? ( <> {pager} {pager} - ) - : ( - <> - { t('admin:user_management.external_account_none') } - - ) - } + ) : ( + <>{t('admin:user_management.external_account_none')} + )} ); }; -const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccount, [AdminExternalAccountsContainer]); +const ManageExternalAccountWrapper = withUnstatedContainers( + ManageExternalAccount, + [AdminExternalAccountsContainer], +); export default ManageExternalAccountWrapper; diff --git a/apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx b/apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx index 9548f6d987a..ff72aa3cffb 100644 --- a/apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx +++ b/apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx @@ -1,13 +1,15 @@ /* eslint-disable react/no-danger */ import React, { useCallback } from 'react'; - import { useTranslation } from 'next-i18next'; import { - UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, + DropdownItem, + DropdownMenu, + DropdownToggle, + UncontrolledDropdown, } from 'reactstrap'; import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer'; -import { toastSuccess, toastError } from '~/client/util/toastr'; +import { toastError, toastSuccess } from '~/client/util/toastr'; import loggerFactory from '~/utils/logger'; import { withUnstatedContainers } from '../../UnstatedUtils'; @@ -15,24 +17,30 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow'; const logger = loggerFactory('growi:importer'); - type Props = { adminMarkDownContainer: AdminMarkDownContainer; -} +}; const IndentForm = (props: Props) => { const { t } = useTranslation('admin'); - const onClickSubmit = useCallback(async(props) => { - try { - await props.adminMarkDownContainer.updateIndentSetting(); - toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.indent_header'), ns: 'commons' })); - } - catch (err) { - toastError(err); - logger.error(err); - } - }, [t]); + const onClickSubmit = useCallback( + async (props) => { + try { + await props.adminMarkDownContainer.updateIndentSetting(); + toastSuccess( + t('toaster.update_successed', { + target: t('markdown_settings.indent_header'), + ns: 'commons', + }), + ); + } catch (err) { + toastError(err); + logger.error(err); + } + }, + [t], + ); const renderIndentSizeOption = (props) => { const { adminMarkDownContainer } = props; @@ -41,9 +49,14 @@ const IndentForm = (props: Props) => { return (
      - + - + {adminPreferredIndentSize || 4} @@ -51,7 +64,13 @@ const IndentForm = (props: Props) => { {[2, 4].map((num) => { return ( - adminMarkDownContainer.setAdminPreferredIndentSize(num)}> + + adminMarkDownContainer.setAdminPreferredIndentSize(num) + } + > {num} ); @@ -70,7 +89,9 @@ const IndentForm = (props: Props) => { const { adminMarkDownContainer } = props; const { isIndentSizeForced } = adminMarkDownContainer.state; - const helpIndentInComment = { __html: t('markdown_settings.indent_options.disallow_indent_change_desc') }; + const helpIndentInComment = { + __html: t('markdown_settings.indent_options.disallow_indent_change_desc'), + }; return (
      @@ -81,14 +102,22 @@ const IndentForm = (props: Props) => { id="isIndentSizeForced" checked={isIndentSizeForced || false} onChange={() => { - adminMarkDownContainer.setState({ isIndentSizeForced: !isIndentSizeForced }); + adminMarkDownContainer.setState({ + isIndentSizeForced: !isIndentSizeForced, + }); }} /> -
      -

      +

      ); }; @@ -101,7 +130,10 @@ const IndentForm = (props: Props) => { {renderIndentSizeOption(props)} {renderIndentForceOption(props)} - onClickSubmit(props)} disabled={adminMarkDownContainer.state.retrieveError != null} /> + onClickSubmit(props)} + disabled={adminMarkDownContainer.state.retrieveError != null} + /> ); }; @@ -109,6 +141,8 @@ const IndentForm = (props: Props) => { /** * Wrapper component for using unstated */ -const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]); +const IndentFormWrapper = withUnstatedContainers(IndentForm, [ + AdminMarkDownContainer, +]); export default IndentFormWrapper; diff --git a/apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx b/apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx index 1d8ed94e0e3..bb50567e739 100644 --- a/apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx +++ b/apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx @@ -1,11 +1,10 @@ /* eslint-disable react/no-danger */ import React from 'react'; - import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer'; -import { toastSuccess, toastError } from '~/client/util/toastr'; +import { toastError, toastSuccess } from '~/client/util/toastr'; import loggerFactory from '~/utils/logger'; import { withUnstatedContainers } from '../../UnstatedUtils'; @@ -14,22 +13,24 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow'; const logger = loggerFactory('growi:importer'); class LineBreakForm extends React.Component { - constructor(props) { super(props); this.onClickSubmit = this.onClickSubmit.bind(this); } - async onClickSubmit() { const { t } = this.props; try { await this.props.adminMarkDownContainer.updateLineBreakSetting(); - toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.lineBreak_header'), ns: 'commons' })); - } - catch (err) { + toastSuccess( + t('toaster.update_successed', { + target: t('markdown_settings.lineBreak_header'), + ns: 'commons', + }), + ); + } catch (err) { toastError(err); logger.error(err); } @@ -39,7 +40,9 @@ class LineBreakForm extends React.Component { const { t, adminMarkDownContainer } = this.props; const { isEnabledLinebreaks } = adminMarkDownContainer.state; - const helpLineBreak = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc') }; + const helpLineBreak = { + __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc'), + }; return (
      @@ -49,13 +52,23 @@ class LineBreakForm extends React.Component { className="form-check-input" id="isEnabledLinebreaks" checked={isEnabledLinebreaks} - onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }} + onChange={() => { + adminMarkDownContainer.setState({ + isEnabledLinebreaks: !isEnabledLinebreaks, + }); + }} /> -
      -

      +

      ); } @@ -64,7 +77,11 @@ class LineBreakForm extends React.Component { const { t, adminMarkDownContainer } = this.props; const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state; - const helpLineBreakInComment = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc') }; + const helpLineBreakInComment = { + __html: t( + 'markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc', + ), + }; return (
      @@ -74,13 +91,25 @@ class LineBreakForm extends React.Component { className="form-check-input" id="isEnabledLinebreaksInComments" checked={isEnabledLinebreaksInComments} - onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }} + onChange={() => { + adminMarkDownContainer.setState({ + isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments, + }); + }} /> -
      -

      +

    ); } @@ -94,11 +123,13 @@ class LineBreakForm extends React.Component { {this.renderLineBreakOption()} {this.renderLineBreakInCommentOption()} - + ); } - } const LineBreakFormFC = (props) => { @@ -109,11 +140,14 @@ const LineBreakFormFC = (props) => { /** * Wrapper component for using unstated */ -const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [AdminMarkDownContainer]); +const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [ + AdminMarkDownContainer, +]); LineBreakForm.propTypes = { t: PropTypes.func.isRequired, - adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired, + adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer) + .isRequired, }; export default LineBreakFormWrapper; diff --git a/apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx b/apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx index 9a65dfca656..21757bf7f92 100644 --- a/apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx +++ b/apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, type JSX } from 'react'; - +import React, { type JSX, useEffect } from 'react'; import { useTranslation } from 'next-i18next'; import { Card, CardBody } from 'reactstrap'; @@ -9,30 +8,28 @@ import { toArrayIfNot } from '~/utils/array-utils'; import loggerFactory from '~/utils/logger'; import { withUnstatedContainers } from '../../UnstatedUtils'; - import IndentForm from './IndentForm'; import LineBreakForm from './LineBreakForm'; import XssForm from './XssForm'; const logger = loggerFactory('growi:MarkDown'); -type Props ={ - adminMarkDownContainer: AdminMarkDownContainer -} +type Props = { + adminMarkDownContainer: AdminMarkDownContainer; +}; const MarkDownSettingContents = React.memo((props: Props): JSX.Element => { const { t } = useTranslation('admin'); const { adminMarkDownContainer } = props; useEffect(() => { - const fetchMarkdownData = async() => { + const fetchMarkdownData = async () => { await adminMarkDownContainer.retrieveMarkdownData(); }; try { fetchMarkdownData(); - } - catch (err) { + } catch (err) { const errs = toArrayIfNot(err); toastError(errs); logger.error(errs); @@ -42,23 +39,35 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => { return (
    {/* Line Break Setting */} -

    {t('markdown_settings.lineBreak_header')}

    +

    + {t('markdown_settings.lineBreak_header')} +

    - { t('markdown_settings.lineBreak_desc') } + + {t('markdown_settings.lineBreak_desc')} + {/* Indent Setting */} -

    {t('markdown_settings.indent_header')}

    +

    + {t('markdown_settings.indent_header')} +

    - {t('markdown_settings.indent_desc') } + + {t('markdown_settings.indent_desc')} + {/* XSS Setting */} -

    { t('markdown_settings.xss_header') }

    +

    + {t('markdown_settings.xss_header')} +

    - { t('markdown_settings.xss_desc') } + + {t('markdown_settings.xss_desc')} +
    @@ -66,8 +75,9 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => { }); MarkDownSettingContents.displayName = 'MarkDownSettingContents'; - -const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkDownSettingContents, [AdminMarkDownContainer]); - +const MarkdownSettingWithUnstatedContainer = withUnstatedContainers( + MarkDownSettingContents, + [AdminMarkDownContainer], +); export default MarkdownSettingWithUnstatedContainer; diff --git a/apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx b/apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx index 33374b656bb..5513237f70e 100644 --- a/apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx +++ b/apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx @@ -1,24 +1,25 @@ -import { useCallback, type JSX } from 'react'; - +import { type JSX, useCallback } from 'react'; import { useTranslation } from 'next-i18next'; import type { UseFormRegister, UseFormSetValue } from 'react-hook-form'; import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer'; -import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist'; +import { + attributes as recommendedAttributes, + tagNames as recommendedTagNames, +} from '~/services/renderer/recommended-whitelist'; type FormValues = { - tagWhitelist: string, - attrWhitelist: string, -} + tagWhitelist: string; + attrWhitelist: string; +}; -type Props ={ - adminMarkDownContainer: AdminMarkDownContainer, - register: UseFormRegister, - setValue: UseFormSetValue, -} +type Props = { + adminMarkDownContainer: AdminMarkDownContainer; + register: UseFormRegister; + setValue: UseFormSetValue; +}; export const WhitelistInput = (props: Props): JSX.Element => { - const { t } = useTranslation('admin'); const { adminMarkDownContainer, register, setValue } = props; @@ -39,8 +40,14 @@ export const WhitelistInput = (props: Props): JSX.Element => {
    {t('markdown_settings.xss_options.tag_names')} -

    - {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })} +

    + {t('markdown_settings.xss_options.import_recommended', { + target: 'Tags', + })}