Explorar o código

support APIv3 updating page

Yuki Takei %!s(int64=2) %!d(string=hai) anos
pai
achega
116e2821cc

+ 22 - 56
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -19,10 +19,9 @@ import { throttle, debounce } from 'throttle-debounce';
 
 
 import { useShouldExpandContent } from '~/client/services/layout';
-import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
+import { useUpdateStateAfterSave, updatePage } from '~/client/services/page-operation';
 import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
   useDefaultIndentSize, useCurrentUser,
@@ -32,7 +31,6 @@ import {
 import {
   useEditorSettings,
   useCurrentIndentSize, useIsSlackEnabled, usePageTagsForEditors,
-  useIsEnabledUnsavedWarning,
   useIsConflict,
   useEditingMarkdown,
   useWaitingSaveProcessing,
@@ -90,10 +88,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const router = useRouter();
 
   const previewRef = useRef<HTMLDivElement>(null);
-  const codeMirrorEditorContainerRef = useRef<HTMLDivElement>(null);
 
   const { data: isNotFound } = useIsNotFound();
-  const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage } = useSWRxCurrentPage();
@@ -125,14 +122,12 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: socket } = useGlobalSocket();
 
   const { data: rendererOptions } = usePreviewOptions();
-  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsConflict } = useIsConflict();
 
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
 
   const shouldExpandContent = useShouldExpandContent(currentPage);
 
-  const saveOrUpdate = useSaveOrUpdate();
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
   const { resolvedTheme } = useNextThemes();
@@ -216,40 +211,27 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   }, [socket, checkIsConflict]);
 
-  const optionsToSave = useMemo((): OptionsToSave | undefined => {
-    if (grantData == null) {
-      return;
-    }
-    const userRelatedGrantedGroups = grantData.userRelatedGrantedGroups?.map((group) => {
-      return { item: group.id, type: group.type };
-    });
-    const optionsToSave = {
-      isSlackEnabled: isSlackEnabled ?? false,
-      slackChannels: '', // set in save method by opts in SavePageControlls.tsx
-      grant: grantData.grant,
-      // pageTags: pageTags ?? [],
-      userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
-    };
-    return optionsToSave;
-  }, [grantData, isSlackEnabled]);
-
-
   const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
-    if (currentPathname == null || optionsToSave == null) {
-      logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
+    if (pageId == null || currentPagePath == null || currentRevisionId == null || grantData == null) {
+      logger.error('Some materials to save are invalid', {
+        pageId, currentPagePath, currentRevisionId, grantData,
+      });
       throw new Error('Some materials to save are invalid');
     }
 
-    const options = Object.assign(optionsToSave, opts);
-
     try {
       mutateWaitingSaveProcessing(true);
 
-      const { page } = await saveOrUpdate(
-        codeMirrorEditor?.getDoc() ?? '',
-        { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
-        options,
-      );
+      const { page } = await updatePage({
+        pageId,
+        revisionId: currentRevisionId,
+        body: codeMirrorEditor?.getDoc() ?? '',
+        grant: grantData?.grant,
+        userRelatedGrantUserGroupIds: grantData?.userRelatedGrantedGroups?.map((group) => {
+          return { item: group.id, type: group.type };
+        }),
+        ...(opts ?? {}),
+      });
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
       mutatePageTree();
@@ -271,12 +253,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       mutateWaitingSaveProcessing(false);
     }
 
-  }, [
-    codeMirrorEditor,
-    currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
-    currentPagePath, currentRevisionId,
-    mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
-  ]);
+  // eslint-disable-next-line max-len
+  }, [codeMirrorEditor, grantData, pageId, currentPagePath, currentRevisionId, mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     const page = await save(opts);
@@ -284,14 +262,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       return;
     }
 
-    if (isNotFound) {
-      await router.push(`/${page._id}`);
-    }
-    else {
-      updateStateAfterSave?.();
-    }
     mutateEditorMode(EditorMode.View);
-  }, [save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
+    updateStateAfterSave?.();
+  }, [mutateEditorMode, save, updateStateAfterSave]);
 
   const saveWithShortcut = useCallback(async() => {
     const page = await save();
@@ -299,16 +272,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       return;
     }
 
-    if (isNotFound) {
-      await router.push(`/${page._id}#edit`);
-    }
-    else {
-      updateStateAfterSave?.();
-    }
     toastSuccess(t('toaster.save_succeeded'));
-    mutateEditorMode(EditorMode.Editor);
-
-  }, [isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
+    updateStateAfterSave?.();
+  }, [save, t, updateStateAfterSave]);
 
 
   // the upload event handler

+ 13 - 7
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback, useState } from 'react';
 
