Просмотр исходного кода

Merge pull request #7861 from weseek/master

Release v6.1.6
Yuki Takei 2 лет назад
Родитель
Сommit
f07168fc08
31 измененных файлов с 346 добавлено и 132 удалено
  1. 2 2
      .devcontainer/Dockerfile
  2. 1 1
      .github/workflows/reusable-app-prod.yml
  3. 1 1
      apps/app/cypress.config.ts
  4. 1 0
      apps/app/docker/Dockerfile
  5. 0 1
      apps/app/docker/Dockerfile.dockerignore
  6. 4 4
      apps/app/package.json
  7. 15 2
      apps/app/src/components/PageEditor.tsx
  8. 7 0
      apps/app/src/components/TemplateModal/TemplateModal.module.scss
  9. 62 32
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  10. 19 5
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  11. 21 27
      apps/app/src/pages/[[...path]].page.tsx
  12. 19 5
      apps/app/src/server/service/search-delegator/elasticsearch-client.ts
  13. 6 6
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  14. 10 10
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  15. 1 0
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts
  16. 90 0
      apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts
  17. 2 2
      apps/slackbot-proxy/package.json
  18. 2 1
      package.json
  19. 2 2
      packages/core/package.json
  20. 2 2
      packages/hackmd/package.json
  21. 1 1
      packages/pluginkit/package.json
  22. 2 2
      packages/presentation/package.json
  23. 1 1
      packages/preset-templates/package.json
  24. 1 1
      packages/preset-themes/package.json
  25. 2 2
      packages/remark-attachment-refs/package.json
  26. 2 2
      packages/remark-drawio/package.json
  27. 2 2
      packages/remark-growi-directive/package.json
  28. 2 2
      packages/remark-lsx/package.json
  29. 2 2
      packages/slack/package.json
  30. 2 2
      packages/ui/package.json
  31. 62 12
      yarn.lock

+ 2 - 2
.devcontainer/Dockerfile

@@ -39,9 +39,9 @@ RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-ke
 RUN apt-get update \
     && apt-get -y install --no-install-recommends git-lfs \
 
-    # Uncomment below lines to install Chrome and libs for Cypress
+    # Uncomment below lines to install Chromium
     # --- works only on AMD64 ---
-    # && apt-get -y install --no-install-recommends google-chrome-stable \
+    # && apt-get -y install --no-install-recommends chromium \
     #    libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
 
     # Clean up

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -312,7 +312,7 @@ jobs:
     - name: Cypress Run
       uses: cypress-io/github-action@v5
       with:
-        browser: chrome
+        browser: chromium
         working-directory: ./apps/app
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         install: false

+ 1 - 1
apps/app/cypress.config.ts

