mirror of https://gitee.com/answerdev/answer.git
commit
19138a1c67
|
@ -882,6 +882,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1317,13 +1320,11 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
|
||||
|
||||
admin:
|
||||
admin_header:
|
||||
|
|
|
@ -62,7 +62,7 @@ backend:
|
|||
description:
|
||||
other: 等级 3 (成熟社区所需的高声望)
|
||||
rank_question_add_label:
|
||||
other: 提问s
|
||||
other: 提问
|
||||
rank_answer_add_label:
|
||||
other: 写入答案
|
||||
rank_comment_add_label:
|
||||
|
@ -1274,13 +1274,10 @@ ui:
|
|||
plugins: 插件
|
||||
installed_plugins: 已安装插件
|
||||
website_welcome: 欢迎来到 {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: 登录
|
||||
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。
|
||||
login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。
|
||||
oauth:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 移除 {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
|
|
|
@ -2,8 +2,9 @@ package cli
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
@ -101,8 +102,10 @@ func BuildNewAnswer(outputPath string, plugins []string, originalAnswerInfo Orig
|
|||
builder := newAnswerBuilder(outputPath, plugins, originalAnswerInfo)
|
||||
builder.DoTask(createMainGoFile)
|
||||
builder.DoTask(downloadGoModFile)
|
||||
builder.DoTask(copyUIFiles)
|
||||
builder.DoTask(overwriteIndexTs)
|
||||
builder.DoTask(buildUI)
|
||||
builder.DoTask(mergeI18nFiles)
|
||||
builder.DoTask(replaceNecessaryFile)
|
||||
builder.DoTask(buildBinary)
|
||||
builder.DoTask(cleanByproduct)
|
||||
return builder.BuildError
|
||||
|
@ -120,6 +123,7 @@ func formatPlugins(plugins []string) (formatted []*pluginInfo) {
|
|||
return formatted
|
||||
}
|
||||
|
||||
// createMainGoFile creates main.go file in tmp dir that content is mainGoTpl
|
||||
func createMainGoFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("[build] tmp dir: %s\n", b.tmpDir)
|
||||
err = dir.CreateDirIfNotExist(b.tmpDir)
|
||||
|
@ -169,6 +173,7 @@ func createMainGoFile(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// downloadGoModFile run go mod commands to download dependencies
|
||||
func downloadGoModFile(b *buildingMaterial) (err error) {
|
||||
// If user specify a module replacement, use it. Otherwise, use the latest version.
|
||||
if len(b.answerModuleReplacement) > 0 {
|
||||
|
@ -191,6 +196,81 @@ func downloadGoModFile(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// copyUIFiles copy ui files from answer module to tmp dir
|
||||
func copyUIFiles(b *buildingMaterial) (err error) {
|
||||
goListCmd := b.newExecCmd("go", "list", "-mod=mod", "-m", "-f", "{{.Dir}}", "github.com/answerdev/answer")
|
||||
buf := new(bytes.Buffer)
|
||||
goListCmd.Stdout = buf
|
||||
if err = goListCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to run go list: %w", err)
|
||||
}
|
||||
|
||||
goModUIDir := filepath.Join(strings.TrimSpace(buf.String()), "ui")
|
||||
localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui/")
|
||||
if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir); err != nil {
|
||||
return fmt.Errorf("failed to copy ui files: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// overwriteIndexTs overwrites index.ts file in ui/src/plugins/ dir
|
||||
func overwriteIndexTs(b *buildingMaterial) (err error) {
|
||||
localUIPluginDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui/src/plugins/")
|
||||
|
||||
folders, err := getFolders(localUIPluginDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get folders: %w", err)
|
||||
}
|
||||
|
||||
content := generateIndexTsContent(folders)
|
||||
err = os.WriteFile(filepath.Join(localUIPluginDir, "index.ts"), []byte(content), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write index.ts: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFolders(dir string) ([]string, error) {
|
||||
var folders []string
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() && file.Name() != "builtin" {
|
||||
folders = append(folders, file.Name())
|
||||
}
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func generateIndexTsContent(folders []string) string {
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString("export default null;\n\n")
|
||||
for _, folder := range folders {
|
||||
builder.WriteString(fmt.Sprintf("export { default as %s } from './%s';\n", folder, folder))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// buildUI run pnpm install and pnpm build commands to build ui
|
||||
func buildUI(b *buildingMaterial) (err error) {
|
||||
localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
|
||||
|
||||
pnpmInstallCmd := b.newExecCmd("pnpm", "install")
|
||||
pnpmInstallCmd.Dir = localUIBuildDir
|
||||
if err = pnpmInstallCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pnpmBuildCmd := b.newExecCmd("pnpm", "build")
|
||||
pnpmBuildCmd.Dir = localUIBuildDir
|
||||
if err = pnpmBuildCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceNecessaryFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to replace ui build directory\n")
|
||||
uiBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
|
||||
|
@ -198,6 +278,7 @@ func replaceNecessaryFile(b *buildingMaterial) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// mergeI18nFiles merge i18n files
|
||||
func mergeI18nFiles(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to merge i18n files\n")
|
||||
|
||||
|
@ -285,37 +366,60 @@ func mergeI18nFiles(b *buildingMaterial) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
func copyDirEntries(sourceFs embed.FS, sourceDir string, targetDir string) (err error) {
|
||||
entries, err := ui.Build.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyDirEntries(sourceFs fs.FS, sourceDir string, targetDir string) (err error) {
|
||||
err = dir.CreateDirIfNotExist(targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
err = copyDirEntries(sourceFs, filepath.Join(sourceDir, entry.Name()), filepath.Join(targetDir, entry.Name()))
|
||||
err = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes
|
||||
path = filepath.ToSlash(path)
|
||||
|
||||
// Construct the absolute path for the source file/directory
|
||||
srcPath := filepath.Join(sourceDir, path)
|
||||
|
||||
// Construct the absolute path for the destination file/directory
|
||||
dstPath := filepath.Join(targetDir, path)
|
||||
|
||||
if d.IsDir() {
|
||||
// Create the directory in the destination
|
||||
err := os.MkdirAll(dstPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create directory %s: %w", dstPath, err)
|
||||
}
|
||||
} else {
|
||||
// Open the source file
|
||||
srcFile, err := sourceFs.Open(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file %s: %w", srcPath, err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Create the destination file
|
||||
dstFile, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file %s: %w", dstPath, err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
// Copy the file contents
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file contents from %s to %s: %w", srcPath, dstPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
file, err := sourceFs.ReadFile(filepath.Join(sourceDir, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := filepath.Join(targetDir, entry.Name())
|
||||
err = os.WriteFile(filename, file, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// buildBinary build binary file
|
||||
func buildBinary(b *buildingMaterial) (err error) {
|
||||
versionInfo := b.originalAnswerInfo
|
||||
cmdPkg := "github.com/answerdev/answer/cmd"
|
||||
|
@ -329,6 +433,7 @@ func buildBinary(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// cleanByproduct delete tmp dir
|
||||
func cleanByproduct(b *buildingMaterial) (err error) {
|
||||
return os.RemoveAll(b.tmpDir)
|
||||
}
|
||||
|
|
|
@ -31,3 +31,8 @@ yarn.lock
|
|||
package-lock.json
|
||||
.eslintcache
|
||||
/.vscode/
|
||||
|
||||
/* !/src/plugins
|
||||
/src/plugins/*
|
||||
!/src/plugins/builtin
|
||||
!/src/plugins/Demo
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
"build": "react-app-rewired build",
|
||||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
||||
"prepare": "cd .. && husky install",
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
|
|
|
@ -560,13 +560,10 @@ export interface OauthBindEmailReq {
|
|||
must: boolean;
|
||||
}
|
||||
|
||||
export interface OauthConnectorItem {
|
||||
export interface UserOauthConnectorItem {
|
||||
icon: string;
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface UserOauthConnectorItem extends OauthConnectorItem {
|
||||
binding: boolean;
|
||||
external_id: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { FC, ReactNode, memo } from 'react';
|
||||
|
||||
import builtin from '@/plugins/builtin';
|
||||
import * as plugins from '@/plugins';
|
||||
import { Plugin, PluginType } from '@/utils/pluginKit';
|
||||
|
||||
/**
|
||||
* Note:Please set at least either of the `slug_name` and `type` attributes, otherwise no plugins will be rendered.
|
||||
*
|
||||
* @field slug_name: The `slug_name` of the plugin needs to be rendered.
|
||||
* If this property is set, `PluginRender` will use it first (regardless of whether `type` is set)
|
||||
* to find the corresponding plugin and render it.
|
||||
* @field type: Used to formulate the rendering of all plugins of this type.
|
||||
* (if the `slug_name` attribute is set, it will be ignored)
|
||||
* @field prop: Any attribute you want to configure, e.g. `className`
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
slug_name?: string;
|
||||
type?: PluginType;
|
||||
children?: ReactNode;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
const findPlugin: (s, k: 'slug_name' | 'type', v) => Plugin[] = (
|
||||
source,
|
||||
k,
|
||||
v,
|
||||
) => {
|
||||
const ret: Plugin[] = [];
|
||||
if (source) {
|
||||
Object.keys(source).forEach((i) => {
|
||||
const p = source[i];
|
||||
if (p && p.component && p.info && p.info[k] === v) {
|
||||
ret.push(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ slug_name, type, children, ...props }) => {
|
||||
const fk = slug_name ? 'slug_name' : 'type';
|
||||
const fv = fk === 'slug_name' ? slug_name : type;
|
||||
const bp = findPlugin(builtin, fk, fv);
|
||||
const vp = findPlugin(plugins, fk, fv);
|
||||
const pluginSlice = [...bp, ...vp];
|
||||
|
||||
if (!pluginSlice.length) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* TODO: Rendering control for non-builtin plug-ins
|
||||
* ps: Logic such as version compatibility determination can be placed here
|
||||
*/
|
||||
|
||||
return (
|
||||
<>
|
||||
{pluginSlice.map((ps) => {
|
||||
const PluginFC = ps.component;
|
||||
return (
|
||||
// @ts-ignore
|
||||
<PluginFC key={ps.info.slug_name} {...props}>
|
||||
{children}
|
||||
</PluginFC>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,6 +1,3 @@
|
|||
---
|
||||
sidebar_position: 0
|
||||
---
|
||||
# Schema Form
|
||||
|
||||
## Introduction
|
||||
|
@ -12,7 +9,7 @@ A React component capable of building HTML forms out of a [JSON schema](https://
|
|||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
||||
import { SchemaForm, initFormData, JSONSchema, UISchema, FormKit } from '@/components';
|
||||
|
||||
const schema: JSONSchema = {
|
||||
title: 'General',
|
||||
|
@ -51,6 +48,14 @@ const uiSchema: UISchema = {
|
|||
|
||||
const Form = () => {
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const formRef = useRef<{
|
||||
validator: () => Promise<boolean>;
|
||||
}>(null);
|
||||
|
||||
const refreshConfig: FormKit['refreshConfig'] = async () => {
|
||||
// refreshFormConfig();
|
||||
};
|
||||
|
||||
const handleChange = (data) => {
|
||||
setFormData(data);
|
||||
|
@ -58,27 +63,56 @@ const Form = () => {
|
|||
|
||||
return (
|
||||
<SchemaForm
|
||||
ref={formRef}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={handleChange}
|
||||
refreshConfig={refreshConfig}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
||||
```
|
||||
|
||||
## Props
|
||||
---
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ---------------------------------------- | ------------------------------------- | ------- |
|
||||
| schema | Describe the form structure with schema | [JSONSchema](#json-schema) | - |
|
||||
| uiSchema | Describe the properties of the field | [UISchema](#uischema) | - |
|
||||
| formData | Describe form data | [FormData](#formdata) | - |
|
||||
| onChange | Callback function when form data changes | (data: [FormData](#formdata)) => void | - |
|
||||
| onSubmit | Callback function when form is submitted | (data: React.FormEvent) => void | - |
|
||||
## Form Props
|
||||
|
||||
```ts
|
||||
interface FormProps {
|
||||
// Describe the form structure with schema
|
||||
schema: JSONSchema | null;
|
||||
// Describe the properties of the field
|
||||
uiSchema?: UISchema;
|
||||
// Describe form data
|
||||
formData: Type.FormDataType | null;
|
||||
// Callback function when form data changes
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
// Handler for when a form fires a `submit` event
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
/**
|
||||
* Callback method for updating form configuration
|
||||
* information (schema/uiSchema) in UIAction
|
||||
*/
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
}
|
||||
```
|
||||
|
||||
## Form Ref
|
||||
|
||||
```ts
|
||||
export interface FormRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
When you need to validate a form and get the result outside the form, you can create a `FormRef` with `useRef` and pass it to the form using the `ref` property.
|
||||
|
||||
This allows you to validate the form and get the result outside the form using `formRef.current.validator()`.
|
||||
|
||||
---
|
||||
|
||||
## Types Definition
|
||||
### JSONSchema
|
||||
|
@ -102,24 +136,70 @@ export interface JSONSchema {
|
|||
}
|
||||
```
|
||||
|
||||
### UIOptions
|
||||
|
||||
### UISchema
|
||||
```ts
|
||||
export interface UIOptions {
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### UIWidget
|
||||
```ts
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch'
|
||||
| 'legend'
|
||||
| 'button';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UIOptions
|
||||
```ts
|
||||
export type UIOptions =
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions
|
||||
| ButtonOptions;
|
||||
```
|
||||
|
||||
#### BaseUIOptions
|
||||
```ts
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
className?: string | string[];
|
||||
// Will be appended to the className of the form component itself
|
||||
className?: classnames.Argument;
|
||||
// The className that will be attached to a form field container
|
||||
fieldClassName?: classnames.Argument;
|
||||
// Make a form component render into simplified mode
|
||||
readOnly?: boolean;
|
||||
simplify?: boolean;
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
}
|
||||
```
|
||||
### InputOptions
|
||||
|
||||
#### InputOptions
|
||||
```ts
|
||||
export interface InputOptions extends UIOptions {
|
||||
export interface InputOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
type?:
|
||||
inputType?:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
|
@ -136,82 +216,88 @@ export interface InputOptions extends UIOptions {
|
|||
| 'week';
|
||||
}
|
||||
```
|
||||
### SelectOptions
|
||||
|
||||
#### SelectOptions
|
||||
```ts
|
||||
export interface SelectOptions extends UIOptions {}
|
||||
```
|
||||
### UploadOptions
|
||||
|
||||
#### UploadOptions
|
||||
```ts
|
||||
export interface UploadOptions extends UIOptions {
|
||||
export interface UploadOptions extends BaseUIOptions {
|
||||
acceptType?: string;
|
||||
imageType?: 'post' | 'avatar' | 'branding';
|
||||
imageType?: Type.UploadType;
|
||||
}
|
||||
```
|
||||
### SwitchOptions
|
||||
|
||||
#### SwitchOptions
|
||||
```ts
|
||||
export interface SwitchOptions extends UIOptions {}
|
||||
export interface SwitchOptions extends BaseUIOptions {
|
||||
label?: string;
|
||||
}
|
||||
```
|
||||
### TimezoneOptions
|
||||
|
||||
#### TimezoneOptions
|
||||
```ts
|
||||
export interface TimezoneOptions extends UIOptions {
|
||||
placeholder?: string;
|
||||
}
|
||||
```
|
||||
### CheckboxOptions
|
||||
|
||||
#### CheckboxOptions
|
||||
```ts
|
||||
export interface CheckboxOptions extends UIOptions {}
|
||||
```
|
||||
### RadioOptions
|
||||
|
||||
#### RadioOptions
|
||||
```ts
|
||||
export interface RadioOptions extends UIOptions {}
|
||||
```
|
||||
### TextareaOptions
|
||||
|
||||
#### TextareaOptions
|
||||
```ts
|
||||
export interface TextareaOptions extends UIOptions {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
}
|
||||
```
|
||||
### UIWidget
|
||||
|
||||
#### ButtonOptions
|
||||
```ts
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch';
|
||||
export interface ButtonOptions extends BaseUIOptions {
|
||||
text: string;
|
||||
icon?: string;
|
||||
action?: UIAction;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
```
|
||||
|
||||
### UISchema
|
||||
|
||||
#### UIAction
|
||||
```ts
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?:
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions;
|
||||
export interface UIAction {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'delete';
|
||||
loading?: {
|
||||
text: string;
|
||||
state?: 'none' | 'pending' | 'completed';
|
||||
};
|
||||
on_complete?: {
|
||||
toast_return_message?: boolean;
|
||||
refresh_form_config?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### FormKit
|
||||
```ts
|
||||
export interface FormKit {
|
||||
refreshConfig(): void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FormData
|
||||
```ts
|
||||
export interface FormValue<T = any> {
|
||||
|
@ -226,6 +312,44 @@ export interface FormDataType {
|
|||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend API
|
||||
|
||||
For backend generating modal form you can return json like this.
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"slug_name": "string",
|
||||
"description": "string",
|
||||
"version": "string",
|
||||
"config_fields": [
|
||||
{
|
||||
"name": "string",
|
||||
"type": "textarea",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"required": true,
|
||||
"value": "string",
|
||||
"ui_options": {
|
||||
"placeholder": "placeholder",
|
||||
"rows": 4
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"value": "string",
|
||||
"label": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## reference
|
||||
|
||||
- [json schema](https://json-schema.org/understanding-json-schema/index.html)
|
||||
|
|
|
@ -12,7 +12,13 @@ import classnames from 'classnames';
|
|||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
import type { JSONSchema, UISchema, BaseUIOptions, FormKit } from './types';
|
||||
import type {
|
||||
JSONSchema,
|
||||
FormProps,
|
||||
FormRef,
|
||||
BaseUIOptions,
|
||||
FormKit,
|
||||
} from './types';
|
||||
import {
|
||||
Legend,
|
||||
Select,
|
||||
|
@ -27,20 +33,6 @@ import {
|
|||
|
||||
export * from './types';
|
||||
|
||||
interface IProps {
|
||||
schema: JSONSchema | null;
|
||||
formData: Type.FormDataType | null;
|
||||
uiSchema?: UISchema;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
interface IRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.
|
||||
|
@ -60,7 +52,7 @@ interface IRef {
|
|||
* @param onChange change event
|
||||
* @param onSubmit submit event
|
||||
*/
|
||||
const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||
const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
|
||||
{
|
||||
schema,
|
||||
uiSchema = {},
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import { ButtonProps } from 'react-bootstrap';
|
||||
import React from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
export interface FormProps {
|
||||
schema: JSONSchema | null;
|
||||
uiSchema?: UISchema;
|
||||
formData: Type.FormDataType | null;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
export interface FormRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
|
@ -41,6 +41,7 @@ import HttpErrorContent from './HttpErrorContent';
|
|||
import CustomSidebar from './CustomSidebar';
|
||||
import ImgViewer from './ImgViewer';
|
||||
import SideNav from './SideNav';
|
||||
import PluginRender from './PluginRender';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -88,5 +89,6 @@ export {
|
|||
CustomSidebar,
|
||||
ImgViewer,
|
||||
SideNav,
|
||||
PluginRender,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -4,22 +4,42 @@ import i18next from 'i18next';
|
|||
import en_US from '@i18n/en_US.yaml';
|
||||
import zh_CN from '@i18n/zh_CN.yaml';
|
||||
|
||||
import { DEFAULT_LANG } from '@/common/constants';
|
||||
import { DEFAULT_LANG, LANG_RESOURCE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
/**
|
||||
* Prevent i18n from re-initialising when the page is refreshed and switching to `fallbackLng`.
|
||||
*/
|
||||
const initLng = i18next.resolvedLanguage || DEFAULT_LANG;
|
||||
const initResources = {
|
||||
en_US: {
|
||||
translation: en_US.ui,
|
||||
},
|
||||
zh_CN: {
|
||||
translation: zh_CN.ui,
|
||||
},
|
||||
};
|
||||
|
||||
const storageLang = Storage.get(LANG_RESOURCE_STORAGE_KEY);
|
||||
if (
|
||||
storageLang &&
|
||||
storageLang.resources &&
|
||||
storageLang.lng &&
|
||||
storageLang.lng !== 'en_US' &&
|
||||
storageLang.lng !== 'zh_CN'
|
||||
) {
|
||||
initResources[storageLang.lng] = {
|
||||
translation: storageLang.resources,
|
||||
};
|
||||
}
|
||||
|
||||
i18next
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en_US: {
|
||||
translation: en_US.ui,
|
||||
},
|
||||
zh_CN: {
|
||||
translation: zh_CN.ui,
|
||||
},
|
||||
},
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
|
||||
resources: initResources,
|
||||
lng: initLng,
|
||||
fallbackLng: DEFAULT_LANG,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getLoginConf, checkLoginResult } from './service';
|
|||
|
||||
let checkTimer: NodeJS.Timeout;
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'user_center' });
|
||||
const navigate = useNavigate();
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const agentName = ucAgent?.agent_info?.name || '';
|
||||
|
|
|
@ -28,7 +28,7 @@ const data = [
|
|||
];
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'user_center' });
|
||||
const ucAgent = userCenterStore().agent;
|
||||
return (
|
||||
<Col lg={4} className="mx-auto mt-3 py-5">
|
||||
|
|
|
@ -9,8 +9,7 @@ import type {
|
|||
ImgCodeRes,
|
||||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth, PluginUcLogin } from '@/plugins';
|
||||
import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
|
@ -172,15 +171,16 @@ const Index: React.FC = () => {
|
|||
usePageTags({
|
||||
title: t('login', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
{step === 1 ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
{ucAgentInfo ? (
|
||||
<PluginUcLogin className="mb-5" />
|
||||
<PluginRender slug_name="uc_login" className="mb-5" />
|
||||
) : (
|
||||
<PluginOauth className="mb-5" />
|
||||
<PluginRender type="Connector" className="mb-5" />
|
||||
)}
|
||||
{canOriginalLogin ? (
|
||||
<>
|
||||
|
|
|
@ -3,8 +3,7 @@ import { Container, Col } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
@ -27,7 +26,7 @@ const Index: React.FC = () => {
|
|||
|
||||
{showForm ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<PluginRender type="Connector" className="mb-5" />
|
||||
<SignUpForm callback={onStep} />
|
||||
</Col>
|
||||
) : (
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index = () => {
|
|||
});
|
||||
|
||||
const { t: t2 } = useTranslation('translation', {
|
||||
keyPrefix: 'plugins.oauth',
|
||||
keyPrefix: 'oauth',
|
||||
});
|
||||
|
||||
const deleteLogins = (e, item) => {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package demo
|
||||
|
||||
import "github.com/answerdev/answer/plugin"
|
||||
|
||||
type DemoPlugin struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.Register(&DemoPlugin{})
|
||||
}
|
||||
|
||||
func (d DemoPlugin) Info() plugin.Info {
|
||||
return plugin.Info{
|
||||
Name: plugin.MakeTranslator("i18n.demo.name"),
|
||||
SlugName: "demo_plugin",
|
||||
Description: plugin.MakeTranslator("i18n.demo.description"),
|
||||
Author: "answerdev",
|
||||
Version: "0.0.1",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
plugin:
|
||||
ui_plugin_demo:
|
||||
ui:
|
||||
msg: UI Plugin Demo
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
plugin:
|
||||
ui_plugin_demo:
|
||||
ui:
|
||||
msg: UI 插件示例
|
|
@ -0,0 +1,24 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import './i18n';
|
||||
|
||||
import info from './info.yaml';
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
};
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
|
||||
return <Alert variant="info">{t('msg')}</Alert>;
|
||||
};
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
slug_name: ui_plugin_demo
|
||||
version: 0.0.1
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
plugin:
|
||||
connector:
|
||||
ui:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
connector:
|
||||
ui:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 解绑 {{ auth_name }}
|
||||
|
|
@ -4,14 +4,25 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useGetStartUseOauthConnector } from '@/services';
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import { SvgIcon } from '@/components';
|
||||
|
||||
import info from './info.yaml';
|
||||
import { useGetStartUseOauthConnector } from './services';
|
||||
import './i18n';
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
type: info.type,
|
||||
};
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
|
||||
const { data } = useGetStartUseOauthConnector();
|
||||
|
||||
if (!data?.length) return null;
|
||||
|
@ -29,4 +40,7 @@ const Index: FC<Props> = ({ className }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
slug_name: connector
|
||||
type: Connector
|
||||
version: 0.0.1
|
||||
link: https://github.com/answerdev/plugins/tree/main/connector/
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface OauthConnectorItem {
|
||||
icon: string;
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const useGetStartUseOauthConnector = () => {
|
||||
const { data, error } = useSWR<OauthConnectorItem[]>(
|
||||
'/answer/api/v1/connector/info',
|
||||
request.instance.get,
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
uc_login:
|
||||
ui:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
uc_login:
|
||||
ui:
|
||||
login: 登录
|
||||
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
|
||||
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
|
|
@ -4,14 +4,25 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import { SvgIcon } from '@/components';
|
||||
import { userCenterStore } from '@/stores';
|
||||
import './i18n';
|
||||
|
||||
import info from './info.yaml';
|
||||
|
||||
interface Props {
|
||||
className?: classnames.Argument;
|
||||
}
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const ucLoginRedirect =
|
||||
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
|
||||
|
@ -31,5 +42,7 @@ const Index: FC<Props> = ({ className }) => {
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
slug_name: uc_login
|
||||
version: 0.0.1
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Connector from './Connector';
|
||||
import UcLogin from './UcLogin';
|
||||
|
||||
export default {
|
||||
Connector,
|
||||
UcLogin,
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
import PluginOauth from './PluginOauth';
|
||||
import PluginUcLogin from './PluginUcLogin';
|
||||
export default null;
|
||||
|
||||
export { PluginOauth, PluginUcLogin };
|
||||
// export { default as Demo } from './Demo';
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
|
@ -2,7 +2,29 @@ import qs from 'qs';
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@/utils/request';
|
||||
import type { PluginConfig } from '@/plugins/types';
|
||||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
||||
|
||||
export const useQueryPlugins = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;
|
||||
|
|
|
@ -23,14 +23,3 @@ export const useOauthConnectorInfoByUser = () => {
|
|||
export const userOauthUnbind = (data: { external_id: string }) => {
|
||||
return request.delete('/answer/api/v1/connector/user/unbinding', data);
|
||||
};
|
||||
|
||||
export const useGetStartUseOauthConnector = () => {
|
||||
const { data, error } = useSWR<Type.OauthConnectorItem[]>(
|
||||
'/answer/api/v1/connector/info',
|
||||
request.instance.get,
|
||||
);
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
|
||||
import Storage from './storage';
|
||||
|
||||
/**
|
||||
* localize kit for i18n
|
||||
*/
|
||||
export const loadLanguageOptions = async (forAdmin = false) => {
|
||||
const languageOptions = forAdmin
|
||||
? await getAdminLanguageOptions()
|
||||
|
@ -68,13 +71,6 @@ const addI18nResource = async (langName) => {
|
|||
}
|
||||
};
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const localeDayjs = (langName) => {
|
||||
langName = langName.replace('_', '-').toLowerCase();
|
||||
dayjs.locale(langName);
|
||||
};
|
||||
|
||||
export const getCurrentLang = () => {
|
||||
const loggedUser = loggedUserInfoStore.getState().user;
|
||||
const adminInterface = interfaceStore.getState().interface;
|
||||
|
@ -88,6 +84,16 @@ export const getCurrentLang = () => {
|
|||
return currentLang;
|
||||
};
|
||||
|
||||
/**
|
||||
* localize for Day.js
|
||||
*/
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const localeDayjs = (langName) => {
|
||||
langName = langName.replace('_', '-').toLowerCase();
|
||||
dayjs.locale(langName);
|
||||
};
|
||||
|
||||
export const setupAppLanguage = async () => {
|
||||
const lang = getCurrentLang();
|
||||
if (!i18next.getDataByLanguage(lang)) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { NamedExoticComponent, FC } from 'react';
|
||||
|
||||
import i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* This information is to be defined for all components.
|
||||
* It may be used for feature upgrades or version compatibility processing.
|
||||
*
|
||||
* @field slug_name: Unique identity string for the plugin, usually configured in `info.yaml`
|
||||
* @field type: The type of plugin is defined and a single type of plugin can have multiple implementations.
|
||||
* For example, a plugin of type `Connector` can have a `google` implementation and a `github` implementation.
|
||||
* `PluginRender` automatically renders the plug-in types already included in `PluginType`.
|
||||
* @field name: Plugin name, optionally configurable. Usually read from the `i18n` file
|
||||
* @field description: Plugin description, optionally configurable. Usually read from the `i18n` file
|
||||
*/
|
||||
|
||||
const I18N_NS = 'plugin';
|
||||
|
||||
export type PluginType = 'Connector';
|
||||
export interface PluginInfo {
|
||||
slug_name: string;
|
||||
type?: PluginType;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
info: PluginInfo;
|
||||
component: NamedExoticComponent | FC;
|
||||
}
|
||||
|
||||
interface I18nResource {
|
||||
[lng: string]: {
|
||||
plugin: {
|
||||
[slug_name: string]: {
|
||||
ui: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const addResourceBundle = (resource: I18nResource) => {
|
||||
if (resource) {
|
||||
Object.keys(resource).forEach((lng) => {
|
||||
const r = resource[lng];
|
||||
|
||||
i18next.addResourceBundle(lng, I18N_NS, r.plugin, true, true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initI18nResource = (resource: I18nResource) => {
|
||||
addResourceBundle(resource);
|
||||
/**
|
||||
* Note: In development mode,
|
||||
* when the base i18n file is changed, `i18next` will reinitialise the updated resource file,
|
||||
* which will cause the resource package added in the plugin to be lost
|
||||
* and will need to be automatically re-added by listening for events
|
||||
*/
|
||||
i18next.on('initialized', () => {
|
||||
addResourceBundle(resource);
|
||||
});
|
||||
};
|
||||
|
||||
const getTransNs = () => {
|
||||
return I18N_NS;
|
||||
};
|
||||
|
||||
const getTransKeyPrefix = (info: PluginInfo) => {
|
||||
const kp = `${info.slug_name}.ui`;
|
||||
return kp;
|
||||
};
|
||||
|
||||
export default {
|
||||
initI18nResource,
|
||||
getTransNs,
|
||||
getTransKeyPrefix,
|
||||
};
|
Loading…
Reference in New Issue