-import { isPopulated, GroupType, type IGrantedGroup } from '@growi/core';
+import {
+  PageGrant, isPopulated, GroupType, type IGrantedGroup,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledDropdown,
@@ -16,24 +18,28 @@ import { useMyUserGroups } from './use-my-user-groups';
 
 const AVAILABLE_GRANTS = [
   {
-    grant: 1, iconClass: 'icon-people', btnStyleClass: 'outline-info', label: 'Public',
+    grant: PageGrant.GRANT_PUBLIC, iconClass: 'icon-people', btnStyleClass: 'outline-info', label: 'Public',
   },
   {
-    grant: 2, iconClass: 'icon-link', btnStyleClass: 'outline-teal', label: 'Anyone with the link',
+    grant: PageGrant.GRANT_RESTRICTED, iconClass: 'icon-link', btnStyleClass: 'outline-teal', label: 'Anyone with the link',
   },
   // { grant: 3, iconClass: '', label: 'Specified users only' },
   {
-    grant: 4, iconClass: 'icon-lock', btnStyleClass: 'outline-danger', label: 'Only me',
+    grant: PageGrant.GRANT_OWNER, iconClass: 'icon-lock', btnStyleClass: 'outline-danger', label: 'Only me',
   },
   {
-    grant: 5, iconClass: 'icon-options', btnStyleClass: 'outline-purple', label: 'Only inside the group', reselectLabel: 'Reselect the group',
+    grant: PageGrant.GRANT_USER_GROUP,
+    iconClass: 'icon-options',
+    btnStyleClass: 'outline-purple',
+    label: 'Only inside the group',
+    reselectLabel: 'Reselect the group',
   },
 ];
 
 
 type Props = {
   disabled?: boolean,
-  grant: number,
+  grant: PageGrant,
   userRelatedGrantedGroups?: {
     id: string,
     name: string,
@@ -72,7 +78,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
   /**
    * change event handler for grant selector
    */
-  const changeGrantHandler = useCallback((grant: number) => {
+  const changeGrantHandler = useCallback((grant: PageGrant) => {
     // select group
     if (grant === 5) {
       showSelectGroupModal();

+ 1 - 1
apps/app/src/interfaces/page.ts

@@ -11,7 +11,7 @@ export {
 export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean, processData?: IPageOperationProcessData}>;
 
 export type IPageGrantData = {
-  grant: number,
+  grant: PageGrant,
   userRelatedGrantedGroups?: {
     id: string,
     name: string,

+ 1 - 0
apps/app/src/server/models/page-operation.ts

@@ -42,6 +42,7 @@ type IUserForResuming = {
 };
 
 type IOptionsForResuming = {
+  format: 'md' | 'pdf',
   updateMetadata?: boolean,
   createRedirectPage?: boolean,
   prevDescendantCount?: number,

+ 3 - 9
apps/app/src/server/service/page/index.ts

@@ -3744,8 +3744,6 @@ class PageService implements IPageService {
    * Set options.isSynchronously to true to await all process when you want to run this method multiple times at short intervals.
    */
   async create(path: string, body: string, user, options: IOptionsForCreate = {}): Promise<PageDocument> {
-    const Page = mongoose.model('Page') as unknown as PageModel;
-
     // Switch method
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
     if (!isV5Compatible) {
@@ -3755,9 +3753,7 @@ class PageService implements IPageService {
     // Values
     // eslint-disable-next-line no-param-reassign
     path = this.crowi.xss.process(path); // sanitize path
-    const {
-      format = 'markdown', grantUserGroupIds,
-    } = options;
+    const { grantUserGroupIds } = options;
     const grant = isTopPage(path) ? PageGrant.GRANT_PUBLIC : options.grant;
     const grantData = {
       grant,
@@ -3797,7 +3793,7 @@ class PageService implements IPageService {
 
     // Create revision
     const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
-    const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
+    const newRevision = Revision.prepareRevision(savedPage, body, null, user);
     savedPage = await pushRevision(savedPage, newRevision, user);
     await savedPage.populateDataToShowRevision();
 
@@ -4016,7 +4012,6 @@ class PageService implements IPageService {
     const options: IOptionsForUpdate = {
       grant,
       userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
-      isSyncRevisionToHackmd: false,
     };
 
     return this.updatePage(page, null, null, user, options);
@@ -4091,7 +4086,7 @@ class PageService implements IPageService {
       pageData: PageDocument,
       body: string | null,
       previousBody: string | null,
-      user,
+      user: IUserHasId,
       options: IOptionsForUpdate = {},
   ): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
@@ -4230,7 +4225,6 @@ class PageService implements IPageService {
     const grantUserGroupIds = options.userRelatedGrantUserGroupIds != null
       ? (await this.getNewGrantedGroups(options.userRelatedGrantUserGroupIds, pageData, user))
       : pageData.grantedGroups;
-    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
 
     // validate multiple group grant before save using pageData and options
     await this.pageGrantService.validateGrantChange(user, pageData.grantedGroups, grant, grantUserGroupIds);

+ 5 - 2
apps/app/src/server/service/page/page-service.ts

@@ -1,15 +1,18 @@
 import type EventEmitter from 'events';
 
-import type { IPageInfo, IPageInfoForEntity, IUser } from '@growi/core';
+import type {
+  IPageInfo, IPageInfoForEntity, IUser,
+} from '@growi/core';
 import type { ObjectId } from 'mongoose';
 
-import type { IOptionsForCreate } from '~/interfaces/page';
+import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 import type { PageDocument } from '~/server/models/page';
 
 export interface IPageService {
   create(path: string, body: string, user: IUser, options: IOptionsForCreate): Promise<PageDocument>,
+  updatePage(pageData: PageDocument, body: string | null, previousBody: string | null, user: IUser, options: IOptionsForUpdate,): Promise<PageDocument>,
   updateDescendantCountOfAncestors: (pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean) => Promise<void>,
   deleteCompletelyOperation: (pageIds: string[], pagePaths: string[]) => Promise<void>,
   getEventEmitter: () => EventEmitter,