@@ -9,7 +9,7 @@ export default defineConfig({
       // change screen size
       // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
       on('before:browser:launch', (browser, launchOptions) => {
-        if (browser.name === 'chrome' && browser.isHeadless) {
+        if (browser.name === 'chromium' && browser.isHeadless) {
           launchOptions.args.push('--window-size=1400,1024');
           launchOptions.args.push('--force-device-scale-factor=1');
         }

+ 1 - 0
apps/app/docker/Dockerfile

@@ -83,6 +83,7 @@ RUN tar -xf node_modules.tar
 RUN rm node_modules.tar
 
 # build
+RUN turbo run clean
 RUN turbo run build
 
 # make artifacts

+ 0 - 1
apps/app/docker/Dockerfile.dockerignore

@@ -1,5 +1,4 @@
 **/node_modules
-**/dist
 **/coverage
 **/Dockerfile
 **/*.dockerignore

+ 4 - 4
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -8,8 +8,8 @@
     "start": "yarn next start",
     "build:client": "yarn next build",
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server-tsc-alias.json",
-    "postbuild:server": "npx -y shx echo \"Listing files under transpiled\" && npx -y shx ls transpiled && npx -y shx rm -rf dist && npx -y shx mv transpiled/src dist && npx -y shx rm -rf transpiled",
-    "clean": "npx -y shx rm -rf dist transpiled",
+    "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
+    "clean": "shx rm -rf dist transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
@@ -24,7 +24,7 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
-    "cy:run": "cypress run --browser chrome",
+    "cy:run": "cypress run --browser chromium",
     "//// for CI": "",
     "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tsc",

+ 15 - 2
apps/app/src/components/PageEditor.tsx

@@ -83,7 +83,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage } = useSWRxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
-  const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
+  const { data: grantData } = useSelectedGrant();
   const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
@@ -110,7 +110,13 @@ const PageEditor = React.memo((): JSX.Element => {
 
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
-  const currentRevisionId = currentPage?.revision?._id;
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState('');
+
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  const currentRevisionId = currentPage?.revision?._id ?? createdPageRevisionIdWithAttachment;
 
   const initialValue = useMemo(() => {
     if (!isNotFound) {
@@ -149,6 +155,12 @@ const PageEditor = React.memo((): JSX.Element => {
 
   }, [markdownToPreview, mutateIsConflict]);
 
+  // TODO: remove workaround
+  // for https://redmine.weseek.co.jp/issues/125923
+  useEffect(() => {
+    setCreatedPageRevisionIdWithAttachment('');
+  }, [router]);
+
   useEffect(() => {
     markdownToSave.current = initialValue;
     setMarkdownToPreview(initialValue);
@@ -327,6 +339,7 @@ const PageEditor = React.memo((): JSX.Element => {
         logger.info('Page is created', res.page._id);
         globalEmitter.emit('resetInitializedHackMdStatus');
         mutateIsLatestRevision(true);
+        setCreatedPageRevisionIdWithAttachment(res.page.revision);
         await mutateCurrentPageId(res.page._id);
         await mutateCurrentPage();
       }

+ 7 - 0
apps/app/src/components/TemplateModal/TemplateModal.module.scss

@@ -0,0 +1,7 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.dm-templates :global {
+  .dropdown-item:not(:first-child) {
+    border-top: 1px solid bs.$border-color;
+  }
+}

+ 62 - 32
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -30,6 +30,10 @@ import Preview from '../PageEditor/Preview';
 
 import { useFormatter } from './use-formatter';
 
+
+import styles from './TemplateModal.module.scss';
+
+
 const logger = loggerFactory('growi:components:TemplateModal');
 
 
@@ -39,7 +43,7 @@ function constructTemplateId(templateSummary: TemplateSummary): string {
   return `${defaultTemplate.pluginId ?? ''}_${defaultTemplate.id}`;
 }
 
-type TemplateItemProps = {
+type TemplateSummaryItemProps = {
   templateSummary: TemplateSummary,
   selectedLocale?: string,
   onClick?: () => void,
@@ -47,7 +51,7 @@ type TemplateItemProps = {
   usersDefaultLang?: Lang,
 }
 
-const TemplateItem: React.FC<TemplateItemProps> = ({
+const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
   templateSummary,
   onClick,
   isSelected,
@@ -73,6 +77,32 @@ const TemplateItem: React.FC<TemplateItemProps> = ({
   );
 };
 
+
+const TemplateDropdownItem: React.FC<TemplateSummaryItemProps> = ({
+  templateSummary,
+  onClick,
+  usersDefaultLang,
+}) => {
+
+  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
+  const templateLocales = extractSupportedLocales(templateSummary);
+
+  assert(localizedTemplate?.isValid);
+
+  return (
+    <DropdownItem
+      onClick={onClick}
+      className="px-4 py-3"
+    >
+      <h4 className="mb-1 text-wrap">{localizedTemplate.title}</h4>
+      <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
+      { templateLocales != null && Array.from(templateLocales).map(locale => (
+        <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
+      ))}
+    </DropdownItem>
+  );
+};
+
 type TemplateModalSubstanceProps = {
   templateModalStatus: TemplateModalStatus,
   close: () => void,
@@ -85,7 +115,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
 
   const { data: personalSettingsInfo } = usePersonalSettings();
   const { data: rendererOptions } = usePreviewOptions();
-  const { data: templateSummaries } = useSWRxTemplates();
+  const { data: templateSummaries, isLoading } = useSWRxTemplates();
 
   const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
   const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
@@ -138,12 +168,8 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
     }
   }, [templateModalStatus.isOpened]);
 
-  if (templateSummaries == null) {
-    return <></>;
-  }
-
   return (
-    <>
+    <div data-testid='template-modal'>
       <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
         {t('template.modal_label.Select template')}
       </ModalHeader>
@@ -151,13 +177,20 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
         <div className="row">
           {/* List Group */}
           <div className="d-none d-lg-block col-lg-4">
+
+            { isLoading && (
+              <div className='h-100 d-flex justify-content-center align-items-center'>
+                <i className="fa fa-2x fa-spinner fa-pulse text-muted mx-auto"></i>
+              </div>
+            ) }
+
             <div className="list-group">
-              {templateSummaries.map((templateSummary) => {
+              { templateSummaries != null && templateSummaries.map((templateSummary) => {
                 const templateId = constructTemplateId(templateSummary);
                 const isSelected = selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId;
 
                 return (
-                  <TemplateItem
+                  <TemplateListGroupItem
                     key={templateId}
                     templateSummary={templateSummary}
                     onClick={() => onClickHandler(templateSummary)}
@@ -165,41 +198,38 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
                     usersDefaultLang={usersDefaultLang}
                   />
                 );
-              })}
+              }) }
             </div>
           </div>
           {/* Dropdown */}
           <div className='d-lg-none col mb-3'>
             <UncontrolledDropdown>
-              <DropdownToggle caret type="button" outline className='w-100 text-right'>
+              <DropdownToggle caret type="button" outline className='w-100 text-right' disabled={isLoading}>
                 <span className="float-left">
-                  {selectedLocalizedTemplate != null && selectedLocalizedTemplate.isValid
-                    ? selectedLocalizedTemplate.title
-                    : t('Select template')}
+                  { (() => {
+                    if (isLoading) {
+                      return 'Loading..';
+                    }
+
+                    return selectedLocalizedTemplate != null && selectedLocalizedTemplate.isValid
+                      ? selectedLocalizedTemplate.title
+                      : t('Select template');
+                  })() }
                 </span>
               </DropdownToggle>
-              <DropdownMenu role="menu" className='p-0'>
-                {templateSummaries.map((templateSummary, index) => {
+              <DropdownMenu role="menu" className={`p-0 ${styles['dm-templates']}`}>
+                { templateSummaries != null && templateSummaries.map((templateSummary) => {
                   const templateId = constructTemplateId(templateSummary);
-                  const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
-                  const templateLocales = extractSupportedLocales(templateSummary);
-
-                  assert(localizedTemplate?.isValid);
 
                   return (
-                    <DropdownItem
+                    <TemplateDropdownItem
                       key={templateId}
+                      templateSummary={templateSummary}
                       onClick={() => onClickHandler(templateSummary)}
-                      className={`px-4 py-3 ${index === 0 ? '' : 'border-top'}`}
-                    >
-                      <h4 className="mb-1 text-wrap">{localizedTemplate.title}</h4>
-                      <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
-                      { templateLocales != null && Array.from(templateLocales).map(locale => (
-                        <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
-                      ))}
-                    </DropdownItem>
+                      usersDefaultLang={usersDefaultLang}
+                    />
                   );
-                })}
+                }) }
               </DropdownMenu>
             </UncontrolledDropdown>
           </div>
@@ -249,7 +279,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
           {t('commons:Insert')}
         </button>
       </ModalFooter>
-    </>
+    </div>
   );
 };
 

+ 19 - 5
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -41,18 +41,32 @@ module.exports = (crowi) => {
     // scan preset templates
     if (presetTemplateSummaries == null) {
       const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
-      presetTemplateSummaries = await scanAllTemplates(presetTemplatesRoot, {
-        returnsInvalidTemplates: includeInvalidTemplates,
-      });
+
+      try {
+        presetTemplateSummaries = await scanAllTemplates(presetTemplatesRoot, {
+          returnsInvalidTemplates: includeInvalidTemplates,
+        });
+      }
+      catch (err) {
+        logger.error(err);
+        presetTemplateSummaries = [];
+      }
     }
 
     // load plugin templates
-    const plugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Template);
+    let pluginsTemplateSummaries: TemplateSummary[] = [];
+    try {
+      const plugins = await GrowiPlugin.findEnabledPluginsByType(GrowiPluginType.Template);
+      pluginsTemplateSummaries = plugins.flatMap(p => p.meta.templateSummaries);
+    }
+    catch (err) {
+      logger.error(err);
+    }
 
     return res.apiv3({
       summaries: [
         ...presetTemplateSummaries,
-        ...plugins.flatMap(p => p.meta.templateSummaries),
+        ...pluginsTemplateSummaries,
       ],
     });
   });

