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

Merge pull request #10262 from weseek/support/156162-170473-app-callout-comment-templates-features-biome

support: Configure biome for app callout/comment/templates features
Yuki Takei 7 месяцев назад
Родитель
Сommit
c4e3e3910a

+ 3 - 0
apps/app/.eslintrc.js

@@ -30,6 +30,9 @@ module.exports = {
     'config/**',
     'config/**',
     'src/linter-checker/**',
     'src/linter-checker/**',
     'src/migrations/**',
     'src/migrations/**',
+    'src/features/callout/**',
+    'src/features/comment/**',
+    'src/features/templates/**',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript

+ 32 - 31
apps/app/src/features/callout/components/CalloutViewer.tsx

@@ -1,9 +1,9 @@
 // Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/README.md
 // Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/README.md
 
 
-import type { ReactNode, JSX } from 'react';
+import type { JSX, ReactNode } from 'react';
 import React from 'react';
 import React from 'react';
 
 
-import { type Callout } from '../services/consts';
+import type { Callout } from '../services/consts';
 
 
 import styles from './CalloutViewer.module.scss';
 import styles from './CalloutViewer.module.scss';
 
 
@@ -11,7 +11,7 @@ const moduleClass = styles['callout-viewer'];
 
 
 type CALLOUT_TO = {
 type CALLOUT_TO = {
   [key in Callout]: string;
   [key in Callout]: string;
-}
+};
 
 
 const CALLOUT_TO_TYPE: CALLOUT_TO = {
 const CALLOUT_TO_TYPE: CALLOUT_TO = {
   note: 'Note',
   note: 'Note',
@@ -34,38 +34,39 @@ const CALLOUT_TO_ICON: CALLOUT_TO = {
 };
 };
 
 
 type CalloutViewerProps = {
 type CalloutViewerProps = {
-  children: ReactNode,
-  node: Element,
-  type: string,
-  label?: string,
-}
-
-export const CalloutViewer = React.memo((props: CalloutViewerProps): JSX.Element => {
+  children: ReactNode;
+  node: Element;
+  type: string;
+  label?: string;
+};
 
 
-  const {
-    node, type, label, children,
-  } = props;
+export const CalloutViewer = React.memo(
+  (props: CalloutViewerProps): JSX.Element => {
+    const { node, type, label, children } = props;
 
 
-  if (node == null) {
-    return <></>;
-  }
+    if (node == null) {
+      return <></>;
+    }
 
 
-  return (
-    <div className={`${moduleClass} callout-viewer`}>
-      <div className={`callout callout-${CALLOUT_TO_TYPE[type].toLowerCase()}`}>
-        <div className="callout-indicator">
-          <div className="callout-hint">
-            <span className="material-symbols-outlined">{CALLOUT_TO_ICON[type]}</span>
+    return (
+      <div className={`${moduleClass} callout-viewer`}>
+        <div
+          className={`callout callout-${CALLOUT_TO_TYPE[type].toLowerCase()}`}
+        >
+          <div className="callout-indicator">
+            <div className="callout-hint">
+              <span className="material-symbols-outlined">
+                {CALLOUT_TO_ICON[type]}
+              </span>
+            </div>
+            <div className="callout-title">
+              {label ?? CALLOUT_TO_TYPE[type]}
+            </div>
           </div>
           </div>
-          <div className="callout-title">
-            {label ?? CALLOUT_TO_TYPE[type]}
-          </div>
-        </div>
-        <div className="callout-content">
-          {children}
+          <div className="callout-content">{children}</div>
         </div>
         </div>
       </div>
       </div>
-    </div>
-  );
-});
+    );
+  },
+);
 CalloutViewer.displayName = 'CalloutViewer';
 CalloutViewer.displayName = 'CalloutViewer';

+ 11 - 5
apps/app/src/features/callout/services/callout.spec.ts

@@ -3,7 +3,7 @@ import remarkDirective from 'remark-directive';
 import remarkParse from 'remark-parse';
 import remarkParse from 'remark-parse';
 import { unified } from 'unified';
 import { unified } from 'unified';
 import { visit } from 'unist-util-visit';
 import { visit } from 'unist-util-visit';
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
 
 
 import * as callout from './callout';
 import * as callout from './callout';
 
 
@@ -23,7 +23,7 @@ This is an info callout.
     const tree = processor.parse(markdown);
     const tree = processor.parse(markdown);
     processor.runSync(tree);
     processor.runSync(tree);
 
 
-    let calloutNode;
+    let calloutNode: ContainerDirective | undefined;
     visit(tree, 'containerDirective', (node) => {
     visit(tree, 'containerDirective', (node) => {
       calloutNode = node;
       calloutNode = node;
     });
     });
@@ -41,7 +41,9 @@ This is an info callout.
     assert('value' in calloutNode.children[0].children[0]);
     assert('value' in calloutNode.children[0].children[0]);
 
 
     expect(calloutNode.children.length).toBe(1);
     expect(calloutNode.children.length).toBe(1);
-    expect(calloutNode.children[0].children[0].value).toBe('This is an info callout.');
+    expect(calloutNode.children[0].children[0].value).toBe(
+      'This is an info callout.',
+    );
   });
   });
 
 
   it('should transform containerDirective to callout with custom label', () => {
   it('should transform containerDirective to callout with custom label', () => {
@@ -77,7 +79,9 @@ This is an info callout.
     assert('value' in calloutNode.children[0].children[0]);
     assert('value' in calloutNode.children[0].children[0]);
 
 
     expect(calloutNode.children.length).toBe(1);
     expect(calloutNode.children.length).toBe(1);
-    expect(calloutNode.children[0].children[0].value).toBe('This is an info callout.');
+    expect(calloutNode.children[0].children[0].value).toBe(
+      'This is an info callout.',
+    );
   });
   });
 
 
   it('should transform containerDirective to callout with empty label', () => {
   it('should transform containerDirective to callout with empty label', () => {
@@ -113,6 +117,8 @@ This is an info callout.
     assert('value' in calloutNode.children[0].children[0]);
     assert('value' in calloutNode.children[0].children[0]);
 
 
     expect(calloutNode.children.length).toBe(1);
     expect(calloutNode.children.length).toBe(1);
-    expect(calloutNode.children[0].children[0].value).toBe('This is an info callout.');
+    expect(calloutNode.children[0].children[0].value).toBe(
+      'This is an info callout.',
+    );
   });
   });
 });
 });

