- { hasResolveConflictHandler
+ {hasResolveConflictHandler
? <>{t('modal_resolve_conflict.file_conflicting_with_newer_remote')}>
: <> {t('edited this page')}>
}
diff --git a/apps/app/src/client/components/PageTags/RenderTagLabels.tsx b/apps/app/src/client/components/PageTags/RenderTagLabels.tsx
index 7379366e137..388d2c815e0 100644
--- a/apps/app/src/client/components/PageTags/RenderTagLabels.tsx
+++ b/apps/app/src/client/components/PageTags/RenderTagLabels.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import SimpleBar from 'simplebar-react';
-import { useKeywordManager } from '~/client/services/search-operation';
+import { useSetSearchKeyword } from '~/states/search';
type RenderTagLabelsProps = {
tags: string[],
@@ -11,7 +11,7 @@ type RenderTagLabelsProps = {
const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
const { tags } = props;
- const { pushState } = useKeywordManager();
+ const setSearchKeyword = useSetSearchKeyword();
return (
@@ -21,7 +21,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
key={tag}
type="button"
className="grw-tag badge me-1 mb-1 text-truncate mw-100"
- onClick={() => pushState(`tag:${tag}`)}
+ onClick={() => setSearchKeyword(`tag:${tag}`)}
>
{tag}
diff --git a/apps/app/src/client/components/PageTags/TagEditModal.tsx b/apps/app/src/client/components/PageTags/TagEditModal/TagEditModal.tsx
similarity index 67%
rename from apps/app/src/client/components/PageTags/TagEditModal.tsx
rename to apps/app/src/client/components/PageTags/TagEditModal/TagEditModal.tsx
index ea2c04225cc..384c0bfb0ed 100644
--- a/apps/app/src/client/components/PageTags/TagEditModal.tsx
+++ b/apps/app/src/client/components/PageTags/TagEditModal/TagEditModal.tsx
@@ -1,5 +1,5 @@
import React, {
- useState, useCallback, useEffect,
+ useState, useCallback, useEffect, useMemo,
} from 'react';
import { useTranslation } from 'next-i18next';
@@ -10,7 +10,8 @@ import {
import { useUpdateStateAfterSave } from '~/client/services/page-operation';
import { apiPost } from '~/client/util/apiv1-client';
import { toastError, toastSuccess } from '~/client/util/toastr';
-import { useTagEditModal, type TagEditModalStatus } from '~/stores/modal';
+import { useTagEditModalStatus, useTagEditModalActions, type TagEditModalStatus } from '~/states/ui/modal/tag-edit';
+import { useSWRxTagsInfo } from '~/stores/page';
import { TagsInput } from './TagsInput';
@@ -28,18 +29,25 @@ const TagEditModalSubstance: React.FC = (props: TagE
const pageId = tagEditModalData.pageId;
const revisionId = tagEditModalData.revisionId;
const updateStateAfterSave = useUpdateStateAfterSave(pageId);
-
- const [tags, setTags] = useState([]);
+ const { mutate: mutateTags } = useSWRxTagsInfo(pageId);
+ const [tags, setTags] = useState(initTags ?? []);
// use to take initTags when redirect to other page
useEffect(() => {
setTags(initTags);
}, [initTags]);
- const handleSubmit = useCallback(async() => {
+ // Memoized API request data
+ const updateTagsData = useMemo(() => ({
+ pageId,
+ revisionId,
+ tags,
+ }), [pageId, revisionId, tags]);
+ const handleSubmit = useCallback(async() => {
try {
- await apiPost('/tags.update', { pageId, revisionId, tags });
+ await apiPost('/tags.update', updateTagsData);
+ mutateTags();
updateStateAfterSave?.();
toastSuccess('updated tags successfully');
@@ -48,7 +56,12 @@ const TagEditModalSubstance: React.FC = (props: TagE
catch (err) {
toastError(err);
}
- }, [closeTagEditModal, tags, pageId, revisionId, updateStateAfterSave]);
+ }, [updateTagsData, mutateTags, updateStateAfterSave, closeTagEditModal]);
+
+ // Memoized tags update handler
+ const handleTagsUpdate = useCallback((newTags: string[]) => {
+ setTags(newTags);
+ }, []);
return (
@@ -56,7 +69,7 @@ const TagEditModalSubstance: React.FC = (props: TagE
{t('tag_edit_modal.edit_tags')}
- setTags(tags)} autoFocus />
+
@@ -69,7 +82,8 @@ const TagEditModalSubstance: React.FC = (props: TagE
};
export const TagEditModal: React.FC = () => {
- const { data: tagEditModalData, close: closeTagEditModal } = useTagEditModal();
+ const tagEditModalData = useTagEditModalStatus();
+ const { close: closeTagEditModal } = useTagEditModalActions();
if (!tagEditModalData?.isOpen) {
return <>>;
diff --git a/apps/app/src/client/components/PageTags/TagsInput.module.scss b/apps/app/src/client/components/PageTags/TagEditModal/TagsInput.module.scss
similarity index 100%
rename from apps/app/src/client/components/PageTags/TagsInput.module.scss
rename to apps/app/src/client/components/PageTags/TagEditModal/TagsInput.module.scss
diff --git a/apps/app/src/client/components/PageTags/TagsInput.tsx b/apps/app/src/client/components/PageTags/TagEditModal/TagsInput.tsx
similarity index 100%
rename from apps/app/src/client/components/PageTags/TagsInput.tsx
rename to apps/app/src/client/components/PageTags/TagEditModal/TagsInput.tsx
diff --git a/apps/app/src/client/components/PageTags/TagEditModal/dynamic.tsx b/apps/app/src/client/components/PageTags/TagEditModal/dynamic.tsx
new file mode 100644
index 00000000000..10357d7d7d2
--- /dev/null
+++ b/apps/app/src/client/components/PageTags/TagEditModal/dynamic.tsx
@@ -0,0 +1,18 @@
+import type { JSX } from 'react';
+
+import { useLazyLoader } from '~/components/utils/use-lazy-loader';
+import { useTagEditModalStatus } from '~/states/ui/modal/tag-edit';
+
+type TagEditModalProps = Record;
+
+export const TagEditModalLazyLoaded = (): JSX.Element => {
+ const status = useTagEditModalStatus();
+
+ const TagEditModal = useLazyLoader(
+ 'tag-edit-modal',
+ () => import('./TagEditModal').then(mod => ({ default: mod.TagEditModal })),
+ status?.isOpen ?? false,
+ );
+
+ return TagEditModal ? : <>>;
+};
diff --git a/apps/app/src/client/components/PageTags/TagEditModal/index.ts b/apps/app/src/client/components/PageTags/TagEditModal/index.ts
new file mode 100644
index 00000000000..d618efad2aa
--- /dev/null
+++ b/apps/app/src/client/components/PageTags/TagEditModal/index.ts
@@ -0,0 +1 @@
+export { TagEditModalLazyLoaded } from './dynamic';
diff --git a/apps/app/src/client/components/PageTags/index.ts b/apps/app/src/client/components/PageTags/index.ts
index 862377e7ed0..384f92afef2 100644
--- a/apps/app/src/client/components/PageTags/index.ts
+++ b/apps/app/src/client/components/PageTags/index.ts
@@ -1,2 +1 @@
export * from './PageTags';
-export * from './TagsInput';
diff --git a/apps/app/src/client/components/PageTimeline.tsx b/apps/app/src/client/components/PageTimeline.tsx
index 9761ffd19da..fccf9c1eeed 100644
--- a/apps/app/src/client/components/PageTimeline.tsx
+++ b/apps/app/src/client/components/PageTimeline.tsx
@@ -4,7 +4,7 @@ import type { IPageHasId } from '@growi/core';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
-import { useCurrentPagePath } from '~/stores/page';
+import { useCurrentPagePath } from '~/states/page';
import { useSWRINFxPageTimeline } from '~/stores/page-timeline';
import { useTimelineOptions } from '~/stores/renderer';
@@ -46,9 +46,9 @@ export const PageTimeline = (): JSX.Element => {
const PER_PAGE = 3;
const { t } = useTranslation();
- const { data: currentPagePath } = useCurrentPagePath();
+ const currentPagePath = useCurrentPagePath();
- const swrInfinitexPageTimeline = useSWRINFxPageTimeline(currentPagePath, PER_PAGE);
+ const swrInfinitexPageTimeline = useSWRINFxPageTimeline(currentPagePath ?? undefined, PER_PAGE);
const { data } = swrInfinitexPageTimeline;
const isEmpty = data?.[0]?.pages.length === 0;
diff --git a/apps/app/src/client/components/PasswordResetRequestForm.tsx b/apps/app/src/client/components/PasswordResetRequestForm.tsx
index 29aec600f30..8e760e360fb 100644
--- a/apps/app/src/client/components/PasswordResetRequestForm.tsx
+++ b/apps/app/src/client/components/PasswordResetRequestForm.tsx
@@ -1,16 +1,17 @@
import type { FC } from 'react';
import React, { useState, useCallback } from 'react';
+import { useAtomValue } from 'jotai';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { apiv3Post } from '~/client/util/apiv3-client';
import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useIsMailerSetup } from '~/stores-universal/context';
+import { isMailerSetupAtom } from '~/states/server-configurations';
const PasswordResetRequestForm: FC = () => {
const { t } = useTranslation();
- const { data: isMailerSetup } = useIsMailerSetup();
+ const isMailerSetup = useAtomValue(isMailerSetupAtom);
const [email, setEmail] = useState('');
const changeEmail = useCallback((inputValue) => {
diff --git a/apps/app/src/client/components/PrivateLegacyPages.tsx b/apps/app/src/client/components/PrivateLegacyPages.tsx
deleted file mode 100644
index 65730fadeb2..00000000000
--- a/apps/app/src/client/components/PrivateLegacyPages.tsx
+++ /dev/null
@@ -1,493 +0,0 @@
-import React, {
- useCallback, useMemo, useRef, useState, useEffect, type JSX,
-} from 'react';
-
-import { useGlobalSocket } from '@growi/core/dist/swr';
-import { LoadingSpinner } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
-import {
- UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
-import type { V5MigrationStatus } from '~/interfaces/page-listing-results';
-import type { IFormattedSearchResult } from '~/interfaces/search';
-import type { PageMigrationErrorData } from '~/interfaces/websocket';
-import { SocketEventName } from '~/interfaces/websocket';
-import { useIsAdmin } from '~/stores-universal/context';
-import type { ILegacyPrivatePage } from '~/stores/modal';
-import { usePrivateLegacyPagesMigrationModal } from '~/stores/modal';
-import { mutatePageTree, useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import {
- useSWRxSearch,
-} from '~/stores/search';
-
-import { MenuItemType } from './Common/Dropdown/PageItemControl';
-import PaginationWrapper from './PaginationWrapper';
-import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
-import { OperateAllControl } from './SearchPage/OperateAllControl';
-import SearchControl from './SearchPage/SearchControl';
-import type { IReturnSelectedPageIds } from './SearchPage/SearchPageBase';
-import { SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
-
-
-// TODO: replace with "customize:showPageLimitationS"
-const INITIAL_PAGING_SIZE = 20;
-
-const initQ = '/';
-
-
-/**
- * SearchResultListHead
- */
-
-type SearchResultListHeadProps = {
- searchResult: IFormattedSearchResult,
- offset: number,
- pagingSize: number,
- onPagingSizeChanged: (size: number) => void,
- migrationStatus?: V5MigrationStatus,
-}
-
-const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.Element => {
- const { t } = useTranslation();
-
- const {
- searchResult, offset, pagingSize,
- onPagingSizeChanged, migrationStatus,
- } = props;
-
- if (migrationStatus == null) {
- return (
-
-
-
- );
- }
-
- const { took, total, hitsCount } = searchResult.meta;
- const leftNum = offset + 1;
- const rightNum = offset + hitsCount;
-
- const isSuccess = migrationStatus.migratablePagesCount === 0;
-
- if (isSuccess) {
- return (
-
-
-
{t('private_legacy_pages.nopages_title')}
-
- {t('private_legacy_pages.nopages_desc1')}
- {/* eslint-disable-next-line react/no-danger */}
-
-
-
-
- );
- }
-
- return (
- <>
-
-
- {t('search_result.result_meta')}
- {`${leftNum}-${rightNum}`} / {total}
- { took != null && (
- ({took}ms)
- ) }
-
-
-
- {t('search_result.number_of_list_to_display')}
-
-
onPagingSizeChanged(Number(e.target.value))}
- >
- {[20, 50, 100, 200].map((limit) => {
- return {limit} {t('search_result.page_number_unit')} ;
- })}
-
-
-
-
-
-
{t('private_legacy_pages.alert_title')}
-
- {t('private_legacy_pages.alert_desc1', { delete_all_selected_page: t('search_result.delete_all_selected_page') })}
- {/* eslint-disable-next-line react/no-danger */}
-
-
-
-
- >
- );
-});
-
-SearchResultListHead.displayName = 'SearchResultListHead';
-
-/*
- * ConvertByPathModal
- */
-type ConvertByPathModalProps = {
- isOpen: boolean,
- close?: () => void,
- onSubmit?: (convertPath: string) => Promise | void,
-}
-const ConvertByPathModal = React.memo((props: ConvertByPathModalProps): JSX.Element => {
- const { t } = useTranslation();
-
- const [currentInput, setInput] = useState('');
- const [checked, setChecked] = useState(false);
-
- useEffect(() => {
- setChecked(false);
- }, [props.isOpen]);
-
- return (
-
-
- { t('private_legacy_pages.by_path_modal.title') }
-
-
- {t('private_legacy_pages.by_path_modal.description')}
- setInput(e.target.value)} />
-
- { t('private_legacy_pages.by_path_modal.alert') }
-
-
-
-
- setChecked(e.target.checked)}
- />
- { t('private_legacy_pages.by_path_modal.checkbox_label') }
-
- props.onSubmit?.(currentInput)}
- >
- refresh
- { t('private_legacy_pages.by_path_modal.button_label') }
-
-
-
- );
-});
-
-ConvertByPathModal.displayName = 'ConvertByPathModal';
-
-/**
- * LegacyPage
- */
-
-const PrivateLegacyPages = (): JSX.Element => {
- const { t } = useTranslation();
-
- const { data: isAdmin } = useIsAdmin();
-
- const [keyword, setKeyword] = useState(initQ);
- const [offset, setOffset] = useState(0);
- const [limit, setLimit] = useState(INITIAL_PAGING_SIZE);
- const [isOpenConvertModal, setOpenConvertModal] = useState(false);
-
- const [isControlEnabled, setControlEnabled] = useState(false);
-
- const selectAllControlRef = useRef(null);
- const searchPageBaseRef = useRef(null);
-
- const { data, conditions, mutate } = useSWRxSearch(keyword, 'PrivateLegacyPages', {
- offset,
- limit,
- includeUserPages: true,
- includeTrashPages: false,
- });
-
- const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
-
- const searchInvokedHandler = useCallback((_keyword: string) => {
- mutateMigrationStatus();
- setKeyword(_keyword);
- setOffset(0);
- }, [mutateMigrationStatus]);
-
- const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModal();
- const { data: socket } = useGlobalSocket();
-
- useEffect(() => {
- socket?.on(SocketEventName.PageMigrationSuccess, () => {
- toastSuccess(t('private_legacy_pages.toaster.page_migration_succeeded'));
- });
-
- socket?.on(SocketEventName.PageMigrationError, (data?: PageMigrationErrorData) => {
- if (data == null || data.paths.length === 0) {
- toastError(t('private_legacy_pages.toaster.page_migration_failed'));
- }
- else {
- const errorPaths = data.paths.length > 3
- ? `${data.paths.slice(0, 3).join(', ')}...`
- : data.paths.join(', ');
- toastError(t('private_legacy_pages.toaster.page_migration_failed_with_paths', { paths: errorPaths }));
- }
- });
-
- return () => {
- socket?.off(SocketEventName.PageMigrationSuccess);
- socket?.off(SocketEventName.PageMigrationError);
- };
- }, [socket, t]);
-
- const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
- const instance = searchPageBaseRef.current;
-
- if (instance == null) {
- return;
- }
-
- if (isChecked) {
- instance.selectAll();
- setControlEnabled(true);
- }
- else {
- instance.deselectAll();
- setControlEnabled(false);
- }
- }, []);
-
- const selectedPagesByCheckboxesChangedHandler = useCallback((selectedCount: number, totalCount: number) => {
- const instance = selectAllControlRef.current;
-
- if (instance == null) {
- return;
- }
-
- if (selectedCount === 0) {
- instance.deselect();
- setControlEnabled(false);
- }
- else if (selectedCount === totalCount) {
- instance.select();
- setControlEnabled(true);
- }
- else {
- instance.setIndeterminate();
- setControlEnabled(true);
- }
- }, []);
-
- // for bulk deletion
- const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
-
- const convertMenuItemClickedHandler = useCallback(() => {
- if (data == null) {
- return;
- }
-
- const instance = searchPageBaseRef.current;
- if (instance == null || instance.getSelectedPageIds == null) {
- return;
- }
-
- const selectedPageIds = instance.getSelectedPageIds();
-
- if (selectedPageIds.size === 0) {
- return;
- }
-
- const selectedPages = data.data
- .filter(pageWithMeta => selectedPageIds.has(pageWithMeta.data._id))
- .map(pageWithMeta => ({ pageId: pageWithMeta.data._id, path: pageWithMeta.data.path } as ILegacyPrivatePage));
-
- openModal(
- selectedPages,
- () => {
- toastSuccess(t('Successfully requested'));
- closeModal();
- mutateMigrationStatus();
- mutate();
- mutatePageTree();
- },
- );
- }, [data, openModal, t, closeModal, mutateMigrationStatus, mutate]);
-
- const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
- setOffset(0);
- setLimit(pagingSize);
- mutate();
- }, [mutate]);
-
- const pagingNumberChangedHandler = useCallback((activePage: number) => {
- setOffset((activePage - 1) * limit);
- mutate();
- }, [limit, mutate]);
-
- const openConvertModalHandler = useCallback(() => {
- if (!isAdmin) { return }
- setOpenConvertModal(true);
- }, [isAdmin]);
-
- const hitsCount = data?.meta.hitsCount;
-
- const renderOpenModalButton = useCallback(() => {
- return (
-
- openConvertModalHandler()}>
- {t('private_legacy_pages.input_path_to_convert')}
-
-
- );
- }, [t, openConvertModalHandler]);
-
- const extraControls = useMemo(() => {
- const isCheckboxDisabled = hitsCount === 0;
-
- return (
-
-
-
-
-
- {t('private_legacy_pages.bulk_operation')}
-
-
-
- refresh
- {t('private_legacy_pages.convert_all_selected_pages')}
-
-
-
- delete
- {t('search_result.delete_all_selected_page')}
-
-
-
-
-
-
- {isAdmin && renderOpenModalButton()}
-
- );
- // eslint-disable-next-line max-len
- }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t]);
-
- const searchControl = useMemo(() => {
- return (
-
- );
- }, [searchInvokedHandler, extraControls]);
-
- const searchResultListHead = useMemo(() => {
- if (data == null) {
- return <>>;
- }
- return (
-
- );
- }, [data, limit, offset, pagingSizeChangedHandler, migrationStatus]);
-
- const searchPager = useMemo(() => {
- // when pager is not needed
- if (data == null || data.meta.hitsCount === data.meta.total) {
- return <>>;
- }
-
- const { total } = data.meta;
- const { offset, limit } = conditions;
-
- return (
-
- );
- }, [conditions, data, pagingNumberChangedHandler]);
-
- return (
- <>
-
-
-
- setOpenConvertModal(false)}
- onSubmit={async(convertPath: string) => {
- try {
- await apiv3Post('/pages/convert-pages-by-path', {
- convertPath,
- });
- toastSuccess(t('private_legacy_pages.by_path_modal.success'));
- setOpenConvertModal(false);
- mutate();
- mutatePageTree();
- }
- catch (errs) {
- if (errs.length === 1) {
- switch (errs[0].code) {
- case V5ConversionErrCode.GRANT_INVALID:
- toastError(t('private_legacy_pages.by_path_modal.error_grant_invalid'));
- break;
- case V5ConversionErrCode.PAGE_NOT_FOUND:
- toastError(t('private_legacy_pages.by_path_modal.error_page_not_found'));
- break;
- case V5ConversionErrCode.DUPLICATE_PAGES_FOUND:
- toastError(t('private_legacy_pages.by_path_modal.error_duplicate_pages_found'));
- break;
- default:
- toastError(t('private_legacy_pages.by_path_modal.error'));
- }
- }
- else {
- toastError(t('private_legacy_pages.by_path_modal.error'));
- }
- }
- }}
- />
- >
- );
-};
-
-export default PrivateLegacyPages;
diff --git a/apps/app/src/client/components/PrivateLegacyPagesMigrationModal.tsx b/apps/app/src/client/components/PrivateLegacyPagesMigrationModal.tsx
index 7ed72015192..ac6f61be6ad 100644
--- a/apps/app/src/client/components/PrivateLegacyPagesMigrationModal.tsx
+++ b/apps/app/src/client/components/PrivateLegacyPagesMigrationModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState, type JSX } from 'react';
+import React, { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import {
@@ -6,29 +6,39 @@ import {
} from 'reactstrap';
import { apiv3Post } from '~/client/util/apiv3-client';
-import { usePrivateLegacyPagesMigrationModal } from '~/stores/modal';
+import type { ILegacyPrivatePage, PrivateLegacyPagesMigrationModalSubmitedHandler } from '~/states/ui/modal/private-legacy-pages-migration';
+import { usePrivateLegacyPagesMigrationModalActions, usePrivateLegacyPagesMigrationModalStatus } from '~/states/ui/modal/private-legacy-pages-migration';
import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
- const { t } = useTranslation();
-
- const { data: status, close } = usePrivateLegacyPagesMigrationModal();
+/**
+ * PrivateLegacyPagesMigrationModalSubstance - Presentation component (all logic here)
+ */
+type PrivateLegacyPagesMigrationModalSubstanceProps = {
+ status: {
+ isOpened: boolean;
+ pages?: ILegacyPrivatePage[];
+ onSubmit?: PrivateLegacyPagesMigrationModalSubmitedHandler;
+ } | null;
+ close: () => void;
+};
- const isOpened = status?.isOpened ?? false;
+const PrivateLegacyPagesMigrationModalSubstance = ({ status, close }: PrivateLegacyPagesMigrationModalSubstanceProps): React.JSX.Element => {
+ const { t } = useTranslation();
const [isRecursively, setIsRecursively] = useState(true);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [errs, setErrs] = useState(null);
- async function submit() {
+ // Memoize submit handler
+ const submit = useCallback(async() => {
if (status == null || status.pages == null || status.pages.length === 0) {
return;
}
- const { pages, onSubmited } = status;
+ const { pages, onSubmit } = status;
const pageIds = pages.map(page => page.pageId);
try {
await apiv3Post('/pages/legacy-pages-migration', {
@@ -36,16 +46,22 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
isRecursively: isRecursively ? true : undefined,
});
- if (onSubmited != null) {
- onSubmited(pages, isRecursively);
+ if (onSubmit != null) {
+ onSubmit(pages, isRecursively);
}
}
catch (err) {
setErrs([err]);
}
- }
+ }, [status, isRecursively]);
+
+ // Memoize checkbox handler
+ const handleRecursivelyChange = useCallback((e: React.ChangeEvent) => {
+ setIsRecursively(e.target.checked);
+ }, []);
- function renderForm() {
+ // Memoize form rendering
+ const renderForm = useMemo(() => {
return (
{
id="convertRecursively"
type="checkbox"
checked={isRecursively}
- onChange={(e) => {
- setIsRecursively(e.target.checked);
- }}
+ onChange={handleRecursivelyChange}
/>
{ t('private_legacy_pages.modal.convert_recursively_label') }
@@ -63,17 +77,18 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
);
- }
+ }, [isRecursively, handleRecursivelyChange, t]);
- const renderPageIds = () => {
+ // Memoize page IDs rendering
+ const renderPageIds = useMemo(() => {
if (status != null && status.pages != null) {
return status.pages.map(page => { page.path }
);
}
return <>>;
- };
+ }, [status]);
return (
-
+
{ t('private_legacy_pages.modal.title') }
@@ -82,9 +97,9 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
{ t('private_legacy_pages.modal.converting_pages') }:
{/* Todo: change the way to show path on modal when too many pages are selected */}
{/* https://redmine.weseek.co.jp/issues/82787 */}
- {renderPageIds()}
+ {renderPageIds}
- {renderForm()}
+ {renderForm}
@@ -93,7 +108,26 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
{ t('private_legacy_pages.modal.button_label') }
-
+
+ );
+};
+
+/**
+ * PrivateLegacyPagesMigrationModal - Container component (lightweight, always rendered)
+ */
+export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
+ const status = usePrivateLegacyPagesMigrationModalStatus();
+ const { close } = usePrivateLegacyPagesMigrationModalActions();
+ const isOpened = status?.isOpened ?? false;
+ return (
+
);
};
diff --git a/apps/app/src/client/components/PutbackPageModal.jsx b/apps/app/src/client/components/PutbackPageModal.jsx
deleted file mode 100644
index c34dfadd733..00000000000
--- a/apps/app/src/client/components/PutbackPageModal.jsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useState, useCallback } from 'react';
-
-
-import { useTranslation } from 'next-i18next';
-import {
- Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { apiPost } from '~/client/util/apiv1-client';
-import { usePutBackPageModal } from '~/stores/modal';
-import { mutateAllPageInfo } from '~/stores/page';
-
-import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-
-const PutBackPageModal = () => {
- const { t } = useTranslation();
-
- const { data: pageDataToRevert, close: closePutBackPageModal } = usePutBackPageModal();
- const { isOpened, page } = pageDataToRevert;
- const { pageId, path } = page;
- const onPutBacked = pageDataToRevert.opts?.onPutBacked;
-
- const [errs, setErrs] = useState(null);
- const [targetPath, setTargetPath] = useState(null);
-
- const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
-
- function changeIsPutbackRecursivelyHandler() {
- setIsPutbackRecursively(!isPutbackRecursively);
- }
-
- async function putbackPageButtonHandler() {
- setErrs(null);
-
- try {
- // control flag
- // If is it not true, Request value must be `null`.
- const recursively = isPutbackRecursively ? true : null;
-
- const response = await apiPost('/pages.revertRemove', {
- page_id: pageId,
- recursively,
- });
- mutateAllPageInfo();
-
- if (onPutBacked != null) {
- onPutBacked(response.page.path);
- }
- closePutBackPageModal();
- }
- catch (err) {
- setTargetPath(err.data);
- setErrs([err]);
- }
- }
-
- const HeaderContent = () => {
- if (!isOpened) {
- return <>>;
- }
- return (
- <>
-
{ t('modal_putback.label.Put Back Page') }
- >
- );
- };
-
- const BodyContent = () => {
- if (!isOpened) {
- return <>>;
- }
- return (
- <>
-
- >
- );
-
- };
- const FooterContent = () => {
- if (!isOpened) {
- return <>>;
- }
- return (
- <>
-
- >
- );
- };
-
- const closeModalHandler = useCallback(() => {
- closePutBackPageModal();
- setErrs(null);
- }, [closePutBackPageModal]);
-
- return (
-
- );
-
-};
-
-export default PutBackPageModal;
diff --git a/apps/app/src/client/components/PutbackPageModal/PutbackPageModal.tsx b/apps/app/src/client/components/PutbackPageModal/PutbackPageModal.tsx
new file mode 100644
index 00000000000..c2f602b99e3
--- /dev/null
+++ b/apps/app/src/client/components/PutbackPageModal/PutbackPageModal.tsx
@@ -0,0 +1,156 @@
+import type { FC } from 'react';
+import { useState, useCallback, useMemo } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import {
+ Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { apiPost } from '~/client/util/apiv1-client';
+import type { PutBackPageModalStatus } from '~/states/ui/modal/put-back-page';
+import { usePutBackPageModalActions, usePutBackPageModalStatus } from '~/states/ui/modal/put-back-page';
+import { mutateAllPageInfo } from '~/stores/page';
+
+import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList';
+
+type ApiError = {
+ data?: string;
+};
+
+type ApiResponse = {
+ page: {
+ path: string;
+ };
+};
+
+type PutBackPageModalSubstanceProps = {
+ pageDataToRevert: PutBackPageModalStatus & { page: NonNullable