jam411 пре 3 година
родитељ
комит
872ee54607

+ 0 - 0
packages/app/src/client/legacy/crowi.js → packages/app/_obsolete/src/client/legacy/crowi.js


+ 4 - 2
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -2,7 +2,10 @@ import React, { useCallback } from 'react';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
+// import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxPlugins } from '~/stores/plugin';
+
+// TODO: error notification (toast, loggerFactory)
 // TODO: i18n
 export const PluginInstallerForm = (): JSX.Element => {
   const { mutate } = useSWRxPlugins();
@@ -25,12 +28,11 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins-extension', { pluginInstallerForm });
+      await apiv3Post('/plugins', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
     }
     catch (err) {
       toastError(err);
-      // logger.error(err);
     }
     finally {
       mutate();

+ 1 - 4
packages/app/src/components/BookmarkButtons.tsx

@@ -41,15 +41,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
   };
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isBookmarked) {
       return 'tooltip.cancel_bookmark';
     }
     return 'tooltip.bookmark';
-  }, [isGuestUser, isBookmarked]);
+  }, [isBookmarked]);
 
   return (
     <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">

+ 1 - 4
packages/app/src/components/LikeButtons.tsx

@@ -34,15 +34,12 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
   } = props;
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isLiked) {
       return 'tooltip.cancel_like';
     }
     return 'tooltip.like';
-  }, [isGuestUser, isLiked]);
+  }, [isLiked]);
 
   return (
     <div className={`btn-group btn-group-like ${styles['btn-group-like']}`} role="group" aria-label="Like buttons">

+ 0 - 5
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -110,11 +110,6 @@ function PageEditorModeManager(props) {
           </>
         )}
       </div>