+ 20 - 9
apps/app/src/features/callout/services/callout.ts

@@ -8,19 +8,31 @@ import { AllCallout } from './consts';
 export const remarkPlugin: Plugin = () => {
 export const remarkPlugin: Plugin = () => {
   return (tree) => {
   return (tree) => {
     visit(tree, 'containerDirective', (node: ContainerDirective) => {
     visit(tree, 'containerDirective', (node: ContainerDirective) => {
-      if (AllCallout.some(name => name === node.name.toLowerCase())) {
+      if (AllCallout.some((name) => name === node.name.toLowerCase())) {
         const type = node.name.toLowerCase();
         const type = node.name.toLowerCase();
-        const data = node.data ?? (node.data = {});
+        if (node.data == null) {
+          node.data = {};
+        }
+        const data = node.data;
 
 
         // extract directive label
         // extract directive label
-        const paragraphs = (node.children ?? []).filter((child): child is Paragraph => child.type === 'paragraph');
-        const paragraphForDirectiveLabel = paragraphs.find(p => p.data?.directiveLabel);
-        const label = paragraphForDirectiveLabel != null && paragraphForDirectiveLabel.children.length > 0
-          ? (paragraphForDirectiveLabel.children[0] as Text).value
-          : undefined;
+        const paragraphs = (node.children ?? []).filter(
+          (child): child is Paragraph => child.type === 'paragraph',
+        );
+        const paragraphForDirectiveLabel = paragraphs.find(
+          (p) => p.data?.directiveLabel,
+        );
+        const label =
+          paragraphForDirectiveLabel != null &&
+          paragraphForDirectiveLabel.children.length > 0
+            ? (paragraphForDirectiveLabel.children[0] as Text).value
+            : undefined;
         // remove directive label from children
         // remove directive label from children
         if (paragraphForDirectiveLabel != null) {
         if (paragraphForDirectiveLabel != null) {
-          node.children.splice(node.children.indexOf(paragraphForDirectiveLabel), 1);
+          node.children.splice(
+            node.children.indexOf(paragraphForDirectiveLabel),
+            1,
+          );
         }
         }
 
 
         data.hName = 'callout';
         data.hName = 'callout';
@@ -28,7 +40,6 @@ export const remarkPlugin: Plugin = () => {
           type,
           type,
           label,
           label,
         };
         };
-
       }
       }
     });
     });
   };
   };

+ 10 - 2
apps/app/src/features/callout/services/consts.ts

@@ -1,5 +1,13 @@
 // Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/themes/github/index.js
 // Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/themes/github/index.js
 // Ref: https://github.com/orgs/community/discussions/16925
 // Ref: https://github.com/orgs/community/discussions/16925
 
 
-export const AllCallout = ['note', 'tip', 'important', 'info', 'warning', 'danger', 'caution'] as const;
-export type Callout = typeof AllCallout[number];
+export const AllCallout = [
+  'note',
+  'tip',
+  'important',
+  'info',
+  'warning',
+  'danger',
+  'caution',
+] as const;
+export type Callout = (typeof AllCallout)[number];

+ 1 - 1
apps/app/src/features/callout/services/index.ts

@@ -1 +1 @@
-export { sanitizeOption, remarkPlugin } from './callout';
+export { remarkPlugin, sanitizeOption } from './callout';

+ 1 - 1
apps/app/src/features/comment/server/events/consts.ts

@@ -3,4 +3,4 @@ export const CommentEvent = {
   UPDATE: 'update',
   UPDATE: 'update',
   DELETE: 'delete',
   DELETE: 'delete',
 } as const;
 } as const;
