Merge branch 'develop' into next

# Conflicts:
#	packages/core/client/src/locale/it-IT.json
#	packages/plugins/@nocobase/plugin-action-import/src/client/ImportAction.tsx
This commit is contained in:
chenos 2025-03-25 12:40:54 +08:00
commit 07ac4717b3
450 changed files with 14519 additions and 2606 deletions

View File

@ -1,5 +1,5 @@
{
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"],

View File

@ -52,7 +52,9 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"nwsapi": "2.2.7",
"antd": "5.12.8",
"antd": "5.24.2",
"@formily/antd-v5": "1.2.3",
"dayjs": "1.11.13",
"@ant-design/icons": "^5.6.1"
},
"config": {

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/acl",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/resourcer": "1.7.0-beta.8",
"@nocobase/utils": "1.7.0-beta.8",
"@nocobase/resourcer": "1.7.0-alpha.4",
"@nocobase/utils": "1.7.0-alpha.4",
"minimatch": "^5.1.1"
},
"repository": {

View File

@ -0,0 +1,579 @@
/**
* 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 { ACL } from '..';
describe('multiple roles merge', () => {
let acl: ACL;
beforeEach(() => {
acl = new ACL();
});
describe('filter merge', () => {
test('should allow all(params:{}) when filter1 = undefined, filter2 is not exists', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: undefined,
},
},
});
acl.define({
role: 'role2',
actions: {},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params:{}) when filter1 = undefined, filter2 = {}', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: undefined,
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: {},
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when filter1 = {}, filter2 = {}', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: {},
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: {},
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should union filter(params.filter={$or:[{id:1}, {id:2}]}) when filter1 = {id: 1}, filter2 = {id: 2}', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { id: 1 },
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: { id: 2 },
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {
filter: {
$or: expect.arrayContaining([{ id: 1 }, { id: 2 }]),
},
},
});
});
test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = {name: zhangsan}', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { id: 1 },
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: { name: 'zhangsan' },
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {
filter: {
$or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]),
},
},
});
});
test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = { $or: [{name: zhangsan}]', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { id: 1 },
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: { name: 'zhangsan' },
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {
filter: {
$or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]),
},
},
});
});
});
describe('feilds merge', () => {
test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
fields: [],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
fields: undefined,
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when fields1 = [], fields2 =[]', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
fields: [],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: [],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should union fields(params={ fields: [a,b]}) when fields1 = [a], fields2 =[b]', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
fields: ['a'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: ['b'],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {
fields: expect.arrayContaining(['a', 'b']),
},
});
});
test('should union no repeat fields(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[b,c]', () => {
acl.setAvailableAction('edit', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
fields: ['a', 'b'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: ['b', 'c'],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {
fields: expect.arrayContaining(['a', 'b', 'c']),
},
});
expect(canResult.params.fields.length).toStrictEqual(3);
});
});
describe('whitelist', () => {
test('should union whitelist(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[c]', () => {
acl.setAvailableAction('update');
acl.define({
role: 'role1',
actions: {
'posts:update': {
whitelist: ['a', 'b'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:update': {
whitelist: ['c'],
},
},
});
const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'update',
params: {
whitelist: expect.arrayContaining(['a', 'b', 'c']),
},
});
});
});
describe('appends', () => {
test('should union appends(params={ appends: [a,b,c]}) when appends = [a,b], appends =[c]', () => {
acl.setAvailableAction('update');
acl.define({
role: 'role1',
actions: {
'posts:update': {
appends: ['a', 'b'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:update': {
appends: ['c'],
},
},
});
const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'update',
params: {
appends: expect.arrayContaining(['a', 'b', 'c']),
},
});
});
test('should union appends(params={ appends: [a,b]}) when appends = [a,b], appends =[]', () => {
acl.setAvailableAction('update');
acl.define({
role: 'role1',
actions: {
'posts:update': {
appends: ['a', 'b'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:update': {
appends: [],
},
},
});
const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'update',
params: {
appends: expect.arrayContaining(['a', 'b']),
},
});
});
});
describe('filter & fields merge', () => {
test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: []}', () => {
acl.setAvailableAction('view', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: {},
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: [],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: [a]}', () => {
acl.setAvailableAction('view', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: {},
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: ['a'],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: []}', () => {
acl.setAvailableAction('view', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { a: 1 },
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: [],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: [a]}', () => {
acl.setAvailableAction('view', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { a: 1 },
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
fields: ['a'],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: {},
});
});
test('should union filter&fields(params={ filter:{ $or:[{a:1},{a:2}]}, fields:[a,b]}) when actions1={filter:{a:1}, fields:[a]}, actions2={filter: {a:1}},fields:[b]}', () => {
acl.setAvailableAction('view', {
type: 'old-data',
});
acl.define({
role: 'role1',
actions: {
'posts:view': {
filter: { a: 1 },
fields: ['a'],
},
},
});
acl.define({
role: 'role2',
actions: {
'posts:view': {
filter: { a: 2 },
fields: ['b'],
},
},
});
const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
expect(canResult).toStrictEqual({
role: 'role1',
resource: 'posts',
action: 'view',
params: expect.objectContaining({
filter: { $or: expect.arrayContaining([{ a: 1 }, { a: 2 }]) },
fields: expect.arrayContaining(['a', 'b']),
}),
});
});
});
});

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { default as _, default as lodash } from 'lodash';
import minimatch from 'minimatch';
import { ACL, DefineOptions } from './acl';
import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
import { ACLResource } from './acl-resource';
import lodash from 'lodash';
import minimatch from 'minimatch';
export interface RoleActionParams {
fields?: string[];
@ -185,12 +185,12 @@ export class ACLRole {
}
}
return {
return _.cloneDeep({
role: this.name,
strategy: this.strategy,
actions,
snippets: Array.from(this.snippets),
};
});
}
protected getResourceActionFromPath(path: string) {

View File

@ -19,6 +19,7 @@ import { AllowManager, ConditionFunc } from './allow-manager';
import FixedParamsManager, { Merger } from './fixed-params-manager';
import SnippetManager, { SnippetOptions } from './snippet-manager';
import { NoPermissionError } from './errors/no-permission-error';
import { mergeAclActionParams, removeEmptyParams } from './utils';
interface CanResult {
role: string;
@ -54,11 +55,12 @@ export interface ListenerContext {
type Listener = (ctx: ListenerContext) => void;
interface CanArgs {
role: string;
role?: string;
resource: string;
action: string;
rawResourceName?: string;
ctx?: any;
roles?: string[];
}
export class ACL extends EventEmitter {
@ -169,6 +171,10 @@ export class ACL extends EventEmitter {
return this.roles.get(name);
}
getRoles(names: string[]): ACLRole[] {
return names.map((name) => this.getRole(name)).filter((x) => Boolean(x));
}
removeRole(name: string) {
return this.roles.delete(name);
}
@ -202,6 +208,36 @@ export class ACL extends EventEmitter {
}
can(options: CanArgs): CanResult | null {
if (options.role) {
return lodash.cloneDeep(this.getCanByRole(options));
}
if (options.roles?.length) {
return lodash.cloneDeep(this.getCanByRoles(options));
}
return null;
}
private getCanByRoles(options: CanArgs) {
let canResult: CanResult | null = null;
for (const role of options.roles) {
const result = this.getCanByRole({
role,
...options,
});
if (!canResult) {
canResult = result;
canResult && removeEmptyParams(canResult.params);
} else if (canResult && result) {
canResult.params = mergeAclActionParams(canResult.params, result.params);
}
}
return canResult;
}
private getCanByRole(options: CanArgs) {
const { role, resource, action, rawResourceName } = options;
const aclRole = this.roles.get(role);
@ -351,9 +387,12 @@ export class ACL extends EventEmitter {
}
ctx.can = (options: Omit<CanArgs, 'role'>) => {
const canResult = acl.can({ role: roleName, ...options });
return canResult;
const roles = ctx.state.currentRoles || [roleName];
const can = acl.can({ roles, ...options });
if (!can) {
return null;
}
return can;
};
ctx.permission = {
@ -370,7 +409,7 @@ export class ACL extends EventEmitter {
* @internal
*/
async getActionParams(ctx) {
const roleName = ctx.state.currentRole || 'anonymous';
const roleNames = ctx.state.currentRoles?.length ? ctx.state.currentRoles : 'anonymous';
const { resourceName: rawResourceName, actionName } = ctx.action;
let resourceName = rawResourceName;
@ -386,11 +425,11 @@ export class ACL extends EventEmitter {
}
ctx.can = (options: Omit<CanArgs, 'role'>) => {
const can = this.can({ role: roleName, ...options });
if (!can) {
return null;
}
const can = this.can({ roles: roleNames, ...options });
if (can) {
return lodash.cloneDeep(can);
}
return null;
};
ctx.permission = {
@ -421,6 +460,23 @@ export class ACL extends EventEmitter {
}
}
// 检查 $or 条件中的 createdById
if (params?.filter?.$or?.length) {
const checkCreatedById = (items) => {
return items.some(
(x) =>
'createdById' in x || x.$or?.some((y) => 'createdById' in y) || x.$and?.some((y) => 'createdById' in y),
);
};
if (checkCreatedById(params.filter.$or)) {
const collection = ctx.db.getCollection(resourceName);
if (!collection || !collection.getField('createdById')) {
throw new NoPermissionError('createdById field not found');
}
}
}
return params;
}

View File

@ -14,3 +14,4 @@ export * from './acl-resource';
export * from './acl-role';
export * from './skip-middleware';
export * from './errors';
export * from './utils';

View File

@ -0,0 +1,213 @@
/**
* 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 { assign } from '@nocobase/utils';
import _ from 'lodash';
import { ACLRole } from '../acl-role';
export function mergeRole(roles: ACLRole[]) {
const result: Record<string, any> = {
roles: [],
strategy: {},
actions: null,
snippets: [],
resources: null,
};
const allSnippets: string[][] = [];
for (const role of roles) {
const jsonRole = role.toJSON();
result.roles = mergeRoleNames(result.roles, jsonRole.role);
result.strategy = mergeRoleStrategy(result.strategy, jsonRole.strategy);
result.actions = mergeRoleActions(result.actions, jsonRole.actions);
result.resources = mergeRoleResources(result.resources, [...role.resources.keys()]);
if (_.isArray(jsonRole.snippets)) {
allSnippets.push(jsonRole.snippets);
}
}
result.snippets = mergeRoleSnippets(allSnippets);
return result;
}
function mergeRoleNames(sourceRoleNames, newRoleName) {
return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames;
}
function mergeRoleStrategy(sourceStrategy, newStrategy) {
if (!newStrategy) {
return sourceStrategy;
}
if (_.isArray(newStrategy.actions)) {
if (!sourceStrategy.actions) {
sourceStrategy.actions = newStrategy.actions;
} else {
const actions = sourceStrategy.actions.concat(newStrategy.actions);
return {
...sourceStrategy,
actions: [...new Set(actions)],
};
}
}
return sourceStrategy;
}
function mergeRoleActions(sourceActions, newActions) {
if (_.isEmpty(sourceActions)) return newActions;
if (_.isEmpty(newActions)) return sourceActions;
const result = {};
[...new Set(Reflect.ownKeys(sourceActions).concat(Reflect.ownKeys(newActions)))].forEach((key) => {
if (_.has(sourceActions, key) && _.has(newActions, key)) {
result[key] = mergeAclActionParams(sourceActions[key], newActions[key]);
return;
}
result[key] = _.has(sourceActions, key) ? sourceActions[key] : newActions[key];
});
return result;
}
function mergeRoleSnippets(allRoleSnippets: string[][]): string[] {
if (!allRoleSnippets.length) {
return [];
}
const allSnippets = allRoleSnippets.flat();
const isExclusion = (value) => value.startsWith('!');
const includes = new Set(allSnippets.filter((x) => !isExclusion(x)));
const excludes = new Set(allSnippets.filter(isExclusion));
// 统计 xxx.* 在多少个角色中存在
const domainRoleMap = new Map<string, Set<number>>();
allRoleSnippets.forEach((roleSnippets, i) => {
roleSnippets
.filter((x) => x.endsWith('.*') && !isExclusion(x))
.forEach((include) => {
const domain = include.slice(0, -1);
if (!domainRoleMap.has(domain)) {
domainRoleMap.set(domain, new Set());
}
domainRoleMap.get(domain).add(i);
});
});
// 处理黑名单交集(只有所有角色都有 `!xxx` 才保留)
const excludesSet = new Set<string>();
for (const snippet of excludes) {
if (allRoleSnippets.every((x) => x.includes(snippet))) {
excludesSet.add(snippet);
}
}
for (const [domain, indexes] of domainRoleMap.entries()) {
const fullDomain = `${domain}.*`;
// xxx.* 存在时,覆盖 !xxx.*
if (includes.has(fullDomain)) {
excludesSet.delete(`!${fullDomain}`);
}
// 计算 !xxx.yyy当所有 xxx.* 角色都包含 !xxx.yyy 时才保留
for (const roleIndex of indexes) {
for (const exclude of allRoleSnippets[roleIndex]) {
if (exclude.startsWith(`!${domain}`) && exclude !== `!${fullDomain}`) {
if ([...indexes].every((i) => allRoleSnippets[i].includes(exclude))) {
excludesSet.add(exclude);
}
}
}
}
}
// 确保 !xxx.yyy 只有在 xxx.* 存在时才有效,同时解决 [xxx] 和 [!xxx] 冲突
if (includes.size > 0) {
for (const x of [...excludesSet]) {
const exactMatch = x.slice(1);
const segments = exactMatch.split('.');
if (segments.length > 1 && segments[1] !== '*') {
const parentDomain = segments[0] + '.*';
if (!includes.has(parentDomain)) {
excludesSet.delete(x);
}
}
}
}
return [...includes, ...excludesSet];
}
function mergeRoleResources(sourceResources, newResources) {
if (sourceResources === null) {
return newResources;
}
return [...new Set(sourceResources.concat(newResources))];
}
export function mergeAclActionParams(sourceParams, targetParams) {
if (_.isEmpty(sourceParams) || _.isEmpty(targetParams)) {
return {};
}
// source 和 target 其中之一没有 fields 字段时, 最终希望没有此字段
removeUnmatchedParams(sourceParams, targetParams, ['fields', 'whitelist', 'appends']);
const andMerge = (x, y) => {
if (_.isEmpty(x) || _.isEmpty(y)) {
return [];
}
return _.uniq(x.concat(y)).filter(Boolean);
};
const mergedParams = assign(targetParams, sourceParams, {
own: (x, y) => x || y,
filter: (x, y) => {
if (_.isEmpty(x) || _.isEmpty(y)) {
return {};
}
const xHasOr = _.has(x, '$or'),
yHasOr = _.has(y, '$or');
let $or = [x, y];
if (xHasOr && !yHasOr) {
$or = [...x.$or, y];
} else if (!xHasOr && yHasOr) {
$or = [x, ...y.$or];
} else if (xHasOr && yHasOr) {
$or = [...x.$or, ...y.$or];
}
return { $or: _.uniqWith($or, _.isEqual) };
},
fields: andMerge,
whitelist: andMerge,
appends: 'union',
});
removeEmptyParams(mergedParams);
return mergedParams;
}
export function removeEmptyParams(params) {
if (!_.isObject(params)) {
return;
}
Object.keys(params).forEach((key) => {
if (_.isEmpty(params[key])) {
delete params[key];
}
});
}
function removeUnmatchedParams(source, target, keys: string[]) {
for (const key of keys) {
if (_.has(source, key) && !_.has(target, key)) {
delete source[key];
}
if (!_.has(source, key) && _.has(target, key)) {
delete target[key];
}
}
}

View File

@ -0,0 +1,10 @@
/**
* 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.
*/
export * from './acl-role';

View File

@ -1,14 +1,14 @@
{
"name": "@nocobase/actions",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/cache": "1.7.0-beta.8",
"@nocobase/database": "1.7.0-beta.8",
"@nocobase/resourcer": "1.7.0-beta.8"
"@nocobase/cache": "1.7.0-alpha.4",
"@nocobase/database": "1.7.0-alpha.4",
"@nocobase/resourcer": "1.7.0-alpha.4"
},
"repository": {
"type": "git",

View File

@ -1,17 +1,17 @@
{
"name": "@nocobase/app",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/database": "1.7.0-beta.8",
"@nocobase/preset-nocobase": "1.7.0-beta.8",
"@nocobase/server": "1.7.0-beta.8"
"@nocobase/database": "1.7.0-alpha.4",
"@nocobase/preset-nocobase": "1.7.0-alpha.4",
"@nocobase/server": "1.7.0-alpha.4"
},
"devDependencies": {
"@nocobase/client": "1.7.0-beta.8"
"@nocobase/client": "1.7.0-alpha.4"
},
"repository": {
"type": "git",

View File

@ -1,16 +1,16 @@
{
"name": "@nocobase/auth",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/actions": "1.7.0-beta.8",
"@nocobase/cache": "1.7.0-beta.8",
"@nocobase/database": "1.7.0-beta.8",
"@nocobase/resourcer": "1.7.0-beta.8",
"@nocobase/utils": "1.7.0-beta.8",
"@nocobase/actions": "1.7.0-alpha.4",
"@nocobase/cache": "1.7.0-alpha.4",
"@nocobase/database": "1.7.0-alpha.4",
"@nocobase/resourcer": "1.7.0-alpha.4",
"@nocobase/utils": "1.7.0-alpha.4",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1"
},

View File

@ -22,7 +22,7 @@ describe('middleware', () => {
app = await createMockServer({
registerActions: true,
acl: true,
plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler'],
plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler', 'system-settings'],
});
// app.plugin(ApiKeysPlugin);

View File

@ -267,6 +267,24 @@ export class BaseAuth extends Auth {
return null;
}
async signNewToken(userId: number) {
const tokenInfo = await this.tokenController.add({ userId });
const expiresIn = Math.floor((await this.tokenController.getConfig()).tokenExpirationTime / 1000);
const token = this.jwt.sign(
{
userId,
temp: true,
iat: Math.floor(tokenInfo.issuedTime / 1000),
signInTime: tokenInfo.signInTime,
},
{
jwtid: tokenInfo.jti,
expiresIn,
},
);
return token;
}
async signIn() {
let user: Model;
try {
@ -282,20 +300,7 @@ export class BaseAuth extends Auth {
code: AuthErrorCode.NOT_EXIST_USER,
});
}
const tokenInfo = await this.tokenController.add({ userId: user.id });
const expiresIn = Math.floor((await this.tokenController.getConfig()).tokenExpirationTime / 1000);
const token = this.jwt.sign(
{
userId: user.id,
temp: true,
iat: Math.floor(tokenInfo.issuedTime / 1000),
signInTime: tokenInfo.signInTime,
},
{
jwtid: tokenInfo.jti,
expiresIn,
},
);
const token = await this.signNewToken(user.id);
return {
user,
token,

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,12 +1,12 @@
{
"name": "@nocobase/cache",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/lock-manager": "1.6.0-alpha.6",
"@nocobase/lock-manager": "1.7.0-alpha.4",
"bloom-filters": "^3.0.1",
"cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2"

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cli",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js"
},
"dependencies": {
"@nocobase/app": "1.7.0-beta.8",
"@nocobase/app": "1.7.0-alpha.4",
"@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20",
"chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.19.0"
},
"devDependencies": {
"@nocobase/devtools": "1.7.0-beta.8"
"@nocobase/devtools": "1.7.0-alpha.4"
},
"repository": {
"type": "git",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
"version": "1.7.0-beta.8",
"version": "1.7.0-alpha.4",
"license": "AGPL-3.0",
"main": "lib/index.js",
"module": "es/index.mjs",
@ -17,7 +17,7 @@
"@dnd-kit/modifiers": "^6.0.0",
"@dnd-kit/sortable": "^6.0.0",
"@emotion/css": "^11.7.1",
"@formily/antd-v5": "1.1.9",
"@formily/antd-v5": "1.2.3",
"@formily/core": "^2.2.27",
"@formily/grid": "^2.2.27",
"@formily/json-schema": "^2.2.27",
@ -27,11 +27,11 @@
"@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.7.0-beta.8",
"@nocobase/sdk": "1.7.0-beta.8",
"@nocobase/utils": "1.7.0-beta.8",
"@nocobase/evaluators": "1.7.0-alpha.4",
"@nocobase/sdk": "1.7.0-alpha.4",
"@nocobase/utils": "1.7.0-alpha.4",
"ahooks": "^3.7.2",
"antd": "5.12.8",
"antd": "5.24.2",
"antd-style": "3.7.1",
"axios": "^1.7.0",
"bignumber.js": "^9.1.2",

View File

@ -103,6 +103,11 @@ export const useRoleRecheck = () => {
};
};
export const useCurrentRoleMode = () => {
const ctx = useContext(ACLContext);
return ctx?.data?.data?.roleMode;
};
export const useACLContext = () => {
return useContext(ACLContext);
};

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Checkbox, message, Table } from 'antd';
import { Checkbox, message, Table, TableProps } from 'antd';
import { omit } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -102,7 +102,8 @@ export const SettingsCenterConfigure = () => {
expandable={{
defaultExpandAllRows: true,
}}
columns={[
columns={
[
{
dataIndex: 'title',
title: t('Plugin name'),
@ -139,7 +140,8 @@ export const SettingsCenterConfigure = () => {
return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
},
},
]}
] as TableProps['columns']
}
dataSource={settings
.filter((v) => {
return v.isTopLevel !== false;

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Checkbox, message, Table } from 'antd';
import { Checkbox, message, Table, TableProps } from 'antd';
import { uniq } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -121,7 +121,8 @@ export const MenuConfigure = () => {
expandable={{
defaultExpandAllRows: true,
}}
columns={[
columns={
[
{
dataIndex: 'title',
title: t('Menu item title'),
@ -149,12 +150,13 @@ export const MenuConfigure = () => {
{t('Accessible')}
</>
),
render: (_, schema) => {
render: (_, schema: { uid: string }) => {
const checked = uids.includes(schema.uid);
return <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
},
},
]}
] as TableProps['columns']
}
dataSource={translateTitle(items)}
/>
);

View File

@ -10,7 +10,7 @@
import { FormItem, FormLayout } from '@formily/antd-v5';
import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react';
import { Checkbox, Table, Tag } from 'antd';
import { Checkbox, Table, Tag, TableProps } from 'antd';
import { isEmpty } from 'lodash';
import React, { createContext } from 'react';
import { useTranslation } from 'react-i18next';
@ -105,7 +105,8 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell}
size={'small'}
pagination={false}
columns={[
columns={
[
{
dataIndex: 'displayName',
title: t('Action display name'),
@ -146,7 +147,8 @@ export const RolesResourcesActions = connect((props) => {
/>
),
},
]}
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => {
let enabled = false;
let scope = null;
@ -169,7 +171,8 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell}
pagination={false}
dataSource={fieldPermissions}
columns={[
columns={
[
{
dataIndex: ['uiSchema', 'title'],
title: t('Field display name'),
@ -222,7 +225,8 @@ export const RolesResourcesActions = connect((props) => {
),
};
}),
]}
] as TableProps['columns']
}
/>
</FormItem>
</FormLayout>

View File

@ -9,7 +9,7 @@
import { ArrayField } from '@formily/core';
import { connect, useField } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import { Checkbox, Select, Table, Tag, TableProps } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
@ -55,7 +55,8 @@ export const StrategyActions = connect((props) => {
size={'small'}
pagination={false}
rowKey={'name'}
columns={[
columns={
[
{
dataIndex: 'displayName',
title: t('Action display name'),
@ -110,7 +111,8 @@ export const StrategyActions = connect((props) => {
/>
),
},
]}
] as TableProps['columns']
}
dataSource={availableActions?.map((item) => {
let scope = 'all';
let enabled = false;

View File

@ -9,6 +9,7 @@
import { get, set } from 'lodash';
import React, { ComponentType, createContext, useContext } from 'react';
import { matchRoutes } from 'react-router';
import {
BrowserRouterProps,
createBrowserRouter,
@ -42,6 +43,7 @@ export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRo
export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
Component?: ComponentTypeAndString;
skipAuthCheck?: boolean;
}
export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
@ -134,6 +136,18 @@ export class RouterManager {
this.options.basename = basename;
}
matchRoutes(pathname: string) {
const routes = Object.values(this.routes);
// @ts-ignore
return matchRoutes<RouteType>(routes, pathname, this.basename);
}
isSkippedAuthCheckRoute(pathname: string) {
const matchedRoutes = this.matchRoutes(pathname);
return matchedRoutes.some((match) => {
return match?.route?.skipAuthCheck === true;
});
}
/**
* @internal
*/

View File

@ -30,7 +30,7 @@ describe('Router', () => {
let router: RouterManager;
beforeEach(() => {
router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app);
router = new RouterManager({ type: 'memory', initialEntries: ['/'], basename: '/nocobase/apps/test1' }, app);
});
it('basic', () => {
@ -132,6 +132,38 @@ describe('Router', () => {
router.add('test', route);
expect(router.getRoutesTree()).toEqual([{ path: '/', element: <Hello />, children: undefined }]);
});
it('add skipAuthCheck route', () => {
router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true });
router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' });
const RouterComponent = router.getRouterComponent();
const BaseLayout: FC = (props) => {
return <div>BaseLayout {props.children}</div>;
};
render(<RouterComponent BaseLayout={BaseLayout} />);
router.navigate('/skip-auth-check');
const state = router.state;
const { pathname, search } = state.location;
const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname);
expect(isSkipedAuthCheck).toBe(true);
});
it('add not skipAuthCheck route', () => {
router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true });
router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' });
const RouterComponent = router.getRouterComponent();
const BaseLayout: FC = (props) => {
return <div>BaseLayout {props.children}</div>;
};
render(<RouterComponent BaseLayout={BaseLayout} />);
router.navigate('/not-skip-auth-check');
const state = router.state;
const { pathname, search } = state.location;
const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname);
expect(isSkipedAuthCheck).toBe(false);
});
});
describe('remove', () => {

View File

@ -12,3 +12,4 @@ export * from './useAppSpin';
export * from './usePlugin';
export * from './useRouter';
export * from './useGlobalVariable';
export * from './useAclSnippets';

View File

@ -12,7 +12,6 @@ import { useField, useFieldSchema } from '@formily/react';
import { useUpdate } from 'ahooks';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { useCollectionManager_deprecated } from '../collection-manager';
import { useCollection, useCollectionRecordData } from '../data-source';
import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
@ -103,8 +102,7 @@ const useCompatDetailsBlockParams = (props) => {
export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
const { params, parseVariableLoading } = useCompatDetailsBlockParams(props);
const record = useCollectionRecordData();
const { association, dataSource, action } = props;
const { getCollection } = useCollectionManager_deprecated(dataSource);
const { association, action } = props;
const { __collection } = record || {};
const { designable } = useDesignable();
const collectionName = props.collection;

View File

@ -1157,6 +1157,7 @@ export const useDetailsPaginationProps = () => {
current: ctx.service?.data?.meta?.page || 1,
pageSize: 1,
showSizeChanger: false,
align: 'center',
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });
@ -1182,6 +1183,7 @@ export const useDetailsPaginationProps = () => {
total: count,
pageSize: 1,
showSizeChanger: false,
align: 'center',
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });

View File

@ -25,6 +25,7 @@ import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect';
import * as components from './components';
import { useFieldInterfaceOptions } from './interfaces';
import { ItemType, MenuItemType } from 'antd/es/menu/interface';
const getSchema = (schema: CollectionFieldInterface, record: any, compile) => {
if (!schema) {
@ -231,7 +232,7 @@ export const AddFieldAction = (props) => {
}, [getTemplate, record]);
const items = useMemo<MenuProps['items']>(() => {
return getFieldOptions()
.map((option) => {
.map((option): ItemType & { title: string; children?: ItemType[] } => {
if (option?.children?.length === 0) {
return null;
}

View File

@ -96,7 +96,7 @@ export const PresetFields = observer(
rowSelection={{
type: 'checkbox',
selectedRowKeys,
getCheckboxProps: (record) => ({
getCheckboxProps: (record: { name: string }) => ({
name: record.name,
disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name),
}),

View File

@ -47,6 +47,7 @@ export const CSSVariableProvider = ({ children }) => {
document.body.style.setProperty('--colorBgScrollBarActive', colorBgScrollBarActive);
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
document.body.style.setProperty('--colorTemplateBgSettingsHover', token.colorTemplateBgSettingsHover);
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
document.body.style.setProperty('--colorBgMenuItemSelected', token.colorBgHeaderMenuActive);
@ -60,6 +61,7 @@ export const CSSVariableProvider = ({ children }) => {
token.colorBgContainer,
token.colorBgLayout,
token.colorBgSettingsHover,
token.colorTemplateBgSettingsHover,
token.colorBorderSettingsHover,
token.colorInfoBg,
token.colorInfoBorder,

View File

@ -24,11 +24,13 @@ const defaultTheme: ThemeConfig = {
// UI 配置组件
colorSettings: '#F18B62',
colorBgSettingsHover: 'rgba(241, 139, 98, 0.06)',
colorTemplateBgSettingsHover: 'rgba(98, 200, 241, 0.06)', // 默认为colorBgSettingsHover的互补色
colorBorderSettingsHover: 'rgba(241, 139, 98, 0.3)',
// 动画相关
motionUnit: 0.03,
motion: !process.env.__E2E__,
// ant design 升级到5.24.2后Modal.confirm在E2E中如果关闭动画会出现ant-modal-mask不销毁的问题
// motion: !process.env.__E2E__,
},
};

View File

@ -30,6 +30,8 @@ export interface CustomToken extends AliasToken {
colorSettings: string;
/** 鼠标悬浮时显示的背景色 */
colorBgSettingsHover: string;
/** 鼠标悬浮模板区块时显示的背景色 */
colorTemplateBgSettingsHover: string;
/** 鼠标悬浮时显示的边框色 */
colorBorderSettingsHover: string;

View File

@ -164,6 +164,7 @@
"Chart type": "Chart type",
"Chart config": "Chart config",
"Templates": "Templates",
"Template": "Template",
"Select template": "Select template",
"Action logs": "Action logs",
"Create template": "Create template",
@ -884,5 +885,8 @@
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.",
"Deprecated": "Deprecated",
"The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.",
"Full permissions": "Full permissions"
}

View File

@ -148,6 +148,7 @@
"Chart type": "Tipo del gráfico",
"Chart config": "Configuración del gráfico",
"Templates": "Plantillas",
"Template": "Plantilla",
"Select template": "Seleccione plantilla",
"Action logs": "Acción logs",
"Create template": "Crear plantilla",
@ -801,5 +802,8 @@
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla.",
"Deprecated": "Obsoleto",
"The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.",
"Full permissions": "Todos los derechos"
}

View File

@ -159,6 +159,7 @@
"Chart type": "Type de graphique",
"Chart config": "Configuration du graphique",
"Templates": "Modèles",
"Template": "Modèle",
"Select template": "Sélectionner un modèle",
"Action logs": "Logs d'action",
"Create template": "Créer un modèle",
@ -821,5 +822,8 @@
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer.",
"Deprecated": "Déprécié",
"The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.",
"Full permissions": "Tous les droits"
}

View File

@ -145,6 +145,7 @@
"Chart type": "チャートタイプ",
"Chart config": "チャート設定",
"Templates": "テンプレート",
"Template": "テンプレート",
"Select template": "テンプレートを選択してください",
"Action logs": "操作履歴",
"Create template": "テンプレートを作成",
@ -1039,5 +1040,8 @@
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。"
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
"Deprecated": "非推奨",
"The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。",
"Full permissions": "すべての権限"
}

View File

@ -183,6 +183,7 @@
"Chart type": "차트 유형",
"Chart config": "차트 구성",
"Templates": "템플릿",
"Template": "템플릿",
"Select template": "템플릿 선택",
"Action logs": "작업 로그",
"Create template": "템플릿 생성",
@ -912,5 +913,8 @@
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
"Deprecated": "사용 중단됨",
"The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.",
"Full permissions": "모든 권한"
}

View File

@ -112,6 +112,7 @@
"Chart type": "Tipo de gráfico",
"Chart config": "Configuração do gráfico",
"Templates": "Modelos",
"Template": "Modelo",
"Select template": "Selecione um modelo",
"Action logs": "Registros de ação",
"Create template": "Criar modelo",
@ -778,5 +779,8 @@
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.",
"Are you sure you want to hide this tab?": "Tem certeza de que deseja ocultar esta guia?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la.",
"Deprecated": "Descontinuado",
"The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.",
"Full permissions": "Todas as permissões"
}

View File

@ -102,6 +102,7 @@
"Chart type": "Тип диаграммы",
"Chart config": "Конфиг. диаграммы",
"Templates": "Шаблоны",
"Template": "Шаблон",
"Select template": "Выбрать шаблон",
"Action logs": "Журналы действий",
"Create template": "Создать шаблон",
@ -607,5 +608,8 @@
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
"Are you sure you want to hide this tab?": "Вы уверены, что хотите скрыть эту вкладку?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.",
"Deprecated": "Устаревший",
"The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.",
"Full permissions": "Полные права"
}

View File

@ -102,6 +102,7 @@
"Chart type": "Grafik türü",
"Chart config": "Grafik yapılandırması",
"Templates": "Şablonlar",
"Template": "Şablon",
"Select template": "Şablon seç",
"Action logs": "Eylem günlükleri",
"Create template": "Şablon oluştur",
@ -605,5 +606,8 @@
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.",
"Are you sure you want to hide this tab?": "Bu sekmeyi gizlemek istediğinizden emin misiniz?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor.",
"Deprecated": "Kullanımdan kaldırıldı",
"The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.",
"Full permissions": "Tüm izinler"
}

View File

@ -159,6 +159,7 @@
"Chart type": "Тип діаграми",
"Chart config": "Налаштування діаграми",
"Templates": "Шаблони",
"Template": "Шаблон",
"Select template": "Вибрати шаблон",
"Action logs": "Журнал дій",
"Create template": "Створити шаблон",
@ -821,5 +822,8 @@
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
"Are you sure you want to hide this tab?": "Ви впевнені, що хочете приховати цю вкладку?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її."
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.",
"Deprecated": "Застаріло",
"The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.",
"Full permissions": "Повні права"
}

View File

@ -183,6 +183,7 @@
"Chart type": "图表类型",
"Chart config": "图表配置",
"Templates": "模板",
"Template": "模板",
"Select template": "选择模板",
"Action logs": "操作日志",
"Create template": "创建模板",
@ -1082,8 +1083,11 @@
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
"Are you sure you want to hide this tab?": "你确定要隐藏该标签页吗?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。",
"Date scope": "日期范围",
"Deprecated": "已弃用",
"The following old template features have been deprecated and will be removed in next version.": "以下旧的模板功能已弃用,将在下个版本移除。",
"Full permissions": "全部权限",
"Enable index column": "启用序号列",
"Date scope": "日期范围",
"Icon only": "仅显示图标",
"Valid range: 100-900": "有效范围100-900",
"Valid range: 10-40": "有效范围10-40",

View File

@ -183,6 +183,7 @@
"Chart type": "圖表型別",
"Chart config": "圖表設定",
"Templates": "模板",
"Template": "模板",
"Select template": "選擇模板",
"Action logs": "動作日誌",
"Create template": "建立模板",
@ -912,6 +913,8 @@
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
"Are you sure you want to hide this tab?": "你確定要隱藏這個標籤嗎?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。"
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
"Deprecated": "已棄用",
"The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。",
"Full permissions": "完全權限"
}

View File

@ -28,7 +28,7 @@ test.describe('bulk-destroy', () => {
// 3. 点击批量删除按钮Table 显示无数据
await page.getByLabel('action-Action-Delete-destroy-').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible();
});
test('Secondary confirmation', async ({ page, mockPage, mockRecords }) => {
@ -45,6 +45,7 @@ test.describe('bulk-destroy', () => {
await page.getByLabel('designer-schema-settings-Action-actionSettings:bulkDelete-general').hover();
await page.getByRole('menuitem', { name: 'Secondary confirmation' }).click();
await page.getByLabel('Enable secondary confirmation').uncheck();
await expect(page.getByRole('button', { name: 'OK' })).toHaveCount(1);
await page.getByRole('button', { name: 'OK' }).click();
await page.mouse.move(500, 0);
@ -53,6 +54,6 @@ test.describe('bulk-destroy', () => {
// 3. 点击批量删除按钮Table 显示无数据
await page.getByLabel('action-Action-Delete-destroy-').click();
await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible();
});
});

View File

@ -28,6 +28,9 @@ test.describe('Link', () => {
// 2. config the Link button
await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').hover();
await expect(
page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }),
).toHaveCount(1);
await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover();
await page.getByRole('menuitem', { name: 'Edit link' }).click();
await page

View File

@ -15,7 +15,7 @@ import {
} from './templates';
test.describe('Submit: should refresh data after submit', () => {
test('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
test.skip('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
const nocoPage = await mockPage(submitInReferenceTemplateBlock).waitForInit();
await mockRecord('collection', { nickname: 'abc' });
await nocoPage.goto();

View File

@ -29,6 +29,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
await expect(page.getByRole('tooltip').getByText('Disassociate')).toBeVisible();
await page.getByLabel('block-item-CardItem-cc-table').hover();
await page.getByRole('menuitem', { name: 'Associate' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-ActionBar-table:configureActions-cc').hover();
await page.getByRole('menuitem', { name: 'Associate' }).click();
//点击 associate 出现弹窗

View File

@ -18,7 +18,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
// 2. Table 中显示 Role UID 字段

View File

@ -14,6 +14,7 @@ import { useCollection } from '../../data-source/collection/CollectionProvider';
import { useCompile } from '../../schema-component';
import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner';
import { useSchemaTemplate } from '../../schema-templates';
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
export const BlockSchemaToolbar = (props) => {
const { t } = useTranslation();
@ -22,6 +23,7 @@ export const BlockSchemaToolbar = (props) => {
const template = useSchemaTemplate();
const { association, collection } = useDataBlockProps() || {};
const compile = useCompile();
const { isMobileLayout } = useMobileLayout();
if (association) {
const [collectionName] = association.split('.');
@ -51,7 +53,7 @@ export const BlockSchemaToolbar = (props) => {
].filter(Boolean);
}, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]);
return <SchemaToolbar title={toolbarTitle} {...props} />;
return <SchemaToolbar title={toolbarTitle} {...props} draggable={!isMobileLayout} />;
};
export function getCollectionTitle(arg: {

View File

@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -31,6 +32,7 @@ test.describe('where multi data details block can be added', () => {
// 1. 打开弹窗,通过 Associated records 添加一个详情区块
await page.getByLabel('action-Action.Link-View').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
@ -41,6 +43,7 @@ test.describe('where multi data details block can be added', () => {
await expect(page.getByLabel('block-item-CollectionField-').getByText('admin')).toBeVisible();
// 2. 打开弹窗,通过 Other records 添加一个详情区块
await page.getByRole('menuitem', { name: 'Details right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
@ -116,6 +119,7 @@ test.describe('configure actions', () => {
await page.getByText('Delete').click();
await page.mouse.move(300, 0);
await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();

View File

@ -33,7 +33,7 @@ test.describe('multi data details block schema settings', () => {
'Linkage rules',
'Set the data scope',
'Set default sorting rules',
'Save as template',
// 'Save as template',
'Delete',
],
});
@ -76,6 +76,7 @@ test.describe('actions schema settings', () => {
await expectSettingsMenu({
page,
showMenu: async () => {
await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1);
await page.getByRole('button', { name: 'Edit' }).hover();
await page.getByRole('button', { name: 'designer-schema-settings-Action' }).hover();
},

View File

@ -50,10 +50,10 @@ test.describe('setDataLoadingModeSettingsItem', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 所有区块应该显示 No data
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible();
// 3. 在筛选表单中数据一个筛选条件,点击筛选按钮,区块内应该显示数据
await page.getByLabel('block-item-CollectionField-').getByRole('textbox').click();
@ -67,10 +67,10 @@ test.describe('setDataLoadingModeSettingsItem', () => {
// 4. 点击筛选表单的 Reset 按钮,区块内应该显示 No data
await page.getByLabel('action-Action-Reset to empty-users-').click();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible();
await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible();
});
test('When the data block has data scope settings and dataLoadingMode is manual, data should not be displayed after the first page load', async ({
@ -78,7 +78,7 @@ test.describe('setDataLoadingModeSettingsItem', () => {
mockPage,
}) => {
await mockPage(TableBlockWithDataScope).goto();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 此时点击 filter 按钮,应该还是没数据,因为表单没有值
await page.getByLabel('action-Action-Filter-submit-').click({
@ -87,7 +87,7 @@ test.describe('setDataLoadingModeSettingsItem', () => {
y: 10,
},
});
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 点击 Reset 按钮,也是一样
await page.getByLabel('action-Action-Reset-users-').click({
@ -96,6 +96,6 @@ test.describe('setDataLoadingModeSettingsItem', () => {
y: 10,
},
});
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
});
});

View File

@ -26,7 +26,7 @@ test.describe('where single data details block can be added', () => {
});
// https://nocobase.height.app/T-3848/description
test('popup opened by clicking on the button for the relationship field', async ({
test.skip('popup opened by clicking on the button for the relationship field', async ({
page,
mockPage,
mockRecord,
@ -69,7 +69,7 @@ test.describe('where single data details block can be added', () => {
// 3.通过 Associated records 创建一个详情区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToOne' }).hover();
await page.getByRole('menuitem', { name: 'Blank block' }).click();
await page.mouse.move(300, 0);
@ -82,7 +82,7 @@ test.describe('where single data details block can be added', () => {
// 4.通过 Associated records 创建一个详情区块,使用模板
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToOne' }).hover();
await page.getByRole('menuitem', { name: 'Duplicate template' }).hover();
await page.getByRole('menuitem', { name: 'example_Details (Fields only)' }).click();

View File

@ -24,7 +24,7 @@ test.describe('single details block schema settings', () => {
await page.getByLabel('block-item-CardItem-general-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover();
},
supportedOptions: ['Edit block title', 'Linkage rules', 'Save as block template', 'Delete'],
supportedOptions: ['Edit block title', 'Linkage rules', 'Delete'],
});
});
});

View File

@ -60,10 +60,10 @@ test.describe('configure fields', () => {
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).toBeChecked();
// add association fields
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked();
await page.mouse.move(300, 0);
@ -72,13 +72,14 @@ test.describe('configure fields', () => {
// delete fields
await page.getByLabel('schema-initializer-Grid-form:configureFields-general').hover();
await expect(page.getByRole('menuitem', { name: 'ID', exact: true })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked();
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked();
await page.mouse.move(300, 0);
@ -119,6 +120,7 @@ test.describe('configure actions', () => {
// add button
await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.mouse.move(300, 0);
await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
// delete button

View File

@ -279,7 +279,7 @@ test.describe('set default value', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 2. 设置的 abcd 应该立即显示在 Nickname 字段的输入框中
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abcd');
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox').last()).toHaveValue('abcd');
});
test('Current popup record', async ({ page, mockPage }) => {

View File

@ -51,7 +51,8 @@ test.describe('creation form block schema settings', () => {
await runExpect();
});
test('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
// deprecated
test.skip('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
await mockPage(oneTableBlockWithActionsAndFormBlocks).goto();
await page.getByRole('button', { name: 'Add new' }).click();
@ -115,7 +116,7 @@ test.describe('creation form block schema settings', () => {
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
});
test('save as block Template', async ({ page, mockPage }) => {
test.skip('save as block Template', async ({ page, mockPage }) => {
await mockPage(oneEmptyForm).goto();
// 先保存为模板 ------------------------------------------------------------------------
@ -247,7 +248,7 @@ test.describe('creation form block schema settings', () => {
// 重新选择一下数据,字段值才会被填充
// TODO: 保存后,数据应该直接被填充上
await page.getByLabel('icon-close-select').click();
await page.getByLabel('icon-close-select').last().click();
await page.getByTestId('select-object-single').click();
await page.getByRole('option', { name: '2' }).click();
@ -270,7 +271,7 @@ test.describe('creation form block schema settings', () => {
});
});
test('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
test.skip('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
// 确保测试结束后已保存的模板会被清空
await clearBlockTemplates();
const nocoPage = await mockPage({

View File

@ -128,9 +128,14 @@ test.describe('linkage rules', () => {
// 增加一条规则:当 number 字段的值等于 123 时
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click();
await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click();
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add condition', { exact: true }).click();
await page
.getByLabel('Linkage rules')
.getByRole('tabpanel')
.getByText('Add condition', { exact: true })
.last()
.click();
await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
@ -146,19 +151,19 @@ test.describe('linkage rules', () => {
// action: 为 longText 字段赋上常量值
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click();
await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('tree').getByText('longText').click();
await page.getByRole('tree').getByText('longText').last().click();
await page.getByRole('button', { name: 'action', exact: true }).click();
await page.getByRole('option', { name: 'Value', exact: true }).click();
await page.getByRole('option', { name: 'Value', exact: true }).last().click();
await page.getByLabel('dynamic-component-linkage-rules').getByRole('textbox').fill('456');
// action: 为 integer 字段附上一个表达式,使其值等于 number 字段的值
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click();
await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('tree').getByText('integer').click();
await page.getByRole('tree').getByText('integer').last().click();
await page.getByRole('button', { name: 'action', exact: true }).click();
await page.getByRole('option', { name: 'Value', exact: true }).click();
await page.getByTestId('select-linkage-value-type').nth(1).click();
await page.getByRole('option', { name: 'Value', exact: true }).last().click();
await page.getByTestId('select-linkage-value-type').last().click();
await page.getByText('Expression').click();
await page.getByText('xSelect a variable').click();
@ -236,7 +241,7 @@ test.describe('linkage rules', () => {
});
// https://nocobase.height.app/T-3806
test('after save as block template', async ({ page, mockPage }) => {
test.skip('after save as block template', async ({ page, mockPage }) => {
await mockPage(T3806).goto();
// 1. 一开始联动规则应该正常

View File

@ -33,6 +33,7 @@ test.describe('bulk edit form', () => {
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 3. 输入值,点击提交
await expect(page.getByLabel('block-item-BulkEditField-').getByRole('textbox')).toHaveCount(1);
await page.getByLabel('block-item-BulkEditField-').getByRole('textbox').fill('123');
await page.getByRole('button', { name: 'Submit' }).click();
@ -65,6 +66,7 @@ test.describe('bulk edit form', () => {
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 4. 点击提交按钮,应该提示一个错误
await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1);
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByLabel('block-item-BulkEditField-').getByText('The field value is required')).toBeVisible();

View File

@ -34,6 +34,7 @@ test.describe('deprecated variables', () => {
// 表达式输入框也是一样
await page.getByText('xSelect a variable').click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveCount(1);
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
@ -45,6 +46,7 @@ test.describe('deprecated variables', () => {
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
await page.locator('button').filter({ hasText: /^x$/ }).click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible();

View File

@ -26,7 +26,7 @@ test.describe('where edit form block can be added', () => {
});
// https://nocobase.height.app/T-3848/description
test('popup opened by clicking on the button for the relationship field', async ({
test.skip('popup opened by clicking on the button for the relationship field', async ({
page,
mockPage,
mockRecord,

View File

@ -18,6 +18,7 @@ import {
import { T3825 } from './templatesOfBug';
const clickOption = async (page: Page, optionName: string) => {
await page.getByLabel('block-item-CardItem-general-form').hover();
await page.getByRole('menuitem', { name: optionName }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
await page.getByRole('menuitem', { name: optionName }).click();
};
@ -84,7 +85,7 @@ test.describe('edit form block schema settings', () => {
await runExpect();
});
test('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
test.skip('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit();
await mockRecord('general');
await nocoPage.goto();

View File

@ -12,9 +12,8 @@ import { useDetailsParentRecord } from '../../details-single/hooks/useDetailsDec
import { useHiddenForInherit } from './useHiddenForInherit';
export function useEditFormBlockDecoratorProps(props) {
const params = useFormBlockParams();
const params = useFormBlockParams(props);
let parentRecord;
const { hidden } = useHiddenForInherit(props);
// association 的值是固定不变的,所以这里可以使用 hooks
@ -31,6 +30,6 @@ export function useEditFormBlockDecoratorProps(props) {
};
}
function useFormBlockParams() {
return useParamsFromRecord();
function useFormBlockParams(props) {
return useParamsFromRecord(props);
}

View File

@ -13,6 +13,7 @@ import { oneGridCardWithInheritFields } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -47,6 +48,7 @@ test.describe('where grid card block can be added', () => {
// 2. 通过 Other records 创建一个列表区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Other records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Grid Card right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
@ -151,10 +153,10 @@ test.describe('configure fields', () => {
// add association fields
await page.mouse.wheel(0, 300);
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked();
await page.mouse.move(300, 0);
@ -165,14 +167,14 @@ test.describe('configure fields', () => {
// delete fields
await formItemInitializer.hover();
await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked();
await page.getByRole('menuitem', { name: 'ID', exact: true }).first().click();
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch').first()).not.toBeChecked();
await page.mouse.wheel(0, 300);
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked();
await page.mouse.move(300, 0);
@ -185,7 +187,7 @@ test.describe('configure fields', () => {
// add markdown
await formItemInitializer.hover();
await page.getByRole('menuitem', { name: 'ID', exact: true }).hover();
await page.getByRole('menuitem', { name: 'ID', exact: true }).first().hover();
await page.mouse.wheel(0, 300);
await page.getByRole('menuitem', { name: 'Add Markdown' }).click();

View File

@ -18,6 +18,7 @@ test.describe('grid card block schema settings', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-BlockItem-general-grid-card').hover();
await page.waitForTimeout(1000);
await page.getByLabel('designer-schema-settings-BlockItem-GridCard.Designer-general').hover();
},
supportedOptions: [
@ -25,7 +26,7 @@ test.describe('grid card block schema settings', () => {
'Set the data scope',
'Set default sorting rules',
'Records per page',
'Save as template',
// 'Save as template',
'Delete',
],
});

View File

@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from '../../details-multi/__e2e__/templatesOfB
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -71,6 +72,9 @@ test.describe('configure global actions', () => {
await page.getByRole('menuitem', { name: 'Refresh' }).click();
await page.mouse.move(300, 0);
await expect(page.getByRole('button', { name: 'Filter' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Add new' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Refresh' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible();

View File

@ -24,7 +24,7 @@ test.describe('list block schema settings', () => {
'Set the data scope',
'Set default sorting rules',
'Records per page',
'Save as template',
// 'Save as template',
'Delete',
],
});

View File

@ -90,7 +90,7 @@ test.describe('configure actions column', () => {
// 列宽度默认为 100
await expectActionsColumnWidth(100);
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users').hover();
await page.getByRole('menuitem', { name: 'Column width' }).click();

View File

@ -10,7 +10,7 @@
import { expect, test } from '@nocobase/test/e2e';
import { ordinaryBlockTemplatesCannotBeUsedToCreateAssociationBlocksAndViceVersa } from './templatesOfBug';
test.describe('block template', () => {
test.skip('block template', () => {
test('Ordinary block templates cannot be used to create association blocks, and vice versa', async ({
page,
mockPage,

View File

@ -25,6 +25,7 @@ test.describe('hide column', () => {
// 2. Sub table: hide column
await page.getByRole('button', { name: 'Role name' }).hover();
await page.getByRole('menuitem', { name: 'Hide column question-circle' }).waitFor({ state: 'detached' });
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-roles' })
.hover();

View File

@ -295,19 +295,19 @@ test.describe('configure actions column', () => {
await nocoPage.goto();
// add view & Edit & Delete & Duplicate ------------------------------------------------------------
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'View' }).click();
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
@ -330,11 +330,11 @@ test.describe('configure actions column', () => {
await expect(page.getByLabel('action-Action.Link-Duplicate-duplicate-t_unp4scqamw9-table-0')).not.toBeVisible();
// add custom action ------------------------------------------------------------
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Popup' }).click();
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Update record' }).click();
@ -351,7 +351,7 @@ test.describe('configure actions column', () => {
// 列宽度默认为 100
await expect(page.getByRole('columnheader', { name: 'Actions', exact: true })).toHaveJSProperty('offsetWidth', 100);
await page.getByText('Actions', { exact: true }).hover();
await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Column width' }).click();

View File

@ -12,6 +12,7 @@ import { T3686, T4005 } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -37,7 +38,8 @@ test.describe('where table block can be added', () => {
// 添加当前表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'childAssociationField' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'childAssociationField' }).click();
await page
.getByTestId('drawer-Action.Container-childCollection-View record')
@ -46,9 +48,10 @@ test.describe('where table block can be added', () => {
await page.getByRole('menuitem', { name: 'childTargetText' }).click();
// 添加父表关系区块
await page.getByRole('menuitem', { name: 'Table right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'parentAssociationField' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();
await page.getByRole('menuitem', { name: 'parentTargetText' }).click();

View File

@ -87,7 +87,7 @@ test.describe('actions schema settings', () => {
// 切换为 dialog
await page.getByRole('menuitem', { name: 'Open mode' }).click();
await page.getByRole('option', { name: 'Dialog' }).click();
await page.getByRole('option', { name: 'Dialog' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible();
@ -97,7 +97,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action-Add new-create-').hover();
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
await page.getByRole('menuitem', { name: 'Open mode Dialog' }).click();
await page.getByRole('option', { name: 'Page' }).click();
await page.getByRole('option', { name: 'Page' }).last().click();
// 点击按钮后会跳转到一个页面
await page.getByLabel('action-Action-Add new-create-').click();
@ -116,7 +116,7 @@ test.describe('actions schema settings', () => {
// 创建一条数据后返回,列表中应该有这条数据
await page.getByTestId('select-single').click();
await page.getByRole('option', { name: 'option3' }).click();
await page.getByRole('option', { name: 'option3' }).last().click();
// 提交后会自动返回
await page.getByLabel('action-Action-Submit-submit-').click();
@ -136,7 +136,7 @@ test.describe('actions schema settings', () => {
// 切换为 small
await page.getByRole('menuitem', { name: 'Popup size' }).click();
await page.getByRole('option', { name: 'Small' }).click();
await page.getByRole('option', { name: 'Small' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
const drawerWidth =
@ -148,7 +148,7 @@ test.describe('actions schema settings', () => {
// 切换为 large
await showMenu(page);
await page.getByRole('menuitem', { name: 'Popup size' }).click();
await page.getByRole('option', { name: 'Large' }).click();
await page.getByRole('option', { name: 'Large' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
const drawerWidth2 =
@ -325,7 +325,7 @@ test.describe('actions schema settings', () => {
await page.getByText('Add property').click();
await page.getByLabel('block-item-ArrayCollapse-general').click();
await page.getByTestId('select-linkage-properties').click();
await page.getByRole('option', { name: 'Disabled' }).click();
await page.getByRole('option', { name: 'Disabled' }).last().click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByLabel('action-Action.Link-View record-view-general-table-0')).toHaveAttribute(
@ -336,10 +336,10 @@ test.describe('actions schema settings', () => {
// 设置第二组规则 --------------------------------------------------------------------------
await openLinkageRules();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click();
await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click();
// 添加一个条件ID 等于 1
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).click();
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click();
@ -348,7 +348,7 @@ test.describe('actions schema settings', () => {
// action: 使按钮可用
await page.getByRole('tabpanel').getByText('Add property').click();
await page.locator('.ant-select', { hasText: 'action' }).click();
await page.getByRole('option', { name: 'Enabled' }).click();
await page.getByRole('option', { name: 'Enabled' }).last().click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 后面的 action 会覆盖前面的
@ -533,7 +533,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action.Link-View').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
await page.getByRole('option', { name: 'Page' }).click();
await page.getByRole('option', { name: 'Page' }).last().click();
// 跳转到子页面后,其内容应该和弹窗中的内容一致
await page.getByLabel('action-Action.Link-View').click();
@ -706,7 +706,7 @@ test.describe('actions schema settings', () => {
.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:view-roles' })
.hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
await page.getByRole('option', { name: 'Page' }).click();
await page.getByRole('option', { name: 'Page' }).last().click();
// 点击按钮跳转到子页面
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click();
@ -955,7 +955,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action.Link-Add child-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-treeCollection').hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
await page.getByRole('option', { name: 'Page' }).click();
await page.getByRole('option', { name: 'Page' }).last().click();
// open popup with page mode
await page.getByLabel('action-Action.Link-Add child-').click();
@ -994,7 +994,7 @@ test.describe('table column schema settings', () => {
// 1. 关系字段下拉框中应该有数据
await page.locator('.nb-sub-table-addNew').click();
await page.getByTestId('select-object-multiple').click();
await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible();
await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible();
// 2. 为该关系字段设置一个数据范围后,下拉框中应该有一个匹配项
await page.getByRole('button', { name: 'manyToMany1', exact: true }).hover();
@ -1009,7 +1009,7 @@ test.describe('table column schema settings', () => {
await page.reload();
await page.locator('.nb-sub-table-addNew').click();
await page.getByTestId('select-object-multiple').click();
await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible();
await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible();
});
test('fixed column', async ({ page, mockPage }) => {

View File

@ -34,7 +34,7 @@ test.describe('table block schema settings', () => {
'Set the data scope',
'Records per page',
'Connect data blocks',
'Save as template',
// 'Save as template',
'Delete',
],
});

View File

@ -9,8 +9,8 @@
import { expect, test } from '@nocobase/test/e2e';
test.describe('save as template', () => {
test('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
test.skip('save as template', () => {
test.skip('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
// 1. 创建一个区块,然后保存为模板
await mockPage().goto();
await page.getByLabel('schema-initializer-Grid-page:').hover();

View File

@ -25,7 +25,7 @@ test.describe('tree table block schema settings', () => {
'Set default sorting rules',
'Records per page',
'Connect data blocks',
'Save as template',
// 'Save as template',
'Delete',
],
});

View File

@ -35,7 +35,11 @@ test.describe('where filter block can be added', () => {
// 3. 与 Table、Details、List、GridCard 等区块建立连接
const connectByForm = async (name: string) => {
await page
.getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users')
.waitFor({ state: 'hidden' });
await page.getByLabel('block-item-CardItem-users-filter-form').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover();
await page.getByRole('menuitem', { name }).click();
@ -43,6 +47,7 @@ test.describe('where filter block can be added', () => {
const connectByCollapse = async (name: string) => {
await page.mouse.move(-500, 0);
await page.getByLabel('block-item-CardItem-users-filter-collapse').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterCollapse-users').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover();
await page.getByRole('menuitem', { name }).click();
@ -150,7 +155,9 @@ test.describe('where filter block can be added', () => {
}
// 2. 测试用表单筛选其它区块
await page.getByRole('menuitem', { name: 'Form right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Users' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover();

View File

@ -25,7 +25,7 @@ test.describe('collapse schema settings', () => {
await page.getByLabel('block-item-CardItem-general-filter-collapse').hover();
await page.getByLabel('designer-schema-settings-CardItem-AssociationFilter.BlockDesigner-general').hover();
},
supportedOptions: ['Edit block title', 'Save as template', 'Connect data blocks', 'Delete'],
supportedOptions: ['Edit block title', 'Connect data blocks', 'Delete'],
});
});
@ -46,7 +46,7 @@ test.describe('collapse schema settings', () => {
await page.getByRole('menuitem', { name: 'General' }).click();
// 点击一个选项,进行筛选
await page.getByRole('button', { name: 'right singleSelect search' }).click();
await page.getByRole('button', { name: 'collapsed singleSelect search' }).click();
await page.getByLabel('block-item-CardItem-general-filter-collapse').getByText('Option1').click();
// 注意:在本地运行时,由于运行结束后不会清空之前创建的数据,所以在第一次运行之后,下面会报错。如果遇到这种情况,可以先不管

View File

@ -126,7 +126,7 @@ test.describe('filter form', () => {
y: 10,
},
});
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 4. 此时点击 Reset 按钮,应该只显示一条数据,因为会把 nickname 的值重置为 {{$user.nickname}}
await page.getByLabel('action-Action-Reset-users-').click({
@ -152,6 +152,6 @@ test.describe('filter form', () => {
y: 10,
},
});
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
});
});

View File

@ -28,7 +28,7 @@ test.describe('filter block schema settings', () => {
},
supportedOptions: [
'Edit block title',
'Save as block template',
// 'Save as block template',
'Linkage rules',
'Connect data blocks',
'Delete',
@ -37,7 +37,7 @@ test.describe('filter block schema settings', () => {
});
test.describe('connect data blocks', () => {
test('connecting two blocks of the same collection', async ({
test.skip('connecting two blocks of the same collection', async ({
page,
mockPage,
mockRecords,

View File

@ -53,7 +53,7 @@ test.describe('AssociationSelect ', () => {
.getByLabel('block-item-CollectionField-test-form-test.b-b')
.getByTestId('select-object-multiple')
.click();
await expect(page.getByText('No data')).toBeVisible();
await expect(page.getByText('No data').last()).toBeVisible();
// 2. 当给字段 a 选择一个值后,字段 b 的下拉列表中会显示符合条件的值
await page

View File

@ -34,6 +34,6 @@ test.describe('data scope of component Select', () => {
await page.getByTestId('select-object-multiple').click();
await expect(page.getByRole('option', { name: 'admin' })).toBeHidden();
await expect(page.getByRole('option', { name: 'member' })).toBeHidden();
await expect(page.getByText('No data')).toBeVisible();
await expect(page.getByText('No data').last()).toBeVisible();
});
});

View File

@ -63,9 +63,11 @@ test.describe('page schema settings', () => {
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new tab');
// 选择一个图标
await page.getByRole('button', { name: 'Select icon' }).click();
await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await page.getByLabel('account-book').locator('svg').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('new tab')).toBeVisible();
await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});
});
@ -92,10 +94,12 @@ test.describe('tabs schema settings', () => {
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab');
await page.getByRole('button', { name: 'Select icon' }).click();
await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await page.getByLabel('account-book').locator('svg').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('new name of page tab')).toBeVisible();
await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});

View File

@ -55,7 +55,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
@ -87,7 +87,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
await page.mouse.move(-300, 0);
await page
@ -135,7 +135,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page
.getByTestId('drawer-Action.Container-users-View record')

View File

@ -124,10 +124,13 @@ test.describe('where to open a popup and what can be added to it', () => {
async function addBlock(names: string[]) {
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.waitForTimeout(500);
for (let i = 0; i < names.length - 1; i++) {
const name = names[i];
await page.getByRole('menuitem', { name }).hover();
await page.waitForTimeout(500);
}
await expect(page.getByRole('menuitem', { name: names[names.length - 1] })).toHaveCount(1);
await page.getByRole('menuitem', { name: names[names.length - 1] }).click();
await page.mouse.move(300, 0);
}
@ -206,13 +209,14 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.mouse.move(300, 0);
@ -272,13 +276,14 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.mouse.move(300, 0);

View File

@ -44,6 +44,7 @@ test.describe('tabs schema settings', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('Add new with new name')).toBeVisible();
await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});

View File

@ -55,6 +55,7 @@ test.describe('variable: parent object', () => {
// 1. Use "Current form" and "Parent object" variables in nested subforms and subtables
await page.getByLabel('block-item-CollectionField-collection1-form-collection1.m2m1-m2m1').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).waitFor({ state: 'detached' });
await page
.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection1-collection1.m2m1', {
exact: true,

View File

@ -10,7 +10,7 @@
export * from './PluginManagerLink';
import { PageHeader } from '@ant-design/pro-layout';
import { useDebounce } from 'ahooks';
import { Button, Col, Divider, Input, List, Modal, Result, Row, Space, Spin, Table, Tabs } from 'antd';
import { Button, Col, Divider, Input, List, Modal, Result, Row, Space, Spin, Table, Tabs, TableProps } from 'antd';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -127,7 +127,8 @@ function BulkEnableButton({ plugins = [] }) {
}}
size={'small'}
pagination={false}
columns={[
columns={
[
{
title: t('Plugin'),
dataIndex: 'displayName',
@ -145,7 +146,8 @@ function BulkEnableButton({ plugins = [] }) {
width: 300,
ellipsis: true,
},
]}
] as TableProps['columns']
}
dataSource={items}
/>
</Modal>

View File

@ -13,6 +13,7 @@ import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useApp, useNavigateNoUpdate } from '../application';
import { useMobileLayout } from '../route-switch/antd/admin-layout';
import { useCompile } from '../schema-component';
import { useToken } from '../style';
@ -20,6 +21,12 @@ export const PluginManagerLink = () => {
const { t } = useTranslation();
const navigate = useNavigateNoUpdate();
const { token } = useToken();
const { isMobileLayout } = useMobileLayout();
if (isMobileLayout) {
return null;
}
return (
<Tooltip title={t('Plugin manager')}>
<Button
@ -62,6 +69,12 @@ export const SettingsCenterDropdown = () => {
};
}, [app.pluginSettingsManager]);
const { isMobileLayout } = useMobileLayout();
if (isMobileLayout) {
return null;
}
return (
<Dropdown
menu={{

View File

@ -29,12 +29,13 @@ export class PMPlugin extends Plugin {
}
addSettings() {
this.app.pluginSettingsManager.add('ui-schema-storage', {
title: '{{t("Block templates")}}',
icon: 'LayoutOutlined',
Component: BlockTemplatesPane,
aclSnippet: 'pm.ui-schema-storage.block-templates',
});
// hide old block template settings page
// 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")}}',

View File

@ -11,7 +11,7 @@ import { EllipsisOutlined } from '@ant-design/icons';
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
import { css } from '@emotion/css';
import { Popover, Tooltip } from 'antd';
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
@ -29,6 +29,7 @@ import {
RemoteSchemaTemplateManagerProvider,
SortableItem,
useDesignable,
useGlobalTheme,
useMenuDragEnd,
useParseURLAndParams,
useRequest,
@ -153,7 +154,10 @@ const layoutContentClass = css`
display: flex;
flex-direction: column;
position: relative;
/* 基础高度(所有浏览器支持) */
height: calc(100vh - var(--nb-header-height));
/* 动态视口高度(现代浏览器支持) */
height: calc(100dvh - var(--nb-header-height));
> div {
position: relative;
}
@ -496,22 +500,44 @@ const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => {
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>;
};
const IsMobileLayoutContext = React.createContext<{
isMobileLayout: boolean;
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
}>({
isMobileLayout: false,
setIsMobileLayout: () => {},
});
const MobileLayoutProvider: FC = (props) => {
const [isMobileLayout, setIsMobileLayout] = useState(false);
const value = useMemo(() => ({ isMobileLayout, setIsMobileLayout }), [isMobileLayout]);
return <IsMobileLayoutContext.Provider value={value}>{props.children}</IsMobileLayoutContext.Provider>;
};
export const useMobileLayout = () => {
const { isMobileLayout, setIsMobileLayout } = useContext(IsMobileLayoutContext);
return { isMobileLayout, setIsMobileLayout };
};
export const InternalAdminLayout = () => {
const { allAccessRoutes } = useAllAccessDesktopRoutes();
const { designable } = useDesignable();
const { designable: _designable } = useDesignable();
const location = useLocation();
const { onDragEnd } = useMenuDragEnd();
const { token } = useToken();
const [isMobile, setIsMobile] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const { isMobileLayout, setIsMobileLayout } = useMobileLayout();
const [collapsed, setCollapsed] = useState(isMobileLayout);
const doNotChangeCollapsedRef = useRef(false);
const { t } = useMenuTranslation();
const designable = isMobileLayout ? false : _designable;
const route = useMemo(() => {
return {
path: '/',
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile, t }),
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile: isMobileLayout, t }),
};
}, [allAccessRoutes, designable, isMobile, t]);
}, [allAccessRoutes, designable, isMobileLayout, t]);
const layoutToken = useMemo(() => {
return {
header: {
@ -535,6 +561,21 @@ export const InternalAdminLayout = () => {
bgLayout: token.colorBgLayout,
};
}, [token]);
const { theme, isDarkTheme } = useGlobalTheme();
const mobileTheme = useMemo(() => {
return {
...theme,
token: {
...theme.token,
paddingPageHorizontal: 8, // Horizontal page padding
paddingPageVertical: 8, // Vertical page padding
marginBlock: 12, // Spacing between blocks
borderRadiusBlock: 8, // Block border radius
fontSize: 14, // Font size
},
algorithm: isDarkTheme ? [antdTheme.compactAlgorithm, antdTheme.darkAlgorithm] : antdTheme.compactAlgorithm, // Set mobile mode to always use compact algorithm
};
}, [theme, isDarkTheme]);
const onCollapse = useCallback((collapsed: boolean) => {
if (doNotChangeCollapsedRef.current) {
@ -576,11 +617,15 @@ export const InternalAdminLayout = () => {
{(value: RouteContextType) => {
const { isMobile: _isMobile } = value;
if (_isMobile !== isMobile) {
setIsMobile(_isMobile);
if (_isMobile !== isMobileLayout) {
setIsMobileLayout(_isMobile);
}
return <LayoutContent />;
return (
<ConfigProvider theme={_isMobile ? mobileTheme : theme}>
<LayoutContent />
</ConfigProvider>
);
}}
</RouteContext.Consumer>
</ProLayout>
@ -697,6 +742,7 @@ export class AdminLayoutPlugin extends Plugin {
async load() {
this.app.schemaSettingsManager.add(userCenterSettings);
this.app.addComponents({ AdminLayout, AdminDynamicPage });
this.app.use(MobileLayoutProvider);
}
}
@ -718,36 +764,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
return null;
}
const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => {
const { inHeader } = useContext(headerContext);
return (
<RouteContext.Consumer>
{(value: RouteContextType) => {
const { collapsed } = value;
if (collapsed && !inHeader) {
return props.icon ? (
<Icon type={props.icon} />
) : (
<span
style={{
display: 'inline-block',
width: '100%',
textAlign: 'center',
}}
>
{props.title.charAt(0)}
</span>
);
}
return props.icon ? <Icon type={props.icon} /> : null;
}}
</RouteContext.Consumer>
);
};
const MenuDesignerButton: FC<{ testId: string }> = (props) => {
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
@ -767,7 +783,7 @@ const MenuTitleWithIcon: FC<{ icon: any; title: string }> = (props) => {
);
}
return props.title;
return <>{props.title}</>;
};
function convertRoutesToLayout(

View File

@ -12,15 +12,15 @@ import React from 'react';
import { useActionContext } from '.';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
import { ComposedActionDrawer } from './types';
import { ActionDrawer } from './Action.Drawer';
import { ComposedActionDrawer } from './types';
const PopupLevelContext = React.createContext(0);
export const ActionContainer: ComposedActionDrawer = observer(
(props: any) => {
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {};
const { openMode = defaultOpenMode } = useActionContext();
const { openMode = props.openMode || defaultOpenMode } = useActionContext();
const popupLevel = React.useContext(PopupLevelContext);
const currentLevel = popupLevel + 1;

View File

@ -187,6 +187,13 @@ export function AssignedFieldValues() {
'x-component': 'Grid',
'x-initializer': 'assignFieldValuesForm:configureFields',
};
if (fieldSchema['x-template-uid']) {
initialSchema['x-template-root-ref'] = {
'x-template-uid': fieldSchema['x-template-uid'],
'x-path': 'x-action-settings.schemaUid',
};
}
const tips = {
'customize:update': t(
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',

View File

@ -25,7 +25,7 @@ export const useStyles = genStyleHook('nb-action-drawer', (token) => {
},
},
'&.nb-record-picker-selector': {
'.ant-drawer-wrapper-body': {
'.ant-drawer-content': {
backgroundColor: token.colorBgLayout,
},
'.nb-block-item': {

View File

@ -179,4 +179,17 @@ ActionModal.Footer = observer(
{ displayName: 'ActionModal.Footer' },
);
ActionModal.FootBar = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return (
<div className="ant-modal-footer">
<NocoBaseRecursionField basePath={field.address} schema={schema} onlyRenderProperties />
</div>
);
},
{ displayName: 'ActionModal.FootBar' },
);
export default ActionModal;

View File

@ -20,7 +20,8 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
right: 0,
bottom: 0,
backgroundColor: token.colorBgLayout,
overflow: 'auto',
overflowX: 'hidden',
overflowY: 'auto',
'.ant-tabs-nav': {
background: token.colorBgContainer,

View File

@ -34,6 +34,9 @@ const useStyles = genStyleHook('nb-action', (token) => {
background: 'var(--colorBgSettingsHover)',
border: '0',
pointerEvents: 'none',
'&.nb-in-template': {
background: 'var(--colorTemplateBgSettingsHover)',
},
'> .general-schema-designer-icons': {
position: 'absolute',
right: '2px',

View File

@ -18,7 +18,7 @@ export function useSetAriaLabelForDrawer(visible: boolean) {
if (visible) {
// 因为 Action 是点击后渲染内容,所以需要延迟一下
setTimeout(() => {
const wrappers = [...document.querySelectorAll('.ant-drawer-wrapper-body')];
const wrappers = [...document.querySelectorAll('.ant-drawer-body')];
const masks = [...document.querySelectorAll('.ant-drawer-mask')];
// 如果存在多个 mask最后一个 mask 为当前打开的 maskwrapper 也是同理
const currentMask = masks[masks.length - 1];

View File

@ -7,12 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CloseCircleFilled } from '@ant-design/icons';
import { CloseCircleFilled, DownOutlined } from '@ant-design/icons';
import { Tag, TreeSelect } from 'antd';
import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
import type { TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated, parseCollectionName, useApp, useCompile } from '../../..';
import { DefaultOptionType } from 'antd/es/select';
export type AppendsTreeSelectProps = {
value: string[] | string;
@ -261,6 +262,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
treeData={treeData}
loadData={loadData}
{...restProps}
suffixIcon={<DownOutlined />}
/>
);
};

Some files were not shown because too many files have changed in this diff Show More