Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
440 changes: 440 additions & 0 deletions .serena/memories/page-state-hooks-useLatestRevision-degradation.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions apps/app/src/client/components/Page/DisplaySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { JSX } from 'react';
import dynamic from 'next/dynamic';

import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
import { useIsEditable, useLatestRevision } from '~/states/page';
import { useIsEditable } from '~/states/page';
import { EditorMode, useEditorMode, useReservedNextCaretLine } from '~/states/ui/editor';
import { useSWRxIsLatestRevision } from '~/stores/page';

import { LazyRenderer } from '../Common/LazyRenderer';

Expand All @@ -17,14 +18,14 @@ export const DisplaySwitcher = (): JSX.Element => {

const { editorMode } = useEditorMode();
const isEditable = useIsEditable();
const isLatestRevision = useLatestRevision();
const { data: isLatestRevision } = useSWRxIsLatestRevision();

useHashChangedEffect();
useReservedNextCaretLine();

return (
<LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
{ isLatestRevision
{ isLatestRevision !== false
? <PageEditor />
: <PageEditorReadOnly />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import {
Modal, ModalHeader, ModalBody, ModalFooter,
} from 'reactstrap';


import { useCurrentUser } from '~/states/global';
import {
useCurrentPageData,
useRemoteRevisionBody,
useRemoteRevisionId,
useRemoteRevisionLastUpdatedAt,
useRemoteRevisionLastUpdateUser,
} from '~/states/page';
Expand Down Expand Up @@ -206,12 +206,11 @@ export const ConflictDiffModal = (): React.JSX.Element => {
const conflictDiffModalStatus = useConflictDiffModalStatus();

// state for latest page
const remoteRevisionId = useRemoteRevisionId();
const remoteRevisionBody = useRemoteRevisionBody();
const remoteRevisionLastUpdateUser = useRemoteRevisionLastUpdateUser();
const remoteRevisionLastUpdatedAt = useRemoteRevisionLastUpdatedAt();

const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
const isRemotePageDataInappropriate = remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;

const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { CodeMirrorEditorReadOnly } from '@growi/editor/dist/client/components/C
import { throttle } from 'throttle-debounce';

import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
import { useCurrentPageData, useLatestRevision } from '~/states/page';
import { useCurrentPageData } from '~/states/page';
import { useSWRxIsLatestRevision } from '~/stores/page';
import { usePreviewOptions } from '~/stores/renderer';

import { EditorNavbar } from './EditorNavbar';
Expand All @@ -21,7 +22,7 @@ export const PageEditorReadOnly = react.memo(({ visibility }: Props): JSX.Elemen

const currentPage = useCurrentPageData();
const { data: rendererOptions } = usePreviewOptions();
const isLatestRevision = useLatestRevision();
const { data: isLatestRevision } = useSWRxIsLatestRevision();
const shouldExpandContent = useShouldExpandContent(currentPage);

const { scrollEditorHandler, scrollPreviewHandler } = useScrollSync(GlobalCodeMirrorEditorKey.READONLY, previewRef);
Expand All @@ -30,7 +31,8 @@ export const PageEditorReadOnly = react.memo(({ visibility }: Props): JSX.Elemen

const revisionBody = currentPage?.revision?.body;

if (rendererOptions == null || isLatestRevision) {
// Show read-only editor only when viewing an old revision
if (rendererOptions == null || isLatestRevision !== false) {
return <></>;
}

Expand Down
9 changes: 3 additions & 6 deletions apps/app/src/client/components/PageStatusAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import React, { useCallback, type JSX } from 'react';
import { useTranslation } from 'next-i18next';

import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
import { useCurrentPageData, useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/states/page';
import { useRemoteRevisionLastUpdateUser } from '~/states/page';
import { useEditorMode } from '~/states/ui/editor';
import { usePageStatusAlertStatus } from '~/states/ui/modal/page-status-alert';
import { useIsRevisionOutdated } from '~/stores/page';

import { Username } from '../../components/User/Username';

Expand All @@ -18,9 +19,8 @@ export const PageStatusAlert = (): JSX.Element => {
const isGuestUser = useIsGuestUser();
const isReadOnlyUser = useIsReadOnlyUser();
const pageStatusAlertData = usePageStatusAlertStatus();
const remoteRevisionId = useRemoteRevisionId();
const isRevisionOutdated = useIsRevisionOutdated();
const remoteRevisionLastUpdateUser = useRemoteRevisionLastUpdateUser();
const pageData = useCurrentPageData();

const onClickRefreshPage = useCallback(() => {
pageStatusAlertData?.onRefleshPage?.();
Expand All @@ -33,9 +33,6 @@ export const PageStatusAlert = (): JSX.Element => {
const hasResolveConflictHandler = pageStatusAlertData?.onResolveConflict != null;
const hasRefreshPageHandler = pageStatusAlertData?.onRefleshPage != null;

const currentRevisionId = pageData?.revision?._id;
const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null) && currentRevisionId !== remoteRevisionId;

if (!pageStatusAlertData?.isOpen || !!isGuestUser || !!isReadOnlyUser || !isRevisionOutdated) {
return <></>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { useTranslation } from 'next-i18next';

import { useCurrentPageYjsData } from '~/features/collaborative-editor/states';
import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
import { useIsRevisionOutdated } from '~/states/page';
import { useShareLinkId } from '~/states/page/hooks';
import { useIsRevisionOutdated } from '~/stores/page';

import '@growi/remark-drawio/dist/style.css';
import styles from './DrawioViewerWithEditButton.module.scss';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { Element } from 'hast';
import type { LaunchHandsonTableModalEventDetail } from '~/client/interfaces/handsontable-modal';
import { useCurrentPageYjsData } from '~/features/collaborative-editor/states';
import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
import { useIsRevisionOutdated } from '~/states/page';
import { useShareLinkId } from '~/states/page/hooks';
import { useIsRevisionOutdated } from '~/stores/page';

import styles from './TableWithEditButton.module.scss';

Expand Down
9 changes: 8 additions & 1 deletion apps/app/src/client/services/side-effects/page-updated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { RemoteRevisionData } from '~/states/page';
import { useGlobalSocket } from '~/states/socket-io';
import { useEditorMode, EditorMode } from '~/states/ui/editor';
import { usePageStatusAlertActions } from '~/states/ui/modal/page-status-alert';
import { useSWRxPageInfo } from '~/stores/page';


export const usePageUpdatedEffect = (): void => {
Expand All @@ -18,6 +19,8 @@ export const usePageUpdatedEffect = (): void => {
const { fetchCurrentPage } = useFetchCurrentPage();
const { open: openPageStatusAlert, close: closePageStatusAlert } = usePageStatusAlertActions();

const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id);

const remotePageDataUpdateHandler = useCallback((data) => {
// Set remote page data
const { s2cMessagePageUpdated } = data;
Expand All @@ -32,6 +35,9 @@ export const usePageUpdatedEffect = (): void => {
if (currentPage?._id != null && currentPage._id === s2cMessagePageUpdated.pageId) {
setRemoteLatestPageData(remoteData);

// Update PageInfo cache
mutatePageInfo();

// Open PageStatusAlert
const currentRevisionId = currentPage?.revision?._id;
const remoteRevisionId = s2cMessagePageUpdated.revisionId;
Expand All @@ -47,7 +53,8 @@ export const usePageUpdatedEffect = (): void => {
closePageStatusAlert();
}
}
}, [currentPage?._id, currentPage?.revision?._id, editorMode, fetchCurrentPage, openPageStatusAlert, closePageStatusAlert, setRemoteLatestPageData]);
// eslint-disable-next-line max-len
}, [currentPage?._id, currentPage?.revision?._id, setRemoteLatestPageData, mutatePageInfo, editorMode, openPageStatusAlert, fetchCurrentPage, closePageStatusAlert]);

// listen socket for someone updating this page
useEffect(() => {
Expand Down
12 changes: 5 additions & 7 deletions apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ import { useRouter } from 'next/router';
import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
import { useTranslation } from 'react-i18next';

import {
useCurrentPageData,
useFetchCurrentPage,
useLatestRevision,
} from '~/states/page';
import { useCurrentPageData, useFetchCurrentPage } from '~/states/page';
import { useSWRxIsLatestRevision } from '~/stores/page';

export const OldRevisionAlert = (): JSX.Element => {
const router = useRouter();
const { t } = useTranslation();

const isOldRevisionPage = useLatestRevision();
const { data: isLatestRevision } = useSWRxIsLatestRevision();
const page = useCurrentPageData();
const { fetchCurrentPage } = useFetchCurrentPage();

Expand All @@ -27,7 +24,8 @@ export const OldRevisionAlert = (): JSX.Element => {
fetchCurrentPage({ force: true });
}, [fetchCurrentPage, page, router]);

if (page == null || isOldRevisionPage) {
// Show alert only when viewing an old revision (isLatestRevision === false)
if (isLatestRevision !== false) {
// biome-ignore lint/complexity/noUselessFragments: ignore
return <></>;
}
Expand Down
15 changes: 15 additions & 0 deletions apps/app/src/pages/[[...path]]/index.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useEffect } from 'react';
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import EventEmitter from 'node:events';
import { isIPageInfo } from '@growi/core';
import { isClient } from '@growi/core/dist/utils';

// biome-ignore-start lint/style/noRestrictedImports: no-problem lazy loaded components
Expand All @@ -29,6 +31,7 @@ import {
useSetupGlobalSocketForPage,
} from '~/states/socket-io';
import { useSetEditingMarkdown } from '~/states/ui/editor';
import { useSWRxPageInfo } from '~/stores/page';

import type { NextPageWithLayout } from '../_app.page';
import { useHydrateBasicLayoutConfigurationAtoms } from '../basic-layout-page/hydrate';
Expand All @@ -50,6 +53,7 @@ import {
import type { EachProps, InitialProps } from './types';
import { useSameRouteNavigation } from './use-same-route-navigation';
import { useShallowRouting } from './use-shallow-routing';
import { useSyncRevisionIdFromUrl } from './use-sync-revision-id-from-url';

// call superjson custom register
registerPageToShowRevisionWithMeta();
Expand Down Expand Up @@ -117,6 +121,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
const rendererConfig = useRendererConfig();
const setEditingMarkdown = useSetEditingMarkdown();

// Sync URL query parameter to atom
useSyncRevisionIdFromUrl();

// setup socket.io
useSetupGlobalSocket();
useSetupGlobalSocketForPage();
Expand All @@ -135,6 +142,14 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
}
}, [currentPagePath, currentPage?.revision?.body, setEditingMarkdown]);

// Optimistically update PageInfo SWR cache with SSR data
const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id);
useEffect(() => {
if (isInitialProps(props) && pageMeta != null && isIPageInfo(pageMeta)) {
mutatePageInfo(pageMeta, { revalidate: false });
}
}, [pageMeta, mutatePageInfo, props]);

// If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
// So preferentially take page data from useSWRxCurrentPage
const pagePath = currentPagePath ?? props.currentPathname;
Expand Down
23 changes: 23 additions & 0 deletions apps/app/src/pages/[[...path]]/use-sync-revision-id-from-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSetAtom } from 'jotai';

import { _atomsForSyncRevisionIdFromUrl } from '~/states/page';

const { revisionIdFromUrlAtom } = _atomsForSyncRevisionIdFromUrl;

/**
* Sync URL query parameter (revisionId) to Jotai atom
* This hook should be called in the main page component to keep the atom in sync with the URL
*/
export const useSyncRevisionIdFromUrl = (): void => {
const router = useRouter();
const setRevisionIdFromUrl = useSetAtom(revisionIdFromUrlAtom);

useEffect(() => {
const revisionId = router.query.revisionId;
setRevisionIdFromUrl(
typeof revisionId === 'string' ? revisionId : undefined,
);
}, [router.query.revisionId, setRevisionIdFromUrl]);
};
3 changes: 3 additions & 0 deletions apps/app/src/server/service/page/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2602,6 +2602,9 @@ class PageService implements IPageService {
contentAge: page.getContentAge(),
descendantCount: page.descendantCount,
commentCount: page.commentCount,
// the page must have a revision if it is not empty
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
latestRevisionId: getIdStringForRef(page.revision!),
};

return infoForEntity;
Expand Down
24 changes: 10 additions & 14 deletions apps/app/src/states/page/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ import {
currentPagePathAtom,
isForbiddenAtom,
isIdenticalPathAtom,
isRevisionOutdatedAtom,
isTrashPageAtom,
isUntitledPageAtom,
latestRevisionAtom,
pageNotFoundAtom,
redirectFromAtom,
remoteRevisionBodyAtom,
remoteRevisionIdAtom,
remoteRevisionLastUpdatedAtAtom,
remoteRevisionLastUpdateUserAtom,
revisionIdFromUrlAtom,
shareLinkIdAtom,
templateBodyAtom,
templateTagsAtom,
Expand All @@ -45,17 +43,22 @@ export const useIsIdenticalPath = () => useAtomValue(isIdenticalPathAtom);

export const useIsForbidden = () => useAtomValue(isForbiddenAtom);

export const useLatestRevision = () => useAtomValue(latestRevisionAtom);

export const useShareLinkId = () => useAtomValue(shareLinkIdAtom);

export const useTemplateTags = () => useAtomValue(templateTagsAtom);

export const useTemplateBody = () => useAtomValue(templateBodyAtom);

// Remote revision hooks (replacements for stores/remote-latest-page.ts)
export const useRemoteRevisionId = () => useAtomValue(remoteRevisionIdAtom);
/**
* Hook to get revisionId from URL query parameters
* Returns undefined if revisionId is not present in the URL
*
* This hook reads from the revisionIdFromUrlAtom which should be updated
* by the page component when router.query.revisionId changes
*/
export const useRevisionIdFromUrl = () => useAtomValue(revisionIdFromUrlAtom);

// Remote revision hooks (replacements for stores/remote-latest-page.ts)
export const useRemoteRevisionBody = () => useAtomValue(remoteRevisionBodyAtom);

export const useRemoteRevisionLastUpdateUser = () =>
Expand Down Expand Up @@ -91,13 +94,6 @@ export const useCurrentPagePath = (): string | undefined => {
*/
export const useIsTrashPage = (): boolean => useAtomValue(isTrashPageAtom);

/**
* Check if current revision is outdated
* Pure Jotai replacement for stores/page.tsx useIsRevisionOutdated
*/
export const useIsRevisionOutdated = (): boolean =>
useAtomValue(isRevisionOutdatedAtom);

/**
* Computed hook for checking if current page is creatable
*/
Expand Down
Loading
Loading