-export type CommentEvent = typeof CommentEvent[keyof typeof CommentEvent];
+export type CommentEvent = (typeof CommentEvent)[keyof typeof CommentEvent];

+ 48 - 43
apps/app/src/features/comment/server/models/comment.ts

@@ -1,7 +1,5 @@
 import type { IUser } from '@growi/core/dist/interfaces';
 import type { IUser } from '@growi/core/dist/interfaces';
-import type {
-  Types, Document, Model, Query,
-} from 'mongoose';
+import type { Document, Model, Query, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import { Schema } from 'mongoose';
 
 
 import type { IComment } from '~/interfaces/comment';
 import type { IComment } from '~/interfaces/comment';
@@ -11,11 +9,10 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:models:comment');
 const logger = loggerFactory('growi:models:comment');
 
 
 export interface CommentDocument extends IComment, Document {
 export interface CommentDocument extends IComment, Document {
-  removeWithReplies: () => Promise<void>
-  findCreatorsByPage: (pageId: Types.ObjectId) => Promise<CommentDocument[]>
+  removeWithReplies: () => Promise<void>;
+  findCreatorsByPage: (pageId: Types.ObjectId) => Promise<CommentDocument[]>;
 }
 }
 
 
-
 type Add = (
 type Add = (
   pageId: Types.ObjectId,
   pageId: Types.ObjectId,
   creatorId: Types.ObjectId,
   creatorId: Types.ObjectId,
@@ -24,38 +21,45 @@ type Add = (
   commentPosition: number,
   commentPosition: number,
   replyTo?: Types.ObjectId | null,
   replyTo?: Types.ObjectId | null,
 ) => Promise<CommentDocument>;
 ) => Promise<CommentDocument>;
-type FindCommentsByPageId = (pageId: Types.ObjectId) => Query<CommentDocument[], CommentDocument>;
-type FindCommentsByRevisionId = (revisionId: Types.ObjectId) => Query<CommentDocument[], CommentDocument>;
-type FindCreatorsByPage = (pageId: Types.ObjectId) => Promise<IUser[]>
-type CountCommentByPageId = (pageId: Types.ObjectId) => Promise<number>
+type FindCommentsByPageId = (
+  pageId: Types.ObjectId,
+) => Query<CommentDocument[], CommentDocument>;
+type FindCommentsByRevisionId = (
+  revisionId: Types.ObjectId,
+) => Query<CommentDocument[], CommentDocument>;
+type FindCreatorsByPage = (pageId: Types.ObjectId) => Promise<IUser[]>;
+type CountCommentByPageId = (pageId: Types.ObjectId) => Promise<number>;
 
 
 export interface CommentModel extends Model<CommentDocument> {
 export interface CommentModel extends Model<CommentDocument> {
-  add: Add
-  findCommentsByPageId: FindCommentsByPageId
-  findCommentsByRevisionId: FindCommentsByRevisionId
-  findCreatorsByPage: FindCreatorsByPage
-  countCommentByPageId: CountCommentByPageId
+  add: Add;
+  findCommentsByPageId: FindCommentsByPageId;
+  findCommentsByRevisionId: FindCommentsByRevisionId;
+  findCreatorsByPage: FindCreatorsByPage;
+  countCommentByPageId: CountCommentByPageId;
 }
 }
 
 
-const commentSchema = new Schema<CommentDocument, CommentModel>({
-  page: { type: Schema.Types.ObjectId, ref: 'Page', index: true },
-  creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
-  revision: { type: Schema.Types.ObjectId, ref: 'Revision', index: true },
-  comment: { type: String, required: true },
-  commentPosition: { type: Number, default: -1 },
-  replyTo: { type: Schema.Types.ObjectId },
-}, {
-  timestamps: true,
-});
+const commentSchema = new Schema<CommentDocument, CommentModel>(
+  {
+    page: { type: Schema.Types.ObjectId, ref: 'Page', index: true },
+    creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
+    revision: { type: Schema.Types.ObjectId, ref: 'Revision', index: true },
+    comment: { type: String, required: true },
+    commentPosition: { type: Number, default: -1 },
+    replyTo: { type: Schema.Types.ObjectId },
+  },
+  {
+    timestamps: true,
+  },
+);
 
 
-const add: Add = async function(
-    this: CommentModel,
-    pageId,
-    creatorId,
-    revisionId,
-    comment,
-    commentPosition,
-    replyTo?,
+const add: Add = async function (
+  this: CommentModel,
+  pageId,
+  creatorId,
+  revisionId,
+  comment,
+  commentPosition,
+  replyTo?,
 ): Promise<CommentDocument> {
 ): Promise<CommentDocument> {
   try {
   try {
     const data = await this.create({
     const data = await this.create({
@@ -69,35 +73,36 @@ const add: Add = async function(
     logger.debug('Comment saved.', data);
     logger.debug('Comment saved.', data);
 
 
     return data;
     return data;
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug('Error on saving comment.', err);
     logger.debug('Error on saving comment.', err);
     throw err;
     throw err;
   }
   }
 };
 };
 commentSchema.statics.add = add;
 commentSchema.statics.add = add;
 
 
-commentSchema.statics.findCommentsByPageId = function(id) {
+commentSchema.statics.findCommentsByPageId = function (id) {
   return this.find({ page: id }).sort({ createdAt: -1 });
   return this.find({ page: id }).sort({ createdAt: -1 });
 };
 };
 
 
-commentSchema.statics.findCommentsByRevisionId = function(id) {
+commentSchema.statics.findCommentsByRevisionId = function (id) {
   return this.find({ revision: id }).sort({ createdAt: -1 });
   return this.find({ revision: id }).sort({ createdAt: -1 });
 };
 };
 
 
-commentSchema.statics.findCreatorsByPage = async function(page) {
+commentSchema.statics.findCreatorsByPage = async function (page) {
   return this.distinct('creator', { page }).exec();
   return this.distinct('creator', { page }).exec();
 };
 };
 
 
-commentSchema.statics.countCommentByPageId = async function(page) {
+commentSchema.statics.countCommentByPageId = async function (page) {
   return this.count({ page });
   return this.count({ page });
 };
 };
 
 
-commentSchema.statics.removeWithReplies = async function(comment) {
+commentSchema.statics.removeWithReplies = async function (comment) {
   await this.deleteMany({
   await this.deleteMany({
-    $or:
-      [{ replyTo: comment._id }, { _id: comment._id }],
+    $or: [{ replyTo: comment._id }, { _id: comment._id }],
   });
   });
 };
 };
 
 
-export const Comment = getOrCreateModel<CommentDocument, CommentModel>('Comment', commentSchema);
+export const Comment = getOrCreateModel<CommentDocument, CommentModel>(
+  'Comment',
+  commentSchema,
+);

+ 89 - 70
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -1,14 +1,15 @@
-import path from 'path';
-
 import { GrowiPluginType } from '@growi/core';
 import { GrowiPluginType } from '@growi/core';
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
 import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
-import { scanAllTemplates, getMarkdown } from '@growi/pluginkit/dist/v4/server/index.cjs';
+import {
+  getMarkdown,
+  scanAllTemplates,
+} from '@growi/pluginkit/dist/v4/server/index.cjs';
 import express from 'express';
 import express from 'express';
 import { param, query } from 'express-validator';
 import { param, query } from 'express-validator';
-
+import path from 'path';
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
@@ -21,22 +22,17 @@ const logger = loggerFactory('growi:routes:apiv3:templates');
 const router = express.Router();
 const router = express.Router();
 
 
 const validator = {
 const validator = {
-  list: [
-    query('includeInvalidTemplates').optional().isBoolean(),
-  ],
-  get: [
-    param('templateId').isString(),
-    param('locale').isString(),
-  ],
+  list: [query('includeInvalidTemplates').optional().isBoolean()],
+  get: [param('templateId').isString(), param('locale').isString()],
 };
 };
 
 
-
 // cache object
 // cache object
 let presetTemplateSummaries: TemplateSummary[];
 let presetTemplateSummaries: TemplateSummary[];
 
 
-
 module.exports = (crowi: Crowi) => {
 module.exports = (crowi: Crowi) => {
-  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(
+    crowi,
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -79,41 +75,52 @@ module.exports = (crowi: Crowi) => {
    *                       title:
    *                       title:
    *                         type: string
    *                         type: string
    */
    */
-  router.get('/', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
-    const { includeInvalidTemplates } = req.query;
-
-    // scan preset templates
-    if (presetTemplateSummaries == null) {
-      const presetTemplatesRoot = resolveFromRoot('node_modules/@growi/preset-templates');
+  router.get(
+    '/',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    validator.list,
+    apiV3FormValidator,
+    async (req, res: ApiV3Response) => {
+      const { includeInvalidTemplates } = req.query;
+
+      // scan preset templates
+      if (presetTemplateSummaries == null) {
+        const presetTemplatesRoot = resolveFromRoot(
+          'node_modules/@growi/preset-templates',
+        );
+
+        try {
+          presetTemplateSummaries = await scanAllTemplates(
+            presetTemplatesRoot,
+            {
+              returnsInvalidTemplates: includeInvalidTemplates,
+            },
+          );
+        } catch (err) {
+          logger.error(err);
+          presetTemplateSummaries = [];
+        }
+      }
 
 
+      // load plugin templates
+      let pluginsTemplateSummaries: TemplateSummary[] = [];
       try {
       try {
-        presetTemplateSummaries = await scanAllTemplates(presetTemplatesRoot, {
-          returnsInvalidTemplates: includeInvalidTemplates,
-        });
-      }
-      catch (err) {
+        const plugins = await GrowiPlugin.findEnabledPluginsByType(
+          GrowiPluginType.Template,
+        );
+        pluginsTemplateSummaries = plugins.flatMap(
+          (p) => p.meta.templateSummaries,
+        );
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        presetTemplateSummaries = [];
       }
       }
-    }
-
-    // load plugin templates
-    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,
-        ...pluginsTemplateSummaries,
-      ],
-    });
-  });
+
+      return res.apiv3({
+        summaries: [...presetTemplateSummaries, ...pluginsTemplateSummaries],
+      });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -149,23 +156,31 @@ module.exports = (crowi: Crowi) => {
    *                 markdown:
    *                 markdown:
    *                   type: string
    *                   type: string
    */
    */
-  router.get('/preset-templates/:templateId/:locale', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
-    validator.get, apiV3FormValidator,
-    async(req, res: ApiV3Response) => {
-      const {
-        templateId, locale,
-      } = req.params;
-
-      const presetTemplatesRoot = resolveFromRoot('node_modules/@growi/preset-templates');
+  router.get(
+    '/preset-templates/:templateId/:locale',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    validator.get,
+    apiV3FormValidator,
+    async (req, res: ApiV3Response) => {
+      const { templateId, locale } = req.params;
+
+      const presetTemplatesRoot = resolveFromRoot(
+        'node_modules/@growi/preset-templates',
+      );
 
 
       try {
       try {
-        const markdown = await getMarkdown(presetTemplatesRoot, templateId, locale);
+        const markdown = await getMarkdown(
+          presetTemplatesRoot,
+          templateId,
+          locale,
+        );
         return res.apiv3({ markdown });
         return res.apiv3({ markdown });
-      }
-      catch (err) {
+      } catch (err) {
         res.apiv3Err(err);
         res.apiv3Err(err);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -213,24 +228,28 @@ module.exports = (crowi: Crowi) => {
    *                 markdown:
    *                 markdown:
    *                   type: string
    *                   type: string
    */
    */
-  router.get('/plugin-templates/:organizationId/:reposId/:templateId/:locale', accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
-    loginRequiredStrictly, validator.get, apiV3FormValidator, async(
-        req, res: ApiV3Response,
-    ) => {
-      const {
-        organizationId, reposId, templateId, locale,
-      } = req.params;
-
-      const pluginRoot = path.join(PLUGIN_STORING_PATH, `${organizationId}/${reposId}`);
+  router.get(
+    '/plugin-templates/:organizationId/:reposId/:templateId/:locale',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    validator.get,
+    apiV3FormValidator,
+    async (req, res: ApiV3Response) => {
+      const { organizationId, reposId, templateId, locale } = req.params;
+
+      const pluginRoot = path.join(
+        PLUGIN_STORING_PATH,
+        `${organizationId}/${reposId}`,
+      );
 
 
       try {
       try {
         const markdown = await getMarkdown(pluginRoot, templateId, locale);
         const markdown = await getMarkdown(pluginRoot, templateId, locale);
         return res.apiv3({ markdown });
         return res.apiv3({ markdown });
-      }
-      catch (err) {
+      } catch (err) {
         res.apiv3Err(err);
         res.apiv3Err(err);
       }
       }
-    });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 14 - 6
apps/app/src/features/templates/stores/template.tsx

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

+ 9 - 1
biome.json

@@ -27,7 +27,15 @@
       "!apps/app/public/**",
       "!apps/app/public/**",
       "!apps/app/src/client/**",
       "!apps/app/src/client/**",
       "!apps/app/src/components/**",
       "!apps/app/src/components/**",
-      "!apps/app/src/features/**",
+      "!apps/app/src/features/external-user-group/**",
+      "!apps/app/src/features/growi-plugin/**",
+      "!apps/app/src/features/mermaid/**",
+      "!apps/app/src/features/openai/**",
+      "!apps/app/src/features/opentelemetry/**",
+      "!apps/app/src/features/page-bulk-export/**",
+      "!apps/app/src/features/plantuml/**",
+      "!apps/app/src/features/rate-limiter/**",
+      "!apps/app/src/features/search/**",
       "!apps/app/src/interfaces/**",
       "!apps/app/src/interfaces/**",
       "!apps/app/src/models/**",
       "!apps/app/src/models/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/pages/**",