+ 21 - 27
apps/app/src/pages/[[...path]].page.tsx

@@ -238,37 +238,12 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
 
-  const { mutate: mutateInitialPage } = useSWRxCurrentPage();
+  useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
+
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
 
-  // Store initial data
-  useEffect(() => {
-    if (!props.skipSSR) {
-      mutateInitialPage(pageWithMeta?.data ?? null);
-    }
-  }, [mutateInitialPage, pageWithMeta, props.skipSSR]);
-
-  // Store initial data (When revisionBody is not SSR)
-  useEffect(() => {
-    if (!props.skipSSR) {
-      return;
-    }
-
-    if (currentPageId != null && !props.isNotFound) {
-      const mutatePageData = async() => {
-        const pageData = await mutateCurrentPage();
-        mutateEditingMarkdown(pageData?.revision.body);
-      };
-
-      // If skipSSR is true, use the API to retrieve page data.
-      // Because pageWIthMeta does not contain revision.body
-      mutatePageData();
-    }
-  }, [currentPageId, mutateCurrentPage, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);
-
-
   const { mutate: mutateIsNotFound } = useIsNotFound();
 
   const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
@@ -291,6 +266,25 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     ? _isTrashPage(pageWithMeta.data.path)
     : false;
 
+
+  // Store initial data (When revisionBody is not SSR)
+  useEffect(() => {
+    if (!props.skipSSR) {
+      return;
+    }
+
+    if (currentPageId != null && !props.isNotFound) {
+      const mutatePageData = async() => {
+        const pageData = await mutateCurrentPage();
+        mutateEditingMarkdown(pageData?.revision.body);
+      };
+
+      // If skipSSR is true, use the API to retrieve page data.
+      // Because pageWIthMeta does not contain revision.body
+      mutatePageData();
+    }
+  }, [currentPageId, mutateCurrentPage, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);
+
   // sync grant data
   useEffect(() => {
     const grantDataToApply = props.grantData ? props.grantData : grantData?.grantData.currentPageGrant;

+ 19 - 5
apps/app/src/server/service/search-delegator/elasticsearch-client.ts

@@ -1,7 +1,12 @@
 /* eslint-disable implicit-arrow-linebreak */
 /* eslint-disable no-confusing-arrow */
-import { Client as ES7Client, ApiResponse as ES7ApiResponse, RequestParams as ES7RequestParams } from '@elastic/elasticsearch7';
-import { Client as ES8Client, estypes } from '@elastic/elasticsearch8';
+import {
+  Client as ES7Client,
+  ClientOptions as ES7ClientOptions,
+  ApiResponse as ES7ApiResponse,
+  RequestParams as ES7RequestParams,
+} from '@elastic/elasticsearch7';
+import { ClientOptions as ES8ClientOptions, Client as ES8Client, estypes } from '@elastic/elasticsearch8';
 
 import {
   BulkResponse,
@@ -17,12 +22,21 @@ import {
   ReindexResponse,
 } from './elasticsearch-client-types';
 
+
+type ElasticsearchClientParams =
+  | [ isES7: true, options: ES7ClientOptions, rejectUnauthorized: boolean ]
+  | [ isES7: false, options: ES8ClientOptions, rejectUnauthorized: boolean ]
+
 export default class ElasticsearchClient {
 
-  client: ES7Client | ES8Client;
+  private client: ES7Client | ES8Client;
+
+  constructor(...params: ElasticsearchClientParams) {
+    const [isES7, options, rejectUnauthorized] = params;
 
-  constructor(client: ES7Client | ES8Client) {
-    this.client = client;
+    this.client = isES7
+      ? new ES7Client({ ...options, ssl: { rejectUnauthorized } })
+      : new ES8Client({ ...options, tls: { rejectUnauthorized } });
   }
 
   async bulk(params: ES7RequestParams.Bulk & estypes.BulkRequest): Promise<BulkResponse | estypes.BulkResponse> {

+ 6 - 6
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -1,8 +1,6 @@
 import { Writable, Transform } from 'stream';
 import { URL } from 'url';
 
-import elasticsearch7 from '@elastic/elasticsearch7';
-import elasticsearch8 from '@elastic/elasticsearch8';
 import gc from 'expose-gc/function';
 import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
@@ -81,7 +79,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
-    this.elasticsearch = this.isElasticsearchV7 ? elasticsearch7 : elasticsearch8;
     this.isElasticsearchReindexOnBoot = this.configManager.getConfig('crowi', 'app:elasticsearchReindexOnBoot');
     this.client = null;
 
@@ -119,12 +116,15 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   initClient() {
     const { host, auth, indexName } = this.getConnectionInfo();
 
-    this.client = new ElasticsearchClient(new this.elasticsearch.Client({
+    const rejectUnauthorized = this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized');
+
+    const options = {
       node: host,
-      ssl: { rejectUnauthorized: this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized') },
       auth,
       requestTimeout: this.configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
-    }));
+    };
+
+    this.client = new ElasticsearchClient(this.isElasticsearchV7, options, rejectUnauthorized);
     this.indexName = indexName;
   }
 

+ 10 - 10
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts

@@ -81,8 +81,9 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
 
+  const body1 = 'hello';
+  const body2 = ' world!';
   it('View and Edit contents are successfully loaded', () => {
-    const body1 = 'hello';
     cy.visit('/Sandbox/testForUseEditingMarkdown');
 
     openEditor();
@@ -98,22 +99,21 @@ context('Access to page', () => {
   })
 
   it('Editing contents are successfully loaded with shortcut key', () => {
-    const body2 = ' world!';
     const savePageShortcutKey = '{ctrl+s}';
 
     cy.visit('/Sandbox/testForUseEditingMarkdown');
 
     openEditor();
 
+    cy.get('.CodeMirror').contains(body1);
+
     // check editing contents with shortcut key
-    cy.get('.CodeMirror-line').children().first().invoke('text').then((text) => {
-      cy.get('.CodeMirror').type(body2);
-      cy.get('.CodeMirror').contains(text+body2);
-      cy.get('.page-editor-preview-body').contains(text+body2);
-      cy.get('.CodeMirror').type(savePageShortcutKey);
-      cy.get('.CodeMirror').contains(text+body2);
-      cy.get('.page-editor-preview-body').contains(text+body2);
-    })
+    cy.get('.CodeMirror').type(body2);
+    cy.get('.CodeMirror').contains(body1+body2);
+    cy.get('.page-editor-preview-body').contains(body1+body2);
+    cy.get('.CodeMirror').type(savePageShortcutKey);
+    cy.get('.CodeMirror').contains(body1+body2);
+    cy.get('.page-editor-preview-body').contains(body1+body2);
   })
 
   it('/user/admin is successfully loaded', () => {

+ 1 - 0
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts

@@ -116,6 +116,7 @@ context('Access to any page', () => {
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     });
     cy.get('.grw-editor-navbar-bottom').should('be.visible');
+    cy.get('.CodeMirror').should('be.visible');
     cy.screenshot(`${ssPrefix}open-editor-when-sticky`);
   });
 

+ 90 - 0
apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts

@@ -0,0 +1,90 @@
+context('TemplateModal', () => {
+
+  const ssPrefix = 'template-modal-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it("TemplateModal is shown and closed successfully", () => {
+    cy.visit('/Sandbox/TemplateModal');
+    cy.collapseSidebar(true, true);
+
+    // move to edit mode
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    })
+    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+
+    // show TemplateModal
+    cy.waitUntil(() => {
+      // do
+      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
+      // wait until
+      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
+    });
+
+    // close TemplateModal
+    cy.getByTestid('template-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}opened`);
+      cy.get('button.close').click();
+    });
+
+    cy.screenshot(`${ssPrefix}close`);
+  });
+
+  it("Successfully select template and template locale", () => {
+    cy.visit('/Sandbox/TemplateModal');
+    cy.collapseSidebar(true, true);
+
+    // move to edit mode
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    })
+    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+
+    // show TemplateModal
+    cy.waitUntil(() => {
+      // do
+      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
+      // wait until
+      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
+    });
+
+    // select template and template locale
+    cy.getByTestid('template-modal').should('be.visible').within(() => {
+      // select first template
+      cy.get('.list-group > .list-group-item:nth-child(1)').click();
+      // check preview exist
+      cy.get('.card-body > .page-editor-preview-body > .wiki').should('exist');
+      cy.screenshot(`${ssPrefix}select-template`);
+
+      // change template locale
+      cy.get('.modal-body > div:nth-child(1) > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > .dropdown > button').click();
+      cy.get('.modal-body > div:nth-child(1) > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > .dropdown > div > button:nth-child(2)').click();
+      cy.screenshot(`${ssPrefix}select-template-locale`);
+
+      // click insert button
+      cy.get('.modal-footer > button:nth-child(2)').click();
+    });
+
+    // check show template on markdown
+    cy.screenshot(`${ssPrefix}insert-template`);
+  });
+
+});

+ 2 - 2
apps/slackbot-proxy/package.json

@@ -1,10 +1,10 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.1.5-slackbot-proxy.0",
+  "version": "6.1.6-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "cp:public": "cp -RT ./src/public ./dist/public",
     "cp:views": "cp -RT ./src/views ./dist/views",
     "cp:bootstrap": "cp -RT ./node_modules/bootstrap/dist ./dist/public/bootstrap",

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -84,6 +84,7 @@
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-suit": "^0.12.1",
+    "shx": "^0.3.4",
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-node-dev": "^2.0.0",

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 2 - 2
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "type": "module",
@@ -9,7 +9,7 @@
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 1 - 1
packages/pluginkit/package.json

@@ -7,7 +7,7 @@
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 2 - 2
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",

+ 1 - 1
packages/preset-templates/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/preset-templates",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "scripts": {
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 2 - 2
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
     "build": "run-p build:*",
     "build:server": "vite build -c vite.server.config.ts",
     "build:client": "vite build -c vite.client.config.ts",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "run-p dev:*",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",

+ 2 - 2
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [
@@ -21,7 +21,7 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,jsx,ts,tsx}",

+ 2 - 2
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "yarn tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "yarn build",
     "watch": "yarn tsc -w",
     "test": "cross-env NODE_ENV=test npm run test-coverage",

+ 2 - 2
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [
@@ -14,7 +14,7 @@
     "build": "run-p build:*",
     "build:client": "vite build -c vite.client.config.ts",
     "build:server": "vite build -c vite.server.config.ts",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "run-p dev:*",
     "dev:client": "vite build -c vite.client.config.ts --mode dev",
     "dev:server": "vite build -c vite.server.config.ts --mode dev",

+ 2 - 2
packages/slack/package.json

@@ -1,13 +1,13 @@
 {
   "name": "@growi/slack",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "module": "dist/index.mjs",
   "types": "dist/index.d.ts",
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.1.5",
+  "version": "6.1.6-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [
@@ -12,7 +12,7 @@
   ],
   "scripts": {
     "build": "vite build",
-    "clean": "npx -y shx rm -rf dist",
+    "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "watch": "yarn dev -w --emptyOutDir=false",
     "lint:js": "yarn eslint **/*.{js,ts}",

+ 62 - 12
yarn.lock

@@ -2312,13 +2312,13 @@
     xdg-basedir "^4.0.0"
 
 "@growi/core@link:packages/core":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     bson-objectid "^2.0.4"
     escape-string-regexp "^4.0.0"
 
 "@growi/hackmd@link:packages/hackmd":
-  version "6.1.5"
+  version "6.1.6-RC.0"
 
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
@@ -2327,18 +2327,18 @@
     extensible-custom-error "^0.0.7"
 
 "@growi/presentation@link:packages/presentation":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
 
 "@growi/preset-templates@link:packages/preset-templates":
-  version "6.1.5"
+  version "6.1.6-RC.0"
 
 "@growi/preset-themes@link:packages/preset-themes":
-  version "6.1.5"
+  version "6.1.6-RC.0"
 
 "@growi/remark-attachment-refs@link:packages/remark-attachment-refs":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2347,12 +2347,12 @@
     universal-bunyan "^0.9.2"
 
 "@growi/remark-drawio@link:packages/remark-drawio":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     pako "^2.1.0"
 
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@types/mdast" "^3.0.0"
     "@types/unist" "^2.0.0"
@@ -2369,7 +2369,7 @@
     uvu "^0.5.0"
 
 "@growi/remark-lsx@link:packages/remark-lsx":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2380,7 +2380,7 @@
     swr "^2.0.3"
 
 "@growi/slack@link:packages/slack":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@slack/oauth" "^2.0.1"
     axios "^0.24.0"
@@ -2393,7 +2393,7 @@
     url-join "^4.0.0"
 
 "@growi/ui@link:packages/ui":
-  version "6.1.5"
+  version "6.1.6-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
 
@@ -8835,7 +8835,7 @@ glob@^6.0.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
+glob@^7.0.0, glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -9703,6 +9703,11 @@ internal-slot@^1.0.3:
   resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
   integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
 
+interpret@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+
 invariant@^2.2.1, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -9838,6 +9843,13 @@ is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-mod
   dependencies:
     has "^1.0.3"
 
+is-core-module@^2.11.0:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd"
+  integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
+  dependencies:
+    has "^1.0.3"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -12306,6 +12318,11 @@ minimist@1, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5,
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
   integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
 
+minimist@^1.2.3:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
 minipass@^3.0.0:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
@@ -14438,6 +14455,13 @@ readdirp@~3.6.0:
   dependencies:
     picomatch "^2.2.1"
 
+rechoir@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+  integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
+  dependencies:
+    resolve "^1.1.6"
+
 reconnecting-websocket@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
@@ -14966,6 +14990,15 @@ resolve@^1.0.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
+resolve@^1.1.6:
+  version "1.22.2"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+  dependencies:
+    is-core-module "^2.11.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
 resolve@^2.0.0-next.3:
   version "2.0.0-next.3"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
@@ -15329,6 +15362,15 @@ shell-quote@^1.6.1:
   resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
   integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
 
+shelljs@^0.8.5:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
+  integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
+  dependencies:
+    glob "^7.0.0"
+    interpret "^1.0.0"
+    rechoir "^0.6.2"
+
 should-equal@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
@@ -15373,6 +15415,14 @@ should@^13.2.1:
     should-type-adaptors "^1.0.1"
     should-util "^1.0.0"
 
+shx@^0.3.4:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02"
+  integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==
+  dependencies:
+    minimist "^1.2.3"
+    shelljs "^0.8.5"
+
 side-channel@^1.0.3, side-channel@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"