-      {isBtnDisabled && (
-        <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
     </>
   );
 

+ 3 - 1
packages/app/src/components/NotAvailableForGuest.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
@@ -7,6 +8,7 @@ import { useIsGuestUser } from '~/stores/context';
 
 const NotAvailableForGuest = (props) => {
   const { children } = props;
+  const { t } = useTranslation();
 
   const { data: isGuestUser } = useIsGuestUser();
 
@@ -26,7 +28,7 @@ const NotAvailableForGuest = (props) => {
   return (
     <>
       { clonedChild }
-      <UncontrolledTooltip placement="top" target={id}>Not available for guest</UncontrolledTooltip>
+      <UncontrolledTooltip placement="top" target={id}>{t('Not available for guest')}</UncontrolledTooltip>
     </>
   );
 

+ 24 - 0
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -10,6 +10,7 @@ import { SocketEventName } from '~/interfaces/websocket';
 import {
   useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
+import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import {
@@ -51,6 +52,8 @@ const PageView = React.memo((): JSX.Element => {
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
+  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
+
   const isTopPagePath = isTopPage(currentPagePath ?? '');
   const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
 
@@ -64,10 +67,19 @@ const PageView = React.memo((): JSX.Element => {
       remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
       remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
       remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
+      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
     setRemoteLatestPageData(remoteData);
   }, [setRemoteLatestPageData]);
 
+  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+    if (s2cMessagePageUpdated.pageId === currentPage?._id) {
+      mutateIsHackmdDraftUpdatingInRealtime(true);
+    }
+  }, [currentPage?._id, mutateIsHackmdDraftUpdatingInRealtime]);
+
   // listen socket for someone updating this page
   useEffect(() => {
 
@@ -81,6 +93,18 @@ const PageView = React.memo((): JSX.Element => {
 
   }, [setLatestRemotePageData, socket]);
 
+  // listen socket for hackmd saved
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+
+    return () => {
+      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+    };
+  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
+
   return (
     <div className="d-flex flex-column flex-lg-row">
 

+ 14 - 15
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 
 import { useTranslation } from 'next-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
+
+import NotAvailableForGuest from '../NotAvailableForGuest';
 
 type RenderTagLabelsProps = {
   tags: string[],
@@ -31,21 +32,19 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
           </a>
         );
       })}
-      <div id="edit-tags-btn-wrapper-for-tooltip">
-        <a
-          className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
-          onClick={openEditorHandler}
-        >
-          { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-          <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
-        </a>
-      </div>
-      {isGuestUser && (
-        <UncontrolledTooltip placement="top" target="edit-tags-btn-wrapper-for-tooltip" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+      <NotAvailableForGuest>
+        <div id="edit-tags-btn-wrapper-for-tooltip">
+          <a
+            className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
+            onClick={openEditorHandler}
+          >
+            { isTagsEmpty && <>{ t('Add tags for this page') }</>}
+            <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
+          </a>
+        </div>
+      </NotAvailableForGuest>
     </>
+
   );
 
 });

+ 2 - 0
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -114,6 +114,8 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
         remoteRevisionBody: page.revision.body,
         remoteRevisionLastUpdateUser: page.lastUpdateUser,
         remoteRevisionLastUpdatedAt: page.updatedAt,
+        revisionIdHackmdSynced: page.revisionIdHackmdSynced,
+        hasDraftOnHackmd: page.hasDraftOnHackmd,
       };
       setRemoteLatestPageData(remotePageData);
       afterResolvedHandler();

+ 6 - 4
packages/app/src/components/PageEditorByHackmd.tsx

@@ -85,7 +85,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: hasDraftOnHackmd, mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime(false);
+  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId(revision?._id);
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
@@ -94,7 +94,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
     if (editorMode !== EditorMode.HackMD) { return }
 
     try {
-      if (isSlackEnabled == null || currentPathname == null || slackChannels == null || grant == null || revision == null || hackmdEditorRef.current == null) {
+      if (isSlackEnabled == null || currentPathname == null || slackChannels == null || grant == null
+          || revision == null || hackmdEditorRef.current == null || revisionIdHackmdSynced == null) {
         throw new Error('Some materials to save are invalid');
       }
 
@@ -111,7 +112,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
 
       const markdown = await hackmdEditorRef.current.getValue();
 
-      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revision?._id }, optionsToSave);
+      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, optionsToSave);
       await mutatePageData();
       await mutateTagsInfo();
 
@@ -125,6 +126,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         await mutateCurrentPageId(page._id);
         await mutatePageData();
       }
+      setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
     }
     catch (error) {
@@ -132,7 +134,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, mutateCurrentPageId]);
+  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, revisionIdHackmdSynced, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, mutateCurrentPageId]);
 
   // set handler to save and reload Page
   useEffect(() => {

+ 11 - 4
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -14,6 +14,7 @@ import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/p
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
+import NotAvailableForGuest from '~/components/NotAvailableForGuest';
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
@@ -496,19 +497,25 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           >
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-              <i className="icon-options fa fa-rotate-90 p-1"></i>
+              <NotAvailableForGuest>
+                <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
+              </NotAvailableForGuest>
             </DropdownToggle>
           </PageItemControl>
-          {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+        </div>
+
+        {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+          <NotAvailableForGuest>
             <button
+              id='page-create-button-in-page-tree'
               type="button"
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
               onClick={onClickPlusButton}
             >
               <i className="icon-plus d-block p-0" />
             </button>
-          )}
-        </div>
+          </NotAvailableForGuest>
+        )}
       </li>
 
       {isEnableActions && isNewPageInputShown && (

+ 1 - 4
packages/app/src/components/SubscribeButton.tsx

@@ -20,15 +20,12 @@ const SubscribeButton: FC<Props> = (props: Props) => {
   const isSubscribing = status === SubscriptionStatusType.SUBSCRIBE;
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isSubscribing) {
       return 'tooltip.stop_notification';
     }
     return 'tooltip.receive_notifications';
-  }, [isGuestUser, isSubscribing]);
+  }, [isSubscribing]);
 
   return (
     <>

+ 3 - 0
packages/app/src/interfaces/websocket.ts

@@ -22,6 +22,9 @@ export const SocketEventName = {
   PageUpdated: 'page:update',
   PageDeleted: 'page:delete',
 
+  // Hackmd
+  EditingWithHackmd: 'page:editingWithHackmd',
+
 } as const;
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 

+ 6 - 0
packages/app/src/pages/[[...path]].page.tsx

@@ -37,8 +37,10 @@ import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useEditingMarkdown } from '~/stores/editor';
+import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
+import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   useEditorMode, useSelectedGrant,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
@@ -257,6 +259,10 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   const pagePath = pageWithMeta?.data.path ?? (!_isPermalink(props.currentPathname) ? props.currentPathname : undefined);
 
   useCurrentPageId(pageId ?? null);
+  useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
+  useRemoteRevisionId(pageWithMeta?.data.revision._id);
+  usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
+  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
 

+ 2 - 2
packages/app/src/pages/_document.page.tsx

