Explorar el Código

Merge pull request #7842 from weseek/imprv/load-template-plugins-on-server

imprv: Load templates from the server
Yuki Takei hace 2 años
padre
commit
f5de8e8f61
Se han modificado 87 ficheros con 752 adiciones y 360 borrados
  1. 2 0
      apps/app/package.json
  2. 84 48
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  3. 9 15
      apps/app/src/components/TemplateModal/use-formatter.spec.tsx
  4. 5 8
      apps/app/src/components/TemplateModal/use-formatter.tsx
  5. 0 0
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.module.scss
  6. 0 0
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.tsx
  7. 3 3
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx
  8. 2 2
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginsExtensionPageContents.tsx
  9. 0 0
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/index.ts
  10. 1 0
      apps/app/src/features/growi-plugin/client/components/Admin/index.ts
  11. 1 1
      apps/app/src/features/growi-plugin/client/components/GrowiPluginsActivator.tsx
  12. 1 0
      apps/app/src/features/growi-plugin/client/components/index.ts
  13. 24 0
      apps/app/src/features/growi-plugin/client/stores/admin-plugins.tsx
  14. 0 0
      apps/app/src/features/growi-plugin/client/utils/growi-facade-utils.ts
  15. 0 1
      apps/app/src/features/growi-plugin/components/index.ts
  16. 2 10
      apps/app/src/features/growi-plugin/interfaces/growi-plugin.ts
  17. 5 6
      apps/app/src/features/growi-plugin/server/models/growi-plugin.ts
  18. 0 0
      apps/app/src/features/growi-plugin/server/models/index.ts
  19. 0 0
      apps/app/src/features/growi-plugin/server/models/vo/github-url.spec.ts
  20. 0 0
      apps/app/src/features/growi-plugin/server/models/vo/github-url.ts
  21. 2 2
      apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts
  22. 6 7
      apps/app/src/features/growi-plugin/server/services/growi-plugin.ts
  23. 0 0
      apps/app/src/features/growi-plugin/server/services/index.ts
  24. 0 27
      apps/app/src/features/growi-plugin/stores/growi-plugin.tsx
  25. 58 0
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  26. 1 0
      apps/app/src/features/templates/stores/index.ts
  27. 29 0
      apps/app/src/features/templates/stores/template.tsx
  28. 1 1
      apps/app/src/pages/[[...path]].page.tsx
  29. 2 1
      apps/app/src/pages/_document.page.tsx
  30. 1 1
      apps/app/src/pages/admin/plugins.page.tsx
  31. 1 1
      apps/app/src/server/crowi/index.js
  32. 3 5
      apps/app/src/server/routes/apiv3/customize-setting.js
  33. 3 2
      apps/app/src/server/routes/apiv3/index.js
  34. 1 1
      apps/app/src/server/service/customize.ts
  35. 1 1
      apps/app/src/stores/renderer.tsx
  36. 0 141
      apps/app/src/stores/template.tsx
  37. 4 3
      packages/core/src/consts/growi-plugin.ts
  38. 1 0
      packages/core/src/consts/index.ts
  39. 2 14
      packages/core/src/index.ts
  40. 14 0
      packages/core/src/interfaces/index.ts
  41. 5 1
      packages/core/src/interfaces/template.ts
  42. 1 1
      packages/pluginkit/package.json
  43. 0 1
      packages/pluginkit/src/consts/index.ts
  44. 0 1
      packages/pluginkit/src/index.ts
  45. 1 1
      packages/pluginkit/src/model/growi-plugin-validation-data.ts
  46. 2 0
      packages/pluginkit/src/v4/index.ts
  47. 1 0
      packages/pluginkit/src/v4/interfaces/index.ts
  48. 25 0
      packages/pluginkit/src/v4/interfaces/template.ts
  49. 1 0
      packages/pluginkit/src/v4/server/index.ts
  50. 0 0
      packages/pluginkit/src/v4/server/utils/index.ts
  51. 0 0
      packages/pluginkit/src/v4/server/utils/package-json/import.spec.ts
  52. 0 0
      packages/pluginkit/src/v4/server/utils/package-json/import.ts
  53. 0 0
      packages/pluginkit/src/v4/server/utils/package-json/index.ts
  54. 5 4
      packages/pluginkit/src/v4/server/utils/package-json/validate.spec.ts
  55. 2 1
      packages/pluginkit/src/v4/server/utils/package-json/validate.ts
  56. 79 36
      packages/pluginkit/src/v4/server/utils/template.ts
  57. 1 0
      packages/pluginkit/src/v4/utils/index.ts
  58. 11 0
      packages/pluginkit/src/v4/utils/template.ts
  59. 3 0
      packages/preset-templates/dist/daily-report/en_US/meta.json
  60. 30 0
      packages/preset-templates/dist/daily-report/en_US/template.md
  61. 3 0
      packages/preset-templates/dist/daily-report/ja_JP/meta.json
  62. 30 0
      packages/preset-templates/dist/daily-report/ja_JP/template.md
  63. 3 0
      packages/preset-templates/dist/daily-report/zh_CN/meta.json
  64. 30 0
      packages/preset-templates/dist/daily-report/zh_CN/template.md
  65. 3 0
      packages/preset-templates/dist/displaying-child-pages/en_US/meta.json
  66. 4 0
      packages/preset-templates/dist/displaying-child-pages/en_US/template.md
  67. 3 0
      packages/preset-templates/dist/displaying-child-pages/ja_JP/meta.json
  68. 4 0
      packages/preset-templates/dist/displaying-child-pages/ja_JP/template.md
  69. 3 0
      packages/preset-templates/dist/displaying-child-pages/zh_CN/meta.json
  70. 4 0
      packages/preset-templates/dist/displaying-child-pages/zh_CN/template.md
  71. 0 3
      packages/preset-templates/dist/example/ja_JP/meta.json
  72. 0 1
      packages/preset-templates/dist/example/ja_JP/template.md
  73. 3 0
      packages/preset-templates/dist/minutes/en_US/meta.json
  74. 38 0
      packages/preset-templates/dist/minutes/en_US/template.md
  75. 3 0
      packages/preset-templates/dist/minutes/ja_JP/meta.json
  76. 38 0
      packages/preset-templates/dist/minutes/ja_JP/template.md
  77. 3 0
      packages/preset-templates/dist/minutes/zh_CN/meta.json
  78. 42 0
      packages/preset-templates/dist/minutes/zh_CN/template.md
  79. 3 0
      packages/preset-templates/dist/project-proposal/en_US/meta.json
  80. 22 0
      packages/preset-templates/dist/project-proposal/en_US/template.md
  81. 3 0
      packages/preset-templates/dist/project-proposal/ja_JP/meta.json
  82. 21 0
      packages/preset-templates/dist/project-proposal/ja_JP/template.md
  83. 3 0
      packages/preset-templates/dist/project-proposal/zh_CN/meta.json
  84. 21 0
      packages/preset-templates/dist/project-proposal/zh_CN/template.md
  85. 1 1
      packages/preset-templates/test/index.test.ts
  86. 24 7
      turbo.json
  87. 4 1
      yarn.lock

+ 2 - 0
apps/app/package.json

@@ -66,6 +66,8 @@
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "link:../../packages/core",
     "@growi/hackmd": "link:../../packages/hackmd",
+    "@growi/pluginkit": "link:../../packages/pluginkit",
+    "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-themes": "link:../../packages/preset-themes",
     "@growi/remark-attachment-refs": "link:../../packages/remark-attachment-refs",
     "@growi/remark-drawio": "link:../../packages/remark-drawio",

+ 84 - 48
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -2,7 +2,10 @@ import React, {
   useCallback, useEffect, useState,
 } from 'react';
 
-import type { ITemplate } from '@growi/core/dist/interfaces/template';
+import assert from 'assert';
+
+import { Lang } from '@growi/core';
+import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,
@@ -11,9 +14,10 @@ import {
   ModalFooter,
 } from 'reactstrap';
 
+import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
 import { useTemplateModal } from '~/stores/modal';
+import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePreviewOptions } from '~/stores/renderer';
-import { useTemplates } from '~/stores/template';
 import loggerFactory from '~/utils/logger';
 
 import Preview from '../PageEditor/Preview';
@@ -23,28 +27,45 @@ import { useFormatter } from './use-formatter';
 const logger = loggerFactory('growi:components:TemplateModal');
 
 
