mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
Merge branch 'develop' into feat/json-templates
This commit is contained in:
commit
2608e8d4a9
56
CHANGELOG.md
56
CHANGELOG.md
@ -5,6 +5,62 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v1.5.16](https://github.com/nocobase/nocobase/compare/v1.5.15...v1.5.16) - 2025-02-26
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **[Backup manager]** Allow restoring backup to an application even it is missing some plugins by @gchust
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[client]** rich text field component cannot be fully cleared ([#6287](https://github.com/nocobase/nocobase/pull/6287)) by @katherinehhh
|
||||
|
||||
- **[File manager]**
|
||||
- Fix migration and add test cases ([#6288](https://github.com/nocobase/nocobase/pull/6288)) by @mytharcher
|
||||
|
||||
- Fix `path` column type of file collection ([#6294](https://github.com/nocobase/nocobase/pull/6294)) by @mytharcher
|
||||
|
||||
- Fix migration and add test cases ([#6288](https://github.com/nocobase/nocobase/pull/6288)) by @mytharcher
|
||||
|
||||
## [v1.5.15](https://github.com/nocobase/nocobase/compare/v1.5.14...v1.5.15) - 2025-02-25
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **[File manager]**
|
||||
- Increase URL length to 1024 ([#6275](https://github.com/nocobase/nocobase/pull/6275)) by @mytharcher
|
||||
|
||||
- File names during upload will change from random to the original name with a random suffix. ([#6217](https://github.com/nocobase/nocobase/pull/6217)) by @chenos
|
||||
|
||||
- **[Block: Action panel]** Optimize mobile styles ([#6270](https://github.com/nocobase/nocobase/pull/6270)) by @zhangzhonghe
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[cli]** Improve internal logic of nocobase upgrade command ([#6280](https://github.com/nocobase/nocobase/pull/6280)) by @chenos
|
||||
|
||||
## [v1.5.14](https://github.com/nocobase/nocobase/compare/v1.5.13...v1.5.14) - 2025-02-24
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[Backup manager]** The delete icon of the restore from local operation dialog is not working by @gchust
|
||||
|
||||
## [v1.5.13](https://github.com/nocobase/nocobase/compare/v1.5.12...v1.5.13) - 2025-02-22
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[client]** Fix uploaded file missed when one by one ([#6260](https://github.com/nocobase/nocobase/pull/6260)) by @mytharcher
|
||||
|
||||
- **[Workflow: Pre-action event]** Fix error message from response message node not shown by @mytharcher
|
||||
|
||||
## [v1.5.12](https://github.com/nocobase/nocobase/compare/v1.5.11...v1.5.12) - 2025-02-21
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **[Workflow]** Hide node id from node card in workflow canvas ([#6251](https://github.com/nocobase/nocobase/pull/6251)) by @mytharcher
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[File manager]** Upgrade AWS SDK version to fix MinIO upload bug ([#6253](https://github.com/nocobase/nocobase/pull/6253)) by @mytharcher
|
||||
|
||||
## [v1.5.11](https://github.com/nocobase/nocobase/compare/v1.5.10...v1.5.11) - 2025-02-20
|
||||
|
||||
### 🎉 New Features
|
||||
|
@ -5,6 +5,62 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||
|
||||
## [v1.5.16](https://github.com/nocobase/nocobase/compare/v1.5.15...v1.5.16) - 2025-02-26
|
||||
|
||||
### 🚀 优化
|
||||
|
||||
- **[备份管理器]** 允许还原备份到缺少部分插件的应用 by @gchust
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[client]** 富文本字段组件无法删除清空所有内容 ([#6287](https://github.com/nocobase/nocobase/pull/6287)) by @katherinehhh
|
||||
|
||||
- **[文件管理器]**
|
||||
- 修复迁移脚本并补充测试用例 ([#6288](https://github.com/nocobase/nocobase/pull/6288)) by @mytharcher
|
||||
|
||||
- 修复文件表 `path` 列的类型 ([#6294](https://github.com/nocobase/nocobase/pull/6294)) by @mytharcher
|
||||
|
||||
- 修复迁移脚本并补充测试用例 ([#6288](https://github.com/nocobase/nocobase/pull/6288)) by @mytharcher
|
||||
|
||||
## [v1.5.15](https://github.com/nocobase/nocobase/compare/v1.5.14...v1.5.15) - 2025-02-25
|
||||
|
||||
### 🚀 优化
|
||||
|
||||
- **[文件管理器]**
|
||||
- URL 字段长度增加为 1024 ([#6275](https://github.com/nocobase/nocobase/pull/6275)) by @mytharcher
|
||||
|
||||
- 文件上传时生成的文件名由随机改成文件名加随机后缀。 ([#6217](https://github.com/nocobase/nocobase/pull/6217)) by @chenos
|
||||
|
||||
- **[区块:操作面板]** 优化移动端样式 ([#6270](https://github.com/nocobase/nocobase/pull/6270)) by @zhangzhonghe
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[cli]** 优化 nocobase upgrade 命令行 ([#6280](https://github.com/nocobase/nocobase/pull/6280)) by @chenos
|
||||
|
||||
## [v1.5.14](https://github.com/nocobase/nocobase/compare/v1.5.13...v1.5.14) - 2025-02-24
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[备份管理器]** 在"从本地备份还原"操作弹窗中,点击删除图标不会清空文件列表 by @gchust
|
||||
|
||||
## [v1.5.13](https://github.com/nocobase/nocobase/compare/v1.5.12...v1.5.13) - 2025-02-22
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[client]** 修复逐个上传文件后之前的文件消失的问题 ([#6260](https://github.com/nocobase/nocobase/pull/6260)) by @mytharcher
|
||||
|
||||
- **[工作流:操作前事件]** 修复响应消息节点的错误消息不显示的问题 by @mytharcher
|
||||
|
||||
## [v1.5.12](https://github.com/nocobase/nocobase/compare/v1.5.11...v1.5.12) - 2025-02-21
|
||||
|
||||
### 🚀 优化
|
||||
|
||||
- **[工作流]** 在工作流画布的节点上隐藏节点 ID ([#6251](https://github.com/nocobase/nocobase/pull/6251)) by @mytharcher
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[文件管理器]** 升级 AWS SDK 版本以修复 MinIO 上传问题 ([#6253](https://github.com/nocobase/nocobase/pull/6253)) by @mytharcher
|
||||
|
||||
## [v1.5.11](https://github.com/nocobase/nocobase/compare/v1.5.10...v1.5.11) - 2025-02-20
|
||||
|
||||
### 🎉 新特性
|
||||
|
@ -59,8 +59,8 @@ FROM node:20-bookworm-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends wget gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN sh -c 'echo "deb http://mirrors.ustc.edu.cn/postgresql/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
RUN wget --quiet -O - http://mirrors.ustc.edu.cn/postgresql/repos/apt/ACCC4CF8.asc | apt-key add -
|
||||
RUN sh -c 'echo "deb http://mirrors.aliyun.com/postgresql/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
RUN wget --quiet -O - http://mirrors.aliyun.com/postgresql/repos/apt/ACCC4CF8.asc | apt-key add -
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nginx \
|
||||
|
16
package.json
16
package.json
@ -44,15 +44,16 @@
|
||||
"run:example": "tsx -r dotenv/config -r tsconfig-paths/register ./examples/index.ts"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-router": "^6.11.2",
|
||||
"react-router-dom": "6.28.1",
|
||||
"react-router": "6.28.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"nwsapi": "2.2.7",
|
||||
"antd": "5.12.8"
|
||||
"antd": "5.12.8",
|
||||
"@ant-design/icons": "^5.6.1"
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
@ -72,13 +73,14 @@
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@commitlint/prompt-cli": "^16.1.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"eslint-plugin-jest-dom": "^5.0.1",
|
||||
"eslint-plugin-testing-library": "^5.11.0",
|
||||
"ghooks": "^2.0.4",
|
||||
"lint-staged": "^13.2.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"pretty-format": "^24.0.0",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"react": "^18.0.0",
|
||||
|
@ -68,6 +68,9 @@ export abstract class Auth implements IAuth {
|
||||
}
|
||||
|
||||
async skipCheck() {
|
||||
if (this.ctx.skipAuthCheck === true) {
|
||||
return true;
|
||||
}
|
||||
const token = this.ctx.getBearerToken();
|
||||
if (!token && this.ctx.app.options.acl === false) {
|
||||
return true;
|
||||
|
@ -7,10 +7,10 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Collection, Model } from '@nocobase/database';
|
||||
import { Cache } from '@nocobase/cache';
|
||||
import { Collection, Model } from '@nocobase/database';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Auth, AuthConfig, AuthErrorCode, AuthError } from '../auth';
|
||||
import { Auth, AuthConfig, AuthError, AuthErrorCode } from '../auth';
|
||||
import { JwtService } from './jwt-service';
|
||||
import { ITokenControlService } from './token-control-service';
|
||||
|
||||
@ -72,6 +72,7 @@ export class BaseAuth extends Auth {
|
||||
|
||||
async check(): ReturnType<Auth['check']> {
|
||||
const token = this.ctx.getBearerToken();
|
||||
const cache = this.ctx.cache as Cache;
|
||||
|
||||
if (!token) {
|
||||
this.ctx.throw(401, {
|
||||
@ -100,6 +101,41 @@ export class BaseAuth extends Auth {
|
||||
|
||||
const { userId, roleName, iat, temp, jti, exp, signInTime } = payload ?? {};
|
||||
|
||||
const user = userId
|
||||
? await cache.wrap(this.getCacheKey(userId), () =>
|
||||
this.userRepository.findOne({
|
||||
filter: {
|
||||
id: userId,
|
||||
},
|
||||
raw: true,
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
|
||||
if (roleName) {
|
||||
this.ctx.headers['x-role'] = roleName;
|
||||
}
|
||||
|
||||
const blocked = await this.jwt.blacklist.has(jti ?? token);
|
||||
if (blocked) {
|
||||
this.ctx.throw(401, {
|
||||
message: this.ctx.t('Your session has expired. Please sign in again.', { ns: localeNamespace }),
|
||||
code: AuthErrorCode.BLOCKED_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
// api token check first
|
||||
if (!temp) {
|
||||
if (tokenStatus === 'valid') {
|
||||
return user;
|
||||
} else {
|
||||
this.ctx.throw(401, {
|
||||
message: this.ctx.t('Your session has expired. Please sign in again.', { ns: localeNamespace }),
|
||||
code: AuthErrorCode.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tokenPolicy = await this.tokenController.getConfig();
|
||||
|
||||
if (signInTime && Date.now() - signInTime > tokenPolicy.sessionExpirationTime) {
|
||||
@ -113,36 +149,6 @@ export class BaseAuth extends Auth {
|
||||
tokenStatus = 'expired';
|
||||
}
|
||||
|
||||
const blocked = await this.jwt.blacklist.has(jti ?? token);
|
||||
if (blocked) {
|
||||
this.ctx.throw(401, {
|
||||
message: this.ctx.t('Your session has expired. Please sign in again.', { ns: localeNamespace }),
|
||||
code: AuthErrorCode.BLOCKED_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
if (roleName) {
|
||||
this.ctx.headers['x-role'] = roleName;
|
||||
}
|
||||
|
||||
const cache = this.ctx.cache as Cache;
|
||||
|
||||
const user = await cache.wrap(this.getCacheKey(userId), () =>
|
||||
this.userRepository.findOne({
|
||||
filter: {
|
||||
id: userId,
|
||||
},
|
||||
raw: true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!temp && tokenStatus !== 'valid') {
|
||||
this.ctx.throw(401, {
|
||||
message: this.ctx.t('Your session has expired. Please sign in again.', { ns: localeNamespace }),
|
||||
code: AuthErrorCode.INVALID_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenStatus === 'valid' && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) {
|
||||
this.ctx.throw(401, {
|
||||
message: this.ctx.t('User password changed, please signin again.', { ns: localeNamespace }),
|
||||
|
@ -14,6 +14,14 @@ const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs');
|
||||
const { readFile, writeFile } = require('fs').promises;
|
||||
const { createStoragePluginsSymlink, createDevPluginsSymlink } = require('@nocobase/utils/plugin-symlink');
|
||||
|
||||
function runPatchPackage() {
|
||||
// run yarn patch-package
|
||||
// console.log('patching third party packages...');
|
||||
run('yarn', ['patch-package'], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}
|
||||
|
||||
function writeToExclude() {
|
||||
const excludePath = resolve(process.cwd(), '.git', 'info', 'exclude');
|
||||
const content = 'packages/pro-plugins/\n';
|
||||
@ -47,6 +55,7 @@ module.exports = (cli) => {
|
||||
.allowUnknownOption()
|
||||
.option('--skip-umi')
|
||||
.action(async (options) => {
|
||||
runPatchPackage();
|
||||
writeToExclude();
|
||||
generatePlugins();
|
||||
generatePlaywrightPath(true);
|
||||
|
@ -52,8 +52,14 @@ module.exports = (cli) => {
|
||||
}
|
||||
};
|
||||
const pkg = require('../../package.json');
|
||||
let distTag = 'latest';
|
||||
if (pkg.version.includes('alpha')) {
|
||||
distTag = 'alpha';
|
||||
} else if (pkg.version.includes('beta')) {
|
||||
distTag = 'beta';
|
||||
}
|
||||
// get latest version
|
||||
const { stdout } = await run('npm', ['info', options.next ? '@nocobase/cli@next' : '@nocobase/cli', 'version'], {
|
||||
const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
if (pkg.version === stdout) {
|
||||
@ -62,13 +68,7 @@ module.exports = (cli) => {
|
||||
await rmAppDir();
|
||||
return;
|
||||
}
|
||||
const currentY = 1 * pkg.version.split('.')[1];
|
||||
const latestY = 1 * stdout.split('.')[1];
|
||||
if (options.next || currentY > latestY) {
|
||||
await run('yarn', ['add', '@nocobase/cli@next', '@nocobase/devtools@next', '-W']);
|
||||
} else {
|
||||
await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']);
|
||||
}
|
||||
await run('yarn', ['add', `@nocobase/cli@${distTag}`, `@nocobase/devtools@${distTag}`, '-W']);
|
||||
await run('yarn', ['install']);
|
||||
await downloadPro();
|
||||
await runAppCommand('upgrade');
|
||||
|
@ -101,7 +101,7 @@ const Demo = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>{render()}</div>
|
||||
<div>可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}</div>
|
||||
<div>可以进行参数的二次覆盖:{render({ mode: 'inline', style: { color: 'red' } })}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,7 +8,7 @@
|
||||
"dependencies": {
|
||||
"@ahooksjs/use-url-state": "3.5.1",
|
||||
"@ant-design/cssinjs": "^1.11.1",
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/pro-layout": "^7.16.11",
|
||||
"@antv/g2plot": "^2.4.18",
|
||||
"@budibase/handlebars-helpers": "^0.14.0",
|
||||
@ -43,6 +43,7 @@
|
||||
"flat": "^5.0.2",
|
||||
"i18next": "^22.4.9",
|
||||
"i18next-http-backend": "^2.1.1",
|
||||
"ignore": "^5.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "6.0.0",
|
||||
|
@ -29,7 +29,6 @@ import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'
|
||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||
|
||||
import { useApp } from '../application';
|
||||
import { NavigateToSigninWithRedirect } from '../user/CurrentUserProvider';
|
||||
|
||||
// 注意: 必须要对 useBlockRequestContext 进行引用,否则会导致 Data sources 页面报错,原因未知
|
||||
useBlockRequestContext;
|
||||
@ -89,9 +88,6 @@ export const ACLRolesCheckProvider = (props) => {
|
||||
if (result.loading) {
|
||||
return render();
|
||||
}
|
||||
if (result.error) {
|
||||
return <NavigateToSigninWithRedirect />;
|
||||
}
|
||||
return <ACLContext.Provider value={result}>{props.children}</ACLContext.Provider>;
|
||||
};
|
||||
|
||||
@ -308,30 +304,32 @@ export const ACLActionProvider = (props) => {
|
||||
const collection = useCollection();
|
||||
const recordPkValue = useRecordPkValue();
|
||||
const resource = useResourceName();
|
||||
const { parseAction } = useACLRoleContext();
|
||||
const { parseAction, uiButtonSchemasBlacklist } = useACLRoleContext();
|
||||
const schema = useFieldSchema();
|
||||
const currentUid = schema['x-uid'];
|
||||
let actionPath = schema['x-acl-action'];
|
||||
const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
|
||||
|
||||
if (!actionPath && resource && schema['x-action']) {
|
||||
if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) {
|
||||
actionPath = `${resource}:${schema['x-action']}`;
|
||||
}
|
||||
if (!actionPath?.includes(':')) {
|
||||
if (actionPath && !actionPath?.includes(':')) {
|
||||
actionPath = `${resource}:${actionPath}`;
|
||||
}
|
||||
|
||||
const params = useMemo(
|
||||
() => parseAction(actionPath, { schema, recordPkValue }),
|
||||
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
|
||||
[parseAction, actionPath, schema, recordPkValue],
|
||||
);
|
||||
|
||||
if (uiButtonSchemasBlacklist.includes(currentUid)) {
|
||||
return <ACLActionParamsContext.Provider value={false}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
}
|
||||
if (!actionPath) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
if (!resource) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ export type ResourceActionOptions<P = any> = {
|
||||
action?: string;
|
||||
params?: P;
|
||||
url?: string;
|
||||
skipNotify?: boolean | ((error: any) => boolean);
|
||||
skipAuth?: boolean;
|
||||
};
|
||||
|
||||
export type UseRequestService<P> = AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService;
|
||||
|
@ -29,7 +29,8 @@ import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
|
||||
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
||||
import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer';
|
||||
import * as schemaInitializerComponents from './schema-initializer/components';
|
||||
import { SchemaSettings, SchemaSettingsManager } from './schema-settings';
|
||||
import { SchemaSettings, SchemaSettingsManager, SchemaSettingsItemType } from './schema-settings';
|
||||
|
||||
import { compose, normalizeContainer } from './utils';
|
||||
import { defineGlobalDeps } from './utils/globalDeps';
|
||||
import { getRequireJs } from './utils/requirejs';
|
||||
@ -46,6 +47,7 @@ import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||
import type { Plugin } from './Plugin';
|
||||
import { getOperators } from './globalOperators';
|
||||
import type { RequireJS } from './utils/requirejs';
|
||||
import { useAclSnippets } from './hooks/useAclSnippets';
|
||||
|
||||
type JsonLogic = {
|
||||
addOperation: (name: string, fn?: any) => void;
|
||||
@ -236,7 +238,7 @@ export class Application {
|
||||
this.addComponents({
|
||||
Link,
|
||||
Navigate: Navigate as ComponentType,
|
||||
NavLink,
|
||||
NavLink: NavLink as ComponentType,
|
||||
});
|
||||
}
|
||||
|
||||
@ -497,4 +499,20 @@ export class Application {
|
||||
getGlobalVar(key) {
|
||||
return get(this.globalVars, key);
|
||||
}
|
||||
addUserCenterSettingsItem(item: SchemaSettingsItemType & { aclSnippet?: string }) {
|
||||
const useVisibleProp = item.useVisible || (() => true);
|
||||
const useVisible = () => {
|
||||
const { allow } = useAclSnippets();
|
||||
const visible = useVisibleProp();
|
||||
if (!visible) {
|
||||
return false;
|
||||
}
|
||||
return item.aclSnippet ? allow(item.aclSnippet) : true;
|
||||
};
|
||||
|
||||
this.schemaSettingsManager.addItem('userCenterSettings', item.name, {
|
||||
...item,
|
||||
useVisible: useVisible,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
25
packages/core/client/src/application/hooks/useAclSnippets.ts
Normal file
25
packages/core/client/src/application/hooks/useAclSnippets.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useACLRoleContext } from '../../acl/ACLProvider';
|
||||
import ignore from 'ignore';
|
||||
|
||||
export const useAclSnippets = () => {
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
return {
|
||||
allow: (aclSnippet) => {
|
||||
if (aclSnippet) {
|
||||
const ig = ignore().add(snippets);
|
||||
const appAllowed = allowAll || ig.ignores(aclSnippet);
|
||||
return appAllowed;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
};
|
@ -52,7 +52,8 @@ export const useSchemaInitializerStyles = genStyleHook('nb-schema-initializer',
|
||||
},
|
||||
},
|
||||
[`${componentCls}-item-content`]: {
|
||||
marginLeft: token.marginXS,
|
||||
// 相当于 Menu 的 iconMarginInlineEnd,参见:https://github.com/ant-design/ant-design/blob/6a62d9e7eaf3e683c673091e39fe65ba3204d94b/components/menu/style/index.ts#L942
|
||||
marginLeft: token.controlHeightSM - token.fontSize,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -7,16 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useApp } from '../../hooks';
|
||||
import { SchemaSettingOptions } from '../types';
|
||||
import React from 'react';
|
||||
import { SchemaSettingsWrapper } from '../components';
|
||||
import { SchemaSettingsProps } from '../../../schema-settings';
|
||||
import { Schema } from '@formily/json-schema';
|
||||
import { GeneralField } from '@formily/core';
|
||||
import { Schema } from '@formily/json-schema';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Designable } from '../../../schema-component';
|
||||
import { SchemaSettingsProps } from '../../../schema-settings';
|
||||
import { useApp } from '../../hooks';
|
||||
import { SchemaSettingsWrapper } from '../components';
|
||||
import { SchemaSettings } from '../SchemaSettings';
|
||||
import { SchemaSettingOptions } from '../types';
|
||||
|
||||
type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> &
|
||||
Omit<SchemaSettingsProps, 'title' | 'children'> & {
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
|
||||
export interface SchemaSettingOptions<T = {}> {
|
||||
name: string;
|
||||
mode?: 'inline' | 'dropdown';
|
||||
Component?: ComponentType<T>;
|
||||
componentProps?: T;
|
||||
items: SchemaSettingsItemType[];
|
||||
|
@ -63,7 +63,7 @@ export abstract class CollectionTemplate {
|
||||
/** UI configurable CollectionOptions parameters (fields for adding or editing Collection forms) */
|
||||
configurableProperties?: Record<string, ISchema>;
|
||||
/** Available field types for the current template */
|
||||
availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude;
|
||||
availableFieldInterfaces?: AvailableFieldInterfacesInclude & AvailableFieldInterfacesExclude;
|
||||
/** Whether it is a divider */
|
||||
divider?: boolean;
|
||||
/** Template description */
|
||||
|
@ -44,7 +44,7 @@ export const useMenuItem = () => {
|
||||
const renderItems = useRef<() => JSX.Element>(null);
|
||||
const shouldRerender = useRef(false);
|
||||
|
||||
const Component = useCallback(({ limitCount }) => {
|
||||
const Component = useCallback(({ limitCount }: { limitCount?: number }) => {
|
||||
if (!shouldRerender.current) {
|
||||
return null;
|
||||
}
|
||||
|
@ -7,13 +7,17 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, lazy as ReactLazy } from 'react';
|
||||
import React, { lazy as ReactLazy } from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { get } from 'lodash';
|
||||
import { useImported, loadableResource } from 'react-imported-component';
|
||||
|
||||
export const LAZY_COMPONENT_KEY = Symbol('LAZY_COMPONENT_KEY');
|
||||
|
||||
type LazyComponentType<M extends Record<string, any>, K extends keyof M> = {
|
||||
[P in K]: M[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazily loads a React component or multiple components.
|
||||
*
|
||||
@ -31,16 +35,14 @@ export const LAZY_COMPONENT_KEY = Symbol('LAZY_COMPONENT_KEY');
|
||||
* @param {...K[]} componentNames - The names of the components to be lazy-loaded from the module.
|
||||
* @returns {Record<K, React.LazyExoticComponent<M[K]>>} An object containing the lazy-loaded components.
|
||||
*/
|
||||
export function lazy<M extends ComponentType<any>>(
|
||||
factory: () => Promise<{ default: M }>,
|
||||
): React.LazyExoticComponent<M>;
|
||||
export function lazy<M extends Record<'default', any>>(factory: () => Promise<M>): M['default'];
|
||||
|
||||
export function lazy<M extends Record<string, any>, K extends keyof M & string>(
|
||||
export function lazy<M extends Record<string, any>, K extends keyof M = keyof M>(
|
||||
factory: () => Promise<M>,
|
||||
...componentNames: K[]
|
||||
): Record<K, React.LazyExoticComponent<M[K]>>;
|
||||
): LazyComponentType<M, K>;
|
||||
|
||||
export function lazy<M extends Record<string, any>, K extends keyof M & string>(
|
||||
export function lazy<M extends Record<string, any>, K extends keyof M>(
|
||||
factory: () => Promise<M>,
|
||||
...componentNames: K[]
|
||||
) {
|
||||
@ -73,14 +75,14 @@ export function lazy<M extends Record<string, any>, K extends keyof M & string>(
|
||||
};
|
||||
}),
|
||||
);
|
||||
acc[name] = (props) => (
|
||||
acc[name] = ((props) => (
|
||||
<React.Suspense fallback={<Spin />}>
|
||||
<LazyComponent {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
)) as M[K];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<K, React.ComponentType<any>>,
|
||||
{} as LazyComponentType<M, K>,
|
||||
);
|
||||
}
|
||||
|
||||
|
861
packages/core/client/src/locale/it-IT.json
Normal file
861
packages/core/client/src/locale/it-IT.json
Normal file
@ -0,0 +1,861 @@
|
||||
{
|
||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "Visualizza <1><0>10</0><1>20</1><2>50</2><3>100</3></1> articoli per pagina",
|
||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "Soddisfa<1><0>Tutte</0><1>Qualsiasi</1></1>condizioni nel gruppo",
|
||||
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "Apri in<1><0>Modale</0><1>Cassetto</1><2>Finestra</2></1>",
|
||||
"{{count}} filter items": "{{Count}} filtri elementi",
|
||||
"{{count}} more items": "{{Count}} altri elementi",
|
||||
"Total {{count}} items": "{{count}} elementi totali",
|
||||
"Today": "Oggi",
|
||||
"Yesterday": "Ieri",
|
||||
"Tomorrow": "Domani",
|
||||
"Month": "Mese",
|
||||
"Week": "Settimana",
|
||||
"This week": "Questa settimana",
|
||||
"This month": "Questo mese",
|
||||
"This year": "Quest'anno",
|
||||
"Next year": "Anno prossimo",
|
||||
"Last week": "Settimana scorsa",
|
||||
"Next week": "Prossima settimana",
|
||||
"Last month": "Mese scorso",
|
||||
"Next month": "Mese prossimo",
|
||||
"Last quarter": "Ultimo trimestre",
|
||||
"This quarter": "Questo trimestre",
|
||||
"Next quarter": "Prossimo trimestre",
|
||||
"Last year": "Anno scorso",
|
||||
"Last 7 days": "Ultimi 7 giorni",
|
||||
"Last 30 days": "Ultimi 30 giorni",
|
||||
"Last 90 days": "Ultimi 90 giorni",
|
||||
"Next 7 days": "Prossimi 7 giorni",
|
||||
"Next 30 days": "Prossimi 30 giorni",
|
||||
"Next 90 days": "Prossimi 90 giorni",
|
||||
"Work week": "Settimana lavorativa",
|
||||
"Day": "Giorno",
|
||||
"Agenda": "Agenda",
|
||||
"Date": "Data",
|
||||
"Time": "Tempo",
|
||||
"Event": "Evento",
|
||||
"None": "Nessuno",
|
||||
"Unconnected": "Non collegato",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"System title": "Titolo del sistema",
|
||||
"Settings": "Impostazioni",
|
||||
"Logo": "Logo",
|
||||
"Add menu item": "Aggiungi voce di menu",
|
||||
"Page": "Pagina",
|
||||
"Name": "Nome",
|
||||
"Icon": "Icona",
|
||||
"Group": "Gruppo",
|
||||
"Link": "Collegamento",
|
||||
"Save conditions": "Salva condizioni",
|
||||
"Edit menu item": "Modifica voce di menu",
|
||||
"Move to": "Passa a",
|
||||
"Insert left": "Inserisci a sinistra",
|
||||
"Insert right": "Inserire a destra",
|
||||
"Insert inner": "Inserire dentro",
|
||||
"Delete": "Eliminare",
|
||||
"Disassociate": "Dissociare",
|
||||
"Disassociate record": "Dissociare il record",
|
||||
"Are you sure you want to disassociate it?": "Sei sicuro di voler dissociare?",
|
||||
"UI editor": "Editor UI",
|
||||
"Collection": "Raccolta",
|
||||
"Collection selector": "Selettore di raccolta",
|
||||
"Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Fornire alcune raccolte come opzioni per gli utenti, in genere utilizzati negli scenari polimorfici o ereditari",
|
||||
"Collections & Fields": "Raccolte e campi",
|
||||
"All collections": "Tutte le raccolte",
|
||||
"Add category": "Aggiungi categoria",
|
||||
"Enable child collections": "Abilita raccolte figlie",
|
||||
"Allow adding records to the current collection": "Consenti l'aggiunta di record alla raccolta corrente",
|
||||
"Delete category": "Elimina categoria",
|
||||
"Edit category": "Modifica categoria",
|
||||
"Collection category": "Categoria raccolta",
|
||||
"Collection template": "Modello raccolta",
|
||||
"Sort": "Ordina",
|
||||
"Categories": "Categorie",
|
||||
"Visible": "Visibile",
|
||||
"Read only": "Solo lettura",
|
||||
"Easy reading": "Lettura facile",
|
||||
"Hidden": "Nascosto",
|
||||
"Hidden(reserved value)": "Nascosto (valore riservato)",
|
||||
"Not required": "Non richiesto",
|
||||
"Value": "Valore",
|
||||
"Disabled": "Disabilitato",
|
||||
"Enabled": "Abilitato",
|
||||
"Problematic": "Problematico",
|
||||
"Setting": "Impostazioni",
|
||||
"On": "Acceso",
|
||||
"Off": "Spento",
|
||||
"Empty": "Vuoto",
|
||||
"Linkage rule": "Regola di collegamento",
|
||||
"Linkage rules": "Regole di collegamento",
|
||||
"Condition": "Condizione",
|
||||
"Properties": "Proprietà",
|
||||
"Add linkage rule": "Aggiungi regola di collegamento",
|
||||
"Add property": "Aggiungi proprietà",
|
||||
"Category name": "Nome della categoria",
|
||||
"Roles & Permissions": "Ruoli e autorizzazioni",
|
||||
"Edit profile": "Modifica profilo",
|
||||
"Change password": "Cambia password",
|
||||
"Old password": "Vecchia password",
|
||||
"New password": "Nuova password",
|
||||
"Switch role": "Cambia ruolo",
|
||||
"Super admin": "Super Admin",
|
||||
"Language": "Lingua",
|
||||
"Allow sign up": "Consenti iscrizione",
|
||||
"Enable SMS authentication": "Abilita autenticazione SMS",
|
||||
"Sign out": "Disconnessione",
|
||||
"Cancel": "Annulla",
|
||||
"Submit": "Invia",
|
||||
"Close": "Chiudi",
|
||||
"Set the data scope": "Imposta l'ambito dei dati",
|
||||
"Set data loading mode": "Imposta modalità di caricamento dei dati",
|
||||
"Load all data when filter is empty": "Carica tutti i dati quando il filtro è vuoto",
|
||||
"Do not load data when filter is empty": "Non caricare i dati quando il filtro è vuoto",
|
||||
"Data loading mode": "Modalità di caricamento dei dati",
|
||||
"Data blocks": "Blocchi dati",
|
||||
"Filter blocks": "Blocchi filtro",
|
||||
"Table": "Tabella",
|
||||
"Table OID(Inheritance)": "Tabella OID (eredità)",
|
||||
"Form": "Modulo",
|
||||
"List": "Lista",
|
||||
"Grid Card": "Scheda griglia",
|
||||
"pixels": "pixel",
|
||||
"Screen size": "Dimensione dello schermo",
|
||||
"Display title": "Visualizza titolo",
|
||||
"Set the count of columns displayed in a row": "Imposta il conteggio delle colonne visualizzate in una riga",
|
||||
"Column": "Colonna",
|
||||
"Phone device": "Telefono",
|
||||
"Tablet device": "Tablet",
|
||||
"Desktop device": "Desktop",
|
||||
"Large screen device": "Schermo di grandi dimensioni",
|
||||
"Collapse": "Collassa",
|
||||
"Select data source": "Seleziona origine dati",
|
||||
"Calendar": "Calendario",
|
||||
"Delete events": "Elimina eventi",
|
||||
"This event": "Questo evento",
|
||||
"This and following events": "Questo e seguenti eventi",
|
||||
"All events": "Tutti gli eventi",
|
||||
"Delete this event?": "Eliminare questo evento?",
|
||||
"Delete Event": "Elimina evento",
|
||||
"Kanban": "Kanban",
|
||||
"Gantt": "Gantt",
|
||||
"Create gantt block": "Crea blocco Gantt",
|
||||
"Progress field": "Campo avanzamento",
|
||||
"Time scale": "Scala del tempo",
|
||||
"Hour": "Ora",
|
||||
"Quarter of day": "Quarto del giorno",
|
||||
"Half of day": "Metà del giorno",
|
||||
"Year": "Anno",
|
||||
"QuarterYear": "Quarto dell' anno",
|
||||
"Select grouping field": "Seleziona il campo di raggruppamento",
|
||||
"Media": "Media",
|
||||
"Markdown": "Markdown",
|
||||
"Wysiwyg": "Wysiwyg",
|
||||
"Chart blocks": "Blocchi grafici",
|
||||
"Column chart": "Grafico a colonne",
|
||||
"Bar chart": "Grafico a barre",
|
||||
"Line chart": "Grafico a linee",
|
||||
"Pie chart": "Grafico a torta",
|
||||
"Area chart": "Grafico ad area",
|
||||
"Other chart": "Altro grafico",
|
||||
"Other blocks": "Altri blocchi",
|
||||
"In configuration": "In configurazione",
|
||||
"Chart title": "Titolo grafico",
|
||||
"Chart type": "Tipo grafico",
|
||||
"Chart config": "Configurazione grafico",
|
||||
"Templates": "Modelli",
|
||||
"Select template": "Seleziona modello",
|
||||
"Action logs": "Registri eventi",
|
||||
"Create template": "Crea modello",
|
||||
"Edit markdown": "Modifica Markdown",
|
||||
"Add block": "Aggiungi blocco",
|
||||
"Add new": "Aggiungi nuovo",
|
||||
"Add record": "Aggiungi record",
|
||||
"Add child": "Aggiungi figlio",
|
||||
"Collapse all": "Collassare tutto",
|
||||
"Expand all": "Espandere tutto",
|
||||
"Expand/Collapse": "Espandere/Collassare",
|
||||
"Default collapse": "Collassa di default",
|
||||
"Tree table": "Tabella ad albero",
|
||||
"Custom field display name": "Nome visualizzato campo personalizzato ",
|
||||
"Display fields": "Visualizza campi",
|
||||
"Edit record": "Modifica record",
|
||||
"Delete menu item": "Elimina voce di menu",
|
||||
"Add page": "Aggiungi pagina",
|
||||
"Add group": "Aggiungi gruppo",
|
||||
"Add link": "Aggiungi link",
|
||||
"Insert above": "Inserisci sopra",
|
||||
"Insert below": "Inserisci sotto",
|
||||
"Save": "Salva",
|
||||
"Delete block": "Elimina blocco",
|
||||
"Are you sure you want to delete it?": "Sei sicuro di volerlo eliminare?",
|
||||
"This is a demo text, **supports Markdown syntax**.": "Questo è un testo demo, ** supporta la sintassi di Markdown **.",
|
||||
"Filter": "Filtro",
|
||||
"Connect data blocks": "Collega blocchi di dati",
|
||||
"Action type": "Tipo di operazione",
|
||||
"Actions": "Operazioni",
|
||||
"Insert": "Inserisci",
|
||||
"Insert if not exists": "Inserisci se non esiste",
|
||||
"Insert if not exists, or update": "Inserisci se non esiste o aggiorna",
|
||||
"Determine whether a record exists by the following fields": "Determina se un record esiste dai seguenti campi",
|
||||
"Update": "Aggiorna",
|
||||
"Update record": "Aggiorna record",
|
||||
"View": "Visualizza",
|
||||
"View record": "Visualizza record",
|
||||
"Refresh": "Refresh",
|
||||
"Data changes": "Modifiche ai dati",
|
||||
"Field name": "Nome campo",
|
||||
"Before change": "Prima delle modifiche",
|
||||
"After change": "Dopo le modifiche",
|
||||
"Delete record": "Elimina record",
|
||||
"Delete collection": "Elimina raccolta",
|
||||
"Create collection": "Crea raccolta",
|
||||
"Collection display name": "Nome visualizzato raccolta",
|
||||
"Collection name": "Nome raccolta",
|
||||
"Inherits": "Eredita",
|
||||
"Primary key, unique identifier, self growth": "Chiave primaria, identificatore univoco, auto-incremento",
|
||||
"Store the creation user of each record": "Memorizza l'utente della creazione di ogni record",
|
||||
"Store the last update user of each record": "Memorizza l'ultimo utente di aggiornamento di ogni record",
|
||||
"Store the creation time of each record": "Memorizza l'orario di creazione di ogni record",
|
||||
"Store the last update time of each record": "Memorizza l'ultimo orario di aggiornamento di ogni record",
|
||||
"More options": "Più opzioni",
|
||||
"Records can be sorted": "I record possono essere ordinati",
|
||||
"Calendar collection": "Raccolta calendario",
|
||||
"General collection": "Raccolta generale",
|
||||
"Connect to database view": "Connetti alla vista del database",
|
||||
"Sync from database": "Sincronizza dal database",
|
||||
"Source collections": "Sorgente raccolte",
|
||||
"Field source": "Sorgente campo",
|
||||
"Preview": "Anteprima",
|
||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Generato casualmente e può essere modificato. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
|
||||
"Edit": "Modifica",
|
||||
"Edit collection": "Modifica raccolta",
|
||||
"Configure fields": "Configura campi",
|
||||
"Configure columns": "Configura colonne",
|
||||
"Edit field": "Modifica campo",
|
||||
"Override": "Forza",
|
||||
"Override field": "Forza campo",
|
||||
"Configure fields of {{title}}": "Configura campi di {{title}}",
|
||||
"Association fields filter": "Filtro associazione campi",
|
||||
"PK & FK fields": "Campi PK e FK",
|
||||
"Association fields": "Campi associazione",
|
||||
"Choices fields": "Campi scelte",
|
||||
"System fields": "Campi di sistema",
|
||||
"General fields": "Campi generali",
|
||||
"Inherited fields": "Campi ereditati",
|
||||
"Parent collection fields": "Campi raccolta padre",
|
||||
"Basic": "Di base",
|
||||
"Single line text": "Testo a riga singola",
|
||||
"Long text": "Testo lungo",
|
||||
"Phone": "Telefono",
|
||||
"Email": "E-mail",
|
||||
"Number": "Numero",
|
||||
"Integer": "Intero",
|
||||
"Percent": "Percentuale",
|
||||
"Password": "Password",
|
||||
"Advanced type": "Avanzato",
|
||||
"Formula": "Formula",
|
||||
"Formula description": "Calcola un valore in ciascun record in base ad altri campi nello stesso record.",
|
||||
"Choices": "Scelte",
|
||||
"Checkbox": "Casella di controllo",
|
||||
"Single select": "Selezione singola",
|
||||
"Multiple select": "Selezione multipla",
|
||||
"Radio group": "Gruppo radio",
|
||||
"Checkbox group": "Gruppo cassella di controllo",
|
||||
"China region": "Regione cinese",
|
||||
"Date & Time": "Data e ora",
|
||||
"Datetime": "DateTime",
|
||||
"Relation": "Relazione",
|
||||
"Link to": "Collegamento a",
|
||||
"Link to description": "Utilizzato per creare relazioni tra raccolte in modo rapido e compatibile con gli scenari più comuni. Adatto per un uso da non sviluppatore. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta di destinazione. Una volta creato, genererà contemporaneamente i campi associati dell'attuale raccolta nella raccolta di destinazione.",
|
||||
"Sub-table": "Sotto-tabella",
|
||||
"Sub-details": "Sotto-dettagli",
|
||||
"Sub-form(Popover)": "Sotto-modulo (Popover)",
|
||||
"System info": "Informazioni di sistema",
|
||||
"Created at": "Creato il",
|
||||
"Last updated at": "Ultimo aggiornamento il",
|
||||
"Created by": "Creato da",
|
||||
"Last updated by": "Ultimo aggiornamento da",
|
||||
"Add field": "Aggiungi campo",
|
||||
"Field display name": "Nome visualizzazione campo",
|
||||
"Field type": "Tipo campo",
|
||||
"Field interface": "Interfaccia campo",
|
||||
"Date format": "Formato data",
|
||||
"Year/Month/Day": "Anno/Mese/Giorno",
|
||||
"Year-Month-Day": "Anno-Mese-Giorno",
|
||||
"Day/Month/Year": "Giorno/Mese/Anno",
|
||||
"Show time": "Mostra orario",
|
||||
"Time format": "Formato tempo",
|
||||
"12 hour": "12 ore",
|
||||
"24 hour": "24 ore",
|
||||
"Relationship type": "Tipo di relazione",
|
||||
"Inverse relationship type": "Tipo di relazione inversa",
|
||||
"Source collection": "Raccolta sorgente",
|
||||
"Source key": "Chiave sorgente",
|
||||
"Target collection": "Raccolta di destinazione",
|
||||
"Through collection": "Attraverso la raccolta",
|
||||
"Target key": "Chiave di destinazione",
|
||||
"Foreign key": "Chiave esterna",
|
||||
"One to one": "Uno a uno",
|
||||
"One to many": "Uno a molti",
|
||||
"Many to one": "Molti a uno",
|
||||
"Many to many": "Molti a molti",
|
||||
"Foreign key 1": "Chiave esterna 1",
|
||||
"Foreign key 2": "Chiave esterna 2",
|
||||
"One to one description": "Usato per creare relazioni one-to-one. Ad esempio, un utente ha un profilo.",
|
||||
"One to many description": "Utilizzato per creare una relazione da uno a molti. Ad esempio, un paese avrà molte città e una città può essere solo in un paese. Se presente come campo, è una sotto-tabella che mostra i record della raccolta associata. Se creato, un campo molti-a-uno viene generato automaticamente nella raccolta associata.",
|
||||
"Many to one description": "Utilizzato per creare relazioni molti-a-uno. Ad esempio, una città può appartenere a un solo paese e un paese può avere molte città. Se presente come campo, è una selezione a discesa utilizzata per selezionare il record dalla raccolta associata. Una volta creato, un campo da uno a molti viene generato automaticamente nella raccolta associata.",
|
||||
"Many to many description": "Utilizzato per creare relazioni molti-a-molti. Ad esempio, uno studente avrà molti insegnanti e un insegnante avrà molti studenti. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta associata.",
|
||||
"Generated automatically if left blank": "Generato automaticamente se lasciato vuoto",
|
||||
"Display association fields": "Visualizza campi di associazione",
|
||||
"Display field title": "Visualizza titolo campo",
|
||||
"Field component": "Componente campo",
|
||||
"Allow multiple": "Consenti multipli",
|
||||
"Quick upload": "Caricamento rapido",
|
||||
"Select file": "Seleziona file",
|
||||
"Subtable": "Sotto-tabella",
|
||||
"Sub-form": "Sotto-modulo",
|
||||
"Field mode": "Modalità campo",
|
||||
"Allow add new data": "Consenti aggiunta nuovi dati",
|
||||
"Record picker": "Record Picker",
|
||||
"Toggles the subfield mode": "Attiva la modalità Subfield",
|
||||
"Selector mode": "Modalità selettore",
|
||||
"Subtable mode": "Modalità sotto-tabella",
|
||||
"Subform mode": "Modalità sotto-modulo",
|
||||
"Edit block title": "Modifica titolo blocco",
|
||||
"Block title": "Titolo blocco",
|
||||
"Pattern": "Modello",
|
||||
"Operator": "Operatore",
|
||||
"Editable": "Modificabile",
|
||||
"Readonly": "Solo lettura",
|
||||
"Easy-reading": "Lettura facile",
|
||||
"Add filter": "Aggiungi filtro",
|
||||
"Add filter group": "Aggiungi gruppo di filtri",
|
||||
"Comparision": "Confronto",
|
||||
"is": "è",
|
||||
"is not": "non lo è",
|
||||
"contains": "contiene",
|
||||
"does not contain": "non contiene",
|
||||
"starts with": "inizia con",
|
||||
"not starts with": "non inizia con",
|
||||
"ends with": "termina con",
|
||||
"not ends with": "non termina con",
|
||||
"is empty": "è vuoto",
|
||||
"is not empty": "non è vuoto",
|
||||
"Edit chart": "Modifica grafico",
|
||||
"Add text": "Aggiungi testo",
|
||||
"Filterable fields": "Campi filtrabili",
|
||||
"Edit button": "Pulsante Modifica",
|
||||
"Hide": "Nascondi",
|
||||
"Enable actions": "Abilita operazioni",
|
||||
"Import": "Importa",
|
||||
"Export": "Esporta",
|
||||
"Customize": "Personalizza",
|
||||
"Custom": "Personalizzato",
|
||||
"Function": "Funzione",
|
||||
"Popup form": "Modulo Popup",
|
||||
"Flexible popup": "Popup flessibile",
|
||||
"Configure actions": "Configura operazioni",
|
||||
"Display order number": "Visualizza numero ordinamento",
|
||||
"Enable drag and drop sorting": "Abilita l'ordinamento con drag and drop",
|
||||
"Triggered when the row is clicked": "Attivato quando si fa clic sulla riga",
|
||||
"Add tab": "Aggiungi scheda",
|
||||
"Disable tabs": "Disabilita le schede",
|
||||
"Details": "Dettagli",
|
||||
"Edit form": "Modifica modulo",
|
||||
"Create form": "Crea modulo",
|
||||
"Form (Edit)": "Modulo (modifica)",
|
||||
"Form (Add new)": "Modulo (aggiungi nuovo)",
|
||||
"Edit tab": "Modifica scheda",
|
||||
"Relationship blocks": "Blocchi di relazione",
|
||||
"Select record": "Seleziona Record",
|
||||
"Display name": "Visualizza nome",
|
||||
"Select icon": "Seleziona icona",
|
||||
"Custom column name": "Nome colonna personalizzato",
|
||||
"Edit description": "Modifica descrizione",
|
||||
"Required": "Richiesto",
|
||||
"Unique": "Unico",
|
||||
"Primary": "Primario",
|
||||
"Auto increment": "Incremento automatico",
|
||||
"Label field": "Campo etichetta",
|
||||
"Default is the ID field": "L'impostazione predefinita è il campo ID",
|
||||
"Set default sorting rules": "Imposta le regole di ordinamento predefinite",
|
||||
"Set validation rules": "Imposta le regole di convalida",
|
||||
"Max length": "Lunghezza massima",
|
||||
"Min length": "Lunghezza minima",
|
||||
"Maximum": "Massimo",
|
||||
"Minimum": "Minimo",
|
||||
"Max length must greater than min length": "La lunghezza massima deve essere maggiore della lunghezza minima",
|
||||
"Min length must less than max length": "La lunghezza minima deve essere inferiore della lunghezza massima",
|
||||
"Maximum must greater than minimum": "Il massimo deve essere maggiore del minimo",
|
||||
"Minimum must less than maximum": "Il minimo deve essere minore del massimo",
|
||||
"Validation rule": "Regola di convalida",
|
||||
"Add validation rule": "Aggiungi regola di convalida",
|
||||
"Format": "Formato",
|
||||
"Regular expression": "Espressione regolare",
|
||||
"Error message": "Messaggio di errore",
|
||||
"Length": "Lunghezza",
|
||||
"The field value cannot be greater than ": "Il valore del campo non può essere maggiore di",
|
||||
"The field value cannot be less than ": "Il valore del campo non può essere inferiore a",
|
||||
"The field value is not an integer number": "Il valore del campo non è un numero intero",
|
||||
"Set default value": "Imposta valore predefinito",
|
||||
"Default value": "Valore predefinito",
|
||||
"is before": "è prima",
|
||||
"is after": "è dopo",
|
||||
"is on or after": "a partire dal",
|
||||
"is on or before": "entro il",
|
||||
"is between": "è tra",
|
||||
"Upload": "Upload",
|
||||
"Select level": "Seleziona livello",
|
||||
"Province": "Provincia",
|
||||
"City": "Città",
|
||||
"Area": "Zona",
|
||||
"Street": "Strada",
|
||||
"Village": "Villaggio",
|
||||
"Must select to the last level": "Deve selezionare all'ultimo livello",
|
||||
"Move {{title}} to": "Sposta {{title}} a",
|
||||
"Target position": "Posizione di destinazione",
|
||||
"After": "Dopo",
|
||||
"Before": "Prima",
|
||||
"Add {{type}} before \"{{title}}\"": "Aggiungi {{type}} prima di \"{{title}}\"",
|
||||
"Add {{type}} after \"{{title}}\"": "Aggiungi {{type}} dopo \"{{title}}\"",
|
||||
"Add {{type}} in \"{{title}}\"": "Aggiungi {{type}} in \"{{title}}\"",
|
||||
"Original name": "Nome originale",
|
||||
"Custom name": "Nome personalizzato",
|
||||
"Custom Title": "Titolo personalizzato",
|
||||
"Options": "Opzioni",
|
||||
"Option value": "Valore opzione",
|
||||
"Option label": "Etichetta opzione",
|
||||
"Color": "Colore",
|
||||
"Background Color": "Colore sfondo",
|
||||
"Text Align": "Allineamento testo",
|
||||
"Add option": "Aggiungi opzione",
|
||||
"Related collection": "Raccolta correlata",
|
||||
"Allow linking to multiple records": "Consenti il collegamento a più record",
|
||||
"Allow uploading multiple files": "Consenti il caricamento di più file",
|
||||
"Configure calendar": "Configura calendario",
|
||||
"Title field": "Campo titolo",
|
||||
"Custom title": "Titolo personalizzato",
|
||||
"Daily": "Quotidiano",
|
||||
"Weekly": "Settimanale",
|
||||
"Monthly": "Mensile",
|
||||
"Yearly": "Annuale",
|
||||
"Repeats": "Ripeti",
|
||||
"Show lunar": "Mostra lunare",
|
||||
"Start date field": "Campo data di inizio",
|
||||
"End date field": "Campo data di fine",
|
||||
"Navigate": "Naviga",
|
||||
"Title": "Titolo",
|
||||
"Description": "Descrizione",
|
||||
"Select view": "Seleziona vista",
|
||||
"Reset": "Reset",
|
||||
"Importable fields": "Campi importabili",
|
||||
"Exportable fields": "Campi esportabili",
|
||||
"Saved successfully": "Salvataggio riuscito",
|
||||
"Nickname": "Soprannome",
|
||||
"Sign in": "Registrazione",
|
||||
"Sign in via account": "Accedi tramite account",
|
||||
"Sign in via phone": "Accedi via telefono",
|
||||
"Create an account": "Crea un account",
|
||||
"Sign up": "Iscrizione",
|
||||
"Confirm password": "Conferma password",
|
||||
"Log in with an existing account": "Accedi con account esistente",
|
||||
"Signed up successfully. It will jump to the login page.": "Registrazione riuscita. Reindirizzamento alla pagina di accesso.",
|
||||
"Password mismatch": "Password non corretta",
|
||||
"Users": "Utenti",
|
||||
"Verification code": "Codice di verifica",
|
||||
"Send code": "Invia codice",
|
||||
"Retry after {{count}} seconds": "Riprova dopo {{count}} secondi",
|
||||
"Roles": "Ruoli",
|
||||
"Add role": "Aggiungi ruolo",
|
||||
"Role name": "Nome ruolo",
|
||||
"Configure": "Configura",
|
||||
"Configure permissions": "Configura permessi",
|
||||
"Edit role": "Modifica ruolo",
|
||||
"Action permissions": "Permessi su operazioni",
|
||||
"Menu permissions": "Permessi su menu",
|
||||
"Menu item name": "Nome voce di menu",
|
||||
"Allow access": "Consenti accesso",
|
||||
"Action name": "Nome operazione",
|
||||
"Allow action": "Consenti operazione",
|
||||
"Action scope": "Ambito operazione",
|
||||
"Operate on new data": "Operare su nuovi dati",
|
||||
"Operate on existing data": "Operare su dati esistenti",
|
||||
"Yes": "Si",
|
||||
"No": "No",
|
||||
"Red": "Rosso",
|
||||
"Magenta": "Magenta",
|
||||
"Volcano": "Vulcano",
|
||||
"Orange": "Arancione",
|
||||
"Gold": "Oro",
|
||||
"Lime": "Lime",
|
||||
"Green": "Verde",
|
||||
"Cyan": "Ciano",
|
||||
"Blue": "Blu",
|
||||
"Geek blue": "Geek Blue",
|
||||
"Purple": "Viola",
|
||||
"Default": "Predefinito",
|
||||
"Add card": "Aggiungi scheda",
|
||||
"edit title": "modifica titolo",
|
||||
"Turn pages": "Volta pagine",
|
||||
"Others": "Altri",
|
||||
"Other records": "Altri record",
|
||||
"Save as template": "Salva come modello",
|
||||
"Save as block template": "Salva come modello blocco",
|
||||
"Block templates": "Modelli blocco",
|
||||
"Block template": "Modello blocco",
|
||||
"Convert reference to duplicate": "Converti il riferimento a duplicato",
|
||||
"Template name": "Nome modello",
|
||||
"Block type": "Tipo blocco",
|
||||
"No blocks to connect": "Nessun blocco per connettersi",
|
||||
"Action column": "Colonna operazioni",
|
||||
"Records per page": "Record per pagina",
|
||||
"(Fields only)": "(Solo campi)",
|
||||
"Button title": "Titolo pulsante",
|
||||
"Button icon": "Icona pulsante",
|
||||
"Submitted successfully": "Invio riuscito",
|
||||
"Operation succeeded": "L'operazione è riuscita",
|
||||
"Operation failed": "Operazione non riuscita",
|
||||
"Open mode": "Modalità aperta",
|
||||
"Popup size": "Dimensione popup",
|
||||
"Small": "Piccolo",
|
||||
"Middle": "Medio",
|
||||
"Large": "Grande",
|
||||
"Size": "Misura",
|
||||
"Oversized": "Oversize",
|
||||
"Auto": "Auto",
|
||||
"Object Fit": "Adattato all'oggetto",
|
||||
"Cover": "Cover",
|
||||
"Fill": "Riempi",
|
||||
"Contain": "Contiene",
|
||||
"Scale Down": "Ridimensiona",
|
||||
"Menu item title": "Titolo voce di menu",
|
||||
"Menu item icon": "Icona voce di menu",
|
||||
"Target": "Destinazione",
|
||||
"Position": "Posizione",
|
||||
"Insert before": "Inserire prima",
|
||||
"Insert after": "Inserire dopo",
|
||||
"UI Editor": "Editor UI",
|
||||
"ASC": "Asc",
|
||||
"DESC": "Desc",
|
||||
"Add sort field": "Aggiungi campo di ordinamento",
|
||||
"ID": "ID",
|
||||
"Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identificatore per l'utilizzo del programma. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
|
||||
"Drawer": "Cassetto",
|
||||
"Dialog": "Dialogo",
|
||||
"Delete action": "Elimina operazione",
|
||||
"Custom column title": "Titolo colonna personalizzata",
|
||||
"Column title": "Titolo colonna",
|
||||
"Original title: ": "Titolo originale: ",
|
||||
"Delete table column": "Elimina colonna della tabella",
|
||||
"Skip required validation": "Salta convalida richiesta",
|
||||
"Form values": "Valori modulo",
|
||||
"Fields values": "Valori campi",
|
||||
"The field has been deleted": "Il campo è stato eliminato",
|
||||
"When submitting the following fields, the saved values are": "Quando si inviano i seguenti campi, i valori salvati sono",
|
||||
"After successful submission": "Dopo una invio riuscito",
|
||||
"Then": "Poi",
|
||||
"Stay on current page": "Resta sulla pagina corrente",
|
||||
"Redirect to": "Reindirizza a",
|
||||
"Save action": "Salva operazione",
|
||||
"Exists": "Esiste",
|
||||
"Add condition": "Aggiungi condizione",
|
||||
"Add condition group": "Aggiungi gruppo di condizioni",
|
||||
"exists": "esiste",
|
||||
"not exists": "non esiste",
|
||||
"Style": "Stile",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
"≥": "≥",
|
||||
"<": "<",
|
||||
"≤": "≤",
|
||||
"Role UID": "Ruolo UID",
|
||||
"Precision": "Precisione",
|
||||
"Formula mode": "Modalità formula",
|
||||
"Expression": "Espressione",
|
||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, () per calcolare, input @ per aprire le variabili campo.",
|
||||
"Formula error.": "Errore formula.",
|
||||
"Rich Text": "Testo ricco",
|
||||
"Junction collection": "Raccolta giunzione",
|
||||
"Leave it blank, unless you need a custom intermediate table": "Lascialo vuoto, a meno che tu non abbia bisogno di una tabella intermedia personalizzata",
|
||||
"Fields": "Campi",
|
||||
"Edit field title": "Modifica titolo campo",
|
||||
"Field title": "Titolo campo",
|
||||
"Original field title: ": "Titolo campo originale:",
|
||||
"Edit tooltip": "Modifica suggerimento",
|
||||
"Delete field": "Elimina campo",
|
||||
"Select collection": "Seleziona raccolta",
|
||||
"Blank block": "Blocco vuoto",
|
||||
"Duplicate template": "Modello duplicato",
|
||||
"Reference template": "Modello di riferimento",
|
||||
"Create calendar block": "Crea blocco calendario",
|
||||
"Create kanban block": "Crea blocco kanban",
|
||||
"Grouping field": "Campo di raggruppamento",
|
||||
"Single select and radio fields can be used as the grouping field": "I campi di selezione singoli e radio possono essere utilizzati come campo di raggruppamento",
|
||||
"Tab name": "Nome della scheda",
|
||||
"Current record blocks": "Blocchi record attuale",
|
||||
"Popup message": "Messaggio popup",
|
||||
"Delete role": "Elimina il ruolo",
|
||||
"Role display name": "Nome visualizzato ruolo",
|
||||
"Default role": "Ruolo predefinito",
|
||||
"All collections use general action permissions by default; permission configured individually will override the default one.": "Tutte le raccolte utilizzano i permessi di operazioni generali per impostazione predefinita; I permessi configurati individualmente sovrascriveranno quelli predefiniti.",
|
||||
"Allows configuration of the whole system, including UI, collections, permissions, etc.": "Consente la configurazione dell'intero sistema, tra cui interfaccia utente, raccolte, permessi, ecc.",
|
||||
"New menu items are allowed to be accessed by default.": "È possibile accedere a nuove voci di menu per impostazione predefinita.",
|
||||
"Global permissions": "Permessi globali",
|
||||
"General permissions": "Permessi generali",
|
||||
"Global action permissions": "Permessi operazioni globali",
|
||||
"General action permissions": "Permessi operazioni generali",
|
||||
"Plugin settings permissions": "Permessi impostazioni plugin",
|
||||
"Allow to desgin pages": "Consenti progettazione pagine",
|
||||
"Allow to manage plugins": "Consenti gestione plugin",
|
||||
"Allow to configure plugins": "Consenti configurazione plugin",
|
||||
"Allows to configure interface": "Consente di configurare l'interfaccia",
|
||||
"Allows to install, activate, disable plugins": "Consente di installare, attivare, disabilitare i plugin",
|
||||
"Allows to configure plugins": "Consente di configurare i plugin",
|
||||
"Action display name": "Nome visualizzazione azione",
|
||||
"Allow": "Permetti",
|
||||
"Data scope": "Ambito dei dati",
|
||||
"Action on new records": "Operazione su nuovi record",
|
||||
"Action on existing records": "Operazione su record esistenti",
|
||||
"All records": "Tutti i record",
|
||||
"Own records": "Record propri",
|
||||
"Permission policy": "Policy di autorizzazione",
|
||||
"Individual": "Individuale",
|
||||
"General": "Generale",
|
||||
"Accessible": "Accessibile",
|
||||
"Configure permission": "Configura permesso",
|
||||
"Action permission": "Permesso operazione",
|
||||
"Field permission": "Permesso campo",
|
||||
"Scope name": "Nome ambito",
|
||||
"Unsaved changes": "Modifiche non salvate",
|
||||
"Are you sure you don't want to save?": "Sei sicuro di non voler salvare?",
|
||||
"Dragging": "Trascina",
|
||||
"Popup": "Popup",
|
||||
"Trigger workflow": "Trigger flusso di lavoro",
|
||||
"Request API": "Richiesta API",
|
||||
"Assign field values": "Assegna valori del campo",
|
||||
"Constant value": "Valore costante",
|
||||
"Dynamic value": "Valore dinamico",
|
||||
"Current user": "Utente attuale",
|
||||
"Current role": "Ruolo attuale",
|
||||
"Current record": "Record attuale",
|
||||
"Current collection": "Raccolta attuale",
|
||||
"Other collections": "Altre raccolte",
|
||||
"Current popup record": "Record popup attuale",
|
||||
"Parent popup record": "Record popup padre",
|
||||
"Associated records": "Record associati",
|
||||
"Parent record": "Record padre",
|
||||
"Current time": "Ora attuale",
|
||||
"System variables": "Variabili di sistema",
|
||||
"Date variables": "Variabili della data",
|
||||
"Message popup close method": "Metodo di chiusura popup di messaggio",
|
||||
"Automatic close": "Chiudi automaticamente",
|
||||
"Manually close": "Chiudi manualmente",
|
||||
"After successful update": "Dopo un aggiornamento riuscito",
|
||||
"Save record": "Salva record",
|
||||
"Updated successfully": "Aggiornamento riuscito",
|
||||
"After successful save": "Dopo un salvataggio riuscito",
|
||||
"After clicking the custom button, the following field values will be assigned according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti valori verranno assegnati in base al seguente modulo.",
|
||||
"After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti campi del record corrente verranno salvati in base al seguente modulo.",
|
||||
"Button background color": "Colore sfondo del pulsante",
|
||||
"Highlight": "Evidenzia",
|
||||
"Danger red": "Pericolo rosso",
|
||||
"Custom request": "Personalizza richiesta",
|
||||
"Request settings": "Impostazioni richiesta",
|
||||
"Request URL": "URL richiesta",
|
||||
"Request method": "Metodo richiesta",
|
||||
"Request query parameters": "Parametri richiesta query",
|
||||
"Request headers": "Intestazioni richiesta",
|
||||
"Request body": "Corpo richiesta",
|
||||
"Request success": "Successo richiesta",
|
||||
"Invalid JSON format": "Formato JSON non valido",
|
||||
"After successful request": "Dopo una richiesta riuscita",
|
||||
"Add exportable field": "Aggiungi campo esportabile",
|
||||
"Audit logs": "Registri audit",
|
||||
"Record ID": "ID record",
|
||||
"User": "Utente",
|
||||
"Field": "Campo",
|
||||
"Select": "Seleziona",
|
||||
"Select field": "Seleziona campo",
|
||||
"Field value changes": "Modifiche valore del campo",
|
||||
"One to one (has one)": "Uno a uno (ne ha uno)",
|
||||
"One to one (belongs to)": "Uno a uno (appartiene a)",
|
||||
"Use the same time zone (GMT) for all users": "Usa lo stesso fuso orario (GMT) per tutti gli utenti",
|
||||
"Province/city/area name": "Nome provincia/città/area",
|
||||
"Enabled languages": "Lingue abilitate",
|
||||
"View all plugins": "Visualizza tutti i plugin",
|
||||
"Print": "Stampa",
|
||||
"Done": "Fatto",
|
||||
"Sign up successfully, and automatically jump to the sign in page": "Iscriviti correttamente e reindirizza automaticamente alla pagina di accesso",
|
||||
"File manager": "File Manager",
|
||||
"ACL": "ACL",
|
||||
"Collection manager": "Responsabile della raccolta",
|
||||
"Plugin manager": "Plugin Manager",
|
||||
"Local": "Locale",
|
||||
"Built-in": "Incorporato",
|
||||
"Marketplace": "Marketplace",
|
||||
"Add plugin": "Aggiungi plugin",
|
||||
"Plugin source": "Sorgente plugin",
|
||||
"Upgrade": "Aggiornamento",
|
||||
"Plugin dependencies check failed": "Controllo delle dipendenze del plugin non riuscito",
|
||||
"More details": "Maggiori dettagli",
|
||||
"Upload new version": "Carica nuova versione",
|
||||
"Version": "Versione",
|
||||
"Npm package": "Pacchetto Npm",
|
||||
"Npm package name": "Nome pacchetto Npm",
|
||||
"Upload plugin": "Carica plugin",
|
||||
"Official plugin": "Plugin ufficiale",
|
||||
"Add type": "Aggiungi tipo",
|
||||
"Changelog": "Changelog",
|
||||
"Dependencies check": "Controllo delle dipendenze",
|
||||
"Update plugin": "Aggiorna plugin",
|
||||
"Installing": "Installazione",
|
||||
"The deletion was successful.": "Cancellazione riuscita.",
|
||||
"Plugin Zip File": "File zip plugin",
|
||||
"Compressed file url": "URL file compresso",
|
||||
"Last updated": "Ultimo aggiornamento",
|
||||
"PackageName": "Nome pacchetto",
|
||||
"DisplayName": "Nome da visualizzare",
|
||||
"Readme": "Readme",
|
||||
"Dependencies compatibility check": "Controllo compatibilità delle dipendenze",
|
||||
"Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Controllo delle dipendenze del plugin non riuscito, è necessario modificare la versione dipendente per soddisfare i requisiti della versione.",
|
||||
"Version range": "Range versione",
|
||||
"Plugin's version": "Versione plugin",
|
||||
"Result": "Risultato",
|
||||
"No CHANGELOG.md file": "Nessun file Changelog.md",
|
||||
"No README.md file": "Nessun file readme.md",
|
||||
"Homepage": "Homepage",
|
||||
"Drag and drop the file here or click to upload, file size should not exceed 30M": "Trascina e rilascia il file qui o fai clic per caricare, la dimensione del file non deve superare i 30M",
|
||||
"Dependencies check failed, can't enable.": "Il controllo delle dipendenze non è riuscito, impossibile abilitare.",
|
||||
"Plugin starting...": "Avvio plugin...",
|
||||
"Plugin stopping...": "Interruzione plugin ...",
|
||||
"Are you sure to delete this plugin?": "Sei sicuro di eliminare questo plugin?",
|
||||
"Are you sure to disable this plugin?": "Sei sicuro di disabilitare questo plugin?",
|
||||
"re-download file": "ri-scarica file",
|
||||
"Not enabled": "Non abilitato",
|
||||
"Search plugin": "Ricerca plugin",
|
||||
"Author": "Autore",
|
||||
"Plugin loading failed. Please check the server logs.": "Il caricamento del plugin non è riuscito. Si prega di controllare i registri del server.",
|
||||
"Coming soon...": "Prossimamente...",
|
||||
"All plugin settings": "Tutte le impostazioni del plugin",
|
||||
"Bookmark": "Segnalibro",
|
||||
"Manage all settings": "Gestisci tutte le impostazioni",
|
||||
"Create inverse field in the target collection": "Crea campo inverso nella raccolta di destinazione",
|
||||
"Inverse field name": "Nome campo inverso",
|
||||
"Inverse field display name": "Nome visualizzazione campo inverso",
|
||||
"Bulk update": "Aggiornamento di massa",
|
||||
"After successful bulk update": "Dopo un aggiornamento di massa riuscito",
|
||||
"Bulk edit": "Modifica di massa",
|
||||
"Data will be updated": "I dati verranno aggiornati",
|
||||
"Selected": "Selezionato",
|
||||
"All": "Tutto",
|
||||
"Update selected data?": "Aggiornare i dati selezionati?",
|
||||
"Update all data?": "Aggiornare tutti i dati?",
|
||||
"Remains the same": "Rimane lo stesso",
|
||||
"Changed to": "Cambiato in",
|
||||
"Clear": "Pulisci",
|
||||
"Add attach": "Aggiungi allegato",
|
||||
"Please select the records to be updated": "Si prega di selezionare i record da aggiornare",
|
||||
"Selector": "Selettore",
|
||||
"Inner": "Interno",
|
||||
"Search and select collection": "Cerca e seleziona la raccolta",
|
||||
"Please fill in the iframe URL": "Si prega di compilare l'URL iFrame",
|
||||
"Fix block": "Fissa blocco",
|
||||
"Plugin name": "Nome plugin",
|
||||
"Plugin tab name": "Nome scheda plugin",
|
||||
"AutoGenId": "Campo ID generato automaticamente",
|
||||
"CreatedBy": "Creato da",
|
||||
"UpdatedBy": "Aggiornato da",
|
||||
"CreatedAt": "Creato il",
|
||||
"UpdatedAt": "Aggiornato il",
|
||||
"Column width": "Larghezza colonna",
|
||||
"Sortable": "Ordinabile",
|
||||
"Enable link": "Abilita link",
|
||||
"This is likely a NocoBase internals bug. Please open an issue at <1>here</1>": "Questo sembra un bug interno di NocoBase. Si prega di aprire un ticket <1>qui</1>",
|
||||
"Render Failed": "Rendering non riuscito",
|
||||
"App error": "Errore app",
|
||||
"Feedback": "Feedback",
|
||||
"Try again": "Riprova",
|
||||
"Download logs": "Download registri",
|
||||
"Data template": "Modello dati",
|
||||
"Duplicate": "Duplica",
|
||||
"Duplicating": "Duplicazione",
|
||||
"Duplicate mode": "Modalità duplicazione",
|
||||
"Quick duplicate": "Duplicazione veloce",
|
||||
"Duplicate and continue": "Duplica e continua",
|
||||
"Please configure the duplicate fields": "Si prega di configurare i campi duplicati",
|
||||
"Add": "Aggiungi",
|
||||
"Add new mode": "Modalità aggiungi nuovo",
|
||||
"Quick add": "Aggiunta rapida",
|
||||
"Modal add": "Aggiunta modale",
|
||||
"Save mode": "Modalità salvataggio",
|
||||
"First or create": "Prima o crea",
|
||||
"Update or create": "Aggiorna o crea",
|
||||
"Find by the following fields": "Trova dai seguenti campi",
|
||||
"Create": "Crea",
|
||||
"Current form": "Modulo corrente",
|
||||
"Current object": "Oggetto corrente",
|
||||
"Linkage with form fields": "Collegamento con i campi del modulo",
|
||||
"Allow add new, update and delete actions": "Consenti aggiungi nuovo, aggiorna ed elimina",
|
||||
"Date display format": "Formato di visualizzazione della data",
|
||||
"Assign data scope for the template": "Assegna l'ambito dei dati per il modello",
|
||||
"Table selected records": "Tabella record selezionati",
|
||||
"Tag": "Etichetta",
|
||||
"Tag color field": "Campo colore etichetta",
|
||||
"Sync successfully": "Sincronizzazione riuscita",
|
||||
"Sync from form fields": "Sincronizzazione dai campi del modulo",
|
||||
"Select all": "Seleziona tutto",
|
||||
"Restart": "Ricomincia",
|
||||
"Restart application": "Riavvia applicazione",
|
||||
"Cascade Select": "Seleziona in cascata",
|
||||
"Execute": "Esegui",
|
||||
"Please use a valid SELECT or WITH AS statement": "Si prega di utilizzare un' istruzione SELECT o WITH AS valida",
|
||||
"Please confirm the SQL statement first": "Si prega di confermare prima l'istruzione SQL",
|
||||
"Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Elimina automaticamente gli oggetti che dipendono dalla raccolta (come le viste) e, a loro volta, tutti gli oggetti che dipendono da tali oggetti",
|
||||
"Sign in with another account": "Accedi con un altro account",
|
||||
"Return to the main application": "Torna alla applicazione principale",
|
||||
"Permission deined": "Permesso negato",
|
||||
"loading": "caricamento",
|
||||
"name is required": "nome richiesto",
|
||||
"data source": "sorgente dati",
|
||||
"Data source": "Sorgente dati",
|
||||
"DataSource": "Sorgente Dati",
|
||||
"The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Il {{type}} \"{{name}}\" potrebbe essere stato eliminato. Si prega di rimuovere {{blockType}}.",
|
||||
"Preset fields": "Campi preimpostati",
|
||||
"Home page": "Home page",
|
||||
"Handbook": "Manuale",
|
||||
"License": "Licenza",
|
||||
"Generic properties": "Proprietà generiche",
|
||||
"Specific properties": "Proprietà specifiche",
|
||||
"Used for drag and drop sorting scenarios, supporting grouping sorting": "Utilizzato per scenari con drag and drop, supporta ordinamento raggruppato",
|
||||
"Grouped sorting": "Ordinamento raggruppato",
|
||||
"When a field is selected for grouping, it will be grouped first before sorting.": "Quando viene selezionato un campo per il raggruppamento, verrà raggruppato prima dell'ordinamento.",
|
||||
"Departments": "Dipartimenti",
|
||||
"Main department": "Dipartimento principale",
|
||||
"Department name": "Nome del dipartimento",
|
||||
"Superior department": "Dipartimento superiore",
|
||||
"Owners": "Proprietari",
|
||||
"Plugin settings": "Impostazioni plugin",
|
||||
"Menu": "Menu",
|
||||
"Drag and drop sorting field": "Campi ordinamento drag and drop",
|
||||
"This variable has been deprecated and can be replaced with \"Current form\"": "Questa variabile è stata deprecata e può essere sostituita con \"Current form\"",
|
||||
"The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Il valore di questa variabile deriva dalla stringa di ricerca nell'URL della pagina. Questa variabile può essere utilizzata normalmente solo quando la pagina ha una stringa di ricerca.",
|
||||
"URL search params": "Parametri di ricerca URL",
|
||||
"Expand All": "Espandere tutto",
|
||||
"Search": "Ricerca",
|
||||
"Clear default value": "Cancella il valore predefinito",
|
||||
"Open in new window": "Apri in una nuova finestra",
|
||||
"Sorry, the page you visited does not exist.": "Spiacente, la pagina che hai visitato non esiste.",
|
||||
"is none of": "non è nessuno di",
|
||||
"is any of": "è uno di",
|
||||
"Plugin dependency version mismatch": "Mancata corrispondenza della versione della dipendenza del plugin",
|
||||
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "L'attuale versione della dipendenza del plugin non corrisponde alla versione dell'applicazione e potrebbe non funzionare correttamente. Sei sicuro di voler continuare a abilitare il plugin?",
|
||||
"Allow multiple selection": "Consenti selezione multipla",
|
||||
"Parent object": "Oggetto padre",
|
||||
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Ometti calcolo del numero totale di record della tabella durante l'impaginazione per accelerare il caricamento. Si consiglia di abilitare questa opzione per tabelle con grandi quantità di dati",
|
||||
"Enable secondary confirmation": "Abilita conferma secondaria",
|
||||
"Notification": "Notifica",
|
||||
"Ellipsis overflow content": "Contenuto Ellipsis overflow",
|
||||
"Hide column": "Nascondi colonna",
|
||||
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In modalità di configurazione, l'intera colonna diventa trasparente. In modalità non di configurazione, l'intera colonna verrà nascosta. Anche se l'intera colonna è nascosta, i suoi valori predefiniti configurati e le altre impostazioni avranno comunque effetto."
|
||||
}
|
@ -346,7 +346,7 @@
|
||||
"Subform mode": "子表单模式",
|
||||
"Field mode": "字段组件",
|
||||
"Allow add new data": "允许添加数据",
|
||||
"Edit block title": "编辑区块标题",
|
||||
"Edit block title & description": "编辑区块标题和描述",
|
||||
"Block title": "区块标题",
|
||||
"Pattern": "模式",
|
||||
"Operator": "运算符",
|
||||
|
@ -14,6 +14,7 @@ export const DestroyActionInitializer = (props) => {
|
||||
const schema = {
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-action': 'destroy',
|
||||
'x-acl-action': 'destroy',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useDestroyActionProps',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
|
@ -20,6 +20,7 @@ export const LinkActionInitializer = (props) => {
|
||||
'x-settings': 'actionSettings:link',
|
||||
'x-component': props?.['x-component'] || 'Action.Link',
|
||||
'x-use-component-props': 'useLinkActionProps',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
};
|
||||
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
|
@ -16,7 +16,11 @@ import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings';
|
||||
import {
|
||||
SchemaSettingsLinkageRules,
|
||||
SchemaSettingsModalItem,
|
||||
SchemaSettingAccessControl,
|
||||
} from '../../../schema-settings';
|
||||
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
||||
|
||||
export const SchemaSettingsActionLinkItem: FC = () => {
|
||||
@ -103,6 +107,7 @@ export const customizeLinkActionSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
SchemaSettingAccessControl,
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
|
@ -28,6 +28,7 @@ export const PopupActionInitializer = (props) => {
|
||||
openMode: defaultOpenMode,
|
||||
refreshDataBlockRequest: true,
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
|
@ -20,6 +20,7 @@ export const UpdateActionInitializer = (props) => {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-acl-action': 'update',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:edit',
|
||||
'x-component': 'Action',
|
||||
|
@ -14,7 +14,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { useCollection } from '../../../data-source';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
||||
@ -57,6 +57,7 @@ export const customizePopupActionSettings = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
SchemaSettingAccessControl,
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
|
@ -34,6 +34,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
// add blocks
|
||||
await page.getByLabel('schema-initializer-Grid-popup:addNew:addBlock-general').hover();
|
||||
await page.getByText('Markdown').click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByLabel('schema-initializer-Grid-popup:addNew:addBlock-general').hover();
|
||||
await page.getByText('Form').hover();
|
||||
await page.getByRole('menuitem', { name: 'Current collection' }).click();
|
||||
|
@ -29,20 +29,12 @@ export class PMPlugin extends Plugin {
|
||||
}
|
||||
|
||||
addSettings() {
|
||||
// this.app.pluginSettingsManager.add('acl', {
|
||||
// title: '{{t("Access control")}}',
|
||||
// icon: 'LockOutlined',
|
||||
// Component: ACLPane,
|
||||
// aclSnippet: 'pm.acl.roles',
|
||||
// });
|
||||
|
||||
// Replaced by plugin-block-template
|
||||
// this.app.pluginSettingsManager.add('ui-schema-storage', {
|
||||
// title: '{{t("Block templates")}}',
|
||||
// icon: 'LayoutOutlined',
|
||||
// Component: BlockTemplatesPane,
|
||||
// aclSnippet: 'pm.ui-schema-storage.block-templates',
|
||||
// });
|
||||
this.app.pluginSettingsManager.add('ui-schema-storage', {
|
||||
title: '{{t("Block templates")}}',
|
||||
icon: 'LayoutOutlined',
|
||||
Component: BlockTemplatesPane,
|
||||
aclSnippet: 'pm.ui-schema-storage.block-templates',
|
||||
});
|
||||
this.app.pluginSettingsManager.add('system-settings', {
|
||||
icon: 'SettingOutlined',
|
||||
title: '{{t("System settings")}}',
|
||||
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useSystemSettings, SchemaSettingsSelectItem } from '../../..';
|
||||
import locale from '../../../locale';
|
||||
|
||||
export const LanguageSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const api = useAPIClient();
|
||||
const { data } = useSystemSettings() || {};
|
||||
const enabledLanguages: string[] = useMemo(() => data?.data?.enabledLanguages || [], [data?.data?.enabledLanguages]);
|
||||
if (enabledLanguages.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Language')}
|
||||
options={Object.keys(locale)
|
||||
.filter((lang) => enabledLanguages.includes(lang))
|
||||
.map((lang) => {
|
||||
return {
|
||||
label: locale[lang].label,
|
||||
value: lang,
|
||||
};
|
||||
})}
|
||||
value={i18n.language}
|
||||
onChange={async (lang) => {
|
||||
await api.resource('users').updateLang({
|
||||
values: {
|
||||
appLang: lang,
|
||||
},
|
||||
});
|
||||
api.auth.setLocale(lang);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { useToken, useSchemaSettingsRender } from '../../../';
|
||||
|
||||
export const UserCenterButton = () => {
|
||||
const { token } = useToken();
|
||||
return (
|
||||
<div
|
||||
className="nb-user-center"
|
||||
style={{ display: 'inline-block', verticalAlign: 'top', width: '46px', height: '46px' }}
|
||||
>
|
||||
<span
|
||||
data-testid="user-center-button"
|
||||
className={css`
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
line-height: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
style={{ cursor: 'pointer', padding: '16px', color: token.colorTextHeaderMenu }}
|
||||
>
|
||||
<UserOutlined />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function UserCenter() {
|
||||
const { render } = useSchemaSettingsRender('userCenterSettings');
|
||||
return <div style={{ display: 'inline-block' }}>{render()}</div>;
|
||||
}
|
@ -14,7 +14,6 @@ import React, {
|
||||
createContext,
|
||||
FC,
|
||||
memo,
|
||||
// @ts-ignore
|
||||
startTransition,
|
||||
useCallback,
|
||||
useContext,
|
||||
@ -27,10 +26,8 @@ import { Outlet } from 'react-router-dom';
|
||||
import {
|
||||
ACLRolesCheckProvider,
|
||||
CurrentAppInfoProvider,
|
||||
CurrentUser,
|
||||
findByUid,
|
||||
findMenuItem,
|
||||
NavigateIfNotSignIn,
|
||||
PinnedPluginList,
|
||||
RemoteCollectionManagerProvider,
|
||||
RemoteSchemaComponent,
|
||||
@ -58,7 +55,8 @@ import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||
import { Help } from '../../../user/Help';
|
||||
import { KeepAlive } from './KeepAlive';
|
||||
import { convertRoutesToSchema, NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
|
||||
|
||||
import { userCenterSettings } from './userCenterSettings';
|
||||
import { UserCenter } from './UserCenterButton';
|
||||
export { KeepAlive, NocoBaseDesktopRouteType };
|
||||
|
||||
const RouteContext = createContext<NocoBaseDesktopRoute | null>(null);
|
||||
@ -529,7 +527,7 @@ export const InternalAdminLayout = () => {
|
||||
<Divider type="vertical" />
|
||||
</ConfigProvider>
|
||||
<Help />
|
||||
<CurrentUser />
|
||||
<UserCenter />
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
@ -544,17 +542,15 @@ export const AdminProvider = (props) => {
|
||||
<CurrentPageUidProvider>
|
||||
<CurrentTabUidProvider>
|
||||
<IsSubPageClosedByPageMenuProvider>
|
||||
<NavigateIfNotSignIn>
|
||||
<ACLRolesCheckProvider>
|
||||
<MenuSchemaRequestProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
<CurrentAppInfoProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>{props.children}</RemoteSchemaTemplateManagerProvider>
|
||||
</CurrentAppInfoProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
</MenuSchemaRequestProvider>
|
||||
</ACLRolesCheckProvider>
|
||||
</NavigateIfNotSignIn>
|
||||
<ACLRolesCheckProvider>
|
||||
<MenuSchemaRequestProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
<CurrentAppInfoProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>{props.children}</RemoteSchemaTemplateManagerProvider>
|
||||
</CurrentAppInfoProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
</MenuSchemaRequestProvider>
|
||||
</ACLRolesCheckProvider>
|
||||
</IsSubPageClosedByPageMenuProvider>
|
||||
</CurrentTabUidProvider>
|
||||
</CurrentPageUidProvider>
|
||||
@ -574,6 +570,7 @@ export class AdminLayoutPlugin extends Plugin {
|
||||
await this.app.pm.add(RemoteSchemaTemplateManagerPlugin);
|
||||
}
|
||||
async load() {
|
||||
this.app.schemaSettingsManager.add(userCenterSettings);
|
||||
this.app.addComponents({ AdminLayout, AdminDynamicPage });
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { UserCenterButton } from './UserCenterButton';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { LanguageSettings } from './LanguageSettings';
|
||||
|
||||
const userCenterSettings = new SchemaSettings({
|
||||
name: 'userCenterSettings',
|
||||
Component: UserCenterButton,
|
||||
items: [
|
||||
{
|
||||
name: 'langue',
|
||||
Component: LanguageSettings,
|
||||
sort: 350,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export { userCenterSettings };
|
@ -28,12 +28,14 @@ export const createPortalProvider = (id: string | symbol) => {
|
||||
<Fragment>
|
||||
{props.children}
|
||||
<Observer>
|
||||
{() => {
|
||||
if (!props.id) return <></>;
|
||||
const portal = PortalMap.get(props.id);
|
||||
if (portal) return createPortal(portal, document.body);
|
||||
return <></>;
|
||||
}}
|
||||
{
|
||||
(() => {
|
||||
if (!props.id) return <></>;
|
||||
const portal = PortalMap.get(props.id);
|
||||
if (portal) return createPortal(portal, document.body);
|
||||
return <></>;
|
||||
}) as unknown as React.ReactNode
|
||||
}
|
||||
</Observer>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -10,7 +10,6 @@
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { Drawer } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
// @ts-ignore
|
||||
import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||
|
@ -11,9 +11,8 @@ import { css } from '@emotion/css';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Modal, ModalProps } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, startTransition, useEffect, useState } from 'react';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
// @ts-ignore
|
||||
import React, { FC, startTransition, useEffect, useMemo, useState } from 'react';
|
||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||
import { useToken } from '../../../style';
|
||||
import { ErrorFallback } from '../error-fallback';
|
||||
@ -85,17 +84,6 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
||||
return buf;
|
||||
});
|
||||
const { hidden } = useCurrentPopupContext();
|
||||
const styles: any = useMemo(() => {
|
||||
return {
|
||||
mask: {
|
||||
display: hidden ? 'none' : 'block',
|
||||
},
|
||||
content: {
|
||||
display: hidden ? 'none' : 'block',
|
||||
},
|
||||
};
|
||||
}, [hidden]);
|
||||
|
||||
const showFooter = !!footerSchema;
|
||||
if (process.env.__E2E__) {
|
||||
useSetAriaLabelForModal(visible);
|
||||
@ -108,12 +96,11 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
||||
<zIndexContext.Provider value={zIndex}>
|
||||
<TabsContextProvider {...tabContext} tabBarExtraContent={null}>
|
||||
<Modal
|
||||
zIndex={zIndex}
|
||||
zIndex={hidden ? -1 : zIndex}
|
||||
width={actualWidth}
|
||||
title={field.title}
|
||||
{...(others as ModalProps)}
|
||||
{...modalProps}
|
||||
styles={styles}
|
||||
style={{
|
||||
...modalProps?.style,
|
||||
...others?.style,
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
// @ts-ignore
|
||||
import React, { FC, startTransition, useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ActionContextNoRerender, useActionContext } from '.';
|
||||
|
@ -18,24 +18,12 @@ import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'
|
||||
import { useAssociationFieldContext, useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
|
||||
const InternalNesterCss = css`
|
||||
& .ant-formily-item-layout-vertical {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 15px 20px 5px;
|
||||
}
|
||||
.ant-divider-horizontal {
|
||||
margin: 10px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InternalNesterCardCss = css`
|
||||
.ant-card-bordered {
|
||||
border: none;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 0px 20px 20px 0px;
|
||||
padding: 0px 20px 0px 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -56,6 +44,20 @@ export const InternalNester = observer(
|
||||
labelWrap = true,
|
||||
} = fieldSchema?.['x-component-props'] || {};
|
||||
|
||||
const InternalNesterCss = css`
|
||||
margin-top: 0.4em;
|
||||
|
||||
& .ant-formily-item-layout-vertical {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: ${token.padding}px ${token.paddingLG}px;
|
||||
}
|
||||
.ant-divider-horizontal {
|
||||
margin: 10px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
useEffect(() => {
|
||||
insertNester(schema.Nester);
|
||||
}, []);
|
||||
|
@ -48,7 +48,7 @@ export const AssociationFilterItem = withDynamicSchemaProps(
|
||||
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
|
||||
const defaultActiveKeyCollapse = useMemo<React.Key[]>(
|
||||
const defaultActiveKeyCollapse = useMemo<string[]>(
|
||||
() => (defaultCollapse && collectionField?.name ? [collectionField.name] : []),
|
||||
[collectionField?.name, defaultCollapse],
|
||||
);
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Card, CardProps, Space } from 'antd';
|
||||
import { Card, CardProps } from 'antd';
|
||||
import React, { useMemo, useRef, useEffect, createContext, useState } from 'react';
|
||||
import { useToken } from '../../../style';
|
||||
import { MarkdownReadPretty } from '../markdown';
|
||||
@ -38,9 +38,23 @@ export const BlockItemCard = React.forwardRef<HTMLDivElement, CardProps | any>((
|
||||
}, [blockTitle, description]);
|
||||
|
||||
const title = (blockTitle || description) && (
|
||||
<div ref={titleRef}>
|
||||
<div ref={titleRef} style={{ padding: '4px 0px 4px' }}>
|
||||
<span>{blockTitle}</span>
|
||||
{description && <MarkdownReadPretty value={props.description} style={{ fontWeight: 400 }} />}
|
||||
{description && (
|
||||
<MarkdownReadPretty
|
||||
value={props.description}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
fontWeight: 400,
|
||||
color: '#777',
|
||||
lineHeight: '1.6',
|
||||
padding: '4px 12px',
|
||||
backgroundColor: token.colorFillTertiary,
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
|
@ -11,8 +11,19 @@ import { genStyleHook } from '../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-grid-card', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.nb-action-bar': {
|
||||
borderRadius: token.borderRadiusBlock,
|
||||
marginBottom: `${token.marginBlock / 2}px !important`,
|
||||
},
|
||||
|
||||
'.ant-list-pagination': {
|
||||
borderRadius: token.borderRadiusBlock,
|
||||
marginTop: `${token.marginBlock / 2}px !important`,
|
||||
},
|
||||
|
||||
'& > .nb-block-item': {
|
||||
marginBottom: token.marginLG,
|
||||
'& > .nb-action-bar:has(:first-child:not(:empty))': {
|
||||
|
@ -28,15 +28,10 @@ const itemCss = css`
|
||||
const gridCardCss = css`
|
||||
height: 100%;
|
||||
> .ant-card-body {
|
||||
padding: 24px 24px 0px;
|
||||
height: 100%;
|
||||
button {
|
||||
margin-bottom: 0px !important;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
.nb-action-bar {
|
||||
padding: 5px 0;
|
||||
padding-top: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -17,6 +17,7 @@ import { getCardItemSchema } from '../../../block-provider';
|
||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent';
|
||||
import { useToken } from '../../../style/useToken';
|
||||
import { SortableItem } from '../../common';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
@ -141,6 +142,7 @@ const InternalGridCard = withSkeletonComponent(
|
||||
},
|
||||
[fieldSchema.properties],
|
||||
);
|
||||
const { token } = useToken();
|
||||
|
||||
const onPaginationChange: PaginationProps['onChange'] = useCallback(
|
||||
(page, pageSize) => {
|
||||
@ -207,7 +209,7 @@ const InternalGridCard = withSkeletonComponent(
|
||||
...columnCount,
|
||||
sm: columnCount.xs,
|
||||
xl: columnCount.lg,
|
||||
gutter: [rowGutter, rowGutter],
|
||||
gutter: [token.marginBlock / 2, token.marginBlock / 2],
|
||||
}}
|
||||
renderItem={(item, index) => {
|
||||
return (
|
||||
|
@ -18,7 +18,7 @@ describe('IconPicker', () => {
|
||||
const button = container.querySelector('button') as HTMLButtonElement;
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(screen.queryAllByRole('img').length).toBe(422);
|
||||
expect(screen.queryAllByRole('img').length).toBe(448);
|
||||
});
|
||||
|
||||
it('should display the selected icon', async () => {
|
||||
@ -50,9 +50,9 @@ describe('IconPicker', () => {
|
||||
|
||||
const searchInput = screen.queryByRole('search') as HTMLInputElement;
|
||||
await waitFor(() => expect(searchInput).toBeInTheDocument());
|
||||
expect(screen.queryAllByRole('img').length).toBe(422);
|
||||
expect(screen.queryAllByRole('img').length).toBe(448);
|
||||
await userEvent.type(searchInput, 'left');
|
||||
await waitFor(() => expect(screen.queryAllByRole('img').length).toBeLessThan(422));
|
||||
await waitFor(() => expect(screen.queryAllByRole('img').length).toBeLessThan(448));
|
||||
await userEvent.clear(searchInput);
|
||||
await userEvent.type(searchInput, 'abcd');
|
||||
await waitFor(() => {
|
||||
|
@ -463,11 +463,23 @@ export const MenuDesigner = () => {
|
||||
afterEnd: 'insertAfter',
|
||||
};
|
||||
|
||||
// 'beforeEnd' 表示的是插入到一个分组的里面
|
||||
const options =
|
||||
position === 'beforeEnd'
|
||||
? {
|
||||
targetScope: {
|
||||
parentId: current.__route__.id,
|
||||
},
|
||||
}
|
||||
: {
|
||||
targetId: current.__route__.id,
|
||||
};
|
||||
|
||||
await moveRoute({
|
||||
sourceId: (fieldSchema as any).__route__.id,
|
||||
targetId: current.__route__.id,
|
||||
sortField: 'sort',
|
||||
method: positionToMethod[position],
|
||||
...options,
|
||||
});
|
||||
|
||||
dn.loadAPIClientEvents();
|
||||
|
@ -398,7 +398,12 @@ const HeaderMenu = React.memo<{
|
||||
},
|
||||
);
|
||||
|
||||
const SideMenu = React.memo<any>(
|
||||
type SideMenuProps = Omit<MenuProps, 'mode'> & {
|
||||
mode: 'mix' | MenuProps['mode'];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const SideMenu = React.memo<SideMenuProps>(
|
||||
({
|
||||
mode,
|
||||
sideMenuSchema,
|
||||
|
@ -30,9 +30,15 @@ export const getDatePickerLabels = (props): string => {
|
||||
return isArr(labels) ? labels.join('~') : labels;
|
||||
};
|
||||
|
||||
export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag = false, TitleRenderer?: any): any => {
|
||||
export const getLabelFormatValue = (
|
||||
labelUiSchema: ISchema,
|
||||
value: any,
|
||||
isTag = false,
|
||||
targetTitleCollectionField?,
|
||||
TitleRenderer?: any,
|
||||
): any => {
|
||||
if (TitleRenderer) {
|
||||
return <TitleRenderer value={value} />;
|
||||
return <TitleRenderer value={value} collectionField={targetTitleCollectionField} />;
|
||||
}
|
||||
if (Array.isArray(labelUiSchema?.enum) && value) {
|
||||
const opt: any = labelUiSchema.enum.find((option: any) => option.value === value);
|
||||
|
@ -18,7 +18,6 @@ const ReactQuill = lazy(() => import('react-quill'));
|
||||
|
||||
export const RichText = connect(
|
||||
(props) => {
|
||||
const { underFilter } = props;
|
||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||
const modules = {
|
||||
toolbar: [['bold', 'italic', 'underline', 'link'], [{ list: 'ordered' }, { list: 'bullet' }], ['clean']],
|
||||
@ -37,10 +36,7 @@ export const RichText = connect(
|
||||
'image',
|
||||
];
|
||||
const { value, defaultValue, onChange, disabled } = props;
|
||||
const resultValue = isVariable(value || defaultValue) ? undefined : value || defaultValue || '';
|
||||
if (underFilter) {
|
||||
return <Input {...props} />;
|
||||
}
|
||||
const resultValue = isVariable(value || defaultValue) ? undefined : value || '';
|
||||
return wrapSSR(
|
||||
<ReactQuill
|
||||
className={`${componentCls} ${hashId}`}
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
// @ts-ignore
|
||||
import React, { FC, startTransition, useEffect, useState } from 'react';
|
||||
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||
|
||||
|
@ -52,7 +52,7 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
||||
const tabBarExtraContent = useMemo(
|
||||
() => ({
|
||||
right: render(),
|
||||
left: contextProps?.tabBarExtraContent,
|
||||
left: contextProps?.tabBarExtraContent as React.ReactNode,
|
||||
}),
|
||||
[contextProps?.tabBarExtraContent, render],
|
||||
);
|
||||
|
@ -41,6 +41,9 @@ attachmentFileTypes.add({
|
||||
return matchMimetype(file, 'image/*');
|
||||
},
|
||||
getThumbnailURL(file) {
|
||||
if (file.preview) {
|
||||
return file.preview;
|
||||
}
|
||||
if (file.url) {
|
||||
return `${file.url}${file.thumbnailRule || ''}`;
|
||||
}
|
||||
@ -400,8 +403,8 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
||||
if (pendingFiles.length) {
|
||||
setUploadedList(valueList);
|
||||
} else {
|
||||
onChange?.([...(value || []), ...valueList]);
|
||||
setUploadedList([]);
|
||||
onChange?.(valueList);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -413,7 +416,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[multiple, uploadedList, toValueItem, onChange],
|
||||
[multiple, value, uploadedList, toValueItem, onChange],
|
||||
);
|
||||
|
||||
const onDeletePending = useCallback((file) => {
|
||||
|
@ -24,7 +24,8 @@ describe('Upload', () => {
|
||||
render(<App2 />);
|
||||
});
|
||||
|
||||
it('upload single', async () => {
|
||||
// TODO: skip due to not pass but works in browser
|
||||
it.skip('upload single', async () => {
|
||||
await renderAppOptions({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
@ -119,11 +120,12 @@ describe('Upload', () => {
|
||||
await userEvent.upload(document.querySelector('input[type="file"]'), file);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.ant-upload-list-item-image')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('upload multi', async () => {
|
||||
// TODO: skip due to not pass but works in browser
|
||||
it.skip('upload multi', async () => {
|
||||
await renderAppOptions({
|
||||
designable: true,
|
||||
enableUserListDataBlock: true,
|
||||
|
@ -447,6 +447,8 @@ export function TextArea(props) {
|
||||
onPaste={onPaste}
|
||||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
// should use data-placeholder here, but not sure if it is safe to make the change, so add ignore here
|
||||
// @ts-ignore
|
||||
placeholder={props.placeholder}
|
||||
style={style}
|
||||
className={cx(
|
||||
|
@ -35,13 +35,14 @@ interface TextAreaWithGlobalScopeProps {
|
||||
password?: boolean;
|
||||
number?: boolean;
|
||||
boolean?: boolean;
|
||||
expression?: boolean;
|
||||
value?: any;
|
||||
scope?: string | object;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const TextAreaWithGlobalScope = connect((props: TextAreaWithGlobalScopeProps) => {
|
||||
const { supportsLineBreak, password, number, boolean, ...others } = props;
|
||||
const { supportsLineBreak, password, number, boolean, input, expression = true, ...others } = props;
|
||||
const scope = useEnvironmentVariableOptions(props.scope);
|
||||
const fieldNames = { value: 'name', label: 'title' };
|
||||
|
||||
@ -57,5 +58,13 @@ export const TextAreaWithGlobalScope = connect((props: TextAreaWithGlobalScopePr
|
||||
if (boolean) {
|
||||
return <Variable.Input {...props} scope={scope} fieldNames={fieldNames} />;
|
||||
}
|
||||
return <TextArea {...others} scope={scope} fieldNames={fieldNames} />;
|
||||
|
||||
if (input) {
|
||||
return <Variable.Input {...others} scope={scope} fieldNames={fieldNames} />;
|
||||
}
|
||||
if (expression) {
|
||||
return <TextArea {...others} scope={scope} fieldNames={fieldNames} />;
|
||||
}
|
||||
|
||||
return <Variable.Input {...others} scope={scope} fieldNames={fieldNames} />;
|
||||
}, mapReadPretty(Input.ReadPretty));
|
||||
|
@ -416,7 +416,7 @@ export const DataBlockInitializer: FC<DataBlockInitializerProps> = (props) => {
|
||||
},
|
||||
children,
|
||||
},
|
||||
];
|
||||
] as MenuProps['items'];
|
||||
}, [searchedChildren, hideChildrenIfSingleCollection, name, compile, title, icon, onClick, props]);
|
||||
|
||||
if (childItems.length > 1 || (childItems.length === 1 && childItems[0].children?.length > 0)) {
|
||||
|
@ -15,7 +15,6 @@ import classNames from 'classnames';
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
//@ts-ignore
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
@ -416,7 +416,9 @@ export async function replaceVariables(
|
||||
}
|
||||
|
||||
const waitForParsing = value.match(REGEX_OF_VARIABLE_IN_EXPRESSION)?.map(async (item) => {
|
||||
const { value: parsedValue } = await variables.parseVariable(item, localVariables);
|
||||
const { value: parsedValue } = await variables.parseVariable(item, localVariables, {
|
||||
doNotRequest: item.includes('$nForm'),
|
||||
});
|
||||
|
||||
// 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错
|
||||
const id = `_${uid()}`;
|
||||
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App } from 'antd';
|
||||
import { SchemaSettingsActionModalItem } from './SchemaSettings';
|
||||
import { useAPIClient } from '../api-client/hooks/useAPIClient';
|
||||
import { useRequest } from '../api-client';
|
||||
import { useACLContext } from '../acl';
|
||||
|
||||
export function AccessControl() {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const apiClient = useAPIClient();
|
||||
const resource = apiClient.resource('uiSchemas.roles', fieldSchema['x-uid']);
|
||||
const { message } = App.useApp();
|
||||
const { refresh, data }: any = useRequest(
|
||||
{
|
||||
url: `/uiSchemas/${fieldSchema['x-uid']}/roles:list`,
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const { refresh: refreshRoleCheck } = useACLContext();
|
||||
const AccessControl = (
|
||||
<SchemaSettingsActionModalItem
|
||||
scope={t}
|
||||
title={t('Access control')}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
roles: {
|
||||
type: 'array',
|
||||
title: t('Roles'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: t('If not set, all roles can see this action'),
|
||||
},
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
objectValue: true,
|
||||
dataSource: 'main',
|
||||
service: {
|
||||
resource: 'roles',
|
||||
},
|
||||
manual: false,
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
initialValues={{
|
||||
roles: data?.data,
|
||||
}}
|
||||
beforeOpen={() => !data && refresh()}
|
||||
onSubmit={async ({ roles }) => {
|
||||
await resource.set({ values: roles.map((v) => v.name) });
|
||||
await refreshRoleCheck();
|
||||
return message.success(t('Saved successfully'));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return AccessControl;
|
||||
}
|
||||
|
||||
export const SchemaSettingAccessControl = {
|
||||
name: 'accessControl',
|
||||
Component: AccessControl,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return fieldSchema['x-decorator'] === 'ACLActionProvider';
|
||||
},
|
||||
};
|
@ -22,6 +22,7 @@ import {
|
||||
CascaderProps,
|
||||
ConfigProvider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItemProps,
|
||||
MenuProps,
|
||||
Modal,
|
||||
@ -34,7 +35,6 @@ import React, {
|
||||
FC,
|
||||
ReactNode,
|
||||
createContext,
|
||||
// @ts-ignore
|
||||
startTransition,
|
||||
useCallback,
|
||||
useContext,
|
||||
@ -119,6 +119,7 @@ export interface SchemaSettingsProps {
|
||||
field?: GeneralField;
|
||||
fieldSchema?: Schema;
|
||||
children?: ReactNode;
|
||||
mode?: 'inline' | 'dropdown';
|
||||
}
|
||||
|
||||
interface SchemaSettingsContextProps<T = any> {
|
||||
@ -167,7 +168,7 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
|
||||
return <SchemaSettingsContext.Provider value={value}>{children}</SchemaSettingsContext.Provider>;
|
||||
};
|
||||
|
||||
export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => {
|
||||
const InternalSchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => {
|
||||
const { title, dn, ...others } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { Component, getMenuItems } = useMenuItem();
|
||||
@ -232,6 +233,25 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo(
|
||||
);
|
||||
});
|
||||
|
||||
const InternalSchemaSettingsMenu: React.FC<SchemaSettingsProps> = React.memo((props) => {
|
||||
const { title, dn, ...others } = props;
|
||||
const [visible, setVisible] = useState(true);
|
||||
const { Component, getMenuItems } = useMenuItem();
|
||||
const items = getMenuItems(() => props.children);
|
||||
|
||||
return (
|
||||
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
|
||||
<Component />
|
||||
<Menu items={items} />
|
||||
</SchemaSettingsProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => {
|
||||
const { mode } = props;
|
||||
return mode === 'inline' ? <InternalSchemaSettingsMenu {...props} /> : <InternalSchemaSettingsDropdown {...props} />;
|
||||
});
|
||||
|
||||
SchemaSettingsDropdown.displayName = 'SchemaSettingsDropdown';
|
||||
|
||||
const findGridSchema = (fieldSchema) => {
|
||||
@ -705,8 +725,8 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
await onSubmit?.(cloneDeep(visibleValues));
|
||||
setVisible(false);
|
||||
await onSubmit?.(cloneDeep(visibleValues));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -21,11 +21,11 @@ export function SchemaSettingsBlockTitleItem() {
|
||||
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Edit block title')}
|
||||
title={t('Edit block title & description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit block title'),
|
||||
title: t('Edit block title & description'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('title'),
|
||||
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { accessControlActionWithTable } from './template';
|
||||
|
||||
test.describe('Access control', () => {
|
||||
test('popup、link、custom request support access control', async ({ page, mockPage, mockRecord }) => {
|
||||
const nocoPage = await mockPage(accessControlActionWithTable).waitForInit();
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('block-item-CardItem-users-').hover();
|
||||
//popup
|
||||
await page.getByLabel('action-Action-Popup-customize').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
|
||||
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
|
||||
await page.mouse.move(300, 0);
|
||||
|
||||
//link
|
||||
await page.getByLabel('action-Action-Link-customize:').hover();
|
||||
|
||||
await page.getByLabel('designer-schema-settings-Action-actionSettings:link-users').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
|
||||
await page.mouse.move(300, 0);
|
||||
|
||||
// custom request
|
||||
await page.getByLabel('action-CustomRequestAction-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CustomRequestAction-actionSettings:customRequest-users').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Access control' })).toBeVisible();
|
||||
await page.mouse.move(300, 0);
|
||||
});
|
||||
test('access control with role ', async ({ page, mockPage, mockRecord }) => {
|
||||
const nocoPage = await mockPage(accessControlActionWithTable).waitForInit();
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('block-item-CardItem-users-').hover();
|
||||
//popup only member can see
|
||||
await page.getByLabel('action-Action-Popup-customize').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action-actionSettings:popup-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Access control' }).click();
|
||||
await page.getByLabel('block-item-RemoteSelect-users').click();
|
||||
await page.getByText('Member').click();
|
||||
await page.getByRole('option', { name: 'Member' }).locator('div').click();
|
||||
await page.getByLabel('block-item-RemoteSelect-users').click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
//root 角色有权限
|
||||
await expect(page.getByLabel('action-Action-Popup-customize')).toBeVisible();
|
||||
|
||||
//切换 为admin
|
||||
await page.getByTestId('user-center-button').click();
|
||||
await page.getByText('Switch roleRoot').click();
|
||||
await page.getByText('Admin', { exact: true }).click();
|
||||
await expect(page.getByLabel('action-Action-Popup-customize')).not.toBeVisible();
|
||||
|
||||
// 切换 为 member
|
||||
|
||||
await page.getByTestId('user-center-button').click();
|
||||
await page.getByText('Switch roleAdmin').click();
|
||||
await page.getByText('Member').click();
|
||||
await expect(page.getByLabel('action-Action-Popup-customize')).toBeVisible();
|
||||
});
|
||||
});
|
@ -10,6 +10,7 @@
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import {
|
||||
formFieldDependsOnSubtableFieldsWithLinkageRules,
|
||||
whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeCleared,
|
||||
whenSetToHideRetainedValueItShouldNotImpactTheFieldSDefaultValueVariables,
|
||||
} from './template';
|
||||
|
||||
@ -81,6 +82,25 @@ test.describe('linkage rules', () => {
|
||||
page.getByRole('button', { name: 'block-item-CardItem-roles-' }).getByRole('row', { name: '123456789' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('When clearing a relationship field, the value of the associated field should be cleared', async ({
|
||||
page,
|
||||
mockPage,
|
||||
}) => {
|
||||
await mockPage(whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeCleared).goto();
|
||||
|
||||
// 1. 点击 Edit 按钮打开编辑表单弹窗
|
||||
await page.getByLabel('action-Action.Link-Edit-').click();
|
||||
|
||||
// 2. 清空 roles 字段的值,nickname 字段的值应该被清空
|
||||
await page.getByTestId('select-object-multiple').hover();
|
||||
await page.getByLabel('icon-close-select').click();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('button', { name: 'block-item-CollectionField-users-form-users.nickname-Nickname' })
|
||||
.getByRole('textbox'),
|
||||
).toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
function calcResult(record) {
|
||||
|
@ -1534,3 +1534,684 @@ export const whenSetToHideRetainedValueItShouldNotImpactTheFieldSDefaultValueVar
|
||||
'x-async': true,
|
||||
},
|
||||
};
|
||||
export const whenClearingARelationshipFieldTheValueOfTheAssociatedFieldShouldBeCleared = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
'3fiy31f6txn': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
'0rcyz4b3efw': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
u3mg5s9lzv6: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
ezm6z8plx83: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
'x-uid': 'tt6xllenhim',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
jv0ofysjinb: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-toolbar-props': {
|
||||
initializer: 'table:configureItemActions',
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
bngzha8iw7k: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
tkzbokuc018: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:edit',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-action-context': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer-props': {
|
||||
linkageAction: true,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'popup:common:addBlock',
|
||||
properties: {
|
||||
q2nw699fdes: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
'8rf37n96jy1': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
'83f2yyxkgew': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: false,
|
||||
},
|
||||
'x-acl-action': 'users:update',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props':
|
||||
'useEditFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
action: 'get',
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:editForm',
|
||||
'x-component': 'CardItem',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
ztcsjl3kutq: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useEditFormBlockProps',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
grid: {
|
||||
'x-uid': 'jk5os75m30w',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'form:configureFields',
|
||||
'x-app-version': '1.5.16',
|
||||
'x-linkage-rules': [
|
||||
{
|
||||
condition: {
|
||||
$and: [],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
targetFields: ['nickname'],
|
||||
operator: 'value',
|
||||
value: {
|
||||
mode: 'express',
|
||||
value: '{{$nForm.roles.title}}',
|
||||
result: '{{$nForm.roles.title}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
al791aryfpm: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
s2o1ubyy97y: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
roles: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-toolbar':
|
||||
'FormItemSchemaToolbar',
|
||||
'x-settings':
|
||||
'fieldSettings:FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'name',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
'x-uid': 'y5r3pw94du5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '1mlc7m6h27k',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l5bz655ppju',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
jsl0ebbnp4x: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
e19ihogekwg: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.5.16',
|
||||
properties: {
|
||||
nickname: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-toolbar':
|
||||
'FormItemSchemaToolbar',
|
||||
'x-settings':
|
||||
'fieldSettings:FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field':
|
||||
'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '1.5.16',
|
||||
'x-uid': 'nr2wsugko9w',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'vmsrhpme9qo',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'b7v16npd5y2',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
p3zp9livz6w: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'editForm:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
},
|
||||
'x-app-version': '1.5.16',
|
||||
'x-uid': 'd3qzyf73gu6',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'j4j1fy7tkmh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 't4mmnie8cll',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'bnugjy38s7u',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tb4c9r9s844',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'dlbfog0xc8b',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'nvl54jerwsi',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '8d1h9bm3lnb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'f7xutqb1iyw',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'kquc4rim0g2',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'sb10ua8lfsl',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'wa210opt4kb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'sumvmy4epmf',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'jyssn2zrfs6',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'n1iy8cfg2nj',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'q9we7w410nr',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'hajsh396zq6',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'utv8yv0hu55',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const accessControlActionWithTable = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
fvgd0c2akgf: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
'0c4zy47hhyq': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
b1y881c771g: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
hccefuo80kx: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action': 'users:view',
|
||||
'x-decorator': 'DetailsBlockProvider',
|
||||
'x-use-decorator-props': 'useDetailsWithPaginationDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 1,
|
||||
},
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:detailsWithPagination',
|
||||
'x-component': 'CardItem',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
fctprt7i7ut: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Details',
|
||||
'x-read-pretty': true,
|
||||
'x-use-component-props': 'useDetailsWithPaginationProps',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
b2xrbq4a060: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'details:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
e2ccvx3c7o5: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-action': 'customize:popup',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:popup',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
refreshDataBlockRequest: true,
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-action-context': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'popup:common:addBlock',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
'x-uid': '3qfg5qfvsth',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'n0574ydwdua',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'm0039599o9q',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '5t7ol82dt74',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'gn76wlmk619',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'3jat4vour4y': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'CustomRequestAction',
|
||||
'x-action': 'customize:form:request',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:customRequest',
|
||||
'x-decorator': 'CustomRequestAction.Decorator',
|
||||
'x-action-settings': {
|
||||
onSuccess: {
|
||||
manualClose: false,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Request success")}}',
|
||||
},
|
||||
},
|
||||
type: 'void',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
'x-uid': 'gu0shkseqoa',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
'0tevnuro5d4': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Link") }}',
|
||||
'x-action': 'customize:link',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:link',
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useLinkActionProps',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
'x-uid': 'swtrz2mpnm4',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-uid': '32u6fnlj0ti',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'details:configureFields',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
'x-uid': '21ksski5wgs',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
pagination: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Pagination',
|
||||
'x-use-component-props': 'useDetailsPaginationProps',
|
||||
'x-app-version': '1.6.0-beta.9',
|
||||
'x-uid': '2cz3ilk7hmv',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-uid': 'f6xosucy75q',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '33tsqeap83o',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ppglkb7uvt8',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'af78vam04ux',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'askp9xe9uag',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'huon5vcb8u8',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ export * from './SchemaSettingsRenderEngine';
|
||||
export * from './hooks/useGetAriaLabelOfDesigner';
|
||||
export * from './hooks/useIsAllowToSetDefaultValue';
|
||||
export * from './SchemaSettingsLayoutItem';
|
||||
export * from './SchemaSettingAccessControl';
|
||||
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
|
||||
export * from './isPatternDisabled';
|
||||
export { SchemaSettingsPlugin } from './SchemaSettingsPlugin';
|
||||
|
@ -7,219 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { App, Dropdown, Menu, MenuProps } from 'antd';
|
||||
import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..';
|
||||
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
|
||||
import { useChangePassword } from './ChangePassword';
|
||||
import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider';
|
||||
import { useEditProfile } from './EditProfile';
|
||||
import { useLanguageSettings } from './LanguageSettings';
|
||||
import { useSwitchRole } from './SwitchRole';
|
||||
import { createContext } from 'react';
|
||||
import { SelectWithTitle } from '../common';
|
||||
|
||||
const useNickname = () => {
|
||||
const { data } = useCurrentUserContext();
|
||||
const { token } = useToken();
|
||||
|
||||
return useEffect(() => {
|
||||
return {
|
||||
key: 'nickname',
|
||||
disabled: true,
|
||||
label: (
|
||||
<span aria-disabled="false" style={{ cursor: 'text', color: token.colorTextDescription }}>
|
||||
{data?.data?.nickname || data?.data?.username || data?.data?.email}
|
||||
</span>
|
||||
),
|
||||
};
|
||||
}, [data?.data?.email, data?.data?.nickname, data?.data?.username, data?.data.version, token.colorTextDescription]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @note If you want to change here, Note the Setting block on the mobile side
|
||||
*/
|
||||
export const SettingsMenu: React.FC<{
|
||||
redirectUrl?: string;
|
||||
}> = (props) => {
|
||||
const { addMenuItem, getMenuItems } = useCurrentUserSettingsMenu();
|
||||
const { redirectUrl = '' } = props;
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const appAllowed = allowAll || snippets?.includes('app');
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const silenceApi = useAPIClient();
|
||||
const check = useCallback(async () => {
|
||||
return await new Promise((resolve) => {
|
||||
const heartbeat = setInterval(() => {
|
||||
silenceApi
|
||||
.silent()
|
||||
.resource('app')
|
||||
.getInfo()
|
||||
.then((res) => {
|
||||
if (res?.status === 200) {
|
||||
resolve('ok');
|
||||
clearInterval(heartbeat);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
error(err);
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
}, [silenceApi]);
|
||||
const nickname = useNickname();
|
||||
const editProfile = useEditProfile();
|
||||
const changePassword = useChangePassword();
|
||||
const switchRole = useSwitchRole();
|
||||
const languageSettings = useLanguageSettings();
|
||||
const { modal } = App.useApp();
|
||||
const controlApp = useEffect<MenuProps['items']>(() => {
|
||||
if (!appAllowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'cache',
|
||||
label: t('Clear cache'),
|
||||
onClick: async () => {
|
||||
await api.resource('app').clearCache();
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'reboot',
|
||||
label: t('Restart application'),
|
||||
onClick: async () => {
|
||||
modal.confirm({
|
||||
title: t('Restart application'),
|
||||
// content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
|
||||
okText: t('Restart'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await api.resource('app').restart();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'divider_4',
|
||||
type: 'divider',
|
||||
},
|
||||
];
|
||||
}, [api, appAllowed, check, modal, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = [
|
||||
nickname,
|
||||
{
|
||||
key: 'divider_1',
|
||||
type: 'divider',
|
||||
},
|
||||
editProfile,
|
||||
changePassword,
|
||||
editProfile ||
|
||||
(changePassword && {
|
||||
key: 'divider_2',
|
||||
type: 'divider',
|
||||
}),
|
||||
switchRole,
|
||||
{
|
||||
key: 'divider_3',
|
||||
type: 'divider',
|
||||
},
|
||||
...controlApp,
|
||||
{
|
||||
key: 'signout',
|
||||
label: t('Sign out'),
|
||||
onClick: async () => {
|
||||
const { data } = await api.auth.signOut();
|
||||
if (data?.data?.redirect) {
|
||||
window.location.href = data.data.redirect;
|
||||
} else {
|
||||
navigate(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item) {
|
||||
addMenuItem(item);
|
||||
}
|
||||
});
|
||||
if (languageSettings) {
|
||||
addMenuItem(languageSettings, { before: 'divider_3' });
|
||||
}
|
||||
}, [
|
||||
addMenuItem,
|
||||
api.auth,
|
||||
editProfile,
|
||||
changePassword,
|
||||
controlApp,
|
||||
languageSettings,
|
||||
navigate,
|
||||
redirectUrl,
|
||||
switchRole,
|
||||
t,
|
||||
nickname,
|
||||
]);
|
||||
|
||||
return <Menu items={getMenuItems()} />;
|
||||
export const SettingsMenuProvider = (props) => {
|
||||
return SelectWithTitle;
|
||||
};
|
||||
|
||||
export const DropdownVisibleContext = createContext(null);
|
||||
DropdownVisibleContext.displayName = 'DropdownVisibleContext';
|
||||
|
||||
export const CurrentUser = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { token } = useToken();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
open={visible}
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
dropdownRender={() => {
|
||||
return <SettingsMenu />;
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-testid="user-center-button"
|
||||
className={css`
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
line-height: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
style={{ cursor: 'pointer', padding: '16px', color: token.colorTextHeaderMenu }}
|
||||
>
|
||||
<UserOutlined />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</DropdownVisibleContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,10 +8,9 @@
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useACLRoleContext } from '../acl';
|
||||
import { ReturnTypeOfUseRequest, useAPIClient, useRequest } from '../api-client';
|
||||
import { useAppSpin, useLocationNoUpdate } from '../application';
|
||||
import { useAppSpin } from '../application';
|
||||
import { useCompile } from '../schema-component';
|
||||
|
||||
export const CurrentUserContext = createContext<ReturnTypeOfUseRequest>(null);
|
||||
@ -44,13 +43,7 @@ export const CurrentUserProvider = (props) => {
|
||||
api
|
||||
.request({
|
||||
url: '/auth:check',
|
||||
skipNotify: (error) => {
|
||||
const errs = api.toErrMessages(error);
|
||||
if (errs.find((error: { code?: string }) => error.code === 'EMPTY_TOKEN')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
skipNotify: true,
|
||||
skipAuth: true,
|
||||
})
|
||||
.then((res) => res?.data),
|
||||
@ -63,18 +56,3 @@ export const CurrentUserProvider = (props) => {
|
||||
|
||||
return <CurrentUserContext.Provider value={result}>{props.children}</CurrentUserContext.Provider>;
|
||||
};
|
||||
|
||||
export const NavigateToSigninWithRedirect = () => {
|
||||
const { pathname, search } = useLocationNoUpdate();
|
||||
const redirect = `?redirect=${pathname}${search}`;
|
||||
return <Navigate replace to={`/signin${redirect}`} />;
|
||||
};
|
||||
|
||||
export const NavigateIfNotSignIn = ({ children }) => {
|
||||
const result = useCurrentUserContext();
|
||||
|
||||
if (result.loading === false && !result.data?.data?.id) {
|
||||
return <NavigateToSigninWithRedirect />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
@ -1,56 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectWithTitle, useAPIClient, useSystemSettings } from '..';
|
||||
import locale from '../locale';
|
||||
|
||||
export const useLanguageSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const api = useAPIClient();
|
||||
const { data } = useSystemSettings() || {};
|
||||
const enabledLanguages: string[] = useMemo(() => data?.data?.enabledLanguages || [], [data?.data?.enabledLanguages]);
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'language',
|
||||
eventKey: 'LanguageSettings',
|
||||
label: (
|
||||
<SelectWithTitle
|
||||
title={t('Language')}
|
||||
options={Object.keys(locale)
|
||||
.filter((lang) => enabledLanguages.includes(lang))
|
||||
.map((lang) => {
|
||||
return {
|
||||
label: locale[lang].label,
|
||||
value: lang,
|
||||
};
|
||||
})}
|
||||
defaultValue={i18n.language}
|
||||
onChange={async (lang) => {
|
||||
await api.resource('users').updateLang({
|
||||
values: {
|
||||
appLang: lang,
|
||||
},
|
||||
});
|
||||
api.auth.setLocale(lang);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, [api, enabledLanguages, i18n, t]);
|
||||
|
||||
if (enabledLanguages.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { SelectWithTitle } from '../common';
|
||||
import { useCurrentRoles } from './CurrentUserProvider';
|
||||
|
||||
export const useSwitchRole = () => {
|
||||
const api = useAPIClient();
|
||||
const roles = useCurrentRoles();
|
||||
const { t } = useTranslation();
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'role',
|
||||
eventKey: 'SwitchRole',
|
||||
label: (
|
||||
<SelectWithTitle
|
||||
title={t('Switch role')}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
}}
|
||||
options={roles}
|
||||
defaultValue={api.auth.role}
|
||||
onChange={async (roleName) => {
|
||||
api.auth.setRole(roleName);
|
||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, [api, roles, t]);
|
||||
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
@ -1,58 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { render } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import { SettingsMenu } from '../CurrentUser';
|
||||
import { useCurrentUserSettingsMenu } from '../CurrentUserSettingsMenuProvider';
|
||||
|
||||
const AppContextProvider = (props) => {
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
// TODO: AppContextProvider 没有提供足够的上下文环境
|
||||
describe.skip('CurrentUserSettingsMenuProvider', () => {
|
||||
const wrapper = ({ children }) => {
|
||||
return (
|
||||
<AppContextProvider>
|
||||
<SettingsMenu />
|
||||
{children}
|
||||
</AppContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const TestComponent = () => {
|
||||
const { getMenuItems } = useCurrentUserSettingsMenu();
|
||||
getMenuItems();
|
||||
return <div>Test</div>;
|
||||
};
|
||||
|
||||
it('should throw error when CurrentUserSettingsMenuProvider is not provided', () => {
|
||||
expect(() => {
|
||||
render(<TestComponent />);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
'"CurrentUser: You should use `CurrentUserSettingsMenuProvider` in the root of your app."',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error when providing context', () => {
|
||||
expect(() => {
|
||||
render(<TestComponent />, { wrapper });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
// TODO: result.current 是 null,会报错,暂时不知道哪里出了问题
|
||||
// it.skip('add menu item', () => {
|
||||
// const { result } = renderHook(() => useCurrentUserSettingsMenu(), {
|
||||
// wrapper,
|
||||
// });
|
||||
|
||||
// expect(result.current.getMenuItems()).not.toHaveLength(0);
|
||||
// });
|
||||
});
|
@ -24,8 +24,10 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"cytoscape": "3.28.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"react-router-dom": "6.28.1",
|
||||
"react-router": "6.28.1",
|
||||
"antd": "5.12.8",
|
||||
"rollup": "4.24.0"
|
||||
},
|
||||
|
@ -65,6 +65,10 @@ export class InheritedCollection extends Collection {
|
||||
parentFields() {
|
||||
const fields = new Map<string, Field>();
|
||||
|
||||
if (!this.parents) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
for (const parent of this.parents) {
|
||||
if (parent.isInherited()) {
|
||||
for (const [name, field] of (<InheritedCollection>parent).parentFields()) {
|
||||
|
@ -1336,6 +1336,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
},
|
||||
logger: this._logger.child({ module: 'database' }),
|
||||
});
|
||||
|
||||
// NOTE: to avoid listener number warning (default to 10)
|
||||
// See: https://nodejs.org/api/events.html#emittersetmaxlistenersn
|
||||
db.setMaxListeners(100);
|
||||
|
||||
return db;
|
||||
}
|
||||
}
|
||||
|
@ -363,6 +363,10 @@ export class Gateway extends EventEmitter {
|
||||
|
||||
const mainApp = AppSupervisor.getInstance().bootMainApp(options.mainAppOptions);
|
||||
|
||||
// NOTE: to avoid listener number warning (default to 10)
|
||||
// See: https://nodejs.org/api/events.html#emittersetmaxlistenersn
|
||||
mainApp.setMaxListeners(50);
|
||||
|
||||
let runArgs: any = [process.argv, { throwError: true, from: 'node' }];
|
||||
|
||||
if (!isMainThread) {
|
||||
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrentRoles, useAPIClient, SchemaSettingsItem, SelectWithTitle } from '@nocobase/client';
|
||||
|
||||
export const SwitchRole = () => {
|
||||
const { t } = useTranslation();
|
||||
const api = useAPIClient();
|
||||
const roles = useCurrentRoles();
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SchemaSettingsItem eventKey="SwitchRole" title="SwitchRole">
|
||||
<SelectWithTitle
|
||||
title={t('Switch role')}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
}}
|
||||
options={roles}
|
||||
defaultValue={api.auth.role}
|
||||
onChange={async (roleName) => {
|
||||
api.auth.setRole(roleName);
|
||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</SchemaSettingsItem>
|
||||
);
|
||||
};
|
@ -9,9 +9,10 @@
|
||||
|
||||
import { Plugin, lazy } from '@nocobase/client';
|
||||
import { ACLSettingsUI } from './ACLSettingsUI';
|
||||
// import { RolesManagement } from './RolesManagement';
|
||||
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
|
||||
import { RolesManager } from './roles-manager';
|
||||
import { SwitchRole } from './SwitchRole';
|
||||
|
||||
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
|
||||
|
||||
export class PluginACLClient extends Plugin {
|
||||
rolesManager = new RolesManager();
|
||||
@ -25,6 +26,18 @@ export class PluginACLClient extends Plugin {
|
||||
aclSnippet: 'pm.acl.roles',
|
||||
sort: 3,
|
||||
});
|
||||
|
||||
// 个人中心注册 切换角色
|
||||
this.app.addUserCenterSettingsItem({
|
||||
name: 'divider_switchRole',
|
||||
type: 'divider',
|
||||
sort: 200,
|
||||
});
|
||||
this.app.addUserCenterSettingsItem({
|
||||
name: 'switchRole',
|
||||
Component: SwitchRole,
|
||||
sort: 300,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,6 +230,66 @@ describe('list action with acl', () => {
|
||||
expect(data.meta.allowedActions.destroy).toEqual([]);
|
||||
});
|
||||
|
||||
it('should list items meta permissions by m2m association field', async () => {
|
||||
const userRole = app.acl.define({
|
||||
role: 'user',
|
||||
});
|
||||
|
||||
const Tag = app.db.collection({
|
||||
name: 'tags',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
|
||||
app.db.extendCollection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags',
|
||||
through: 'posts_tags',
|
||||
},
|
||||
],
|
||||
});
|
||||
await app.db.sync();
|
||||
|
||||
await Tag.repository.create({
|
||||
values: [{ name: 'a' }, { name: 'b' }, { name: 'c' }],
|
||||
});
|
||||
await Post.repository.create({
|
||||
values: [
|
||||
{ title: 'p1', tags: [1, 2] },
|
||||
{ title: 'p2', tags: [1, 3] },
|
||||
{ title: 'p3', tags: [2, 3] },
|
||||
],
|
||||
});
|
||||
|
||||
userRole.grantAction('posts:view', {});
|
||||
|
||||
userRole.grantAction('posts:update', {
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
tags: {
|
||||
name: {
|
||||
$includes: 'c',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const response = await (await app.agent().login(users[0].id, 'user'))
|
||||
.set('X-With-ACL-Meta', true)
|
||||
.resource('posts')
|
||||
.list();
|
||||
const data = response.body;
|
||||
expect(data.meta.allowedActions.view).toEqual([1, 2, 3]);
|
||||
expect(data.meta.allowedActions.update).toEqual([2, 3]);
|
||||
expect(data.meta.allowedActions.destroy).toEqual([]);
|
||||
});
|
||||
|
||||
it('should list items with meta permission', async () => {
|
||||
const userRole = app.acl.define({
|
||||
role: 'user',
|
||||
|
@ -43,6 +43,23 @@ export async function checkAction(ctx, next) {
|
||||
}
|
||||
|
||||
const availableActions = ctx.app.acl.getAvailableActions();
|
||||
let uiButtonSchemasBlacklist = [];
|
||||
if (currentRole !== 'root') {
|
||||
const eqCurrentRoleList = await ctx.db
|
||||
.getRepository('uiButtonSchemasRoles')
|
||||
.find({
|
||||
filter: { 'roleName.$eq': currentRole },
|
||||
})
|
||||
.then((list) => list.map((v) => v.uid));
|
||||
|
||||
const NECurrentRoleList = await ctx.db
|
||||
.getRepository('uiButtonSchemasRoles')
|
||||
.find({
|
||||
filter: { 'roleName.$ne': currentRole },
|
||||
})
|
||||
.then((list) => list.map((v) => v.uid));
|
||||
uiButtonSchemasBlacklist = NECurrentRoleList.filter((uid) => !eqCurrentRoleList.includes(uid));
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
...role.toJSON(),
|
||||
@ -53,6 +70,7 @@ export async function checkAction(ctx, next) {
|
||||
allowConfigure: roleInstance.get('allowConfigure'),
|
||||
allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
|
||||
allowAnonymous: !!anonymous,
|
||||
uiButtonSchemasBlacklist,
|
||||
};
|
||||
|
||||
await next();
|
||||
|
@ -265,6 +265,7 @@ function createWithACLMetaMiddleware() {
|
||||
}),
|
||||
],
|
||||
include: conditions.map((condition) => condition.include).flat(),
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const allowedActions = inspectActions
|
||||
@ -273,7 +274,9 @@ function createWithACLMetaMiddleware() {
|
||||
return [action, ids];
|
||||
}
|
||||
|
||||
return [action, results.filter((item) => Boolean(item.get(action))).map((item) => item.get(primaryKeyField))];
|
||||
let actionIds = results.filter((item) => Boolean(item[action])).map((item) => item[primaryKeyField]);
|
||||
actionIds = Array.from(new Set(actionIds));
|
||||
return [action, actionIds];
|
||||
})
|
||||
.reduce((acc, [action, ids]) => {
|
||||
acc[action] = ids;
|
||||
|
@ -201,7 +201,7 @@ export const bulkEditFormItemSettings = new SchemaSettings({
|
||||
const { form } = useFormBlockContext();
|
||||
const isFormReadPretty = useIsFormReadPretty();
|
||||
const validateSchema = useValidateSchema();
|
||||
return form && !isFormReadPretty && validateSchema;
|
||||
return form && !isFormReadPretty && Boolean(validateSchema);
|
||||
},
|
||||
},
|
||||
fieldComponentSettingsItem,
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Action, useAPIClient, useRequest, withDynamicSchemaProps } from '@nocobase/client';
|
||||
import { Action, useAPIClient, useRequest, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { listByCurrentRoleUrl } from '../constants';
|
||||
@ -16,23 +16,23 @@ import { CustomRequestActionDesigner } from './CustomRequestActionDesigner';
|
||||
|
||||
export const CustomRequestActionACLDecorator = (props) => {
|
||||
const apiClient = useAPIClient();
|
||||
const isRoot = apiClient.auth.role === 'root';
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { data } = useRequest<{ data: string[] }>(
|
||||
{
|
||||
url: listByCurrentRoleUrl,
|
||||
},
|
||||
{
|
||||
manual: isRoot,
|
||||
cacheKey: listByCurrentRoleUrl,
|
||||
},
|
||||
);
|
||||
const requestId = fieldSchema?.['x-custom-request-id'] || fieldSchema?.['x-uid'];
|
||||
if (!isRoot && !data?.data?.includes(requestId)) {
|
||||
return null;
|
||||
}
|
||||
// const isRoot = apiClient.auth.role === 'root';
|
||||
// const fieldSchema = useFieldSchema();
|
||||
// const { data } = useRequest<{ data: string[] }>(
|
||||
// {
|
||||
// url: listByCurrentRoleUrl,
|
||||
// },
|
||||
// {
|
||||
// manual: isRoot,
|
||||
// cacheKey: listByCurrentRoleUrl,
|
||||
// },
|
||||
// );
|
||||
|
||||
return props.children;
|
||||
// // if (!isRoot && !data?.data?.includes(fieldSchema?.['x-uid'])) {
|
||||
// // return null;
|
||||
// // }
|
||||
|
||||
return <ACLActionProvider>{props.children}</ACLActionProvider>;
|
||||
};
|
||||
|
||||
const components = {
|
||||
|
@ -17,15 +17,13 @@ import {
|
||||
useCollection_deprecated,
|
||||
useDataSourceKey,
|
||||
useDesignable,
|
||||
useRequest,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import { App } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { listByCurrentRoleUrl } from '../constants';
|
||||
import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks';
|
||||
import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource';
|
||||
import { useTranslation } from '../locale';
|
||||
import { CustomRequestACLSchema, CustomRequestConfigurationFieldsSchema } from '../schemas';
|
||||
import { CustomRequestConfigurationFieldsSchema } from '../schemas';
|
||||
|
||||
export function CustomRequestSettingsItem() {
|
||||
const { t } = useTranslation();
|
||||
@ -89,63 +87,6 @@ export function CustomRequestSettingsItem() {
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomRequestACL() {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const customRequestsResource = useCustomRequestsResource();
|
||||
const { message } = App.useApp();
|
||||
const { data, refresh } = useGetCustomRequest();
|
||||
const { dn } = useDesignable();
|
||||
const { refresh: refreshRoleCustomKeys } = useRequest<{ data: string[] }>(
|
||||
{
|
||||
url: listByCurrentRoleUrl,
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
cacheKey: listByCurrentRoleUrl,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SchemaSettingsActionModalItem
|
||||
title={t('Access control')}
|
||||
schema={CustomRequestACLSchema}
|
||||
initialValues={{
|
||||
roles: data?.data?.roles,
|
||||
}}
|
||||
beforeOpen={() => !data && refresh()}
|
||||
onSubmit={async ({ roles }) => {
|
||||
const isSelfRequest =
|
||||
!fieldSchema['x-custom-request-id'] || fieldSchema['x-custom-request-id'] === fieldSchema['x-uid'];
|
||||
|
||||
if (!isSelfRequest) {
|
||||
fieldSchema['x-custom-request-id'] = fieldSchema['x-uid'];
|
||||
await dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-custom-request-id': fieldSchema['x-uid'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await customRequestsResource.updateOrCreate({
|
||||
values: {
|
||||
key: fieldSchema['x-uid'],
|
||||
roles,
|
||||
},
|
||||
filterKeys: ['key'],
|
||||
});
|
||||
refresh();
|
||||
refreshRoleCustomKeys();
|
||||
dn.refresh();
|
||||
return message.success(t('Saved successfully'));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@ -160,10 +101,7 @@ export const customRequestActionSettings = new SchemaSettings({
|
||||
name: 'request settings',
|
||||
Component: CustomRequestSettingsItem,
|
||||
},
|
||||
{
|
||||
name: 'accessControl',
|
||||
Component: CustomRequestACL,
|
||||
},
|
||||
SchemaSettingAccessControl,
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -19,8 +19,9 @@ import {
|
||||
useCollection,
|
||||
useCollectionRecord,
|
||||
useSchemaToolbar,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import { CustomRequestACL, CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
|
||||
import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
|
||||
|
||||
export const customizeCustomRequestActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:customRequest',
|
||||
@ -64,8 +65,10 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({
|
||||
Component: CustomRequestSettingsItem,
|
||||
},
|
||||
{
|
||||
name: 'accessControl',
|
||||
Component: CustomRequestACL,
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'refreshDataBlockRequest',
|
||||
|
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client';
|
||||
import { generateNTemplate } from '../locale';
|
||||
|
||||
export const CustomRequestACLSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roles: {
|
||||
type: 'array',
|
||||
title: generateNTemplate('Roles'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: generateNTemplate('If not set, all roles can see this action'),
|
||||
},
|
||||
'x-component': 'RemoteSelect',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
objectValue: true,
|
||||
dataSource: DEFAULT_DATA_SOURCE_KEY,
|
||||
service: {
|
||||
resource: 'roles',
|
||||
},
|
||||
manual: false,
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -8,4 +8,3 @@
|
||||
*/
|
||||
|
||||
export * from './CustomRequestConfigurationFields';
|
||||
export * from './CustomRequestACL';
|
||||
|
@ -11,7 +11,7 @@ import { ExclamationCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@nocobase/client';
|
||||
import { Button, Modal, Space, Spin } from 'antd';
|
||||
import { saveAs } from 'file-saver';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NAMESPACE } from './constants';
|
||||
import { useImportContext } from './context';
|
||||
@ -25,6 +25,7 @@ export const ImportModal = (props: any) => {
|
||||
const { t } = useTranslation(NAMESPACE);
|
||||
const { importModalVisible, importStatus, importResult, setImportModalVisible } = useImportContext();
|
||||
const { data: fileData, meta } = importResult ?? {};
|
||||
|
||||
const doneHandler = () => {
|
||||
setImportModalVisible(false);
|
||||
};
|
||||
@ -33,6 +34,34 @@ export const ImportModal = (props: any) => {
|
||||
const blob = new Blob([arrayBuffer], { type: 'application/x-xls' });
|
||||
saveAs(blob, `fail.xlsx`);
|
||||
};
|
||||
|
||||
const renderResult = (importResult) => {
|
||||
if (!importResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data, meta } = importResult;
|
||||
if (meta) {
|
||||
return t('{{successCount}} records have been successfully imported', {
|
||||
...(meta ?? {}),
|
||||
});
|
||||
}
|
||||
const stats = data;
|
||||
const parts = [
|
||||
`${t('Total records')}: ${stats.total || 0}`,
|
||||
`${t('Successfully imported')}: ${stats.success || 0}`,
|
||||
];
|
||||
|
||||
if (stats.skipped > 0) {
|
||||
parts.push(`${t('Skipped')}: ${stats.skipped}`);
|
||||
}
|
||||
|
||||
if (stats.updated > 0) {
|
||||
parts.push(`${t('Updated')}: ${stats.updated}`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title={t('Import Data')}
|
||||
@ -57,11 +86,8 @@ export const ImportModal = (props: any) => {
|
||||
{importStatus === ImportStatus.IMPORTED && (
|
||||
<Space direction="vertical" align="center">
|
||||
<ExclamationCircleFilled style={{ fontSize: 72, color: '#1890ff' }} />
|
||||
<p>
|
||||
{t('{{successCount}} records have been successfully imported', {
|
||||
...(meta ?? {}),
|
||||
})}
|
||||
</p>
|
||||
|
||||
<p>{renderResult(importResult)}</p>
|
||||
<Space>
|
||||
{meta?.failureCount > 0 && (
|
||||
<Button onClick={downloadFailureDataHandler}>{t('To download the failure data')}</Button>
|
||||
|
@ -173,6 +173,8 @@ export const useImportStartAction = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const importMode = importSchema?.['x-action-settings']?.importMode || 'auto';
|
||||
|
||||
setVisible(false);
|
||||
setImportModalVisible(true);
|
||||
setImportStatus(ImportStatus.IMPORTING);
|
||||
@ -181,13 +183,13 @@ export const useImportStartAction = () => {
|
||||
const { data } = await (newResource as any).importXlsx(
|
||||
{
|
||||
values: formData,
|
||||
mode: importMode,
|
||||
},
|
||||
{
|
||||
timeout: 10 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
setImportResult(data);
|
||||
form.reset();
|
||||
|
||||
if (!data.data.taskId) {
|
||||
|
@ -39,5 +39,13 @@
|
||||
"Header mismatch at column {{column}}: expected \"{{expected}}\", but got \"{{actual}}\"": "第 {{column}} 列的表头不匹配:预期 \"{{expected}}\",实际是 \"{{actual}}\"",
|
||||
"No data to import": "没有数据可导入",
|
||||
"Failed to import row {{row}}, {{message}}, row data: {{data}}": "导入第 {{row}} 行失败,{{message}},行数据:{{data}}",
|
||||
"import-error": "导入第 {{rowIndex}} 行失败,行数据:{{rowData}}, 原因:{{causeMessage}}"
|
||||
"import-error": "导入第 {{rowIndex}} 行失败,行数据:{{rowData}}, 原因:{{causeMessage}}",
|
||||
"Import completed": "导入完成:{{success}} 条记录已导入,{{updated}} 条记录已更新,{{skipped}} 条记录已跳过,共 {{total}} 条记录",
|
||||
"Successfully imported": "成功导入",
|
||||
"Updated records": "已更新记录",
|
||||
"Skipped records": "已跳过记录",
|
||||
"Total records": "总记录数",
|
||||
"View result": "查看结果",
|
||||
"ImportResult": "已导入 {{success}} 条,更新 {{updated}} 条,跳过 {{skipped}} 条,共 {{total}} 条",
|
||||
"Task result": "任务结果"
|
||||
}
|
||||
|
@ -50,36 +50,34 @@ export function authCheckMiddleware({ app }: { app: Application }) {
|
||||
};
|
||||
const errHandler = (error) => {
|
||||
const newToken = error?.response?.headers?.['x-new-token'];
|
||||
const errors = error?.response?.data?.errors;
|
||||
const firstError = Array.isArray(errors) ? errors[0] : null;
|
||||
|
||||
const state = app.router.state;
|
||||
const { pathname, search } = state.location;
|
||||
const basename = app.router.basename;
|
||||
|
||||
if (newToken) {
|
||||
app.apiClient.auth.setToken(newToken);
|
||||
}
|
||||
if (error.status === 401 && !error.config?.skipAuth) {
|
||||
const requestToken = error?.config?.headers?.Authorization?.replace(/^Bearer\s+/gi, '');
|
||||
const currentToken = app.apiClient.auth.getToken();
|
||||
// if (currentToken && currentToken !== requestToken) {
|
||||
// error.config.skipNotify = true;
|
||||
// return app.apiClient.request(error.config);
|
||||
// }
|
||||
app.apiClient.auth.setToken('');
|
||||
const errors = error?.response?.data?.errors;
|
||||
const firstError = Array.isArray(errors) ? errors[0] : null;
|
||||
if (!firstError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// if the code
|
||||
if (firstError?.code === AuthErrorCode.SKIP_TOKEN_RENEW) {
|
||||
throw error;
|
||||
if (error.status === 401) {
|
||||
app.apiClient.auth.setToken('');
|
||||
if (pathname === app.getHref('signin') && firstError?.code !== AuthErrorCode.EMPTY_TOKEN && error.config) {
|
||||
error.config.skipNotify = false;
|
||||
}
|
||||
|
||||
if (firstError?.code === 'USER_HAS_NO_ROLES_ERR') {
|
||||
// use app error to show error message
|
||||
error.config.skipNotify = true;
|
||||
app.error = firstError;
|
||||
}
|
||||
}
|
||||
|
||||
if (error.status === 401 && !error.config?.skipAuth) {
|
||||
if (!firstError || firstError?.code === AuthErrorCode.SKIP_TOKEN_RENEW) {
|
||||
throw error;
|
||||
}
|
||||
const state = app.router.state;
|
||||
const { pathname, search } = state.location;
|
||||
const basename = app.router.basename;
|
||||
|
||||
if (pathname !== app.getHref('signin')) {
|
||||
const redirectPath = removeBasename(pathname, basename);
|
||||
|
@ -4,7 +4,7 @@
|
||||
"displayName.zh-CN": "区块:模板",
|
||||
"description": "Create and manage block templates for reuse on pages.",
|
||||
"description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。",
|
||||
"version": "1.6.0-alpha.14",
|
||||
"version": "1.6.0-alpha.28",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/server/index.js",
|
||||
"homepage": "https://docs.nocobase.com/handbook/block-template",
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useResource,
|
||||
ISchema,
|
||||
SchemaInitializerItemType,
|
||||
useCurrentUserContext,
|
||||
} from '@nocobase/client';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import PluginBlockTemplateClient from '..';
|
||||
@ -50,6 +51,7 @@ export const BlockTemplateMenusProvider = ({ children }) => {
|
||||
const isMobile = window.location.pathname.startsWith(mobilePlugin.mobileBasename);
|
||||
const location = useLocation();
|
||||
const previousPathRef = React.useRef(location.pathname);
|
||||
const user = useCurrentUserContext();
|
||||
|
||||
const { data, loading, refresh } = useRequest<{
|
||||
data: {
|
||||
@ -74,6 +76,7 @@ export const BlockTemplateMenusProvider = ({ children }) => {
|
||||
},
|
||||
{
|
||||
cacheKey: 'blockTemplates',
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
@ -87,6 +90,12 @@ export const BlockTemplateMenusProvider = ({ children }) => {
|
||||
previousPathRef.current = location.pathname;
|
||||
}, [location.pathname, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.data) {
|
||||
refresh();
|
||||
}
|
||||
}, [user, refresh]);
|
||||
|
||||
const handleTemplateClick = useMemoizedFn(async ({ item }, options?: any, insert?: any) => {
|
||||
const { uid } = item;
|
||||
const { data } = await api.request({
|
||||
|
@ -28,7 +28,7 @@ export async function templateDataMiddleware(ctx: Context, next) {
|
||||
ctx.action.resourceName === 'uiSchemas' &&
|
||||
['getProperties', 'getJsonSchema', 'getParentJsonSchema'].includes(ctx.action.actionName)
|
||||
) {
|
||||
const schema = ctx.body?.data;
|
||||
const schema = ctx.body;
|
||||
const schemaRepository = ctx.db.getRepository<UiSchemaRepository>('uiSchemas');
|
||||
const blockTemplateRepository = ctx.db.getRepository('blockTemplates');
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class PluginBlockTemplateServer extends Plugin {
|
||||
this.app.resourceManager.registerActionHandler('blockTemplates:destroy', destroy);
|
||||
this.app.resourceManager.registerActionHandler('blockTemplates:link', link);
|
||||
this.app.resourceManager.registerActionHandler('blockTemplates:saveSchema', saveSchema);
|
||||
this.app.use(templateDataMiddleware);
|
||||
this.app.resourceManager.use(templateDataMiddleware);
|
||||
}
|
||||
|
||||
async load() {}
|
||||
|
@ -20,11 +20,11 @@ export function CustomSchemaSettingsBlockTitleItem() {
|
||||
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Edit block title')}
|
||||
title={t('Edit block title & description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit block title'),
|
||||
title: t('Edit block title & description'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Block title'),
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps } from '@nocobase/client';
|
||||
import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client';
|
||||
import { Avatar } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useContext } from 'react';
|
||||
@ -18,13 +18,20 @@ import { WorkbenchLayout } from './workbenchBlockSettings';
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
// 支持 css object 的写法
|
||||
action: css`
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
padding-top: 8px;
|
||||
`,
|
||||
avatar: css`
|
||||
width: 4em;
|
||||
`,
|
||||
title: css`
|
||||
margin-top: ${token.marginSM}px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`,
|
||||
@ -39,8 +46,8 @@ function Button() {
|
||||
const compile = useCompile();
|
||||
const title = compile(fieldSchema.title);
|
||||
return layout === WorkbenchLayout.Grid ? (
|
||||
<div title={title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
|
||||
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
|
||||
<div title={title} className={cx(styles.avatar)}>
|
||||
<Avatar style={{ backgroundColor }} size={48} icon={<Icon type={icon} />} />
|
||||
<div className={cx(styles.title)}>{title}</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -54,12 +61,15 @@ export const WorkbenchAction = withDynamicSchemaProps((props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const Component = useComponent(props?.targetComponent) || Action;
|
||||
return (
|
||||
<Component
|
||||
className={cx(className, styles.action, 'nb-action-panel')}
|
||||
{...others}
|
||||
icon={null}
|
||||
title={<Button />}
|
||||
confirmTitle={fieldSchema.title}
|
||||
/>
|
||||
<ACLActionProvider>
|
||||
<Component
|
||||
className={cx(className, styles.action, 'nb-action-panel')}
|
||||
{...others}
|
||||
type="text"
|
||||
icon={null}
|
||||
title={<Button />}
|
||||
confirmTitle={fieldSchema.title}
|
||||
/>
|
||||
</ACLActionProvider>
|
||||
);
|
||||
});
|
||||
|
@ -7,23 +7,21 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
CollectionContext,
|
||||
createStyles,
|
||||
DataSourceContext,
|
||||
DndContext,
|
||||
Icon,
|
||||
NocoBaseRecursionField,
|
||||
useBlockHeight,
|
||||
useDesignable,
|
||||
useOpenModeContext,
|
||||
useSchemaInitializerRender,
|
||||
withDynamicSchemaProps,
|
||||
useBlockHeightProps,
|
||||
useOpenModeContext,
|
||||
} from '@nocobase/client';
|
||||
import { Avatar, List, Space, theme } from 'antd';
|
||||
import React, { createContext, useEffect, useState, useRef, useMemo, useLayoutEffect } from 'react';
|
||||
import { Avatar, Space } from 'antd';
|
||||
import { Grid, List } from 'antd-mobile';
|
||||
import React, { createContext } from 'react';
|
||||
import { WorkbenchLayout } from './workbenchBlockSettings';
|
||||
|
||||
const ConfigureActionsButton = observer(
|
||||
@ -46,105 +44,27 @@ const ResponsiveSpace = () => {
|
||||
const isMobileMedia = isMobile();
|
||||
const { isMobile: underMobileCtx } = useOpenModeContext() || {};
|
||||
const { itemsPerRow = 4 } = fieldSchema.parent['x-decorator-props'] || {};
|
||||
const isUnderMobile = isMobileMedia || underMobileCtx;
|
||||
const containerRef = useRef(null); // 引用容器
|
||||
const [containerWidth, setContainerWidth] = useState(0); // 容器宽度
|
||||
// 使用 ResizeObserver 动态获取容器宽度
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerRef.current) {
|
||||
setContainerWidth(containerRef.current.offsetWidth); // 更新宽度
|
||||
}
|
||||
};
|
||||
// 初始化 ResizeObserver
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
|
||||
// 监听容器宽度变化
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
handleResize(); // 初始化时获取一次宽度
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
resizeObserver.unobserve(containerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setContainerWidth(entry.contentRect.width); // 更新宽度
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(containerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 计算每个元素的宽度
|
||||
const itemWidth = useMemo(() => {
|
||||
if (isUnderMobile) {
|
||||
const totalGapWidth = gap * itemsPerRow;
|
||||
const availableWidth = containerWidth - totalGapWidth;
|
||||
return availableWidth / itemsPerRow;
|
||||
}
|
||||
return 70;
|
||||
}, [itemsPerRow, gap, containerWidth]);
|
||||
|
||||
// 计算 Avatar 的宽度
|
||||
const avatarSize = useMemo(() => {
|
||||
return isUnderMobile ? (Math.floor(itemWidth * 0.8) > 70 ? 60 : Math.floor(itemWidth * 0.8)) : 54; // Avatar 大小为 item 宽度的 60%
|
||||
}, [itemWidth, itemsPerRow, containerWidth]);
|
||||
if (underMobileCtx || isMobileMedia) {
|
||||
return (
|
||||
<Grid columns={itemsPerRow} gap={gap}>
|
||||
{fieldSchema.mapProperties((s, key) => {
|
||||
return (
|
||||
<Grid.Item style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} key={key}>
|
||||
<NocoBaseRecursionField name={key} schema={s} />
|
||||
</Grid.Item>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ width: '100%' }}>
|
||||
<Space
|
||||
wrap
|
||||
style={{ width: '100%', display: 'flex' }}
|
||||
size={gap}
|
||||
align="start"
|
||||
className={css`
|
||||
.ant-space-item {
|
||||
width: ${isUnderMobile ? itemWidth + 'px' : '100%'}
|
||||
display: flex;
|
||||
.ant-nb-action {
|
||||
padding: ${isUnderMobile ? '4px 0px' : null};
|
||||
}
|
||||
.nb-action-panel-container {
|
||||
width: ${itemWidth}px !important;
|
||||
}
|
||||
.ant-avatar-circle {
|
||||
width: ${avatarSize}px !important;
|
||||
height: ${avatarSize}px !important;
|
||||
line-height: ${avatarSize}px !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{fieldSchema.mapProperties((s, key) => (
|
||||
<div
|
||||
key={key}
|
||||
style={
|
||||
isUnderMobile && {
|
||||
flexBasis: `${itemWidth}px`,
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
display: 'flex',
|
||||
}
|
||||
}
|
||||
>
|
||||
<NocoBaseRecursionField name={key} schema={s} />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<Space wrap size={gap} align="start">
|
||||
{fieldSchema.mapProperties((s, key) => {
|
||||
return <NocoBaseRecursionField name={key} schema={s} />;
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
@ -157,36 +77,17 @@ const InternalIcons = () => {
|
||||
{layout === WorkbenchLayout.Grid ? (
|
||||
<ResponsiveSpace />
|
||||
) : (
|
||||
<List itemLayout="horizontal">
|
||||
<List>
|
||||
{fieldSchema.mapProperties((s, key) => {
|
||||
const icon = s['x-component-props']?.['icon'];
|
||||
const backgroundColor = s['x-component-props']?.['iconColor'];
|
||||
return (
|
||||
<List.Item
|
||||
key={key}
|
||||
className={css`
|
||||
.ant-list-item-meta-avatar {
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
.ant-list-item-meta-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
.ant-list-item-meta-title button {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
`}
|
||||
prefix={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar style={{ backgroundColor }} icon={<Icon type={icon} />} />}
|
||||
title={<NocoBaseRecursionField name={key} schema={s} key={key} />}
|
||||
></List.Item.Meta>
|
||||
<NocoBaseRecursionField name={key} schema={s} />
|
||||
</List.Item>
|
||||
);
|
||||
})}
|
||||
@ -199,44 +100,55 @@ const InternalIcons = () => {
|
||||
|
||||
export const WorkbenchBlockContext = createContext({ layout: 'grid' });
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
containerClass: css`
|
||||
&.list {
|
||||
margin: -${token.paddingLG}px;
|
||||
border-radius: ${(token as any).borderRadiusBlock}px;
|
||||
overflow: hidden;
|
||||
|
||||
.adm-list {
|
||||
--padding-left: ${token.paddingLG}px;
|
||||
--padding-right: ${token.paddingLG}px;
|
||||
|
||||
.adm-list-item-content-main {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
padding: 16px 32px;
|
||||
margin: -12px -32px;
|
||||
width: calc(100% + 64px);
|
||||
text-align: start;
|
||||
color: ${token.colorText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button[aria-label*='schema-initializer-WorkbenchBlock.ActionBar-workbench:configureActions'] {
|
||||
margin-bottom: ${token.paddingLG}px;
|
||||
margin-left: ${token.paddingLG}px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
export const WorkbenchBlock: any = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { layout = 'grid' } = fieldSchema['x-component-props'] || {};
|
||||
const { title } = fieldSchema['x-decorator-props'] || {};
|
||||
const targetHeight = useBlockHeight();
|
||||
const { token } = theme.useToken();
|
||||
const { designable } = useDesignable();
|
||||
const titleHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0;
|
||||
const internalHeight = 2 * token.paddingLG + token.controlHeight + token.marginLG + titleHeight;
|
||||
const warperHeight =
|
||||
targetHeight - (designable ? internalHeight : 2 * token.paddingLG + token.marginLG + titleHeight);
|
||||
const targetWarperHeight = warperHeight > 0 ? warperHeight + 'px' : '100%';
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="nb-action-penal-container"
|
||||
style={{ height: targetHeight ? targetHeight - 2 * token.paddingLG - gap - titleHeight : '100%' }}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
.nb-action-panel-warp {
|
||||
height: ${targetHeight ? targetWarperHeight : '100%'};
|
||||
overflow-y: auto;
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<WorkbenchBlockContext.Provider value={{ layout }}>
|
||||
<DataSourceContext.Provider value={undefined}>
|
||||
<CollectionContext.Provider value={undefined}>{props.children}</CollectionContext.Provider>
|
||||
</DataSourceContext.Provider>
|
||||
</WorkbenchBlockContext.Provider>
|
||||
</div>
|
||||
<div className={`nb-action-penal-container ${layout} ${styles.containerClass}`}>
|
||||
<WorkbenchBlockContext.Provider value={{ layout }}>
|
||||
<DataSourceContext.Provider value={undefined}>
|
||||
<CollectionContext.Provider value={undefined}>{props.children}</CollectionContext.Provider>
|
||||
</DataSourceContext.Provider>
|
||||
</WorkbenchBlockContext.Provider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
SchemaSettingsActionLinkItem,
|
||||
useSchemaInitializer,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -30,6 +31,12 @@ export const workbenchActionSettingsCustomRequest = new SchemaSettings({
|
||||
name: 'editLink',
|
||||
Component: SchemaSettingsActionLinkItem,
|
||||
},
|
||||
{
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: 800,
|
||||
name: 'd1',
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -32,6 +33,12 @@ export const workbenchActionSettingsLink = new SchemaSettings({
|
||||
name: 'editLink',
|
||||
Component: SchemaSettingsActionLinkItem,
|
||||
},
|
||||
{
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: 800,
|
||||
name: 'd1',
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
useSchemaInitializer,
|
||||
useOpenModeContext,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -44,6 +45,12 @@ export const workbenchActionSettingsPopup = new SchemaSettings({
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: 800,
|
||||
name: 'd1',
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
ModalActionSchemaInitializerItem,
|
||||
SchemaSettingAccessControl,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -28,6 +29,12 @@ export const workbenchActionSettingsScanQrCode = new SchemaSettings({
|
||||
return { hasIconColor: true };
|
||||
},
|
||||
},
|
||||
{
|
||||
...SchemaSettingAccessControl,
|
||||
useVisible() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'd1',
|
||||
type: 'divider',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user