@@ -58,14 +58,14 @@ const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element =
           elements.push(<>
             {/* eslint-disable-next-line @next/next/no-sync-scripts */ }
             <script type="module" key={`script_${growiPlugin.installedPath}`}
-              src={`/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`} />
+              src={`/static/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`} />
           </>);
         }
         // add link
         if (types.includes(GrowiPluginResourceType.Script) || types.includes(GrowiPluginResourceType.Style)) {
           elements.push(<>
             <link rel="stylesheet" key={`link_${growiPlugin.installedPath}`}
-              href={`/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`} />
+              href={`/static/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`} />
           </>);
         }
 

+ 1 - 1
packages/app/src/server/crowi/express-init.js

@@ -120,7 +120,7 @@ module.exports = function(crowi, app) {
   app.use('/static/preset-themes', express.static(
     resolveFromRoot(`../../node_modules/@growi/preset-themes/${path.dirname(presetThemesManifestPath)}`),
   ));
-  app.use('/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
+  app.use('/static/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
 
   app.engine('html', swig.renderFile);
   // app.set('view cache', false);  // Default: true in production, otherwise undefined. -- 2017.07.04 Yuki Takei

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -104,7 +104,7 @@ module.exports = (crowi, app) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
-  router.use('/plugins-extension', require('./plugins-extension')(crowi));
+  router.use('/plugins', require('./plugins')(crowi));
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 

+ 0 - 0
packages/app/src/server/routes/apiv3/plugins-extension.ts → packages/app/src/server/routes/apiv3/plugins.ts


+ 54 - 41
packages/app/src/server/service/plugin.ts

@@ -1,9 +1,10 @@
-import { execSync } from 'child_process';
 import fs from 'fs';
 import path from 'path';
 
+// eslint-disable-next-line no-restricted-imports
+import axios from 'axios';
 import mongoose from 'mongoose';
-import request from 'superagent';
+import streamToPromise from 'stream-to-promise';
 import unzipper from 'unzipper';
 
 import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
@@ -11,7 +12,6 @@ import type { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
-
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
 const pluginStoringPath = resolveFromRoot('tmp/plugins');
@@ -26,6 +26,8 @@ export class PluginService {
     // download
     const ghUrl = new URL(origin.url);
     const ghPathname = ghUrl.pathname;
+    // TODO: Branch names can be specified.
+    const ghBranch = 'main';
 
     const match = ghPathname.match(githubReposIdPattern);
     if (ghUrl.hostname !== 'github.com' || match == null) {
@@ -34,10 +36,10 @@ export class PluginService {
 
     const ghOrganizationName = match[1];
     const ghReposName = match[2];
-    const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/main.zip`;
+    const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
 
     // download github repository to local file system
-    await this.download(requestUrl, ghOrganizationName, ghReposName);
+    await this.download(requestUrl, ghOrganizationName, ghReposName, ghBranch);
 
     // save plugin metadata
     const installedPath = `${ghOrganizationName}/${ghReposName}`;
@@ -48,52 +50,63 @@ export class PluginService {
     return;
   }
 
-  async download(url: string, ghOrganizationName: string, ghReposName: string): Promise<void> {
+  async download(requestUrl: string, ghOrganizationName: string, ghReposName: string, ghBranch: string): Promise<void> {
 
-    const zipFilePath = path.join(pluginStoringPath, 'main.zip');
+    const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
     const unzippedPath = path.join(pluginStoringPath, ghOrganizationName);
 
-    const downloadZipFile = () => {
-      const writeStream = fs.createWriteStream(zipFilePath);
-
-      return new Promise<void>((resolve, reject) => {
-        request
-          .get(url)
-          .pipe(writeStream)
-          .on('close', () => writeStream.close())
-          .on('finish', () => resolve())
-          .on('error', (error: any) => reject(error));
-      });
+    const renamePath = async(oldPath: fs.PathLike, newPath: fs.PathLike) => {
+      fs.renameSync(oldPath, newPath);
     };
 
-    const unzip = () => {
-      const stream = fs.createReadStream(zipFilePath);
-      const deleteZipFile = (path: fs.PathLike) => {
-        fs.unlink(path, (err) => {
-          if (err) throw err;
-        });
-      };
-
+    const downloadFile = async(requestUrl: string, filePath: string) => {
       return new Promise<void>((resolve, reject) => {
-        stream.pipe(unzipper.Extract({ path: unzippedPath }))
-          .on('finish', () => {
-            deleteZipFile(zipFilePath);
-            resolve();
-          })
-          .on('error', (error: any) => reject(error));
+        axios({
+          method: 'GET',
+          url: requestUrl,
+          responseType: 'stream',
+        })
+          .then((res) => {
+            if (res.status === 200) {
+              const file = fs.createWriteStream(filePath);
+              res.data.pipe(file)
+                .on('close', () => file.close())
+                .on('finish', () => {
+                  return resolve();
+                });
+            }
+            else {
+              return reject(res.status);
+            }
+          }).catch((err) => {
+            return reject(err);
+          });
       });
     };
 
-    const renameUnzipFolderPath = async() => {
-      const oldPath = `${unzippedPath}/${ghReposName}-main`;
-      const newPath = `${unzippedPath}/${ghReposName}`;
-
-      fs.renameSync(oldPath, newPath);
+    const unzip = async(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike) => {
+      const stream = fs.createReadStream(zipFilePath);
+      const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
+      const deleteZipFile = (path: fs.PathLike) => fs.unlink(path, (err) => { return err });
+
+      try {
+        await streamToPromise(unzipStream);
+        deleteZipFile(zipFilePath);
+      }
+      catch (err) {
+        return err;
+      }
     };
 
-    await downloadZipFile();
-    await unzip();
-    await renameUnzipFolderPath();
+    try {
+      await downloadFile(requestUrl, zipFilePath);
+      await unzip(zipFilePath, unzippedPath);
+      await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error(err);
+    }
 
     return;
   }
@@ -191,7 +204,7 @@ export class PluginService {
     // TODO: Check remove
     const ghOrganizationName = 'weseek';
     const unzipTargetPath = path.join(pluginStoringPath, ghOrganizationName);
-    execSync(`rm -rf ${unzipTargetPath}/${targetPluginName}`);
+    // execSync(`rm -rf ${unzipTargetPath}/${targetPluginName}`);
     return [];
   }
 

+ 9 - 2
packages/app/src/stores/remote-latest-page.ts

@@ -2,6 +2,7 @@ import { SWRResponse } from 'swr';
 
 import { IUser } from '~/interfaces/user';
 
+import { useRevisionIdHackmdSynced, useHasDraftOnHackmd } from './hackmd';
 import { useStaticSWR } from './use-static-swr';
 
 
@@ -25,7 +26,9 @@ type RemoteRevisionData = {
   remoteRevisionId: string,
   remoteRevisionBody: string,
   remoteRevisionLastUpdateUser: IUser,
-  remoteRevisionLastUpdatedAt: Date
+  remoteRevisionLastUpdatedAt: Date,
+  revisionIdHackmdSynced: string,
+  hasDraftOnHackmd: boolean,
 }
 
 
@@ -35,15 +38,19 @@ export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageDa
   const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
+  const { mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
 
   const setRemoteLatestPageData = (remoteRevisionData: RemoteRevisionData) => {
     const {
-      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt,
+      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt, revisionIdHackmdSynced, hasDraftOnHackmd,
     } = remoteRevisionData;
     mutateRemoteRevisionId(remoteRevisionId);
     mutateRemoteRevisionBody(remoteRevisionBody);
     mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
     mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
+    mutateRevisionIdHackmdSynced(revisionIdHackmdSynced);
+    mutateHasDraftOnHackmd(hasDraftOnHackmd);
   };
 
   return {

+ 1 - 1
packages/remark-lsx/src/server/routes/index.js

@@ -9,5 +9,5 @@ module.exports = (crowi, app) => {
   const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
   const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
 
-  app.get('/_api/plugins/lsx', accessTokenParser, loginRequired, lsx.listPages);
+  app.get('/_api/lsx', accessTokenParser, loginRequired, lsx.listPages);
 };

+ 1 - 1
packages/remark-lsx/src/stores/lsx.tsx

@@ -99,7 +99,7 @@ const useSWRxLsxResponse = (
     pagePath: string, options?: Record<string, string | undefined>, isImmutable?: boolean,
 ): SWRResponse<LsxResponse, Error> => {
   return useSWR(
-    ['/_api/plugins/lsx', pagePath, options, isImmutable],
+    ['/_api/lsx', pagePath, options, isImmutable],
     (endpoint, pagePath, options) => {
       return axios.get(endpoint, {
         params: {