diff --git a/apps/app/config/i18next.config.js b/apps/app/config/i18next.config.js
new file mode 100644
index 00000000000..6f7d65c306a
--- /dev/null
+++ b/apps/app/config/i18next.config.js
@@ -0,0 +1,16 @@
+const { Lang, AllLang } = require('@growi/core');
+
+/** @type {Lang} */
+const defaultLang = Lang.en_US;
+
+/** @type {import('i18next').InitOptions} */
+const initOptions = {
+ fallbackLng: defaultLang.toString(),
+ supportedLngs: AllLang,
+ defaultNS: 'translation',
+};
+
+module.exports = {
+ defaultLang,
+ initOptions,
+};
diff --git a/apps/app/config/next-i18next.config.js b/apps/app/config/next-i18next.config.js
index eef6aac4941..8ae662f7eaa 100644
--- a/apps/app/config/next-i18next.config.js
+++ b/apps/app/config/next-i18next.config.js
@@ -2,31 +2,41 @@ const isDev = process.env.NODE_ENV === 'development';
const path = require('path');
-const { AllLang, Lang } = require('@growi/core');
+const { AllLang } = require('@growi/core');
const { isServer } = require('@growi/core/dist/utils');
-const I18nextChainedBackend = isDev ? require('i18next-chained-backend').default : undefined;
-const I18NextHttpBackend = require('i18next-http-backend').default;
-const I18NextLocalStorageBackend = require('i18next-localstorage-backend').default;
+
+const { defaultLang } = require('./i18next.config');
const HMRPlugin = isDev ? require('i18next-hmr/plugin').HMRPlugin : undefined;
+/** @type {import('next-i18next').UserConfig} */
module.exports = {
- defaultLang: Lang.en_US,
+ ...require('./i18next.config').initOptions,
+
i18n: {
- defaultLocale: Lang.en_US,
+ defaultLocale: defaultLang.toString(),
locales: AllLang,
},
- defaultNS: 'translation',
+
localePath: path.resolve('./public/static/locales'),
serializeConfig: false,
+
// eslint-disable-next-line no-nested-ternary
use: isDev
? isServer()
? [new HMRPlugin({ webpack: { server: true } })]
- : [I18nextChainedBackend, new HMRPlugin({ webpack: { client: true } })]
+ : [
+ require('i18next-chained-backend').default,
+ new HMRPlugin({ webpack: { client: true } }),
+ ]
: [],
backend: {
- backends: isServer() ? [] : [I18NextLocalStorageBackend, I18NextHttpBackend],
+ backends: isServer()
+ ? []
+ : [
+ require('i18next-localstorage-backend').default,
+ require('i18next-http-backend').default,
+ ],
backendOptions: [
// options for i18next-localstorage-backend
{ expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
@@ -34,4 +44,5 @@ module.exports = {
{ loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
],
},
+
};
diff --git a/apps/app/package.json b/apps/app/package.json
index ab74d0ab635..caa2da2cfcb 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@growi/app",
- "version": "7.0.3",
+ "version": "7.0.4-RC.0",
"license": "MIT",
"scripts": {
"//// for production": "",
@@ -108,7 +108,7 @@
"detect-indent": "^7.0.0",
"diff": "^5.0.0",
"diff_match_patch": "^0.1.1",
- "ejs": "^3.1.8",
+ "ejs": "^3.1.10",
"esa-node": "^0.2.2",
"escape-string-regexp": "^4.0.0",
"eslint-plugin-regex": "^1.8.0",
@@ -125,9 +125,7 @@
"helmet": "^4.6.0",
"http-errors": "^2.0.0",
"i18next": "^23.10.1",
- "i18next-chained-backend": "^4.6.2",
- "i18next-http-backend": "^2.5.0",
- "i18next-localstorage-backend": "^4.2.0",
+ "i18next-resources-to-backend": "^1.2.1",
"is-absolute-url": "^4.0.1",
"is-iso-date": "^0.0.1",
"ldapjs": "^3.0.2",
@@ -233,6 +231,7 @@
"@testing-library/user-event": "^14.5.2",
"@types/express": "^4.17.11",
"@types/jest": "^29.5.2",
+ "@types/react-input-autosize": "^2.2.4",
"@types/react-scroll": "^1.8.4",
"@types/react-stickynode": "^4.0.3",
"@types/throttle-debounce": "^5.0.1",
@@ -244,6 +243,7 @@
"babel-loader": "^8.2.5",
"bootstrap": "^5.3.1",
"connect-browser-sync": "^2.1.0",
+ "cypress-real-events": "^1.12.0",
"diff2html": "^3.4.47",
"downshift": "^8.2.3",
"eazy-logger": "^3.1.0",
@@ -253,7 +253,10 @@
"fslightbox-react": "^1.7.6",
"handsontable": "=6.2.2",
"happy-dom": "^13.2.0",
+ "i18next-chained-backend": "^4.6.2",
"i18next-hmr": "^3.0.4",
+ "i18next-http-backend": "^2.5.0",
+ "i18next-localstorage-backend": "^4.2.0",
"jest": "^29.5.0",
"jest-date-mock": "^1.0.8",
"jest-localstorage-mock": "^2.4.14",
diff --git a/apps/app/public/static/locales/en_US/commons.json b/apps/app/public/static/locales/en_US/commons.json
index 29a9623ae14..eb6c0c50080 100644
--- a/apps/app/public/static/locales/en_US/commons.json
+++ b/apps/app/public/static/locales/en_US/commons.json
@@ -77,6 +77,7 @@
"create_page_dropdown": {
"new_page": "Create New Page",
+ "open_page_create_modal": "Open new page create modal",
"todays": {
"desc": "Create today's memo",
"memo": "memo"
diff --git a/apps/app/public/static/locales/en_US/translation.json b/apps/app/public/static/locales/en_US/translation.json
index 6ee63b6bf99..5437e2da66c 100644
--- a/apps/app/public/static/locales/en_US/translation.json
+++ b/apps/app/public/static/locales/en_US/translation.json
@@ -160,16 +160,20 @@
"not_allowed_to_see_this_page": "You cannot see this page",
"Confirm": "Confirm",
"Successfully requested": "Successfully requested.",
- "form_validation": {
- "error_message": "Some values are incorrect",
- "required": "%s is required",
- "invalid_syntax": "The syntax of %s is invalid.",
- "title_required": "Title is required.",
- "field_required": "{{target}} is required"
- },
- "page_name": "Page name",
- "folder_name": "Folder name",
- "field": "field",
+ "input_validation": {
+ "target": {
+ "page_name": "Page name",
+ "folder_name": "Folder name",
+ "field": "field"
+ },
+ "message": {
+ "error_message": "Some values are incorrect",
+ "required": "%s is required",
+ "invalid_syntax": "The syntax of %s is invalid.",
+ "title_required": "Title is required.",
+ "field_required": "{{target}} is required"
+ }
+ },
"not_creatable_page": {
"message": "Page contents cannot be created in this path."
},
@@ -863,5 +867,8 @@
"show_wip_page": "Show WIP",
"size_s": "Size: S",
"size_l": "Size: L"
+ },
+ "create_page": {
+ "untitled": "Untitled"
}
}
diff --git a/apps/app/public/static/locales/fr_FR/commons.json b/apps/app/public/static/locales/fr_FR/commons.json
index 5765424e1c0..3771c037ee5 100644
--- a/apps/app/public/static/locales/fr_FR/commons.json
+++ b/apps/app/public/static/locales/fr_FR/commons.json
@@ -77,6 +77,7 @@
"create_page_dropdown": {
"new_page": "Créer nouvelle page",
+ "open_page_create_modal": "Ouvrir une nouvelle page créer une fenêtre modale",
"todays": {
"desc": "Créer le mémo du jour",
"memo": "mémo"
diff --git a/apps/app/public/static/locales/fr_FR/translation.json b/apps/app/public/static/locales/fr_FR/translation.json
index 3d3e5011141..0c1c6ff4c6d 100644
--- a/apps/app/public/static/locales/fr_FR/translation.json
+++ b/apps/app/public/static/locales/fr_FR/translation.json
@@ -160,16 +160,20 @@
"not_allowed_to_see_this_page": "Vous ne pouvez pas voir cette page",
"Confirm": "Confirmer",
"Successfully requested": "Demande envoyée.",
- "form_validation": {
- "error_message": "Des champs sont invalides",
- "required": "%s est requis",
- "invalid_syntax": "La syntaxe de %s est invalide.",
- "title_required": "Titre requis.",
- "field_required": "{{target}} est requis"
- },
- "page_name": "Nom de la page",
- "folder_name": "Nom du dossier",
- "field": "champ",
+ "input_validation": {
+ "target": {
+ "page_name": "Nom de la page",
+ "folder_name": "Nom du dossier",
+ "field": "champ"
+ },
+ "message": {
+ "error_message": "Des champs sont invalides",
+ "required": "%s est requis",
+ "invalid_syntax": "La syntaxe de %s est invalide.",
+ "title_required": "Titre requis.",
+ "field_required": "{{target}} est requis"
+ }
+ },
"not_creatable_page": {
"message": "Vous ne pouvez pas créer cette page dans ce chemin."
},
diff --git a/apps/app/public/static/locales/ja_JP/commons.json b/apps/app/public/static/locales/ja_JP/commons.json
index 87103b76983..f85a86db4f9 100644
--- a/apps/app/public/static/locales/ja_JP/commons.json
+++ b/apps/app/public/static/locales/ja_JP/commons.json
@@ -79,6 +79,7 @@
"create_page_dropdown": {
"new_page": "新規ページ作成",
+ "open_page_create_modal": "新規ページ作成モーダルを表示",
"todays": {
"desc": "今日のメモを作成",
"memo": "メモ"
diff --git a/apps/app/public/static/locales/ja_JP/translation.json b/apps/app/public/static/locales/ja_JP/translation.json
index 61a0c2c0c8b..50e7489e884 100644
--- a/apps/app/public/static/locales/ja_JP/translation.json
+++ b/apps/app/public/static/locales/ja_JP/translation.json
@@ -161,16 +161,20 @@
"not_allowed_to_see_this_page": "このページは閲覧できません",
"Confirm": "確認",
"Successfully requested": "正常に処理を受け付けました",
- "form_validation": {
- "error_message": "いくつかの値が設定されていません",
- "required": "%sに値を入力してください",
- "invalid_syntax": "%sの構文が不正です",
- "title_required": "タイトルを入力してください",
- "field_required": "{{target}}に値を入力してください"
- },
- "page_name": "ページ名",
- "folder_name": "フォルダ名",
- "field": "フィールド",
+ "input_validation": {
+ "target": {
+ "page_name": "ページ名",
+ "folder_name": "フォルダ名",
+ "field": "フィールド"
+ },
+ "message": {
+ "error_message": "いくつかの値が設定されていません",
+ "required": "%sに値を入力してください",
+ "invalid_syntax": "%sの構文が不正です",
+ "title_required": "タイトルを入力してください",
+ "field_required": "{{target}}に値を入力してください"
+ }
+ },
"not_creatable_page": {
"message": "このパスではページ コンテンツを作成できません。"
},
@@ -896,5 +900,8 @@
"show_wip_page": "WIP を表示",
"size_s": "サイズ: S",
"size_l": "サイズ: L"
+ },
+ "create_page": {
+ "untitled": "無題のページ"
}
}
diff --git a/apps/app/public/static/locales/zh_CN/commons.json b/apps/app/public/static/locales/zh_CN/commons.json
index ffe0264f011..9108400f2d3 100644
--- a/apps/app/public/static/locales/zh_CN/commons.json
+++ b/apps/app/public/static/locales/zh_CN/commons.json
@@ -80,6 +80,7 @@
"create_page_dropdown": {
"new_page": "新页面",
+ "open_page_create_modal": "打开新页面创建模式",
"todays": {
"desc": "Create today's memo",
"memo": "memo"
diff --git a/apps/app/public/static/locales/zh_CN/translation.json b/apps/app/public/static/locales/zh_CN/translation.json
index 7de4d7c54fa..fd91d55a90b 100644
--- a/apps/app/public/static/locales/zh_CN/translation.json
+++ b/apps/app/public/static/locales/zh_CN/translation.json
@@ -167,16 +167,20 @@
"Confirm": "确定",
"Successfully requested": "进程成功接受",
"copied_to_clipboard": "它已复制到剪贴板。",
- "form_validation": {
- "error_message": "有些值不正确",
- "required": "%s 是必需的",
- "invalid_syntax": "%s的语法无效。",
- "title_required": "标题是必需的。",
- "field_required": "{{target}} 是必需的"
- },
- "page_name": "页面名称",
- "folder_name": "文件夹名称",
- "field": "字段",
+ "input_validation": {
+ "target": {
+ "page_name": "页面名称",
+ "folder_name": "文件夹名称",
+ "field": "字段"
+ },
+ "message": {
+ "error_message": "有些值不正确",
+ "required": "%s 是必需的",
+ "invalid_syntax": "%s的语法无效。",
+ "title_required": "标题是必需的。",
+ "field_required": "{{target}} 是必需的"
+ }
+ },
"not_creatable_page": {
"message": "无法在此路径中创建页面内容。"
},
@@ -866,5 +870,8 @@
"show_wip_page": "显示 WIP",
"size_s": "尺寸: S",
"size_l": "尺寸: L"
+ },
+ "create_page": {
+ "untitled": "Untitled"
}
}
diff --git a/apps/app/src/client/services/renderer/renderer.tsx b/apps/app/src/client/services/renderer/renderer.tsx
index 4b14867830e..847cf663fef 100644
--- a/apps/app/src/client/services/renderer/renderer.tsx
+++ b/apps/app/src/client/services/renderer/renderer.tsx
@@ -1,7 +1,6 @@
import assert from 'assert';
import { isClient } from '@growi/core/dist/utils/browser-utils';
-import * as slides from '@growi/presentation';
import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
import * as drawio from '@growi/remark-drawio';
// eslint-disable-next-line import/extensions
@@ -19,7 +18,6 @@ import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents
import { Header } from '~/components/ReactMarkdownComponents/Header';
import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
import * as mermaid from '~/features/mermaid';
import { RehypeSanitizeOption } from '~/interfaces/rehype';
@@ -68,7 +66,6 @@ export const generateViewOptions = (
attachment.remarkPlugin,
lsxGrowiDirective.remarkPlugin,
refsGrowiDirective.remarkPlugin,
- [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
);
if (config.isEnabledLinebreaks) {
remarkPlugins.push(breaks);
@@ -84,7 +81,6 @@ export const generateViewOptions = (
drawio.sanitizeOption,
mermaid.sanitizeOption,
attachment.sanitizeOption,
- slides.sanitizeOption,
lsxGrowiDirective.sanitizeOption,
refsGrowiDirective.sanitizeOption,
)]
@@ -119,7 +115,6 @@ export const generateViewOptions = (
components.mermaid = mermaid.MermaidViewer;
components.attachment = RichAttachment;
components.img = LightBox;
- components.slide = SlideViewer;
}
if (config.isEnabledXssPrevention) {
@@ -241,6 +236,21 @@ export const generatePresentationViewOptions = (
// based on simple view options
const options = generateSimpleViewOptions(config, pagePath);
+ const { rehypePlugins } = options;
+
+
+ const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
+ ? [sanitize, deepmerge(
+ addLineNumberAttribute.sanitizeOption,
+ )]
+ : () => {};
+
+ // add rehype plugins
+ rehypePlugins.push(
+ addLineNumberAttribute.rehypePlugin,
+ rehypeSanitizePlugin,
+ );
+
if (config.isEnabledXssPrevention) {
verifySanitizePlugin(options, false);
}
@@ -262,7 +272,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
attachment.remarkPlugin,
lsxGrowiDirective.remarkPlugin,
refsGrowiDirective.remarkPlugin,
- [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
);
if (config.isEnabledLinebreaks) {
remarkPlugins.push(breaks);
@@ -281,7 +290,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
lsxGrowiDirective.sanitizeOption,
refsGrowiDirective.sanitizeOption,
addLineNumberAttribute.sanitizeOption,
- slides.sanitizeOption,
)]
: () => {};
@@ -306,7 +314,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
components.mermaid = mermaid.MermaidViewer;
components.attachment = RichAttachment;
components.img = LightBox;
- components.slide = SlideViewer;
}
if (config.isEnabledXssPrevention) {
diff --git a/apps/app/src/client/services/renderer/slide-viewer-renderer.tsx b/apps/app/src/client/services/renderer/slide-viewer-renderer.tsx
deleted file mode 100644
index f0792ea7b7b..00000000000
--- a/apps/app/src/client/services/renderer/slide-viewer-renderer.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
-import * as drawio from '@growi/remark-drawio';
-// eslint-disable-next-line import/extensions
-import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
-import katex from 'rehype-katex';
-import sanitize from 'rehype-sanitize';
-import math from 'remark-math';
-import deepmerge from 'ts-deepmerge';
-import type { Pluggable } from 'unified';
-
-import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
-import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import * as mermaid from '~/features/mermaid';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import type { RendererOptions } from '~/interfaces/renderer-options';
-import type { RendererConfig } from '~/interfaces/services/renderer';
-import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
-import * as attachment from '~/services/renderer/remark-plugins/attachment';
-import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
-import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
-import {
- commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
-} from '~/services/renderer/renderer';
-
-
-export const generatePresentationViewOptions = (
- config: RendererConfig,
- pagePath: string,
-): RendererOptions => {
- const options = generateCommonOptions(pagePath);
-
- const { remarkPlugins, rehypePlugins, components } = options;
-
- // add remark plugins
- remarkPlugins.push(
- math,
- [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
- drawio.remarkPlugin,
- mermaid.remarkPlugin,
- xsvToTable.remarkPlugin,
- attachment.remarkPlugin,
- lsxGrowiDirective.remarkPlugin,
- refsGrowiDirective.remarkPlugin,
- );
-
- if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
- injectCustomSanitizeOption(config);
- }
-
-
- const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
- ? [sanitize, deepmerge(
- commonSanitizeOption,
- drawio.sanitizeOption,
- mermaid.sanitizeOption,
- attachment.sanitizeOption,
- lsxGrowiDirective.sanitizeOption,
- refsGrowiDirective.sanitizeOption,
- addLineNumberAttribute.sanitizeOption,
- )]
- : () => {};
-
- // add rehype plugins
- rehypePlugins.push(
- [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
- [refsGrowiDirective.rehypePlugin, { pagePath }],
- rehypeSanitizePlugin,
- addLineNumberAttribute.rehypePlugin,
- katex,
- );
-
- // add components
- if (components != null) {
- components.lsx = lsxGrowiDirective.LsxImmutable;
- components.ref = refsGrowiDirective.RefImmutable;
- components.refs = refsGrowiDirective.RefsImmutable;
- components.refimg = refsGrowiDirective.RefImgImmutable;
- components.refsimg = refsGrowiDirective.RefsImgImmutable;
- components.gallery = refsGrowiDirective.GalleryImmutable;
- components.drawio = drawio.DrawioViewer;
- components.mermaid = mermaid.MermaidViewer;
- components.attachment = RichAttachment;
- components.img = LightBox;
- }
-
- if (config.isEnabledXssPrevention) {
- verifySanitizePlugin(options, false);
- }
- return options;
-};
diff --git a/apps/app/src/client/services/side-effects/yjs.ts b/apps/app/src/client/services/side-effects/yjs.ts
new file mode 100644
index 00000000000..10c3409550a
--- /dev/null
+++ b/apps/app/src/client/services/side-effects/yjs.ts
@@ -0,0 +1,33 @@
+import { useCallback, useEffect } from 'react';
+
+import { useGlobalSocket } from '@growi/core/dist/swr';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageYjsData } from '~/stores/yjs';
+
+export const useCurrentPageYjsDataEffect = (): void => {
+ const { data: socket } = useGlobalSocket();
+ const { updateHasRevisionBodyDiff, updateAwarenessStateSize } = useCurrentPageYjsData();
+
+ const hasRevisionBodyDiffUpdateHandler = useCallback((hasRevisionBodyDiff: boolean) => {
+ updateHasRevisionBodyDiff(hasRevisionBodyDiff);
+ }, [updateHasRevisionBodyDiff]);
+
+ const awarenessStateSizeUpdateHandler = useCallback(((awarenessStateSize: number) => {
+ updateAwarenessStateSize(awarenessStateSize);
+ }), [updateAwarenessStateSize]);
+
+ useEffect(() => {
+
+ if (socket == null) { return }
+
+ socket.on(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
+ socket.on(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+
+ return () => {
+ socket.off(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
+ socket.off(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+ };
+
+ }, [socket, awarenessStateSizeUpdateHandler, hasRevisionBodyDiffUpdateHandler]);
+};
diff --git a/apps/app/src/client/util/input-validator.ts b/apps/app/src/client/util/input-validator.ts
deleted file mode 100644
index e914051fee3..00000000000
--- a/apps/app/src/client/util/input-validator.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export const AlertType = {
- WARNING: 'warning',
- ERROR: 'error',
-} as const;
-
-export type AlertType = typeof AlertType[keyof typeof AlertType];
-
-export const ValidationTarget = {
- FOLDER: 'folder_name',
- PAGE: 'page_name',
- DEFAULT: 'field',
-};
-
-export type ValidationTarget = typeof ValidationTarget[keyof typeof ValidationTarget];
-
-export type AlertInfo = {
- type?: AlertType
- message?: string,
- target?: string
-}
-
-export const inputValidator = async(title: string | null, target?: string): Promise => {
- const validationTarget = target || ValidationTarget.DEFAULT;
- if (title == null || title === '' || title.trim() === '') {
- return {
- type: AlertType.WARNING,
- message: 'form_validation.field_required',
- target: validationTarget,
- };
- }
- return null;
-};
diff --git a/apps/app/src/client/util/locale-utils.ts b/apps/app/src/client/util/locale-utils.ts
index f6d822c2a9f..1b741ce37d2 100644
--- a/apps/app/src/client/util/locale-utils.ts
+++ b/apps/app/src/client/util/locale-utils.ts
@@ -2,7 +2,7 @@ import type { IncomingHttpHeaders } from 'http';
import { Lang } from '@growi/core';
-import * as nextI18NextConfig from '^/config/next-i18next.config';
+import { defaultLang } from '^/config/i18next.config';
// https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
const DIAGRAMS_NET_LANG_MAP = {
@@ -31,7 +31,7 @@ const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key));
if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
}
- return nextI18NextConfig.defaultLang;
+ return defaultLang;
};
/**
@@ -44,7 +44,7 @@ export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeade
const acceptLanguages = headers['accept-language'];
if (acceptLanguages == null) {
- return nextI18NextConfig.defaultLang;
+ return defaultLang;
}
// 1. trim blank spaces.
diff --git a/apps/app/src/client/util/use-input-validator.ts b/apps/app/src/client/util/use-input-validator.ts
new file mode 100644
index 00000000000..cb9deb12ce7
--- /dev/null
+++ b/apps/app/src/client/util/use-input-validator.ts
@@ -0,0 +1,56 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+export const AlertType = {
+ WARNING: 'Warning',
+ ERROR: 'Error',
+} as const;
+
+export type AlertType = typeof AlertType[keyof typeof AlertType];
+
+export const ValidationTarget = {
+ FOLDER: 'folder_name',
+ PAGE: 'page_name',
+ DEFAULT: 'field',
+};
+
+export type ValidationTarget = typeof ValidationTarget[keyof typeof ValidationTarget];
+
+export type AlertInfo = {
+ type?: AlertType
+ message?: string,
+ target?: string
+}
+
+
+export type InputValidationResult = {
+ type: AlertType
+ typeLabel: string,
+ message: string,
+ target: string
+}
+
+export type InputValidator = (input?: string, alertType?: AlertType) => InputValidationResult | void;
+
+export const useInputValidator = (validationTarget: ValidationTarget = ValidationTarget.DEFAULT): InputValidator => {
+ const { t } = useTranslation();
+
+ const inputValidator: InputValidator = useCallback((input?, alertType = AlertType.WARNING) => {
+ if ((input ?? '').trim() === '') {
+ return {
+ target: validationTarget,
+ type: alertType,
+ typeLabel: t(alertType),
+ message: t(
+ 'input_validation.message.field_required',
+ { target: t(`input_validation.target.${validationTarget}`) },
+ ),
+ };
+ }
+
+ return;
+ }, [t, validationTarget]);
+
+ return inputValidator;
+};
diff --git a/apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx b/apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
index bbe02c5769c..7f9937e8acc 100644
--- a/apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
+++ b/apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
@@ -53,7 +53,7 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
target="_blank"
rel="noopener noreferrer"
- >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
+ >{`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
diff --git a/apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx b/apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
index 2a8da7cb050..67bfb4bbfc9 100644
--- a/apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
+++ b/apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
@@ -32,6 +32,7 @@ type BookmarkFolderItemProps = {
}
export const BookmarkFolderItem: FC = (props: BookmarkFolderItemProps) => {
+
const BASE_FOLDER_PADDING = 15;
const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
const {
@@ -257,12 +258,13 @@ export const BookmarkFolderItem: FC = (props: BookmarkF
{isRenameAction ? (
-
+
+
+
) : (
<>
@@ -302,13 +304,10 @@ export const BookmarkFolderItem: FC
= (props: BookmarkF
{isCreateAction && (
-
-
-
+
)}
{
renderChildFolder()
diff --git a/apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx b/apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx
index f3320a50b41..3094321d947 100644
--- a/apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx
+++ b/apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx
@@ -1,22 +1,68 @@
+import type { ChangeEvent } from 'react';
+import { useCallback, useRef, useState } from 'react';
+
+import { useRect } from '@growi/ui/dist/utils';
import { useTranslation } from 'next-i18next';
+import type { AutosizeInputProps } from 'react-input-autosize';
+import { debounce } from 'throttle-debounce';
+
+import type { InputValidationResult } from '~/client/util/use-input-validator';
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
-import { ValidationTarget } from '~/client/util/input-validator';
-import type { ClosableTextInputProps } from '~/components/Common/ClosableTextInput';
-import ClosableTextInput from '~/components/Common/ClosableTextInput';
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
+import type { SubmittableInputProps } from '../Common/SubmittableInput/types';
-type Props = ClosableTextInputProps;
+type Props = Pick, 'value' | 'onSubmit' | 'onCancel'>;
export const BookmarkFolderNameInput = (props: Props): JSX.Element => {
const { t } = useTranslation();
+ const { value, onSubmit, onCancel } = props;
+
+ const parentRef = useRef(null);
+ const [parentRect] = useRect(parentRef);
+
+ const [validationResult, setValidationResult] = useState();
+
+
+ const inputValidator = useInputValidator(ValidationTarget.FOLDER);
+
+ const changeHandler = useCallback(async(e: ChangeEvent) => {
+ const validationResult = inputValidator(e.target.value);
+ setValidationResult(validationResult ?? undefined);
+ }, [inputValidator]);
+ const changeHandlerDebounced = debounce(300, changeHandler);
+
+ const cancelHandler = useCallback(() => {
+ setValidationResult(undefined);
+ onCancel?.();
+ }, [onCancel]);
+
+ const isInvalid = validationResult != null;
+
+ const maxWidth = parentRect != null
+ ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'md', validationResult != null ? false : undefined)
+ : undefined;
+
return (
-
-
+
+ { isInvalid && (
+
+ {validationResult.message}
+
+ ) }
);
};
diff --git a/apps/app/src/components/Bookmarks/BookmarkItem.tsx b/apps/app/src/components/Bookmarks/BookmarkItem.tsx
index 040d834acf3..0d2a03a2804 100644
--- a/apps/app/src/components/Bookmarks/BookmarkItem.tsx
+++ b/apps/app/src/components/Bookmarks/BookmarkItem.tsx
@@ -12,17 +12,16 @@ import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
import { bookmark, unbookmark, unlink } from '~/client/services/page-operation';
import { addBookmarkToFolder, renamePage } from '~/client/util/bookmark-utils';
-import { ValidationTarget } from '~/client/util/input-validator';
import { toastError, toastSuccess } from '~/client/util/toastr';
import type { BookmarkFolderItems, DragItemDataType } from '~/interfaces/bookmark-info';
import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
import { usePutBackPageModal } from '~/stores/modal';
import { mutateAllPageInfo, useSWRMUTxCurrentPage, useSWRxPageInfo } from '~/stores/page';
-import ClosableTextInput from '../Common/ClosableTextInput';
import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
import { PageListItemS } from '../PageList/PageListItemS';
+import { BookmarkItemRenameInput } from './BookmarkItemRenameInput';
import { BookmarkMoveToRootBtn } from './BookmarkMoveToRootBtn';
import { DragAndDropWrapper } from './DragAndDropWrapper';
@@ -163,13 +162,10 @@ export const BookmarkItem = (props: Props): JSX.Element => {
>
{ isRenameInputShown
? (
- { setRenameInputShown(false) }}
- validationTarget={ValidationTarget.PAGE}
+ onSubmit={rename}
+ onCancel={() => { setRenameInputShown(false) }}
/>
)
: }
diff --git a/apps/app/src/components/Bookmarks/BookmarkItemRenameInput.tsx b/apps/app/src/components/Bookmarks/BookmarkItemRenameInput.tsx
new file mode 100644
index 00000000000..760ed67dde5
--- /dev/null
+++ b/apps/app/src/components/Bookmarks/BookmarkItemRenameInput.tsx
@@ -0,0 +1,68 @@
+import type { ChangeEvent } from 'react';
+import { useCallback, useRef, useState } from 'react';
+
+import { useRect } from '@growi/ui/dist/utils';
+import { useTranslation } from 'next-i18next';
+import type { AutosizeInputProps } from 'react-input-autosize';
+import { debounce } from 'throttle-debounce';
+
+import type { InputValidationResult } from '~/client/util/use-input-validator';
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
+
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
+import type { SubmittableInputProps } from '../Common/SubmittableInput/types';
+
+
+type Props = Pick, 'value' | 'onSubmit' | 'onCancel'>;
+
+export const BookmarkItemRenameInput = (props: Props): JSX.Element => {
+ const { t } = useTranslation();
+
+ const { value, onSubmit, onCancel } = props;
+
+ const parentRef = useRef(null);
+ const [parentRect] = useRect(parentRef);
+
+ const [validationResult, setValidationResult] = useState();
+
+
+ const inputValidator = useInputValidator(ValidationTarget.PAGE);
+
+ const changeHandler = useCallback(async(e: ChangeEvent) => {
+ const validationResult = inputValidator(e.target.value);
+ setValidationResult(validationResult ?? undefined);
+ }, [inputValidator]);
+ const changeHandlerDebounced = debounce(300, changeHandler);
+
+ const cancelHandler = useCallback(() => {
+ setValidationResult(undefined);
+ onCancel?.();
+ }, [onCancel]);
+
+ const isInvalid = validationResult != null;
+
+ const maxWidth = parentRect != null
+ ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'md', validationResult != null ? false : undefined)
+ : undefined;
+
+ return (
+
+
+ { isInvalid && (
+
+ {validationResult.message}
+
+ ) }
+
+ );
+};
diff --git a/apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx b/apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx
index f9968c26467..528fdebbbe6 100644
--- a/apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx
+++ b/apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx
@@ -13,7 +13,6 @@ export const BookmarkMoveToRootBtn: React.FC<{
onClickMoveToRootHandler(pageId)}
className="grw-page-control-dropdown-item"
- data-testid="add-remove-bookmark-btn"
>
bookmark
{t('bookmark_folder.move_to_root')}
diff --git a/apps/app/src/components/Common/ClosableTextInput.tsx b/apps/app/src/components/Common/ClosableTextInput.tsx
deleted file mode 100644
index 035f8c253ea..00000000000
--- a/apps/app/src/components/Common/ClosableTextInput.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import type { FC } from 'react';
-import React, {
- memo, useCallback, useEffect, useRef, useState,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import AutosizeInput from 'react-input-autosize';
-
-import type { AlertInfo } from '~/client/util/input-validator';
-import { AlertType, inputValidator } from '~/client/util/input-validator';
-
-export type ClosableTextInputProps = {
- value?: string
- placeholder?: string
- validationTarget?: string,
- useAutosizeInput?: boolean
- inputClassName?: string,
- onPressEnter?(inputText: string): void
- onPressEscape?(inputText: string): void
- onBlur?(inputText: string): void
- onChange?(inputText: string): void
-}
-
-const ClosableTextInput: FC = memo((props: ClosableTextInputProps) => {
- const { t } = useTranslation();
- const {
- validationTarget, onPressEnter, onPressEscape, onBlur, onChange,
- } = props;
-
- const inputRef = useRef(null);
- const [inputText, setInputText] = useState(props.value ?? '');
- const [currentAlertInfo, setAlertInfo] = useState(null);
- const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
- const [isComposing, setComposing] = useState(false);
-
-
- const createValidation = useCallback(async(inputText: string) => {
- const alertInfo = await inputValidator(inputText, validationTarget);
- if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
- alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
- }
- setAlertInfo(alertInfo);
- }, [t, validationTarget]);
-
- const changeHandler = useCallback(async(e: React.ChangeEvent) => {
- const inputText = e.target.value;
- createValidation(inputText);
- setInputText(inputText);
- setIsAbleToShowAlert(true);
-
- onChange?.(inputText);
- }, [createValidation, onChange]);
-
- const onFocusHandler = useCallback(async(e: React.ChangeEvent) => {
- const inputText = e.target.value;
- await createValidation(inputText);
- }, [createValidation]);
-
- const pressEnterHandler = useCallback(() => {
- if (currentAlertInfo == null) {
- onPressEnter?.(inputText.trim());
- }
- }, [currentAlertInfo, inputText, onPressEnter]);
-
- const onKeyDownHandler = useCallback((e) => {
- switch (e.key) {
- case 'Enter':
- // Do nothing when composing
- if (isComposing) {
- return;
- }
- pressEnterHandler();
- break;
- case 'Escape':
- if (isComposing) {
- return;
- }
- onPressEscape?.(inputText.trim());
- break;
- default:
- break;
- }
- }, [inputText, isComposing, pressEnterHandler, onPressEscape]);
-
- /*
- * Hide when click outside the ref
- */
- const onBlurHandler = useCallback(() => {
- onBlur?.(inputText.trim());
- }, [inputText, onBlur]);
-
- // didMount
- useEffect(() => {
- // autoFocus
- if (inputRef?.current == null) {
- return;
- }
- inputRef.current.focus();
- });
-
-
- const AlertInfo = () => {
- if (currentAlertInfo == null) {
- return <>>;
- }
-
- const alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
- const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
- const alertTextStyle = alertType === AlertType.ERROR ? 'text-danger' : 'text-warning';
- const translation = alertType === AlertType.ERROR ? 'Error' : 'Warning';
- return (
- {t(translation)}: {alertMessage}
- );
- };
-
- const inputProps = {
- 'data-testid': 'closable-text-input',
- value: inputText || '',
- ref: inputRef,
- type: 'text',
- placeholder: props.placeholder,
- name: 'input',
- onFocus: onFocusHandler,
- onChange: changeHandler,
- onKeyDown: onKeyDownHandler,
- onCompositionStart: () => setComposing(true),
- onCompositionEnd: () => setComposing(false),
- onBlur: onBlurHandler,
- };
-
- const inputClassName = `form-control ${props.inputClassName ?? ''}`;
-
- return (
-
- { props.useAutosizeInput
- ?
- :
- }
- {isAbleToShowAlert &&
}
-
- );
-});
-
-ClosableTextInput.displayName = 'ClosableTextInput';
-
-export default ClosableTextInput;
diff --git a/apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx b/apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx
index 6fe1066815b..2e14573379a 100644
--- a/apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx
+++ b/apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx
@@ -28,7 +28,7 @@ describe('PageItemControl.tsx', () => {
render();
// when
- const openPageMoveRenameModalButton = screen.getByTestId('open-page-move-rename-modal-btn');
+ const openPageMoveRenameModalButton = screen.getByTestId('rename-page-btn');
await waitFor(() => userEvent.click(openPageMoveRenameModalButton, { pointerEventsCheck: PointerEventsCheckLevel.Never }));
// then
diff --git a/apps/app/src/components/Common/Dropdown/PageItemControl.tsx b/apps/app/src/components/Common/Dropdown/PageItemControl.tsx
index 53acfad7dee..2b9a33cd8af 100644
--- a/apps/app/src/components/Common/Dropdown/PageItemControl.tsx
+++ b/apps/app/src/components/Common/Dropdown/PageItemControl.tsx
@@ -169,7 +169,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
bookmark
{ pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
@@ -180,7 +180,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
{ !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && !isReadOnlyUser && pageInfo.isMovable && (
redo
diff --git a/apps/app/src/components/Common/SubmittableInput/AutosizeSubmittableInput.tsx b/apps/app/src/components/Common/SubmittableInput/AutosizeSubmittableInput.tsx
new file mode 100644
index 00000000000..66587d47609
--- /dev/null
+++ b/apps/app/src/components/Common/SubmittableInput/AutosizeSubmittableInput.tsx
@@ -0,0 +1,30 @@
+import type {
+ ReactElement,
+} from 'react';
+
+import type { AutosizeInputProps } from 'react-input-autosize';
+import AutosizeInput from 'react-input-autosize';
+
+import type { SubmittableInputProps } from './types';
+import { useSubmittable } from './use-submittable';
+
+
+export const getAdjustedMaxWidthForAutosizeInput = (parentMaxWidth: number, size: 'sm' | 'md' | 'lg' = 'md', isValid?: boolean): number => {
+ // eslint-disable-next-line no-nested-ternary
+ const bsFormPaddingSize = size === 'sm' ? 8 : size === 'md' ? 12 : 16; // by bootstrap form
+ // eslint-disable-next-line no-nested-ternary
+ const bsValidationIconSize = size === 'sm' ? 25 : size === 'md' ? 24 : 26; // by bootstrap form validation
+
+ return parentMaxWidth
+ - bsFormPaddingSize * 2 // minus the padding (12px * 2) because AutosizeInput has "box-sizing: content-box;"
+ - (isValid === false ? bsValidationIconSize : 0); // minus the width for the exclamation icon
+};
+
+export const AutosizeSubmittableInput = (props: SubmittableInputProps): ReactElement => {
+
+ const submittableProps = useSubmittable(props);
+
+ return (
+
+ );
+};
diff --git a/apps/app/src/components/Common/SubmittableInput/SubmittableInput.tsx b/apps/app/src/components/Common/SubmittableInput/SubmittableInput.tsx
new file mode 100644
index 00000000000..f005fc79f26
--- /dev/null
+++ b/apps/app/src/components/Common/SubmittableInput/SubmittableInput.tsx
@@ -0,0 +1,23 @@
+import type {
+ ReactElement,
+} from 'react';
+
+import type { SubmittableInputProps } from './types';
+import { useSubmittable } from './use-submittable';
+
+
+export const SubmittableInput = (props: SubmittableInputProps): ReactElement => {
+ // // autoFocus
+ // useEffect(() => {
+ // if (inputRef?.current == null) {
+ // return;
+ // }
+ // inputRef.current.focus();
+ // });
+
+ const submittableProps = useSubmittable(props);
+
+ return (
+
+ );
+};
diff --git a/apps/app/src/components/Common/SubmittableInput/index.ts b/apps/app/src/components/Common/SubmittableInput/index.ts
new file mode 100644
index 00000000000..cf72105276b
--- /dev/null
+++ b/apps/app/src/components/Common/SubmittableInput/index.ts
@@ -0,0 +1,2 @@
+export * from './SubmittableInput';
+export * from './AutosizeSubmittableInput';
diff --git a/apps/app/src/components/Common/SubmittableInput/types.d.ts b/apps/app/src/components/Common/SubmittableInput/types.d.ts
new file mode 100644
index 00000000000..b98d8b3575c
--- /dev/null
+++ b/apps/app/src/components/Common/SubmittableInput/types.d.ts
@@ -0,0 +1,7 @@
+export type SubmittableInputProps = InputHTMLAttributes> =
+ Omit, 'value' | 'onKeyDown' | 'onSubmit'>
+ & {
+ value?: string,
+ onSubmit?: (inputText: string) => void,
+ onCancel?: () => void,
+ }
diff --git a/apps/app/src/components/Common/SubmittableInput/use-submittable.ts b/apps/app/src/components/Common/SubmittableInput/use-submittable.ts
new file mode 100644
index 00000000000..b06e77219a4
--- /dev/null
+++ b/apps/app/src/components/Common/SubmittableInput/use-submittable.ts
@@ -0,0 +1,80 @@
+import type {
+ CompositionEvent,
+} from 'react';
+import type React from 'react';
+import {
+ useCallback, useState,
+} from 'react';
+
+import type { SubmittableInputProps } from './types';
+
+export const useSubmittable = (props: SubmittableInputProps): Partial> => {
+
+ const {
+ value,
+ onChange, onBlur,
+ onCompositionStart, onCompositionEnd,
+ onSubmit, onCancel,
+ } = props;
+
+ const [inputText, setInputText] = useState(value ?? '');
+ const [isComposing, setComposing] = useState(false);
+
+ const changeHandler = useCallback(async(e: React.ChangeEvent) => {
+ const inputText = e.target.value;
+ setInputText(inputText);
+
+ onChange?.(e);
+ }, [onChange]);
+
+ const keyDownHandler = useCallback((e) => {
+ switch (e.key) {
+ case 'Enter':
+ // Do nothing when composing
+ if (isComposing) {
+ return;
+ }
+ onSubmit?.(inputText.trim());
+ break;
+ case 'Escape':
+ if (isComposing) {
+ return;
+ }
+ onCancel?.();
+ break;
+ }
+ }, [inputText, isComposing, onCancel, onSubmit]);
+
+ const blurHandler = useCallback((e) => {
+ // submit on blur
+ onSubmit?.(inputText.trim());
+ onBlur?.(e);
+ }, [inputText, onSubmit, onBlur]);
+
+ const compositionStartHandler = useCallback((e: CompositionEvent) => {
+ setComposing(true);
+ onCompositionStart?.(e);
+ }, [onCompositionStart]);
+
+ const compositionEndHandler = useCallback((e: CompositionEvent) => {
+ setComposing(false);
+ onCompositionEnd?.(e);
+ }, [onCompositionEnd]);
+
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ value: _value, onSubmit: _onSubmit, onCancel: _onCancel,
+ ...cleanedProps
+ } = props;
+
+ return {
+ ...cleanedProps,
+ value: inputText,
+ onChange: changeHandler,
+ onKeyDown: keyDownHandler,
+ onBlur: blurHandler,
+ onCompositionStart: compositionStartHandler,
+ onCompositionEnd: compositionEndHandler,
+ };
+
+};
diff --git a/apps/app/src/components/InstallerForm.tsx b/apps/app/src/components/InstallerForm.tsx
index 7f6da3b691a..87232fd7a8e 100644
--- a/apps/app/src/components/InstallerForm.tsx
+++ b/apps/app/src/components/InstallerForm.tsx
@@ -10,7 +10,7 @@ import { i18n as i18nConfig } from '^/config/next-i18next.config';
import { apiv3Post } from '~/client/util/apiv3-client';
import { toastError } from '~/client/util/toastr';
-
+import type { IErrorV3 } from '~/interfaces/errors/v3-error';
import styles from './InstallerForm.module.scss';
@@ -28,6 +28,8 @@ const InstallerForm = memo((): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);
const [currentLocale, setCurrentLocale] = useState(isSupportedLang ? i18n.language : Lang.en_US);
+ const [registerErrors, setRegisterErrors] = useState([]);
+
const checkUserName = useCallback(async(event) => {
const axios = require('axios').create({
headers: {
@@ -70,6 +72,7 @@ const InstallerForm = memo((): JSX.Element => {
};
try {
+ setRegisterErrors([]);
await apiv3Post('/installer', data);
router.push('/');
}
@@ -77,6 +80,7 @@ const InstallerForm = memo((): JSX.Element => {
const err = errs[0];
const code = err.code;
setIsLoading(false);
+ setRegisterErrors(errs);
if (code === 'failed_to_login_after_install') {
toastError(t('installer.failed_to_login_after_install'));
@@ -103,6 +107,19 @@ const InstallerForm = memo((): JSX.Element => {
+
+ {
+ registerErrors != null && registerErrors.length > 0 && (
+
+ {registerErrors.map(err => (
+
+ {t(err.message)}
+
+ ))}
+
+ )
+ }
+