+function constructTemplateId(templateSummary: TemplateSummary): string {
+  const defaultTemplate = templateSummary.default;
+
+  return `${defaultTemplate.pluginId ?? ''}_${defaultTemplate.id}`;
+}
+
+
 type TemplateRadioButtonProps = {
-  template: ITemplate,
-  onChange: (selectedTemplate: ITemplate) => void,
+  templateSummary: TemplateSummary,
+  onClick?: () => void,
+  usersDefaultLang?: Lang,
   isSelected?: boolean,
 }
 
-const TemplateRadioButton = ({ template, onChange, isSelected }: TemplateRadioButtonProps): JSX.Element => {
-  const radioButtonId = `rb-${template.id}`;
+const TemplateListGroupItem = ({
+  templateSummary, onClick, usersDefaultLang, isSelected,
+}: TemplateRadioButtonProps): JSX.Element => {
+  const templateId = constructTemplateId(templateSummary);
+  const locales = new Set(Object.values(templateSummary).map(s => s.locale));
+
+  const template = usersDefaultLang != null && usersDefaultLang in templateSummary
+    ? templateSummary[usersDefaultLang]
+    : templateSummary.default;
+
+  assert(template.isValid);
 
   return (
-    <div key={template.id} className="custom-control custom-radio mb-2">
-      <input
-        id={radioButtonId}
-        type="radio"
-        className="custom-control-input"
-        checked={isSelected}
-        onChange={() => onChange(template)}
-      />
-      <label className="custom-control-label" htmlFor={radioButtonId}>
-        {template.name}
-      </label>
-    </div>
+    <a
+      key={templateId}
+      className={`list-group-item list-group-item-action ${isSelected ? 'active' : ''}`}
+      onClick={onClick}
+      aria-current="true"
+    >
+      <h4 className="mb-1">{template.title}</h4>
+      <p className="mb-2">{template.desc}</p>
+      { Array.from(locales).map(locale => (
+        <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
+      ))}
+    </a>
   );
 };
 
@@ -54,74 +75,89 @@ export const TemplateModal = (): JSX.Element => {
 
   const { data: templateModalStatus, close } = useTemplateModal();
 
+  const { data: personalSettingsInfo } = usePersonalSettings();
   const { data: rendererOptions } = usePreviewOptions();
-  const { data: templates } = useTemplates();
+  const { data: templateSummaries } = useSWRxTemplates();
+
+  const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
+  const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
 
-  const [selectedTemplate, setSelectedTemplate] = useState<ITemplate>();
+  const { data: selectedTemplateMarkdown } = useSWRxTemplate(selectedTemplateSummary, selectedTemplateLocale);
 
   const { format } = useFormatter();
 
-  const submitHandler = useCallback((template?: ITemplate) => {
-    if (templateModalStatus == null || selectedTemplate == null) {
+  const submitHandler = useCallback((markdown?: string) => {
+    if (templateModalStatus == null || markdown == null) {
       return;
     }
 
-    if (templateModalStatus.onSubmit == null || template == null) {
+    if (templateModalStatus.onSubmit == null) {
       close();
       return;
     }
 
-    templateModalStatus.onSubmit(format(selectedTemplate));
+    templateModalStatus.onSubmit(format(selectedTemplateMarkdown));
     close();
-  }, [close, format, selectedTemplate, templateModalStatus]);
+  }, [close, format, selectedTemplateMarkdown, templateModalStatus]);
 
   useEffect(() => {
     if (!templateModalStatus?.isOpened) {
-      setSelectedTemplate(undefined);
+      setSelectedTemplateSummary(undefined);
     }
   }, [templateModalStatus?.isOpened]);
 
-  if (templates == null || templateModalStatus == null) {
+  if (templateSummaries == null || templateModalStatus == null) {
     return <></>;
   }
 
   return (
-    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
+    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
       <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
         {t('template.modal_label.Select template')}
       </ModalHeader>
 
       <ModalBody className="container">
         <div className="row">
-          <div className="col-12">
-            { templates.map(template => (
-              <TemplateRadioButton
-                key={template.id}
-                template={template}
-                onChange={selected => setSelectedTemplate(selected)}
-                isSelected={template.id === selectedTemplate?.id}
-              />
-            )) }
+          <div className="d-none d-lg-block col-lg-4">
+            <div className="list-group">
+              { templateSummaries.map((templateSummary) => {
+                const templateId = constructTemplateId(templateSummary);
+
+                return (
+                  <TemplateListGroupItem
+                    key={templateId}
+                    templateSummary={templateSummary}
+                    usersDefaultLang={personalSettingsInfo?.lang}
+                    onClick={() => setSelectedTemplateSummary(templateSummary)}
+                    isSelected={selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId}
+                  />
+                );
+              }) }
+            </div>
           </div>
-        </div>
-
-        <hr />
 
-        <h3>{t('Preview')}</h3>
-        <div className='card'>
-          <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
-            { rendererOptions != null && selectedTemplate != null && (
-              <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplate)}/>
-            ) }
+          <div className="col-12 col-lg-8">
+            <h3>{t('Preview')}</h3>
+            <div className='card'>
+              <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
+                { rendererOptions != null && selectedTemplateSummary != null && (
+                  <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplateMarkdown)}/>
+                ) }
+              </div>
+            </div>
           </div>
         </div>
 
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={close}>
+        <button type="button" className="btn btn-outline-secondary mx-1" onClick={close}>
           {t('Cancel')}
         </button>
-        <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={() => submitHandler(selectedTemplate)} disabled={selectedTemplate == null}>
+        <button
+          type="submit"
+          className="btn btn-primary mx-1"
+          onClick={() => submitHandler(selectedTemplateMarkdown)}
+          disabled={selectedTemplateSummary == null}>
           {t('commons:Insert')}
         </button>
       </ModalFooter>

+ 9 - 15
apps/app/src/components/TemplateModal/use-formatter.spec.tsx

@@ -1,6 +1,3 @@
-import type { ITemplate } from '@growi/core/dist/interfaces/template';
-import { mock } from 'vitest-mock-extended';
-
 import { useFormatter } from './use-formatter';
 
 
@@ -47,26 +44,24 @@ describe('useFormatter', () => {
 
     // when
     const { format } = useFormatter();
-    const template = mock<ITemplate>();
-    template.markdown = 'markdown body';
-    const markdown = format(template);
+    const markdown = 'markdown body';
+    const formatted = format(markdown);
 
     // then
-    expect(markdown).toBe('markdown body');
+    expect(formatted).toBe('markdown body');
   });
 
   it('returns markdown formatted when currentPagePath is undefined', () => {
     // when
     const { format } = useFormatter();
-    const template = mock<ITemplate>();
-    template.markdown = `
+    const markdown = `
 title: {{{title}}}{{^title}}(empty){{/title}}
 path: {{{path}}}
 `;
-    const markdown = format(template);
+    const formatted = format(markdown);
 
     // then
-    expect(markdown).toBe(`
+    expect(formatted).toBe(`
 title: (empty)
 path: /
 `);
@@ -82,16 +77,15 @@ path: /
 
     // when
     const { format } = useFormatter();
-    const template = mock<ITemplate>();
-    template.markdown = `
+    const markdown = `
 title: {{{title}}}
 path: {{{path}}}
 date: {{yyyy}}/{{MM}}/{{dd}} {{HH}}:{{mm}}
 `;
-    const markdown = format(template);
+    const formatted = format(markdown);
 
     // then
-    expect(markdown).toBe(`
+    expect(formatted).toBe(`
 title: Sandbox
 path: /Sandbox
 date: 2023/05/31 15:01

+ 5 - 8
apps/app/src/components/TemplateModal/use-formatter.tsx

@@ -1,6 +1,5 @@
 import path from 'path';
 
-import type { ITemplate } from '@growi/core/dist/interfaces/template';
 import dateFnsFormat from 'date-fns/format';
 import mustache from 'mustache';
 
@@ -10,7 +9,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:components:TemplateModal:use-formatter');
 
 
-type FormatMethod = (selectedTemplate?: ITemplate) => string;
+type FormatMethod = (markdown?: string) => string;
 type FormatterData = {
   format: FormatMethod,
 }
@@ -18,16 +17,15 @@ type FormatterData = {
 export const useFormatter = (): FormatterData => {
   const { data: currentPagePath } = useCurrentPagePath();
 
-  const format: FormatMethod = (selectedTemplate) => {
-    if (selectedTemplate == null) {
+  const format: FormatMethod = (markdown) => {
+    if (markdown == null) {
       return '';
     }
 
     // replace placeholder
-    let markdown = selectedTemplate.markdown;
     const now = new Date();
     try {
-      markdown = mustache.render(selectedTemplate.markdown, {
+      return mustache.render(markdown, {
         title: path.basename(currentPagePath ?? '/'),
         path: currentPagePath ?? '/',
         yyyy: dateFnsFormat(now, 'yyyy'),
@@ -39,9 +37,8 @@ export const useFormatter = (): FormatterData => {
     }
     catch (err) {
       logger.warn('An error occured while ejs processing.', err);
+      return markdown;
     }
-
-    return markdown;
   };
 
   return { format };

+ 0 - 0
apps/app/src/features/growi-plugin/components/Admin/PluginsExtensionPageContents/PluginCard.module.scss → apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.module.scss


+ 0 - 0
apps/app/src/features/growi-plugin/components/Admin/PluginsExtensionPageContents/PluginCard.tsx → apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.tsx


+ 3 - 3
apps/app/src/features/growi-plugin/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx → apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx

@@ -5,11 +5,11 @@ import { useTranslation } from 'next-i18next';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
-import type { IGrowiPluginOrigin } from '../../../interfaces';
-import { useSWRxPlugins } from '../../../stores/growi-plugin';
+import type { IGrowiPluginOrigin } from '../../../../interfaces';
+import { useSWRxAdminPlugins } from '../../../stores/admin-plugins';
 
 export const PluginInstallerForm = (): JSX.Element => {
-  const { mutate } = useSWRxPlugins();
+  const { mutate } = useSWRxAdminPlugins();
   const { t } = useTranslation('admin');
 
   const submitHandler = useCallback(async(e) => {

+ 2 - 2
apps/app/src/features/growi-plugin/components/Admin/PluginsExtensionPageContents/PluginsExtensionPageContents.tsx → apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginsExtensionPageContents.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { Spinner } from 'reactstrap';
 
-import { useSWRxPlugins } from '../../../stores/growi-plugin';
+import { useSWRxAdminPlugins } from '../../../stores/admin-plugins';
 
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
@@ -19,7 +19,7 @@ const Loading = (): JSX.Element => {
 export const PluginsExtensionPageContents = (): JSX.Element => {
   const { t } = useTranslation('admin');
 
-  const { data, mutate } = useSWRxPlugins();
+  const { data, mutate } = useSWRxAdminPlugins();
 
   return (
     <div>

+ 0 - 0
apps/app/src/features/growi-plugin/components/Admin/PluginsExtensionPageContents/index.ts → apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/index.ts


+ 1 - 0
apps/app/src/features/growi-plugin/client/components/Admin/index.ts

@@ -0,0 +1 @@
+export * from './PluginsExtensionPageContents';

+ 1 - 1
apps/app/src/features/growi-plugin/components/GrowiPluginsActivator.client.tsx → apps/app/src/features/growi-plugin/client/components/GrowiPluginsActivator.tsx

@@ -1,6 +1,6 @@
 import { useEffect } from 'react';
 
-import { initializeGrowiFacade, registerGrowiFacade } from '../utils/growi-facade-utils.client';
+import { initializeGrowiFacade, registerGrowiFacade } from '../utils/growi-facade-utils';
 
 declare global {
   // eslint-disable-next-line vars-on-top, no-var

+ 1 - 0
apps/app/src/features/growi-plugin/client/components/index.ts

@@ -0,0 +1 @@
+export * from './GrowiPluginsActivator';

+ 24 - 0
apps/app/src/features/growi-plugin/client/stores/admin-plugins.tsx

@@ -0,0 +1,24 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+
+import type { IGrowiPluginHasId } from '../../interfaces';
+
+type Plugins = {
+  plugins: IGrowiPluginHasId[]
+}
+
+export const useSWRxAdminPlugins = (): SWRResponse<Plugins, Error> => {
+  return useSWR(
+    '/plugins',
+    async(endpoint) => {
+      try {
+        const res = await apiv3Get<Plugins>(endpoint);
+        return res.data;
+      }
+      catch (err) {
+        throw new Error(err);
+      }
+    },
+  );
+};

+ 0 - 0
apps/app/src/features/growi-plugin/utils/growi-facade-utils.client.ts → apps/app/src/features/growi-plugin/client/utils/growi-facade-utils.ts


+ 0 - 1
apps/app/src/features/growi-plugin/components/index.ts

@@ -1 +0,0 @@
-export * from './GrowiPluginsActivator.client';

+ 2 - 10
apps/app/src/features/growi-plugin/interfaces/growi-plugin.ts

@@ -1,12 +1,4 @@
-import { GrowiThemeMetadata, HasObjectId } from '@growi/core';
-
-export const GrowiPluginResourceType = {
-  Template: 'template',
-  Style: 'style',
-  Theme: 'theme',
-  Script: 'script',
-} as const;
-export type GrowiPluginResourceType = typeof GrowiPluginResourceType[keyof typeof GrowiPluginResourceType];
+import { GrowiPluginType, GrowiThemeMetadata, HasObjectId } from '@growi/core';
 
 export type IGrowiPluginOrigin = {
   url: string,
@@ -24,7 +16,7 @@ export type IGrowiPlugin<M extends IGrowiPluginMeta = IGrowiPluginMeta> = {
 
 export type IGrowiPluginMeta = {
   name: string,
-  types: GrowiPluginResourceType[],
+  types: GrowiPluginType[],
   desc?: string,
   author?: string,
 }

+ 5 - 6
apps/app/src/features/growi-plugin/models/growi-plugin.ts → apps/app/src/features/growi-plugin/server/models/growi-plugin.ts

@@ -1,20 +1,19 @@
-import { GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
+import { GrowiPluginType, GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import {
   Schema, type Model, type Document, type Types,
 } from 'mongoose';
 
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
-import { GrowiPluginResourceType } from '../interfaces';
 import type {
   IGrowiPlugin, IGrowiPluginMeta, IGrowiPluginOrigin, IGrowiThemePluginMeta,
-} from '../interfaces';
+} from '../../interfaces';
 
 export interface IGrowiPluginDocument extends IGrowiPlugin, Document {
 }
 export interface IGrowiPluginModel extends Model<IGrowiPluginDocument> {
   findEnabledPlugins(): Promise<IGrowiPlugin[]>
-  findEnabledPluginsIncludingAnyTypes(includingTypes: GrowiPluginResourceType[]): Promise<IGrowiPlugin[]>
+  findEnabledPluginsIncludingAnyTypes(includingTypes: GrowiPluginType[]): Promise<IGrowiPlugin[]>
   activatePlugin(id: Types.ObjectId): Promise<string>
   deactivatePlugin(id: Types.ObjectId): Promise<string>
 }
@@ -37,7 +36,7 @@ const growiPluginMetaSchema = new Schema<IGrowiPluginMeta|IGrowiThemePluginMeta>
   name: { type: String, required: true },
   types: {
     type: [String],
-    enum: GrowiPluginResourceType,
+    enum: GrowiPluginType,
     require: true,
   },
   desc: { type: String },
@@ -63,7 +62,7 @@ growiPluginSchema.statics.findEnabledPlugins = async function(): Promise<IGrowiP
   return this.find({ isEnabled: true });
 };
 
-growiPluginSchema.statics.findEnabledPluginsIncludingAnyTypes = async function(types: GrowiPluginResourceType[]): Promise<IGrowiPlugin[]> {
+growiPluginSchema.statics.findEnabledPluginsIncludingAnyTypes = async function(types: GrowiPluginType[]): Promise<IGrowiPlugin[]> {
   return this.find({
     isEnabled: true,
     'meta.types': { $in: types },

+ 0 - 0
apps/app/src/features/growi-plugin/models/index.ts → apps/app/src/features/growi-plugin/server/models/index.ts


+ 0 - 0
apps/app/src/features/growi-plugin/models/vo/github-url.spec.ts → apps/app/src/features/growi-plugin/server/models/vo/github-url.spec.ts


+ 0 - 0
apps/app/src/features/growi-plugin/models/vo/github-url.ts → apps/app/src/features/growi-plugin/server/models/vo/github-url.ts


+ 2 - 2
apps/app/src/features/growi-plugin/routes/growi-plugins.ts → apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts

@@ -5,8 +5,8 @@ import mongoose from 'mongoose';
 import Crowi from '~/server/crowi';
 import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 
-import { GrowiPlugin } from '../models';
-import { growiPluginService } from '../services';
+import { GrowiPlugin } from '../../../models';
+import { growiPluginService } from '../../../services';
 
 
 const ObjectID = mongoose.Types.ObjectId;

+ 6 - 7
apps/app/src/features/growi-plugin/services/growi-plugin.ts → apps/app/src/features/growi-plugin/server/services/growi-plugin.ts

@@ -1,7 +1,7 @@
 import fs, { readFileSync } from 'fs';
 import path from 'path';
 
-import { GrowiThemeMetadata, ViteManifest } from '@growi/core';
+import { GrowiPluginType, GrowiThemeMetadata, ViteManifest } from '@growi/core';
 // eslint-disable-next-line no-restricted-imports
 import axios from 'axios';
 import mongoose from 'mongoose';
@@ -11,10 +11,9 @@ import unzipper from 'unzipper';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
-import { GrowiPluginResourceType } from '../interfaces';
 import type {
   IGrowiPlugin, IGrowiPluginOrigin, IGrowiThemePluginMeta, IGrowiPluginMeta,
-} from '../interfaces';
+} from '../../interfaces';
 import { GrowiPlugin } from '../models';
 import { GitHubUrl } from '../models/vo/github-url';
 
@@ -255,7 +254,7 @@ export class GrowiPluginService implements IGrowiPluginService {
     };
 
     // add theme metadata
-    if (growiPlugin.types.includes(GrowiPluginResourceType.Theme)) {
+    if (growiPlugin.types.includes(GrowiPluginType.Theme)) {
       (plugin as IGrowiPlugin<IGrowiThemePluginMeta>).meta = {
         ...plugin.meta,
         themes: growiPlugin.themes,
@@ -311,7 +310,7 @@ export class GrowiPluginService implements IGrowiPluginService {
 
     try {
       // retrieve plugin manifests
-      const growiPlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginResourceType.Theme]) as IGrowiPlugin<IGrowiThemePluginMeta>[];
+      const growiPlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginType.Theme]) as IGrowiPlugin<IGrowiThemePluginMeta>[];
 
       growiPlugins
         .forEach(async(growiPlugin) => {
@@ -358,12 +357,12 @@ export class GrowiPluginService implements IGrowiPluginService {
           const manifest = await retrievePluginManifest(growiPlugin);
 
           // add script
-          if (types.includes(GrowiPluginResourceType.Script) || types.includes(GrowiPluginResourceType.Template)) {
+          if (types.includes(GrowiPluginType.Script)) {
             const href = `${PLUGINS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`;
             entries.push([growiPlugin.installedPath, href]);
           }
           // add link
-          if (types.includes(GrowiPluginResourceType.Script) || types.includes(GrowiPluginResourceType.Style)) {
+          if (types.includes(GrowiPluginType.Script) || types.includes(GrowiPluginType.Style)) {
             const href = `${PLUGINS_STATIC_DIR}/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`;
             entries.push([growiPlugin.installedPath, href]);
           }

+ 0 - 0
apps/app/src/features/growi-plugin/services/index.ts → apps/app/src/features/growi-plugin/server/services/index.ts


+ 0 - 27
apps/app/src/features/growi-plugin/stores/growi-plugin.tsx

@@ -1,27 +0,0 @@
-import useSWR, { SWRResponse } from 'swr';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-import type { IGrowiPluginHasId } from '../interfaces';
-
-type Plugins = {
-  plugins: IGrowiPluginHasId[]
-}
-
-const pluginsFetcher = () => {
-  return async() => {
-    const reqUrl = '/plugins';
-
-    try {
-      const res = await apiv3Get(reqUrl);
-      return res.data;
-    }
-    catch (err) {
-      throw new Error(err);
-    }
-  };
-};
-
-export const useSWRxPlugins = (): SWRResponse<Plugins, Error> => {
-  return useSWR('/plugins', pluginsFetcher());
-};

+ 58 - 0
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -0,0 +1,58 @@
+import { scanAllTemplateStatus, getMarkdown } from '@growi/pluginkit/dist/v4/server';
+import express from 'express';
+import { param, query } from 'express-validator';
+
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import loggerFactory from '~/utils/logger';
+import { resolveFromRoot } from '~/utils/project-dir-utils';
+
+const logger = loggerFactory('growi:routes:apiv3:templates');
+
+const router = express.Router();
+
+const validator = {
+  list: [
+    query('includeInvalidTemplates').optional().isBoolean(),
+  ],
+  get: [
+    param('templateId').isString(),
+    param('locale').isString(),
+  ],
+};
+
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+
+  router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
+    const { includeInvalidTemplates } = req.query;
+
+    const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
+    const summaries = await scanAllTemplateStatus(presetTemplatesRoot, {
+      returnsInvalidTemplates: includeInvalidTemplates,
+    });
+
+    return res.apiv3({ summaries });
+  });
+
+  router.get('/preset-templates/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(req, res: ApiV3Response) => {
+    const {
+      templateId, locale,
+    } = req.params;
+
+    const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
+    const markdown = await getMarkdown(presetTemplatesRoot, templateId, locale);
+
+    return res.apiv3({ markdown });
+  });
+
+  router.get('/plugin-templates/:pluginId/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(req, res: ApiV3Response) => {
+    const {
+      pluginId, templateId, locale,
+    } = req.params;
+
+    return res.apiv3({});
+  });
+
+  return router;
+};

+ 1 - 0
apps/app/src/features/templates/stores/index.ts

@@ -0,0 +1 @@
+export * from './template';

+ 29 - 0
apps/app/src/features/templates/stores/template.tsx

@@ -0,0 +1,29 @@
+import { getLocalizedTemplate, type TemplateSummary } from '@growi/pluginkit/dist/v4';
+import useSWR, { type SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+
+export const useSWRxTemplates = (): SWRResponse<TemplateSummary[], Error> => {
+  return useSWR(
+    '/templates',
+    endpoint => apiv3Get<{ summaries: TemplateSummary[] }>(endpoint).then(res => res.data.summaries),
+  );
+};
+
+export const useSWRxTemplate = (summary: TemplateSummary | undefined, locale?: string): SWRResponse<string, Error> => {
+  const pluginId = summary?.default.pluginId;
+  const targetTemplate = getLocalizedTemplate(summary, locale);
+
+  return useSWR(
+    () => {
+      if (targetTemplate == null) {
+        return null;
+      }
+
+      return pluginId == null
+        ? `/templates/preset-templates/${targetTemplate.id}/${targetTemplate.locale}`
+        : `/templates/plugin-templates/${pluginId}/${targetTemplate.id}/${targetTemplate.locale}`;
+    },
+    endpoint => apiv3Get<{ markdown: string }>(endpoint).then(res => res.data.markdown),
+  );
+};

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

@@ -68,7 +68,7 @@ declare global {
 }
 
 
-const GrowiPluginsActivator = dynamic(() => import('~/features/growi-plugin/components').then(mod => mod.GrowiPluginsActivator), { ssr: false });
+const GrowiPluginsActivator = dynamic(() => import('~/features/growi-plugin/client/components').then(mod => mod.GrowiPluginsActivator), { ssr: false });
 const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')

+ 2 - 1
apps/app/src/pages/_document.page.tsx

@@ -6,7 +6,7 @@ import Document, {
   Html, Head, Main, NextScript,
 } from 'next/document';
 
-import { growiPluginService, type GrowiPluginResourceEntries } from '~/features/growi-plugin/services';
+import type { GrowiPluginResourceEntries } from '~/features/growi-plugin/server/services';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import loggerFactory from '~/utils/logger';
 
@@ -57,6 +57,7 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const customNoscript: string | null = customizeService.getCustomNoscript();
 
     // retrieve plugin manifests
+    const growiPluginService = await import('~/features/growi-plugin/server/services').then(mod => mod.growiPluginService);
     const pluginResourceEntries = await growiPluginService.retrieveAllPluginResourceEntries();
 
     return {

+ 1 - 1
apps/app/src/pages/admin/plugins.page.tsx

@@ -18,7 +18,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const PluginsExtensionPageContents = dynamic(
-  () => import('~/features/growi-plugin/components/Admin/PluginsExtensionPageContents').then(mod => mod.PluginsExtensionPageContents),
+  () => import('~/features/growi-plugin/client/components/Admin').then(mod => mod.PluginsExtensionPageContents),
   { ssr: false },
 );
 

+ 1 - 1
apps/app/src/server/crowi/index.js

@@ -706,7 +706,7 @@ Crowi.prototype.setupImport = async function() {
 };
 
 Crowi.prototype.setupGrowiPluginService = async function() {
-  const { growiPluginService } = require('~/features/growi-plugin/services');
+  const growiPluginService = await import('~/features/growi-plugin/server/services').then(mod => mod.growiPluginService);
 
   // download plugin repositories, if document exists but there is no repository
   // TODO: Cannot download unless connected to the Internet at setup.

+ 3 - 5
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -1,13 +1,11 @@
 /* eslint-disable no-unused-vars */
 
-import { ErrorV3 } from '@growi/core';
+import { GrowiPluginType, ErrorV3 } from '@growi/core';
 import express from 'express';
 import { body } from 'express-validator';
-import mongoose from 'mongoose';
 import multer from 'multer';
 
-import { GrowiPluginResourceType } from '~/features/growi-plugin/interfaces';
-import { GrowiPlugin } from '~/features/growi-plugin/models';
+import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
@@ -275,7 +273,7 @@ module.exports = (crowi) => {
       const currentTheme = await crowi.configManager.getConfig('crowi', 'customize:theme');
 
       // retrieve plugin manifests
-      const themePlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginResourceType.Theme]);
+      const themePlugins = await GrowiPlugin.findEnabledPluginsIncludingAnyTypes([GrowiPluginType.Theme]);
 
       const pluginThemesMetadatas = themePlugins
         .map(themePlugin => themePlugin.meta.themes)

+ 3 - 2
apps/app/src/server/routes/apiv3/index.js

@@ -1,3 +1,4 @@
+import growiPlugin from '~/features/growi-plugin/server/routes/apiv3/admin';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -43,6 +44,7 @@ module.exports = (crowi, app) => {
   routerForAdmin.use('/slack-integration-legacy-settings', require('./slack-integration-legacy-settings')(crowi));
   routerForAdmin.use('/activity', require('./activity')(crowi));
   routerForAdmin.use('/g2g-transfer', g2gTransfer(crowi));
+  routerForAdmin.use('/plugins', growiPlugin(crowi));
 
   // auth
   const applicationInstalled = require('../../middlewares/application-installed')(crowi);
@@ -108,12 +110,11 @@ module.exports = (crowi, app) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
-  router.use('/plugins', require('~/features/growi-plugin/routes/growi-plugins')(crowi));
-
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
   router.use('/bookmark-folder', require('./bookmark-folder')(crowi));
   router.use('/questionnaire', require('~/features/questionnaire/server/routes/apiv3/questionnaire')(crowi));
+  router.use('/templates', require('~/features/templates/server/routes/apiv3')(crowi));
 
   return [router, routerForAdmin, routerForAuth];
 };

+ 1 - 1
apps/app/src/server/service/customize.ts

@@ -3,7 +3,7 @@ import { ColorScheme, DevidedPagePath, getForcedColorScheme } from '@growi/core'
 import { DefaultThemeMetadata, PresetThemesMetadatas } from '@growi/preset-themes';
 import uglifycss from 'uglifycss';
 
-import { growiPluginService } from '~/features/growi-plugin/services';
+import { growiPluginService } from '~/features/growi-plugin/server/services';
 import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';

+ 1 - 1
apps/app/src/stores/renderer.tsx

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
 import type { HtmlElementNode } from 'rehype-toc';
 import useSWR, { type SWRResponse } from 'swr';
 
-import { getGrowiFacade } from '~/features/growi-plugin/utils/growi-facade-utils.client';
+import { getGrowiFacade } from '~/features/growi-plugin/client/utils/growi-facade-utils';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 
 

+ 0 - 141
apps/app/src/stores/template.tsx

@@ -1,141 +0,0 @@
-import type { ITemplate } from '@growi/core';
-import useSWR, { type SWRResponse } from 'swr';
-
-import { getGrowiFacade } from '~/features/growi-plugin/utils/growi-facade-utils.client';
-
-const presetTemplates: ITemplate[] = [
-  // preset 1
-  {
-    id: '__preset1__',
-    name: '日報',
-    markdown: `# {{yyyy}}/{{MM}}/{{dd}} 日報
-
-## 今日の目標
-- 目標1
-    - 〇〇の完了
-- 目標2
-    - 〇〇を〇件達成
-
-
-## 内容
-- 10:00 ~ 10:20 今日のタスク確認
-- 10:20 ~ 11:00 全体会議
-
-
-## 進捗
-- 目標1
-    - 完了
-- 目標2
-    - 〇〇件達成
-
-
-## メモ
-- 改善できることの振り返り
-
-
-## 翌営業日の目標
-- 目標1
-    - 〇〇の完了
-- 目標2
-    - 〇〇を〇件達成
-`,
-  },
-
-  // preset 2
-  {
-    id: '__preset2__',
-    name: '議事録',
-    markdown: `# {{{title}}}{{^title}}<会議名>{{/title}}
-
-## 日時
-{{yyyy}}/{{MM}}/{{dd}} {{HH}}:{{mm}}〜hh:mm
-
-
-## 参加者
--
-
-## 議題
-1.
-2.
-
-
-## 1.
-### 内容
-
-
-### 決定事項
-
-
-### Next Action
-
-
-## 2.
-### 内容
-
-
-### 決定事項
-
-
-### Next Action
-
-
-## 次回会議
-- 会議内容
-- 会議時間
-    - {{yyyy}}/{{MM}}/dd
-`,
-  },
-
-  // preset 3
-  {
-    id: '__preset3__',
-    name: '企画書',
-    markdown: `# {{{title}}}{{^title}}<企画タイトル>{{/title}}
-
-## 目的
-
-
-## 現状の課題
-
-
-## 概要
-#### 企画の内容
-
-#### スケジュール
-
-
-## 効果
-#### メリット
-
-#### 数値目標
-
-
-## 参考資料
-
-`,
-  },
-
-  // preset 4
-  {
-    id: '__preset4__',
-    name: '関連ページの一覧表示',
-    markdown: `# 関連ページ
-
-## 子ページ一覧
-$lsx(depth=1)
-`,
-  },
-];
-
-export const useTemplates = (): SWRResponse<ITemplate[], Error> => {
-  return useSWR(
-    'templates',
-    () => [
-      ...presetTemplates,
-      ...Object.values<ITemplate>(getGrowiFacade().customTemplates ?? {}),
-    ],
-    {
-      fallbackData: presetTemplates,
-    },
-  );
-};

+ 4 - 3
packages/pluginkit/src/consts/types.ts → packages/core/src/consts/growi-plugin.ts

@@ -1,6 +1,7 @@
 export const GrowiPluginType = {
-  SCRIPT: 'script',
-  TEMPLATE: 'template',
-  THEME: 'theme',
+  Template: 'template',
+  Style: 'style',
+  Theme: 'theme',
+  Script: 'script',
 } as const;
 export type GrowiPluginType = typeof GrowiPluginType[keyof typeof GrowiPluginType];

+ 1 - 0
packages/core/src/consts/index.ts

@@ -0,0 +1 @@
+export * from './growi-plugin';

+ 2 - 14
packages/core/src/index.ts

@@ -1,17 +1,5 @@
-export * from './interfaces/attachment';
-export * from './interfaces/color-scheme';
-export * from './interfaces/common';
-export * from './interfaces/growi-facade';
-export * from './interfaces/growi-theme-metadata';
-export * from './interfaces/has-object-id';
-export * from './interfaces/lang';
-export * from './interfaces/page';
-export * from './interfaces/revision';
-export * from './interfaces/subscription';
-export * from './interfaces/tag';
-export * from './interfaces/template';
-export * from './interfaces/user';
-export * from './interfaces/vite';
+export * from './consts';
+export * from './interfaces';
 export * from './models/devided-page-path';
 export * from './models/vo/error-apiv3';
 export * from './plugin';

+ 14 - 0
packages/core/src/interfaces/index.ts

@@ -0,0 +1,14 @@
+export * from './attachment';
+export * from './color-scheme';
+export * from './common';
+export * from './growi-facade';
+export * from './growi-theme-metadata';
+export * from './has-object-id';
+export * from './lang';
+export * from './page';
+export * from './revision';
+export * from './subscription';
+export * from './tag';
+export * from './template';
+export * from './user';
+export * from './vite';

+ 5 - 1
packages/core/src/interfaces/template.ts

@@ -1,5 +1,9 @@
-export type ITemplate = {
+export type ITemplateIdentification = {
   id: string,
+  locale: string,
+}
+
+export type ITemplate = ITemplateIdentification & {
   name: string,
   markdown: string,
 }

+ 1 - 1
packages/pluginkit/package.json

@@ -16,7 +16,7 @@
     "test": "vitest run --coverage"
   },
   "dependencies": {
-    "@growi/core": "^6.1.5-RC",
+    "@growi/core": "link:../core",
     "extensible-custom-error": "^0.0.7"
   },
   "devDependencies": {

+ 0 - 1
packages/pluginkit/src/consts/index.ts

@@ -1 +0,0 @@
-export * from './types';

+ 0 - 1
packages/pluginkit/src/index.ts

@@ -1,2 +1 @@
-export * from './consts';
 export * from './model';

+ 1 - 1
packages/pluginkit/src/model/growi-plugin-validation-data.ts

@@ -1,4 +1,4 @@
-import { GrowiPluginType } from '../consts/types';
+import { GrowiPluginType } from '@growi/core/dist/consts';
 
 export type GrowiPluginValidationData = {
   projectDirRoot: string,

+ 2 - 0
packages/pluginkit/src/v4/index.ts

@@ -0,0 +1,2 @@
+export * from './interfaces';
+export * from './utils';

+ 1 - 0
packages/pluginkit/src/v4/interfaces/index.ts

@@ -0,0 +1 @@
+export * from './template';

+ 25 - 0
packages/pluginkit/src/v4/interfaces/template.ts

@@ -0,0 +1,25 @@
+export type TemplateStatusBasis = {
+  id: string,
+  locale: string,
+  pluginId?: string,
+}
+export type TemplateStatusValid = TemplateStatusBasis & {
+  isValid: true,
+  isDefault: boolean,
+  title: string,
+  desc?: string,
+}
+export type TemplateStatusInvalid = TemplateStatusBasis & {
+  isValid: false,
+  invalidReason: string,
+}
+export type TemplateStatus = TemplateStatusValid | TemplateStatusInvalid;
+
+export function isTemplateStatusValid(status: TemplateStatus): status is TemplateStatusValid {
+  return status.isValid;
+}
+
+export type TemplateSummary = {
+  default: TemplateStatusValid,
+  [locale: string]: TemplateStatus,
+}

+ 1 - 0
packages/pluginkit/src/v4/server/index.ts

@@ -0,0 +1 @@
+export * from './utils';

+ 0 - 0
packages/pluginkit/src/server/utils/v4/index.ts → packages/pluginkit/src/v4/server/utils/index.ts


+ 0 - 0
packages/pluginkit/src/server/utils/v4/package-json/import.spec.ts → packages/pluginkit/src/v4/server/utils/package-json/import.spec.ts


+ 0 - 0
packages/pluginkit/src/server/utils/v4/package-json/import.ts → packages/pluginkit/src/v4/server/utils/package-json/import.ts


+ 0 - 0
packages/pluginkit/src/server/utils/v4/package-json/index.ts → packages/pluginkit/src/v4/server/utils/package-json/index.ts


+ 5 - 4
packages/pluginkit/src/server/utils/v4/package-json/validate.spec.ts → packages/pluginkit/src/v4/server/utils/package-json/validate.spec.ts

@@ -1,6 +1,7 @@
+import { GrowiPluginType } from '@growi/core/dist/consts';
+
 import examplePkg from '^/test/fixtures/example-package/template1/package.json';
 
-import { GrowiPluginType } from '~/consts';
 
 import { validatePackageJson } from './validate';
 
@@ -32,7 +33,7 @@ describe('validatePackageJson()', () => {
     mocks.importPackageJsonMock.mockResolvedValue(examplePkg);
 
     // when
-    const data = await validatePackageJson('package.json', GrowiPluginType.TEMPLATE);
+    const data = await validatePackageJson('package.json', GrowiPluginType.Template);
 
     // then
     expect(data).not.toBeNull();
@@ -101,12 +102,12 @@ describe('validatePackageJson()', () => {
       mocks.importPackageJsonMock.mockResolvedValue({
         growiPlugin: {
           schemaVersion: 4,
-          types: [GrowiPluginType.TEMPLATE],
+          types: [GrowiPluginType.Template],
         },
       });
 
       // when
-      const caller = async() => { await validatePackageJson('package.json', GrowiPluginType.SCRIPT) };
+      const caller = async() => { await validatePackageJson('package.json', GrowiPluginType.Script) };
 
       // then
       await expect(caller).rejects.toThrow("The growiPlugin directive does not have expected plugin type in 'types' directive.");

+ 2 - 1
packages/pluginkit/src/server/utils/v4/package-json/validate.ts → packages/pluginkit/src/v4/server/utils/package-json/validate.ts

@@ -1,4 +1,5 @@
-import { GrowiPluginType } from '~/consts';
+import { GrowiPluginType } from '@growi/core/dist/consts';
+
 import { type GrowiPluginValidationData, GrowiPluginValidationError } from '~/model';
 
 import { importPackageJson } from './import';

+ 79 - 36
packages/pluginkit/src/server/utils/v4/template.ts → packages/pluginkit/src/v4/server/utils/template.ts

@@ -1,12 +1,14 @@
-import assert from 'assert';
 import fs from 'fs';
 import path from 'path';
 import { promisify } from 'util';
 
-import { GrowiPluginType } from '~/consts';
+import { GrowiPluginType } from '@growi/core/dist/consts';
+
 import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '~/model';
 import { GrowiPluginValidationError } from '~/model';
 
+import { isTemplateStatusValid, type TemplateStatus, type TemplateSummary } from '../../interfaces';
+
 import { importPackageJson, validatePackageJson } from './package-json';
 
 
@@ -18,7 +20,7 @@ const statAsync = promisify(fs.stat);
  * @param projectDirRoot
  */
 export const validateTemplatePluginPackageJson = async(projectDirRoot: string): Promise<GrowiTemplatePluginValidationData> => {
-  const data = await validatePackageJson(projectDirRoot, GrowiPluginType.TEMPLATE);
+  const data = await validatePackageJson(projectDirRoot, GrowiPluginType.Template);
 
   const pkg = await importPackageJson(projectDirRoot);
 
@@ -40,18 +42,10 @@ export const validateTemplatePluginPackageJson = async(projectDirRoot: string):
   };
 };
 
-export type TemplateStatus = {
-  id: string,
-  locale: string,
-  isValid: boolean,
-  invalidReason?: string,
-}
 
 type TemplateDirStatus = {
   isTemplateExists: boolean,
-  isMetaDataFileExists: boolean,
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  meta?: any,
+  meta?: { [key: string]: string },
 }
 
 async function getStats(tplDir: string): Promise<TemplateDirStatus> {
@@ -65,37 +59,54 @@ async function getStats(tplDir: string): Promise<TemplateDirStatus> {
 
   const result: TemplateDirStatus = {
     isTemplateExists,
-    isMetaDataFileExists,
+    meta: isMetaDataFileExists ? await import(metaDataPath) : undefined,
   };
 
-  if (isMetaDataFileExists) {
-    result.meta = await import(metaDataPath);
-  }
-
   return result;
 }
 
-export const scanTemplateStatus = async(projectDirRoot: string, templateId: string, data: GrowiTemplatePluginValidationData): Promise<TemplateStatus[]> => {
+
+export const scanTemplateStatus = async(
+    projectDirRoot: string,
+    templateId: string,
+    data: GrowiTemplatePluginValidationData,
+    opts?: {
+      pluginId?: string,
+    },
+): Promise<TemplateStatus[]> => {
   const status: TemplateStatus[] = [];
 
   const tplRootDirPath = path.resolve(projectDirRoot, 'dist', templateId);
 
+  let isDefaultPushed = false;
   for await (const locale of data.supportingLocales) {
     const tplDir = path.resolve(tplRootDirPath, locale);
 
     try {
+      const stats = await getStats(tplDir);
       const {
-        isTemplateExists, isMetaDataFileExists, meta,
-      } = await getStats(tplDir);
+        isTemplateExists, meta,
+      } = stats;
 
       if (!isTemplateExists) throw new Error("'template.md does not exist.");
-      if (!isMetaDataFileExists) throw new Error("'meta.md does not exist.");
+      if (meta == null) throw new Error("'meta.md does not exist.");
       if (meta?.title == null) throw new Error("'meta.md does not contain the title.");
 
-      status.push({ id: templateId, locale, isValid: true });
+      const isDefault = !isDefaultPushed;
+      status.push({
+        pluginId: opts?.pluginId,
+        id: templateId,
+        locale,
+        isValid: true,
+        isDefault,
+        title: meta.title,
+        desc: meta.desc,
+      });
+      isDefaultPushed = true;
     }
     catch (err) {
       status.push({
+        pluginId: opts?.pluginId,
         id: templateId,
         locale,
         isValid: false,
@@ -110,26 +121,51 @@ export const scanTemplateStatus = async(projectDirRoot: string, templateId: stri
   return status;
 };
 
-export const scanAllTemplateStatus = async(projectDirRoot: string, data: GrowiTemplatePluginValidationData): Promise<TemplateStatus[]> => {
-  const status: TemplateStatus[] = [];
+export const scanAllTemplateStatus = async(
+    projectDirRoot: string,
+    opts?: {
+      data?: GrowiTemplatePluginValidationData,
+      pluginId?: string,
+      returnsInvalidTemplates?: boolean,
+    },
+): Promise<TemplateSummary[]> => {
+
+  const data = opts?.data ?? await validateTemplatePluginPackageJson(projectDirRoot);
+
+  const summaries: TemplateSummary[] = [];
 
   const distDirPath = path.resolve(projectDirRoot, 'dist');
   const distDirFiles = fs.readdirSync(distDirPath);
 
   for await (const templateId of distDirFiles) {
-    status.push(...await scanTemplateStatus(projectDirRoot, templateId, data));
+    const status = (await scanTemplateStatus(projectDirRoot, templateId, data, { pluginId: opts?.pluginId }))
+      // omit invalid templates if `returnsInvalidTemplates` is true
+      .filter(s => (opts?.returnsInvalidTemplates ? true : s.isValid));
+
+    // determine default locale
+    const defaultTemplateStatus = status.find(s => 'isDefault' in s && s.isDefault);
+
+    if (defaultTemplateStatus == null || !isTemplateStatusValid(defaultTemplateStatus)) {
+      continue;
+    }
+
+    summaries.push({
+      // for the 'default' key
+      default: defaultTemplateStatus,
+      // for each locale keys
+      ...Object.fromEntries(status.map(templateStatus => [templateStatus.locale, templateStatus])),
+    });
   }
 
-  return status;
+  return summaries;
 };
 
-
 export const validateTemplatePlugin = async(projectDirRoot: string): Promise<boolean> => {
   const data = await validateTemplatePluginPackageJson(projectDirRoot);
 
-  const results = await scanAllTemplateStatus(projectDirRoot, data);
+  const results = await scanAllTemplateStatus(projectDirRoot, { data, returnsInvalidTemplates: true });
 
-  if (results.length === 0) {
+  if (Object.keys(results).length === 0) {
     throw new Error('This plugin does not have any templates');
   }
 
@@ -137,19 +173,15 @@ export const validateTemplatePlugin = async(projectDirRoot: string): Promise<boo
   // key: id
   // value: isValid properties
   const idValidMap: { [id: string]: boolean[] } = {};
-  results.forEach((status) => {
-    const validMap = idValidMap[status.id] ?? [];
-    validMap.push(status.isValid);
-    idValidMap[status.id] = validMap;
+  results.forEach((summary) => {
+    idValidMap[summary.default.id] = Object.values(summary).map(s => s?.isValid ?? false);
   });
 
   for (const [id, validMap] of Object.entries(idValidMap)) {
-    assert(validMap.length === data.supportingLocales.length);
-
     // warn
     if (!validMap.every(bool => bool)) {
       // eslint-disable-next-line no-console
-      console.warn(`[WARN] Template '${id}' has invalid status`);
+      console.warn(`[WARN] Template '${id}' has some locales that status is invalid`);
     }
 
     // This means the template directory does not have any valid template
@@ -160,3 +192,14 @@ export const validateTemplatePlugin = async(projectDirRoot: string): Promise<boo
 
   return true;
 };
+
+export const getMarkdown = async(projectDirRoot: string, templateId: string, locale: string): Promise<string> => {
+  const tplDir = path.resolve(projectDirRoot, 'dist', templateId, locale);
+
+  const { isTemplateExists } = await getStats(tplDir);
+
+  if (!isTemplateExists) throw new Error("'template.md does not exist.");
+
+  const markdownPath = path.resolve(tplDir, 'template.md');
+  return fs.readFileSync(markdownPath, { encoding: 'utf-8' });
+};

+ 1 - 0
packages/pluginkit/src/v4/utils/index.ts

@@ -0,0 +1 @@
+export * from './template';

+ 11 - 0
packages/pluginkit/src/v4/utils/template.ts

@@ -0,0 +1,11 @@
+import type { TemplateSummary, TemplateStatus } from '../interfaces';
+
+export const getLocalizedTemplate = (templateSummary: TemplateSummary | undefined, locale?: string): TemplateStatus | undefined => {
+  if (templateSummary == null) {
+    return undefined;
+  }
+
+  return locale != null
+    ? templateSummary[locale]
+    : templateSummary.default;
+};

+ 3 - 0
packages/preset-templates/dist/daily-report/en_US/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "Daily Report"
+}

+ 30 - 0
packages/preset-templates/dist/daily-report/en_US/template.md

@@ -0,0 +1,30 @@
+# {{yyyy}}/{{MM}}/{{dd}} Daily Report
+
+## TODAY'S GOALS
+- GOAL 1
+    - Complete Task A
+- GOAL 2
+    - Reply the email for customer support
+
+
+## WORK DETAILS
+- 10:00 ~ 10:20 Confirm today's tasks
+- 10:20 ~ 11:00 Attend the team meeting
+
+
+## TODAY'S PROGRESS
+- GOAL 1
+    - Acomplished
+- GOAL 2
+    - Acomplished
+
+
+## IMPROVEMENT
+- A look-back on the day
+
+
+## NBD'S GOALS
+- GOAL 1
+    - Complete Task B
+- GOAL 2
+    - Contact Company C

+ 3 - 0
packages/preset-templates/dist/daily-report/ja_JP/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "日報"
+}

+ 30 - 0
packages/preset-templates/dist/daily-report/ja_JP/template.md

@@ -0,0 +1,30 @@
+# {{yyyy}}/{{MM}}/{{dd}} 日報
+
+## 今日の目標
+- 目標1
+    - 〇〇の完了
+- 目標2
+    - 〇〇を〇件達成
+
+
+## 内容
+- 10:00 ~ 10:20 今日のタスク確認
+- 10:20 ~ 11:00 全体会議
+
+
+## 進捗
+- 目標1
+    - 完了
+- 目標2
+    - 〇〇件達成
+
+
+## メモ
+- 改善できることの振り返り
+
+
+## 翌営業日の目標
+- 目標1
+    - 〇〇の完了
+- 目標2
+    - 〇〇を〇件達成

+ 3 - 0
packages/preset-templates/dist/daily-report/zh_CN/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "每日工作报告"
+}

+ 30 - 0
packages/preset-templates/dist/daily-report/zh_CN/template.md

@@ -0,0 +1,30 @@
+# {{yyyy}}/{{MM}}/{{dd}} 工作报告
+
+## 今日目标
+- 目标1
+    - 完成〇〇
+- 目标2
+    - 获取〇〇客源
+
+
+## 内容
+- 10:00 ~ 10:20 确认今天的任务
+- 10:20 ~ 11:00 出席公司全体会议
+
+
+## 进度
+- 目标1
+    - 完成 ✅
+- 目标2
+    - 〇〇客源获得
+
+
+## 改善点
+- 列出今日在工作中可以改善的地方
+
+
+## 下一个工作日的目标
+- 目标1
+    - 完成任务A
+- 目标1
+    - 达成〇〇指标

+ 3 - 0
packages/preset-templates/dist/displaying-child-pages/en_US/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "Template for displaying child pages"
+}

+ 4 - 0
packages/preset-templates/dist/displaying-child-pages/en_US/template.md

@@ -0,0 +1,4 @@
+# RELATED PAGES
+
+## CHILD PAGES
+$lsx(depth=1)

+ 3 - 0
packages/preset-templates/dist/displaying-child-pages/ja_JP/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "関連ページの一覧表示"
+}

+ 4 - 0
packages/preset-templates/dist/displaying-child-pages/ja_JP/template.md

@@ -0,0 +1,4 @@
+# 関連ページ
+
+## 子ページ一覧
+$lsx(depth=1)

+ 3 - 0
packages/preset-templates/dist/displaying-child-pages/zh_CN/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "显示所有的子页面"
+}

+ 4 - 0
packages/preset-templates/dist/displaying-child-pages/zh_CN/template.md

@@ -0,0 +1,4 @@
+# 关联页面
+
+## 子页面一览
+$lsx(depth=1)

+ 0 - 3
packages/preset-templates/dist/example/ja_JP/meta.json

@@ -1,3 +0,0 @@
-{
-  "title": "Example"
-}

+ 0 - 1
packages/preset-templates/dist/example/ja_JP/template.md

@@ -1 +0,0 @@
-# Example

+ 3 - 0
packages/preset-templates/dist/minutes/en_US/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "Minutes"
+}

+ 38 - 0
packages/preset-templates/dist/minutes/en_US/template.md

@@ -0,0 +1,38 @@
+# {{{title}}}{{^title}}<Meeting Title>{{/title}}
+
+## DATE & TIME
+yyyy/mm/dd hh:mm〜hh:mm
+
+
+## ATTENDEES
+- 
+
+## TOPICS
+1. 
+2. 
+
+
+## 1. 
+### Description
+
+
+### Updates
+
+
+### Next Action
+
+
+## 2. 
+### Description
+
+
+### Updates
+
+
+### Next Action
+
+
+## NEXT MEETING
+- Desciption
+- Date & Time
+    - yyyy/mm/dd hh:mm〜hh:mm

+ 3 - 0
packages/preset-templates/dist/minutes/ja_JP/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "議事録"
+}

+ 38 - 0
packages/preset-templates/dist/minutes/ja_JP/template.md

@@ -0,0 +1,38 @@
+# {{{title}}}{{^title}}<会議名>{{/title}}
+
+## 日時
+{{yyyy}}/{{MM}}/{{dd}} {{HH}}:{{mm}}〜hh:mm
+
+
+## 参加者
+-
+
+## 議題
+1.
+2.
+
+
+## 1.
+### 内容
+
+
+### 決定事項
+
+
+### Next Action
+
+
+## 2.
+### 内容
+
+
+### 決定事項
+
+
+### Next Action
+
+
+## 次回会議
+- 会議内容
+- 会議時間
+    - {{yyyy}}/{{MM}}/dd

+ 3 - 0
packages/preset-templates/dist/minutes/zh_CN/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "会议记录"
+}

+ 42 - 0
packages/preset-templates/dist/minutes/zh_CN/template.md

@@ -0,0 +1,42 @@
+# {{{title}}}{{^title}}<会议名称>{{/title}}
+
+## 日時
+{{yyyy}}/{{MM}}/{{dd}} {{HH}}:{{mm}}〜hh:mm
+
+
+## 时间
+yyyy/mm/dd hh:mm〜hh:mm
+
+
+## 出席人员
+- 
+
+## 议题
+1. 
+2. 
+
+
+## 1. 
+### 内容
+
+
+### 结论
+
+
+### Next Action
+
+
+## 2. 
+### 内容
+
+
+### 结论
+
+
+### Next Action
+
+
+## 今后的会议
+- 会议议题
+- 会议时间
+    - yyyy/mm/dd

+ 3 - 0
packages/preset-templates/dist/project-proposal/en_US/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "Project Proposal"
+}

+ 22 - 0
packages/preset-templates/dist/project-proposal/en_US/template.md

@@ -0,0 +1,22 @@
+# {{{title}}}{{^title}}<Project Title>{{/title}}
+
+## GOALS
+
+
+## CURRENT PROBLEMS
+
+
+## OVERVIEW
+#### Project Description
+
+#### Schedule
+
+
+## OUTCOME
+#### Merits/Pros
+
+#### Targets (with statistics)
+
+
+
+## REFERENCES

+ 3 - 0
packages/preset-templates/dist/project-proposal/ja_JP/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "企画書"
+}

+ 21 - 0
packages/preset-templates/dist/project-proposal/ja_JP/template.md

@@ -0,0 +1,21 @@
+# {{{title}}}{{^title}}<企画タイトル>{{/title}}
+
+## 目的
+
+
+## 現状の課題
+
+
+## 概要
+#### 企画の内容
+
+#### スケジュール
+
+
+## 効果
+#### メリット
+
+#### 数値目標
+
+
+## 参考資料

+ 3 - 0
packages/preset-templates/dist/project-proposal/zh_CN/meta.json

@@ -0,0 +1,3 @@
+{
+  "title": "策划书"
+}

+ 21 - 0
packages/preset-templates/dist/project-proposal/zh_CN/template.md

@@ -0,0 +1,21 @@
+# {{{title}}}{{^title}}<方案名称>{{/title}}
+
+## 目的
+
+
+## 当前面临的问题
+
+
+## 策划详情
+#### 策划内容
+
+#### 活动流程
+
+
+## 効果
+#### 意义与利处
+
+#### 最终目标
+
+
+## 参考资料

+ 1 - 1
packages/preset-templates/test/index.test.ts

@@ -1,6 +1,6 @@
 import path from 'node:path';
 
-import { scanAllTemplateStatus, validateTemplatePluginPackageJson, validateTemplatePlugin } from '@growi/pluginkit/dist/server/utils/v4';
+import { scanAllTemplateStatus, validateTemplatePluginPackageJson, validateTemplatePlugin } from '@growi/pluginkit/dist/v4/server';
 
 
 const projectDirRoot = path.resolve(__dirname, '../');

+ 24 - 7
turbo.json

@@ -19,13 +19,13 @@
       "cache": false
     },
 
-    "@growi/remark-attachment-refs#build": {
-      "dependsOn": ["@growi/core#build", "@growi/remark-growi-directive#build", "@growi/ui#build"],
+    "@growi/pluginkit#build": {
+      "dependsOn": ["@growi/core#build"],
       "outputs": ["dist/**"],
       "outputMode": "new-only"
     },
-    "@growi/ui#build": {
-      "dependsOn": ["@growi/core#build"],
+    "@growi/remark-attachment-refs#build": {
+      "dependsOn": ["@growi/core#build", "@growi/remark-growi-directive#build", "@growi/ui#build"],
       "outputs": ["dist/**"],
       "outputMode": "new-only"
     },
@@ -34,6 +34,11 @@
       "outputs": ["dist/**"],
       "outputMode": "new-only"
     },
+    "@growi/ui#build": {
+      "dependsOn": ["@growi/core#build"],
+      "outputs": ["dist/**"],
+      "outputMode": "new-only"
+    },
     "@growi/app#styles-prebuilt": {
       "outputs": ["src/styles/prebuilt/**"],
       "inputs": [
@@ -63,6 +68,11 @@
       "outputMode": "new-only"
     },
 
+    "@growi/pluginkit#dev": {
+      "dependsOn": ["@growi/core#dev"],
+      "outputs": ["dist/**"],
+      "outputMode": "new-only"
+    },
     "@growi/remark-attachment-refs#dev": {
       "dependsOn": ["@growi/core#dev", "@growi/remark-growi-directive#dev", "@growi/ui#dev"],
       "outputs": ["dist/**"],
@@ -134,15 +144,18 @@
       "persistent": true
     },
 
+    "@growi/pluginkit#lint": {
+      "dependsOn": ["@growi/core#dev"]
+    },
     "@growi/remark-attachment-refs#lint": {
       "dependsOn": ["@growi/core#dev", "@growi/remark-growi-directive#dev", "@growi/ui#dev"]
     },
-    "@growi/ui#lint": {
-      "dependsOn": ["@growi/core#dev"]
-    },
     "@growi/remark-lsx#lint": {
       "dependsOn": ["@growi/core#dev", "@growi/remark-growi-directive#dev", "@growi/ui#dev"]
     },
+    "@growi/ui#lint": {
+      "dependsOn": ["@growi/core#dev"]
+    },
     "@growi/app#lint": {
       "dependsOn": ["^dev", "@growi/app#dev:styles-prebuilt"]
     },
@@ -160,6 +173,10 @@
       "dependsOn": ["@growi/slack#dev"],
       "outputMode": "new-only"
     },
+    "@growi/pluginkit#test": {
+      "dependsOn": ["@growi/core#dev"],
+      "outputMode": "new-only"
+    },
     "@growi/preset-templates#test": {
       "dependsOn": ["@growi/pluginkit#dev"],
       "outputMode": "new-only"

+ 4 - 1
yarn.lock

@@ -2323,7 +2323,7 @@
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
   dependencies:
-    "@growi/core" "^6.1.5-RC"
+    "@growi/core" "link:packages/core"
     extensible-custom-error "^0.0.7"
 
 "@growi/presentation@link:packages/presentation":
@@ -2331,6 +2331,9 @@
   dependencies:
     "@growi/core" "link:packages/core"
 
+"@growi/preset-templates@link:packages/preset-templates":
+  version "6.1.5-RC.0"
+
 "@growi/preset-themes@link:packages/preset-themes":
   version "6.1.5-RC.0"