Merge branch 'next' into fix/subtable

This commit is contained in:
chenos 2024-09-30 23:35:40 +08:00
commit 1fe78a5d07
596 changed files with 21964 additions and 2286 deletions

View File

@ -11,7 +11,7 @@ on:
- 'next'
paths:
- 'packages/**'
- 'Dockerfile'
- 'Dockerfile.pro'
- '.github/workflows/build-pro-image.yml'
jobs:
@ -139,7 +139,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
file: Dockerfile.pro
build-args: |
VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA}

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ storage/plugins
storage/tar
storage/tmp
storage/app.watch.ts
storage/.upgrading
storage/logs-e2e
storage/uploads-e2e
storage/.pm2-*

65
Dockerfile.pro Normal file
View File

@ -0,0 +1,65 @@
FROM node:20.13-bullseye as builder
ARG VERDACCIO_URL=http://host.docker.internal:10104/
ARG COMMIT_HASH
ARG APPEND_PRESET_LOCAL_PLUGINS
ARG BEFORE_PACK_NOCOBASE="ls -l"
ARG PLUGINS_DIRS
ENV PLUGINS_DIRS=${PLUGINS_DIRS}
RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL
RUN apt-get update && apt-get install -y jq
WORKDIR /tmp
COPY . /tmp
RUN yarn install && yarn build --no-dts
RUN cd /tmp && \
NEWVERSION="$(cat lerna.json | jq '.version' | tr -d '"').$(date +'%Y%m%d%H%M%S')" \
&& git checkout -b release-$(date +'%Y%m%d%H%M%S') \
&& yarn lerna version ${NEWVERSION} -y --no-git-tag-version
RUN git config user.email "test@mail.com" \
&& git config user.name "test" && git add . \
&& git commit -m "chore(versions): test publish packages"
RUN yarn release:force --registry $VERDACCIO_URL
RUN yarn config set registry $VERDACCIO_URL
WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
&& yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \
&& cd /app/my-nocobase-app \
&& yarn install --production
WORKDIR /app/my-nocobase-app
RUN $BEFORE_PACK_NOCOBASE
RUN cd /app \
&& rm -rf my-nocobase-app/packages/app/client/src/.umi \
&& rm -rf nocobase.tar.gz \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
FROM node:20.13-bullseye-slim
RUN apt-get update && apt-get install -y nginx
RUN rm -rf /etc/nginx/sites-enabled/default
# install postgresql-client and mysql-client
RUN apt update && apt install -y wget postgresql-common gnupg \
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
&& apt install -y postgresql-client-16 \
&& wget https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client-core_8.1.0-1debian11_amd64.deb \
&& dpkg -x mysql-community-client-core_8.1.0-1debian11_amd64.deb /tmp/mysql-client \
&& cp /tmp/mysql-client/usr/bin/mysqldump /usr/bin/ \
&& cp /tmp/mysql-client/usr/bin/mysql /usr/bin/
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
WORKDIR /app/nocobase
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH
COPY ./docker/nocobase/docker-entrypoint.sh /app/
CMD ["/app/docker-entrypoint.sh"]

View File

@ -1,10 +1,8 @@
{
"version": "1.3.27-beta",
"version": "1.4.0-alpha",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": [
"--ignore-engines"
],
"npmClientArgs": ["--ignore-engines"],
"command": {
"version": {
"forcePublish": true,

View File

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

View File

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

View File

@ -75,8 +75,8 @@ export class SortAbleCollection {
// insert source position to target position
async move(sourceInstanceId: TargetKey, targetInstanceId: TargetKey, options: MoveOptions = {}) {
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
const targetInstance = await this.collection.repository.findById(targetInstanceId);
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
const targetInstance = await this.collection.repository.findByTargetKey(targetInstanceId);
if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) {
await sourceInstance.update({
@ -88,7 +88,7 @@ export class SortAbleCollection {
}
async changeScope(sourceInstanceId: TargetKey, targetScope: any, method?: string) {
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
const targetScopeValue = targetScope[this.scopeKey];
if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) {
@ -108,7 +108,7 @@ export class SortAbleCollection {
}
async sticky(sourceInstanceId: TargetKey) {
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
await sourceInstance.update(
{
[this.field.get('name')]: 0,

View File

@ -1,4 +1,4 @@
import { getUmiConfig, IndexGenerator } from '@nocobase/devtools/umiConfig';
import { generatePlugins, getUmiConfig } from '@nocobase/devtools/umiConfig';
import path from 'path';
import { defineConfig } from 'umi';
@ -8,17 +8,11 @@ process.env.MFSU_AD = 'none';
process.env.DID_YOU_KNOW = 'none';
const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉
const pluginDirs = (process.env.PLUGIN_PATH || 'packages/plugins/,packages/samples/,packages/pro-plugins/')
.split(',').map(item => path.join(process.cwd(), item));
const outputPluginPath = path.join(__dirname, 'src', '.plugins');
const indexGenerator = new IndexGenerator(outputPluginPath, pluginDirs);
indexGenerator.generate();
const isDevCmd = !!process.env.IS_DEV_CMD;
const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}';
generatePlugins();
export default defineConfig({
title: 'Loading...',
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,

View File

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

View File

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

View File

@ -71,7 +71,7 @@ describe('middleware', () => {
hasFn.mockImplementation(() => true);
const res = await agent.resource('auth').check();
expect(res.status).toBe(401);
expect(res.text).toContain('token is not available');
expect(res.text).toContain('Token is invalid');
});
});
});

View File

@ -109,7 +109,10 @@ export class AuthManager {
return async (ctx: Context & { auth: Auth }, next: Next) => {
const token = ctx.getBearerToken();
if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) {
return ctx.throw(401, ctx.t('token is not available'));
return ctx.throw(401, {
code: 'TOKEN_INVALID',
message: ctx.t('Token is invalid'),
});
}
const name = ctx.get(this.options.authKey) || this.options.default;

View File

@ -69,14 +69,14 @@ export class BaseAuth extends Auth {
return null;
}
try {
const { userId, roleName } = await this.jwt.decode(token);
const { userId, roleName, iat, temp } = await this.jwt.decode(token);
if (roleName) {
this.ctx.headers['x-role'] = roleName;
}
const cache = this.ctx.cache as Cache;
return await cache.wrap(this.getCacheKey(userId), () =>
const user = await cache.wrap(this.getCacheKey(userId), () =>
this.userRepository.findOne({
filter: {
id: userId,
@ -84,6 +84,10 @@ export class BaseAuth extends Auth {
raw: true,
}),
);
if (temp && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) {
throw new Error('Token is invalid');
}
return user;
} catch (err) {
this.ctx.logger.error(err, { method: 'check' });
return null;
@ -106,6 +110,7 @@ export class BaseAuth extends Auth {
}
const token = this.jwt.sign({
userId: user.id,
temp: true,
});
return {
user,
@ -119,7 +124,7 @@ export class BaseAuth extends Auth {
return;
}
const { userId } = await this.jwt.decode(token);
await this.ctx.app.emitAsync('beforeSignOut', { userId });
await this.ctx.app.emitAsync('cache:del:roles', { userId });
await this.ctx.cache.del(this.getCacheKey(userId));
return await this.jwt.block(token);
}

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cache",
"version": "1.3.27-beta",
"version": "1.4.0-alpha",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cli",
"version": "1.3.27-beta",
"version": "1.4.0-alpha",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js"
},
"dependencies": {
"@nocobase/app": "1.3.27-beta",
"@nocobase/app": "1.4.0-alpha",
"@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.3.27-beta"
"@nocobase/devtools": "1.4.0-alpha"
},
"repository": {
"type": "git",

View File

@ -9,8 +9,12 @@
const chalk = require('chalk');
const { Command } = require('commander');
const { runAppCommand, runInstall, run, postCheck, nodeCheck, promptForTs } = require('../util');
const { generatePlugins, run, postCheck, nodeCheck, promptForTs } = require('../util');
const { getPortPromise } = require('portfinder');
const chokidar = require('chokidar');
const { uid } = require('@formily/shared');
const path = require('path');
const fs = require('fs');
/**
*
@ -27,6 +31,25 @@ module.exports = (cli) => {
.option('--inspect [port]')
.allowUnknownOption()
.action(async (opts) => {
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher
.on('addDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
})
.on('unlinkDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
});
promptForTs();
const { SERVER_TSCONFIG_PATH } = process.env;
process.env.IS_DEV_CMD = true;

View File

@ -8,7 +8,7 @@
*/
const { Command } = require('commander');
const { run, isDev, isProd, promptForTs } = require('../util');
const { run, isDev, isProd, promptForTs, downloadPro } = require('../util');
/**
*
@ -20,10 +20,14 @@ module.exports = (cli) => {
.allowUnknownOption()
.option('-h, --help')
.option('--ts-node-dev')
.action((options) => {
.action(async (options) => {
const cmd = process.argv.slice(2)?.[0];
if (cmd === 'install') {
await downloadPro();
}
if (isDev()) {
promptForTs();
run('tsx', [
await run('tsx', [
'--tsconfig',
SERVER_TSCONFIG_PATH,
'-r',
@ -32,7 +36,7 @@ module.exports = (cli) => {
...process.argv.slice(2),
]);
} else if (isProd()) {
run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]);
await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]);
}
});
};

View File

@ -31,6 +31,7 @@ module.exports = (cli) => {
require('./umi')(cli);
require('./upgrade')(cli);
require('./postinstall')(cli);
require('./pkg')(cli);
if (isPackageValid('@umijs/utils')) {
require('./create-plugin')(cli);
}

View File

@ -0,0 +1,218 @@
/**
* 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.
*/
const { Command } = require('commander');
const axios = require('axios');
const fs = require('fs-extra');
const zlib = require('zlib');
const tar = require('tar');
const path = require('path');
const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink');
const chalk = require('chalk');
class Package {
data;
constructor(packageName, packageManager) {
this.packageName = packageName;
this.packageManager = packageManager;
this.outputDir = path.resolve(process.cwd(), `storage/plugins/${this.packageName}`);
}
get token() {
return this.packageManager.getToken();
}
url(path) {
return this.packageManager.url(path);
}
async mkdir() {
if (await fs.exists(this.outputDir)) {
await fs.rm(this.outputDir, { recursive: true, force: true });
}
await fs.mkdirp(this.outputDir);
}
async getInfo() {
try {
const res = await axios.get(this.url(this.packageName), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
this.data = res.data;
} catch (error) {
return;
}
}
getTarball(version = 'latest') {
if (this.data.versions[version]) {
return [version, this.data.versions[version].dist.tarball];
}
if (version.includes('beta')) {
version = version.split('beta')[0] + 'beta';
} else if (version.includes('alpha')) {
const prefix = (version = version.split('alpha')[0]);
version = Object.keys(this.data.versions)
.filter((ver) => ver.startsWith(`${prefix}alpha`))
.sort()
.pop();
}
if (version === 'latest') {
version = this.data['dist-tags']['latest'];
} else if (version === 'next') {
version = this.data['dist-tags']['next'];
}
if (!this.data.versions[version]) {
console.log(chalk.redBright(`Download failed: ${this.packageName}@${version} package does not exist`));
}
return [version, this.data.versions[version].dist.tarball];
}
async isDevPackage() {
let file = path.resolve(process.cwd(), 'packages/plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
file = path.resolve(process.cwd(), 'packages/pro-plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
return false;
}
async download(options = {}) {
if (await this.isDevPackage()) {
console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dev package`));
return;
}
await this.getInfo();
if (!this.data) {
console.log(chalk.redBright(`Download failed: ${this.packageName} package does not exist`));
return;
}
try {
const [version, url] = this.getTarball(options.version);
const response = await axios({
url,
responseType: 'stream',
method: 'GET',
headers: {
Authorization: `Bearer ${this.token}`,
},
});
await this.mkdir();
await new Promise((resolve, reject) => {
response.data
.pipe(zlib.createGunzip()) // 解压 gzip
.pipe(tar.extract({ cwd: this.outputDir, strip: 1 })) // 解压 tar
.on('finish', resolve)
.on('error', reject);
});
console.log(chalk.greenBright(`Download success: ${this.packageName}@${version}`));
} catch (error) {
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
}
}
}
class PackageManager {
token;
baseURL;
constructor({ baseURL }) {
this.baseURL = baseURL;
}
getToken() {
return this.token;
}
getBaseURL() {
return this.baseURL;
}
url(path) {
return this.baseURL + path;
}
async login(credentials) {
try {
const res1 = await axios.post(`${this.baseURL}-/verdaccio/sec/login`, credentials, {
responseType: 'json',
});
this.token = res1.data.token;
} catch (error) {
console.error(chalk.redBright(`Login failed: ${this.baseURL}`));
}
}
getPackage(packageName) {
return new Package(packageName, this);
}
async getProPackages() {
const res = await axios.get(this.url('pro-packages'), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
return res.data.data;
}
async getPackages() {
const pkgs = await this.getProPackages();
return pkgs;
}
async download(options = {}) {
const { version } = options;
if (!this.token) {
return;
}
const pkgs = await this.getPackages();
for (const pkg of pkgs) {
await this.getPackage(pkg).download({ version });
}
}
}
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
const pkg = cli.command('pkg');
pkg
.command('download-pro')
.option('-V, --version [version]')
.action(async () => {
const { NOCOBASE_PKG_URL, NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
if (!(NOCOBASE_PKG_URL && NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
return;
}
const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD };
const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL });
await pm.login(credentials);
const file = path.resolve(__dirname, '../../package.json');
const json = await fs.readJson(file);
await pm.download({ version: json.version });
await createStoragePluginsSymlink();
});
pkg.command('export-all').action(async () => {
console.log('Todo...');
});
};

View File

@ -8,7 +8,7 @@
*/
const { Command } = require('commander');
const { run, isDev, isPackageValid, generatePlaywrightPath } = require('../util');
const { run, isDev, isPackageValid, generatePlaywrightPath, generatePlugins } = require('../util');
const { dirname, resolve } = require('path');
const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs');
const { readFile, writeFile } = require('fs').promises;
@ -41,7 +41,7 @@ module.exports = (cli) => {
.option('--skip-umi')
.action(async (options) => {
writeToExclude();
generatePlugins();
generatePlaywrightPath(true);
await createStoragePluginsSymlink();
if (!isDev()) {

View File

@ -8,10 +8,11 @@
*/
const { Command } = require('commander');
const { isDev, run, postCheck, runInstall, promptForTs } = require('../util');
const { isDev, run, postCheck, downloadPro, promptForTs } = require('../util');
const { existsSync, rmSync } = require('fs');
const { resolve } = require('path');
const chalk = require('chalk');
const chokidar = require('chokidar');
function deleteSockFiles() {
const { SOCKET_PATH, PM2_HOME } = process.env;
@ -38,6 +39,23 @@ module.exports = (cli) => {
.option('--quickstart')
.allowUnknownOption()
.action(async (opts) => {
if (opts.quickstart) {
await downloadPro();
}
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignoreInitial: true,
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher.on('addDir', async (pathname) => {
console.log('pathname', pathname);
await run('yarn', ['nocobase', 'pm2-restart']);
});
if (opts.port) {
process.env.APP_PORT = opts.port;
}

View File

@ -10,7 +10,7 @@
const chalk = require('chalk');
const { Command } = require('commander');
const { resolve } = require('path');
const { run, promptForTs, runAppCommand, hasCorePackages, updateJsonFile, hasTsNode } = require('../util');
const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util');
const { existsSync, rmSync } = require('fs');
/**
@ -29,15 +29,18 @@ module.exports = (cli) => {
if (hasTsNode()) promptForTs();
if (hasCorePackages()) {
// await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade');
return;
}
if (options.skipCodeUpdate) {
await downloadPro();
await runAppCommand('upgrade');
return;
}
// await runAppCommand('upgrade');
if (!hasTsNode()) {
await downloadPro();
await runAppCommand('upgrade');
return;
}
@ -54,8 +57,9 @@ module.exports = (cli) => {
stdio: 'pipe',
});
if (pkg.version === stdout) {
await downloadPro();
await runAppCommand('upgrade');
rmAppDir();
await rmAppDir();
return;
}
const currentY = 1 * pkg.version.split('.')[1];
@ -66,7 +70,8 @@ module.exports = (cli) => {
await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']);
}
await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade');
rmAppDir();
await rmAppDir();
});
};

View File

@ -72,7 +72,7 @@ class PluginGenerator extends Generator {
});
this.log('');
genTsConfigPaths();
execa.sync('yarn', ['postinstall', '--skip-umi'], { shell: true, stdio: 'inherit' });
execa.sync('yarn', ['postinstall'], { shell: true, stdio: 'inherit' });
this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`);
}
}

View File

@ -163,6 +163,10 @@ exports.promptForTs = () => {
console.log(chalk.green('WAIT: ') + 'TypeScript compiling...');
};
exports.downloadPro = async () => {
await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']);
};
exports.updateJsonFile = async (target, fn) => {
const content = await readFile(target, 'utf-8');
const json = JSON.parse(content);
@ -416,3 +420,13 @@ exports.initEnv = function initEnv() {
);
}
};
exports.generatePlugins = function () {
try {
require.resolve('@nocobase/devtools/umiConfig');
const { generatePlugins } = require('@nocobase/devtools/umiConfig');
generatePlugins();
} catch (error) {
return;
}
};

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
"version": "1.3.27-beta",
"version": "1.4.0-alpha",
"license": "AGPL-3.0",
"main": "lib/index.js",
"module": "es/index.mjs",
@ -11,6 +11,7 @@
"@ant-design/icons": "^5.1.4",
"@ant-design/pro-layout": "^7.16.11",
"@antv/g2plot": "^2.4.18",
"@budibase/handlebars-helpers": "^0.13.2",
"@ctrl/tinycolor": "^3.6.0",
"@dnd-kit/core": "^5.0.1",
"@dnd-kit/modifiers": "^6.0.0",
@ -26,9 +27,9 @@
"@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.3.27-beta",
"@nocobase/sdk": "1.3.27-beta",
"@nocobase/utils": "1.3.27-beta",
"@nocobase/evaluators": "1.4.0-alpha",
"@nocobase/sdk": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"ahooks": "^3.7.2",
"antd": "5.12.8",
"antd-style": "3.4.5",

View File

@ -103,6 +103,13 @@ export const useACLContext = () => {
export const ACLActionParamsContext = createContext<any>({});
ACLActionParamsContext.displayName = 'ACLActionParamsContext';
export const ACLCustomContext = createContext<any>({});
ACLCustomContext.displayName = 'ACLCustomContext';
const useACLCustomContext = () => {
return useContext(ACLCustomContext);
};
export const useACLRolesCheck = () => {
const ctx = useContext(ACLContext);
const dataSourceName = useDataSourceKey();
@ -218,9 +225,10 @@ export function useUIConfigurationPermissions(): { allowConfigUI: boolean } {
export const ACLCollectionProvider = (props) => {
const { allowAll, parseAction } = useACLRoleContext();
const { allowAll: customAllowAll } = useACLCustomContext();
const app = useApp();
const schema = useFieldSchema();
if (allowAll || app.disableAcl) {
if (allowAll || app.disableAcl || customAllowAll) {
return props.children;
}
let actionPath = schema?.['x-acl-action'] || props.actionPath;

View File

@ -98,6 +98,9 @@ export class APIClient extends APIClientSDK {
if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) {
this.auth.setRole(null);
}
if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID')) {
this.auth.setToken(null);
}
throw error;
},
);
@ -130,9 +133,11 @@ export class APIClient extends APIClientSDK {
}
return response;
},
(error) => {
async (error) => {
if (this.silence) {
throw error;
console.error(error);
return;
// throw error;
}
const redirectTo = error?.response?.data?.redirectTo;
if (redirectTo) {

View File

@ -12,10 +12,10 @@ import React, { FC, useMemo } from 'react';
import { useApp } from '../../hooks';
import { SchemaInitializerItems } from '../components';
import { SchemaInitializerButton } from '../components/SchemaInitializerButton';
import { withInitializer } from '../withInitializer';
import { SchemaInitializerOptions } from '../types';
import { SchemaInitializer } from '../SchemaInitializer';
import { SchemaInitializerOptions } from '../types';
import { withInitializer } from '../withInitializer';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => {
const Component: any = options.Component || SchemaInitializerButton;
@ -38,6 +38,18 @@ export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>(
options?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>,
) {
const app = useApp();
const { isMobile } = useOpenModeContext() || {};
// compatible with mobile
// TODO: delete this code
if (
name === 'popup:common:addBlock' &&
app.schemaInitializerManager.has('mobile:popup:common:addBlock') &&
isMobile
) {
name = 'mobile:popup:common:addBlock';
}
const initializer = useMemo(
() => (typeof name === 'object' ? name : app.schemaInitializerManager.get<P1, P2>(name)),
[app.schemaInitializerManager, name],

View File

@ -11,6 +11,7 @@ import { Field, GeneralField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { Col, Row } from 'antd';
import merge from 'deepmerge';
import { isArray } from 'lodash';
import template from 'lodash/template';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
@ -307,7 +308,15 @@ export const useFilterByTk = () => {
const association = getCollectionField(assoc);
return recordData?.[association.targetKey || 'id'];
}
return recordData?.[collection.filterTargetKey || 'id'];
if (isArray(collection.filterTargetKey)) {
const filterByTk = {};
for (const key of collection.filterTargetKey) {
filterByTk[key] = recordData?.[key];
}
return filterByTk;
} else {
return recordData?.[collection.filterTargetKey || 'id'];
}
};
/**

View File

@ -12,7 +12,7 @@ import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useCollectionManager_deprecated } from '../collection-manager';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
import { useTableBlockParams } from '../modules/blocks/data-blocks/table';
import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useBlockHeightProps } from './hooks';

View File

@ -183,9 +183,10 @@ export const useTableFieldProps = () => {
rowKey: (record: any) => {
return field.value?.indexOf?.(record);
},
onRowSelectionChange(selectedRowKeys) {
onRowSelectionChange(selectedRowKeys, selectedRowData) {
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx.field.data.selectedRowData = selectedRowData;
},
onChange({ current, pageSize }) {
ctx.service.run({ page: current, pageSize });

View File

@ -319,9 +319,10 @@ export const useTableSelectorProps = () => {
dragSort: false,
rowKey: ctx.rowKey || 'id',
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
onRowSelectionChange(selectedRowKeys, selectedRows) {
onRowSelectionChange(selectedRowKeys, selectedRowData) {
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx.field.data.selectedRowData = selectedRowData;
},
async onRowDragEnd({ from, to }) {
await ctx.resource.move({

View File

@ -251,6 +251,13 @@ export const useCreateActionProps = () => {
if (!onSuccess?.successMessage) {
message.success(t('Saved successfully'));
await resetFormCorrectly(form);
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
if (isURL(onSuccess.redirectTo)) {
window.location.href = onSuccess.redirectTo;
} else {
navigate(onSuccess.redirectTo);
}
}
return;
}
if (onSuccess?.manualClose) {
@ -1080,13 +1087,25 @@ export const useBulkDestroyActionProps = () => {
const { field } = useBlockRequestContext();
const { resource, service } = useBlockRequestContext();
const { setSubmitted } = useActionContext();
const collection = useCollection_deprecated();
const { filterTargetKey } = collection;
return {
async onClick(e?, callBack?) {
let filterByTk = field.data?.selectedRowKeys;
if (Array.isArray(filterTargetKey)) {
filterByTk = field.data.selectedRowData.map((v) => {
const obj = {};
filterTargetKey.map((j) => {
obj[j] = v[j];
});
return obj;
});
}
if (!field?.data?.selectedRowKeys?.length) {
return;
}
await resource.destroy({
filterByTk: field.data?.selectedRowKeys,
filterByTk,
});
field.data.selectedRowKeys = [];
const currentPage = service.params[0]?.page;
@ -1098,7 +1117,7 @@ export const useBulkDestroyActionProps = () => {
callBack?.();
}
setSubmitted?.(true);
// service?.refresh?.();
service?.refresh?.();
},
};
};
@ -1295,6 +1314,50 @@ export const useAssociationFilterBlockProps = () => {
parseVariableLoading,
]);
const onSelected = useCallback(
(value) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
if (!block.service.params?.[1]?.filters) {
_.set(block.service.params, '[1].filters', {});
}
// 保留原有的 filter
const storedFilter = block.service.params[1].filters;
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
},
[fieldSchema, filterKey, getDataBlocks],
);
if (!collectionField) {
return {};
}
@ -1336,41 +1399,6 @@ export const useAssociationFilterBlockProps = () => {
};
}
const onSelected = (value) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
};
return {
/** 渲染 Collapse 的列表数据 */
list,

View File

@ -16,64 +16,23 @@ import { cloneDeep } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client';
import { CollectionFieldInterface } from '../../data-source';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks';
import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types';
import * as components from './components';
import { useFieldInterfaceOptions } from './interfaces';
const getSchema = (schema: IField, record: any, compile) => {
const getSchema = (schema: CollectionFieldInterface, record: any, compile) => {
if (!schema) {
return;
}
const properties = cloneDeep(schema.properties) as any;
const properties = schema.getConfigureFormProperties();
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
properties.defaultValue.required = false;
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
properties['defaultValue']['x-reactions'] = [
{
dependencies: [
'uiSchema.x-component-props.gmt',
'uiSchema.x-component-props.showTime',
'uiSchema.x-component-props.dateFormat',
'uiSchema.x-component-props.timeFormat',
],
fulfill: {
state: {
componentProps: {
gmt: '{{$deps[0]}}',
showTime: '{{$deps[1]}}',
dateFormat: '{{$deps[2]}}',
timeFormat: '{{$deps[3]}}',
},
},
},
},
{
dependencies: ['primaryKey', 'unique', 'autoIncrement'],
when: '{{$deps[0]||$deps[1]||$deps[2]}}',
fulfill: {
state: {
hidden: true,
value: null,
},
},
otherwise: {
state: {
hidden: false,
},
},
},
];
}
const initialValue: any = {
name: `f_${uid()}`,
...cloneDeep(schema.default),

View File

@ -64,6 +64,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
},
footer: {

View File

@ -16,6 +16,7 @@ import set from 'lodash/set';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client';
import { CollectionFieldInterface } from '../../data-source';
import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
@ -23,60 +24,23 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP
import { useCancelAction } from '../action-hooks';
import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types';
import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
const getSchema = (
schema: CollectionFieldInterface,
defaultValues: any,
record: any,
compile,
getContainer,
): ISchema => {
if (!schema) {
return;
}
const properties = cloneDeep(schema.properties) as any;
const properties = schema.getConfigureFormProperties();
if (properties?.name) {
properties.name['x-disabled'] = true;
}
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema.default.uiSchema) || {};
properties.defaultValue.required = false;
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
properties['defaultValue']['x-reactions'] = [
{
dependencies: [
'uiSchema.x-component-props.gmt',
'uiSchema.x-component-props.showTime',
'uiSchema.x-component-props.dateFormat',
'uiSchema.x-component-props.timeFormat',
],
fulfill: {
state: {
componentProps: {
gmt: '{{$deps[0]}}',
showTime: '{{$deps[1]}}',
dateFormat: '{{$deps[2]}}',
timeFormat: '{{$deps[3]}}',
},
},
},
},
{
dependencies: ['primaryKey', 'unique', 'autoIncrement'],
when: '{{$deps[0]||$deps[1]||$deps[2]}}',
fulfill: {
state: {
hidden: true,
value: undefined,
},
},
otherwise: {
state: {
hidden: false,
},
},
},
];
}
return {
type: 'object',
properties: {
@ -92,7 +56,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
return useRequest(
() =>
Promise.resolve({
data: cloneDeep(omit(schema.default, ['uiSchema.rawTitle'])),
data: cloneDeep(omit(defaultValues, ['uiSchema.rawTitle'])),
}),
options,
);
@ -230,15 +194,7 @@ export const EditFieldAction = (props) => {
set(defaultValues.reverseField, 'name', `f_${uid()}`);
set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title);
}
const schema = getSchema(
{
...interfaceConf,
default: defaultValues,
},
record,
compile,
getContainer,
);
const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer);
setSchema(schema);
setVisible(true);
}}

View File

@ -136,6 +136,6 @@ export const useResourceContext = () => {
resource,
collection,
association,
targetKey: association?.targetKey || collection?.targetKey || 'id',
targetKey: association?.targetKey || collection?.filterTargetKey || collection?.targetKey || 'id',
};
};

View File

@ -52,6 +52,8 @@ import {
UUIDFieldInterface,
NanoidFieldInterface,
UnixTimestampFieldInterface,
DateFieldInterface,
DatetimeNoTzFieldInterface,
} from './interfaces';
import {
GeneralCollectionTemplate,
@ -173,6 +175,8 @@ export class CollectionPlugin extends Plugin {
UUIDFieldInterface,
NanoidFieldInterface,
UnixTimestampFieldInterface,
DateFieldInterface,
DatetimeNoTzFieldInterface,
]);
}

View File

@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true,
},
};
availableTypes = ['date'];
availableTypes = [];
properties = {
...defaultProps,
...dateTimeProps,

View File

@ -0,0 +1,46 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
export class DateFieldInterface extends CollectionFieldInterface {
name = 'date';
type = 'object';
group = 'datetime';
order = 3;
title = '{{t("DateOnly")}}';
sortable = true;
default = {
type: 'dateOnly',
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
dateOnly: true,
},
},
};
availableTypes = ['dateOnly'];
hasDefaultValue = true;
properties = {
...defaultProps,
...dateTimeProps,
'uiSchema.x-component-props.showTime': {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-visible': false,
},
};
filterable = {
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -15,23 +15,39 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
type = 'object';
group = 'datetime';
order = 1;
title = '{{t("Datetime")}}';
title = '{{t("Datetime(with time zone)")}}';
sortable = true;
default = {
type: 'date',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
timezone: true,
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
showTime: false,
utc: true,
},
},
};
availableTypes = ['date', 'dateOnly', 'string'];
availableTypes = ['date', 'string', 'datetime', 'datetimeTz'];
hasDefaultValue = true;
properties = {
...defaultProps,
...dateTimeProps,
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
},
'uiSchema.x-component-props.gmt': {
type: 'boolean',
title: '{{t("GMT")}}',

View File

@ -0,0 +1,65 @@
/**
* 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 { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
export class DatetimeNoTzFieldInterface extends CollectionFieldInterface {
name = 'datetimeNoTz';
type = 'object';
group = 'datetime';
order = 2;
title = '{{t("Datetime(without time zone)")}}';
sortable = true;
default = {
type: 'datetimeNoTz',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
timezone: false,
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
showTime: false,
utc: false,
},
},
};
availableTypes = ['string', 'datetimeNoTz'];
hasDefaultValue = true;
properties = {
...defaultProps,
...dateTimeProps,
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current server time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp to the current server time on update")}}',
},
'uiSchema.x-component-props.gmt': {
type: 'boolean',
title: '{{t("GMT")}}',
'x-hidden': true,
'x-component': 'Checkbox',
'x-content': '{{t("Use the same time zone (GMT) for all users")}}',
'x-decorator': 'FormItem',
default: false,
},
};
filterable = {
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -46,3 +46,5 @@ export * from './sort';
export * from './uuid';
export * from './nanoid';
export * from './unixTimestamp';
export * from './dateOnly';
export * from './datetimeNoTz';

View File

@ -10,7 +10,7 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m';
import { getUniqueKeyFromCollection } from './utils';
import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
export class M2MFieldInterface extends CollectionFieldInterface {

View File

@ -9,7 +9,7 @@
import { ISchema } from '@formily/react';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m';
import { getUniqueKeyFromCollection } from './utils';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
export class M2OFieldInterface extends CollectionFieldInterface {

View File

@ -8,10 +8,9 @@
*/
import { ISchema } from '@formily/react';
import { Collection } from '../../data-source';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
import { getUniqueKeyFromCollection } from './utils';
export class O2MFieldInterface extends CollectionFieldInterface {
name = 'o2m';
type = 'object';
@ -215,7 +214,3 @@ export class O2MFieldInterface extends CollectionFieldInterface {
],
};
}
export function getUniqueKeyFromCollection(collection: Collection) {
return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
}

View File

@ -9,7 +9,7 @@
import { ISchema } from '@formily/react';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m';
import { getUniqueKeyFromCollection } from './utils';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
export class O2OFieldInterface extends CollectionFieldInterface {

View File

@ -10,6 +10,8 @@
import { Field } from '@formily/core';
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { css } from '@emotion/css';
import { DateFormatCom } from '../../../schema-component/antd/expiresRadio';
export * as operators from './operators';
export const type: ISchema = {
@ -225,26 +227,88 @@ export const reverseFieldProperties: Record<string, ISchema> = {
};
export const dateTimeProps: { [key: string]: ISchema } = {
'uiSchema.x-component-props.picker': {
type: 'string',
title: '{{t("Picker")}}',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
default: 'date',
enum: [
{
label: '{{t("Date")}}',
value: 'date',
},
// {
// label: '{{t("Week")}}',
// value: 'week',
// },
{
label: '{{t("Month")}}',
value: 'month',
},
{
label: '{{t("Quarter")}}',
value: 'quarter',
},
{
label: '{{t("Year")}}',
value: 'year',
},
],
},
'uiSchema.x-component-props.dateFormat': {
type: 'string',
title: '{{t("Date format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
'x-component': 'ExpiresRadio',
'x-decorator-props': {},
'x-component-props': {
className: css`
.ant-radio-wrapper {
display: flex;
margin: 5px 0px;
}
`,
defaultValue: 'dddd',
formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'],
},
default: 'YYYY-MM-DD',
enum: [
{
label: '{{t("Year/Month/Day")}}',
value: 'YYYY/MM/DD',
label: DateFormatCom({ format: 'MMMM Do YYYY' }),
value: 'MMMM Do YYYY',
},
{
label: '{{t("Year-Month-Day")}}',
label: DateFormatCom({ format: 'YYYY-MM-DD' }),
value: 'YYYY-MM-DD',
},
{
label: '{{t("Day/Month/Year")}}',
label: DateFormatCom({ format: 'MM/DD/YY' }),
value: 'MM/DD/YY',
},
{
label: DateFormatCom({ format: 'YYYY/MM/DD' }),
value: 'YYYY/MM/DD',
},
{
label: DateFormatCom({ format: 'DD/MM/YYYY' }),
value: 'DD/MM/YYYY',
},
{
label: 'custom',
value: 'custom',
},
],
'x-reactions': {
dependencies: ['uiSchema.x-component-props.picker'],
fulfill: {
state: {
value: `{{ getPickerFormat($deps[0])}}`,
componentProps: { picker: `{{$deps[0]}}` },
},
},
},
},
'uiSchema.x-component-props.showTime': {
type: 'boolean',
@ -253,10 +317,26 @@ export const dateTimeProps: { [key: string]: ISchema } = {
'x-content': '{{t("Show time")}}',
'x-reactions': [
`{{(field) => {
field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none';
});
}}}`,
field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none';
f.value='HH:mm:ss'
});
}}}`,
{
dependencies: ['uiSchema.x-component-props.picker'],
when: '{{$deps[0]==="date"}}',
fulfill: {
state: {
hidden: false,
},
},
otherwise: {
state: {
hidden: true,
value: false,
},
},
},
],
},
'uiSchema.x-component-props.timeFormat': {
@ -275,6 +355,14 @@ export const dateTimeProps: { [key: string]: ISchema } = {
value: 'HH:mm:ss',
},
],
'x-reactions': {
dependencies: ['uiSchema.x-component-props.showTime'],
fulfill: {
state: {
hidden: `{{ !$deps[0] }}`,
},
},
},
},
};

View File

@ -60,13 +60,48 @@ export const object = [
];
export const datetime = [
{ label: "{{ t('is') }}", value: '$dateOn', selected: true },
{ label: "{{ t('is not') }}", value: '$dateNotOn' },
{ label: "{{ t('is before') }}", value: '$dateBefore' },
{ label: "{{ t('is after') }}", value: '$dateAfter' },
{ label: "{{ t('is on or after') }}", value: '$dateNotBefore' },
{ label: "{{ t('is on or before') }}", value: '$dateNotAfter' },
{ label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } },
{
label: "{{ t('is') }}",
value: '$dateOn',
selected: true,
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true, //schema 仅在Filter.Action生效筛选表单中不生效
},
{
label: "{{ t('is not') }}",
value: '$dateNotOn',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is before') }}",
value: '$dateBefore',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is after') }}",
value: '$dateAfter',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is on or after') }}",
value: '$dateNotBefore',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is on or before') }}",
value: '$dateNotAfter',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is between') }}",
value: '$dateBetween',
schema: { 'x-component': 'DatePicker.RangePicker' },
},
{ label: "{{ t('is empty') }}", value: '$empty', noValue: true },
{ label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true },
];
@ -157,18 +192,18 @@ export const collection = [
label: '{{t("is")}}',
value: '$eq',
selected: true,
schema: { 'x-component': 'CollectionSelect' },
schema: { 'x-component': 'DataSourceCollectionCascader' },
},
{
label: '{{t("is not")}}',
value: '$ne',
schema: { 'x-component': 'CollectionSelect' },
schema: { 'x-component': 'DataSourceCollectionCascader' },
},
{
label: '{{t("is any of")}}',
value: '$in',
schema: {
'x-component': 'CollectionSelect',
'x-component': 'DataSourceCollectionCascader',
'x-component-props': { mode: 'tags' },
},
},
@ -176,7 +211,7 @@ export const collection = [
label: '{{t("is none of")}}',
value: '$notIn',
schema: {
'x-component': 'CollectionSelect',
'x-component': 'DataSourceCollectionCascader',
'x-component-props': { mode: 'tags' },
},
},

View File

@ -14,7 +14,7 @@ export class TimeFieldInterface extends CollectionFieldInterface {
name = 'time';
type = 'object';
group = 'datetime';
order = 2;
order = 4;
title = '{{t("Time")}}';
sortable = true;
default = {

View File

@ -8,31 +8,34 @@
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
import { defaultProps, operators, dateTimeProps } from './properties';
export class UnixTimestampFieldInterface extends CollectionFieldInterface {
name = 'unixTimestamp';
type = 'object';
group = 'datetime';
order = 1;
order = 4;
title = '{{t("Unix Timestamp")}}';
sortable = true;
default = {
type: 'bigInt',
type: 'unixTimestamp',
accuracy: 'second',
timezone: true,
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
uiSchema: {
type: 'number',
'x-component': 'UnixTimestamp',
'x-component-props': {
accuracy: 'second',
showTime: true,
},
},
};
availableTypes = ['integer', 'bigInt'];
hasDefaultValue = true;
availableTypes = ['unixTimestamp'];
hasDefaultValue = false;
properties = {
...defaultProps,
'uiSchema.x-component-props.accuracy': {
...dateTimeProps,
accuracy: {
type: 'string',
title: '{{t("Accuracy")}}',
'x-component': 'Radio.Group',
@ -43,9 +46,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
{ value: 'second', label: '{{t("Second")}}' },
],
},
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
},
};
filterable = {
operators: operators.number,
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true,
},
};
availableTypes = ['date'];
availableTypes = [];
properties = {
...defaultProps,
...dateTimeProps,

View File

@ -0,0 +1,20 @@
/**
* 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 { Collection } from '../../../data-source';
export function getUniqueKeyFromCollection(collection: Collection) {
if (collection?.filterTargetKey) {
if (Array.isArray(collection.filterTargetKey)) {
return collection?.filterTargetKey?.[0];
}
return collection?.filterTargetKey;
}
return collection?.getPrimaryKey() || 'id';
}

View File

@ -26,7 +26,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
'x-validator': 'uuid',
},
};
availableTypes = ['string', 'uid', 'uuid'];
availableTypes = ['string', 'uuid'];
properties = {
'uiSchema.title': {
type: 'string',

View File

@ -90,6 +90,9 @@ export class SqlCollectionTemplate extends CollectionTemplate {
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
},
};

View File

@ -153,6 +153,9 @@ export class ViewCollectionTemplate extends CollectionTemplate {
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
},
...getConfigurableProperties('category', 'description'),

View File

@ -8,9 +8,10 @@
*/
import type { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash';
import type { CollectionFieldOptions } from '../collection';
import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager';
import { defaultProps } from '../../collection-manager/interfaces/properties';
export type CollectionFieldInterfaceFactory = new (
collectionFieldInterfaceManager: CollectionFieldInterfaceManager,
) => CollectionFieldInterface;
@ -42,6 +43,7 @@ export abstract class CollectionFieldInterface {
componentOptions?: CollectionFieldInterfaceComponentOption[];
isAssociation?: boolean;
operators?: any[];
properties?: any;
/**
* - Filter
* -
@ -82,4 +84,76 @@ export abstract class CollectionFieldInterface {
}
this.componentOptions.push(componentOption);
}
getConfigureFormProperties() {
const defaultValueProps = this.hasDefaultValue ? this.getDefaultValueProperty() : {};
return {
...cloneDeep({ ...defaultProps, ...this?.properties }),
...defaultValueProps,
};
}
getDefaultValueProperty() {
return {
defaultValue: {
...cloneDeep(this?.default?.uiSchema),
...this?.properties?.uiSchema,
required: false,
title: '{{ t("Default value") }}',
'x-decorator': 'FormItem',
'x-reactions': [
{
dependencies: [
'uiSchema.x-component-props.gmt',
'uiSchema.x-component-props.showTime',
'uiSchema.x-component-props.dateFormat',
'uiSchema.x-component-props.timeFormat',
'uiSchema.x-component-props.picker',
],
fulfill: {
state: {
componentProps: {
gmt: '{{$deps[0]}}',
showTime: '{{$deps[1]}}',
dateFormat: '{{$deps[2]}}',
timeFormat: '{{$deps[3]}}',
picker: '{{$deps[4]}}',
},
},
},
},
{
// 当 picker 改变时,清空 defaultValue
dependencies: ['uiSchema.x-component-props.picker'],
fulfill: {
state: {
value: null,
},
},
},
{
dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'],
when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}',
fulfill: {
state: {
hidden: true,
value: null,
},
},
otherwise: {
state: {
hidden: false,
},
},
},
{
dependencies: ['uiSchema.enum'],
fulfill: {
state: {
dataSource: '{{$deps[0]}}',
},
},
},
],
},
};
}
}

View File

@ -13,7 +13,7 @@ export interface CollectionRecordOptions<DataType = {}, ParentDataType = {}> {
parentRecord?: CollectionRecord<ParentDataType>;
}
export class CollectionRecord<DataType = {}, ParentDataType = {}> {
export class CollectionRecord<DataType = any, ParentDataType = {}> {
public data?: DataType;
public parentRecord?: CollectionRecord<ParentDataType>;
public isNew?: boolean;

View File

@ -8,6 +8,7 @@
*/
import type { SchemaKey } from '@formily/json-schema';
import qs from 'qs';
import type { DataSource } from '../data-source';
import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection';
@ -159,10 +160,23 @@ export class CollectionManager {
);
return;
}
const getTargetKey = (collection: Collection) => collection.filterTargetKey || collection.getPrimaryKey() || 'id';
const buildFilterByTk = (targetKey: string | string[], record: Record<string, any>) => {
if (Array.isArray(targetKey)) {
const filterByTk = {};
targetKey.forEach((key) => {
filterByTk[key] = record[key];
});
return qs.stringify(filterByTk);
} else {
return record[targetKey];
}
};
if (collectionOrAssociation instanceof Collection) {
const key = collectionOrAssociation.filterTargetKey || collectionOrAssociation.getPrimaryKey() || 'id';
return collectionRecordOrAssociationRecord[key];
const targetKey = getTargetKey(collectionOrAssociation);
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
}
if (collectionOrAssociation.includes('.')) {
@ -186,9 +200,8 @@ export class CollectionManager {
);
return;
}
const key = targetCollection?.filterTargetKey || targetCollection?.getPrimaryKey() || 'id';
return collectionRecordOrAssociationRecord[key];
const targetKey = getTargetKey(targetCollection);
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
}
getSourceKeyByAssociation(associationName: string) {

View File

@ -9,7 +9,7 @@
import { IResource } from '@nocobase/sdk';
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
import { isArray } from 'lodash';
import { useAPIClient } from '../../api-client';
import { useCollectionManager } from '../collection';
import { CollectionRecord } from '../collection-record';
@ -34,7 +34,16 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
if (association && parentRecord) {
const sourceKey = cm.getSourceKeyByAssociation(association);
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
return parentRecordData[sourceKey];
if (isArray(sourceKey)) {
const filterByTk = {};
for (const key of sourceKey) {
filterByTk[key] = parentRecordData?.[key];
}
return encodeURIComponent(JSON.stringify(filterByTk));
} else {
return parentRecordData[sourceKey];
}
}
}, [association, sourceId, parentRecord]);

View File

@ -0,0 +1,42 @@
/**
* 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 { TranslationOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import React from 'react';
import { useAPIClient } from '../api-client';
import languageCodes from '../locale';
import { useSystemSettings } from '../system-settings';
export function SwitchLanguage() {
const { data } = useSystemSettings();
const api = useAPIClient();
return (
data?.data?.enabledLanguages.length > 1 && (
<Dropdown
menu={{
onClick(info) {
api.auth.setLocale(info.key);
window.location.reload();
},
selectable: true,
defaultSelectedKeys: [api.auth.locale],
items: data?.data?.enabledLanguages?.map((code) => {
return {
key: code,
label: languageCodes[code].label,
};
}),
}}
>
<TranslationOutlined style={{ fontSize: 24 }} />
</Dropdown>
)
);
}

View File

@ -74,4 +74,9 @@ export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModePr
export { PopupContextProvider } from './modules/popup/PopupContextProvider';
export { usePopupUtils } from './modules/popup/usePopupUtils';
export { SwitchLanguage } from './i18n/SwitchLanguage';
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
export { languageCodes } from './locale';

View File

@ -427,6 +427,7 @@
"Option label": "Option label",
"Color": "Color",
"Background Color": "Background Color",
"Text Align": "Text Align",
"Add option": "Add option",
"Related collection": "Related collection",
"Allow linking to multiple records": "Allow linking to multiple records",
@ -840,5 +841,9 @@
"is none of": "is none of",
"is any of": "is any of",
"Plugin dependency version mismatch": "Plugin dependency version mismatch",
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?"
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data",
"Enable secondary confirmation": "Enable secondary confirmation",
"Notification": "Notification",
"Ellipsis overflow content": "Ellipsis overflow content"
}

View File

@ -765,5 +765,6 @@
"Expand All": "Expandir todo",
"Clear default value": "Borrar valor por defecto",
"Open in new window": "Abrir en una nueva ventana",
"Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe."
"Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe.",
"Ellipsis overflow content": "Contenido de desbordamiento de elipsis"
}

View File

@ -785,5 +785,6 @@
"Expand All": "Tout déplier",
"Clear default value": "Effacer la valeur par défaut",
"Open in new window": "Ouvrir dans une nouvelle fenêtre",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas."
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"Ellipsis overflow content": "Contenu de débordement avec ellipse"
}

View File

@ -7,6 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
/**
* 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 type LocaleOptions = {
label: string;
};
@ -81,7 +90,7 @@ export const dayjsLocale = {
'zh-TW': 'zh-tw',
};
export default {
export const languageCodes = {
'ar-EG': { label: 'العربية' },
'az-AZ': { label: 'Azərbaycan dili' },
'bg-BG': { label: 'Български' },
@ -150,3 +159,5 @@ export default {
'zh-HK': { label: '繁體中文(香港)' },
'zh-TW': { label: '繁體中文(台湾)' },
};
export default languageCodes;

View File

@ -705,6 +705,7 @@
"Clear default value": "デフォルト値をクリア",
"Open in new window": "新しいウィンドウで開く",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、お探しのページは存在しません。",
"Ellipsis overflow content": "省略記号で内容を省略",
"NaN": "なし",
"Settings": "設定",
"Collection selector": "コレクションセレクタ",
@ -1003,8 +1004,5 @@
"Use simple pagination mode": "シンプルなページネーションモードを使用",
"Set Template Engine": "テンプレートエンジンを設定",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "ページング時にテーブルレコードの総数取得をスキップして、読み込み速度を向上させます。データ量が多い場合にこのオプションの使用をお勧めします。",
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。",
"NaN": "なし",
"true": "真",
"false": "偽"
}
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。"
}

View File

@ -876,5 +876,6 @@
"Expand All": "모두 펼치기",
"Clear default value": "기본값 지우기",
"Open in new window": "새 창에서 열기",
"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다."
"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.",
"Ellipsis overflow content": "생략 부호로 내용 줄임"
}

View File

@ -742,5 +742,6 @@
"Current popup record": "Registro pop-up atual",
"Clear default value": "Limpar valor padrão",
"Open in new window": "Abrir em nova janela",
"Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe."
"Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.",
"Ellipsis overflow content": "Conteúdo de transbordamento com reticências"
}

View File

@ -579,5 +579,6 @@
"Expand All": "Развернуть все",
"Clear default value": "Очистить значение по умолчанию",
"Open in new window": "Открыть в новом окне",
"Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует."
"Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует.",
"Ellipsis overflow content": "Содержимое с многоточием при переполнении"
}

View File

@ -577,5 +577,6 @@
"Expand All": "Tümünü genişlet",
"Clear default value": "Varsayılan değeri temizle",
"Open in new window": "Yeni pencerede aç",
"Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil."
"Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil.",
"Ellipsis overflow content": "Üç nokta ile taşan içerik"
}

View File

@ -785,5 +785,6 @@
"Expand All": "Розгорнути все",
"Clear default value": "Очистити значення за замовчуванням",
"Open in new window": "Відкрити в новому вікні",
"Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує."
"Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує.",
"Ellipsis overflow content": "Вміст з багатокрапкою при переповненні"
}

View File

@ -283,7 +283,7 @@
"Checkbox group": "复选框",
"China region": "中国行政区",
"Date & Time": "日期 & 时间",
"Datetime": "日期",
"Datetime": "日期时间",
"Relation": "关系类型",
"Link to": "关联",
"Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。",
@ -448,6 +448,7 @@
"Option label": "选项标签",
"Color": "颜色",
"Background Color": "背景颜色",
"Text Align": "文本对齐",
"Add option": "添加选项",
"Related collection": "关系表",
"Allow linking to multiple records": "允许关联多条记录",
@ -901,6 +902,7 @@
"Collections": "数据表",
"Collection fields": "数据表字段",
"Authentication": "用户认证",
"Notification": "通知",
"Logging and monitoring": "日志与监控",
"Home page": "主页",
"Handbook": "用户手册",
@ -971,8 +973,32 @@
"Use simple pagination mode": "使用简单分页模式",
"Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。",
"Set Template Engine": "设置模板引擎",
"Template engine": "模板引擎",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "在分页时跳过获取表记录总数,以加快加载速度,建议对有大量数据的数据表开启此选项",
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。",
"Plugin dependency version mismatch": "插件依赖版本不一致",
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?"
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?",
"Default value to current time": "设置字段默认值为当前时间",
"Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间",
"Default value to current server time": "设置字段默认值为当前服务端时间",
"Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间",
"Datetime(with time zone)": "日期时间(含时区)",
"Datetime(without time zone)": "日期时间(不含时区)",
"DateOnly": "仅日期",
"Enable secondary confirmation": "启用二次确认",
"Content": "内容",
"Perform the Update record": "执行更新数据",
"Are you sure you want to perform the Update record action?": "你确定执行更新数据操作吗?",
"Perform the Custom request": "执行自定义请求",
"Are you sure you want to perform the Custom request action": "你确定执行自定义请求操作吗?",
"Perform the Refresh": "执行刷新",
"Are you sure you want to perform the Refresh action?": "你确定执行刷新操作吗?",
"Perform the Submit": "执行提交",
"Are you sure you want to perform the Submit action?": "你确定执行提交操作吗?",
"Perform the Trigger workflow": "执行触发工作流",
"Are you sure you want to perform the Trigger workflow action?": "你确定执行触发工作流吗?",
"Ellipsis overflow content": "省略超出长度的内容",
"Picker": "选择器",
"Quarter":"季度",
"Switching the picker, the value and default value will be cleared":"切换选择器时,字段的值和默认值将会被清空"
}

View File

@ -444,6 +444,7 @@
"Option value": "選項值",
"Option label": "選項標籤",
"Color": "顏色",
"Text Align": "文本對齊",
"Add option": "新增選項",
"Related collection": "關聯表",
"Allow linking to multiple records": "允許關聯多條記錄",
@ -874,5 +875,6 @@
"Expand All": "展開全部",
"Clear default value": "清除預設值",
"Open in new window": "新窗口打開",
"Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。"
"Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。",
"Ellipsis overflow content": "省略超出長度的內容"
}

View File

@ -44,6 +44,8 @@ test.describe('bulk-destroy', () => {
await page.getByLabel('action-Action-Delete-destroy-').hover();
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 page.getByRole('button', { name: 'OK' }).click();
await page.mouse.move(500, 0);
// 2. 选中所有行

View File

@ -52,10 +52,6 @@ export const customizeSaveRecordActionSettings = new SchemaSettings({
{
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
},
},
{
name: 'bindWorkflow',

View File

@ -28,6 +28,8 @@ import {
} from '../../../schema-component/antd/action/Action.Designer';
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
import { useDataBlockProps } from '../../../data-source';
const Tree = connect(
AntdTree,
@ -163,6 +165,10 @@ export const createSubmitActionSettings = new SchemaSettings({
{
name: 'saveMode',
Component: SaveMode,
useVisible() {
const { type } = useDataBlockProps();
return type !== 'publicForm';
},
},
{
name: 'assignFieldValues',
@ -175,10 +181,6 @@ export const createSubmitActionSettings = new SchemaSettings({
{
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
},
},
{
name: 'refreshDataBlockRequest',
@ -188,6 +190,10 @@ export const createSubmitActionSettings = new SchemaSettings({
isPopupAction: false,
};
},
useVisible() {
const parentRecord = useParentPopupRecord();
return !!parentRecord;
},
},
{
name: 'remove',

View File

@ -71,10 +71,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
{
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
},
},
{
name: 'refreshDataBlockRequest',

View File

@ -163,11 +163,9 @@ test.describe('edit form block schema settings', () => {
await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click();
await page.getByRole('spinbutton').fill('');
await page.getByRole('spinbutton').fill('10');
await expect(
page
.getByLabel('block-item-CollectionField-general-form-general.formula-formula')
.locator('.nb-read-pretty-input-number'),
).toHaveText('11');
await expect(page.getByLabel('block-item-CollectionField-general-form-general.formula-formula')).toHaveText(
'formula:11',
);
await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click();
await expect(page.getByText('Unsaved changes')).toBeVisible();
});

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { expect, commonFormViewPage, test } from '@nocobase/test/e2e';
import { commonFormViewPage, expect, test } from '@nocobase/test/e2e';
test.describe('field schema settings', () => {
test('linkage style color', async ({ page, mockPage, mockRecord }) => {
@ -60,4 +60,32 @@ test.describe('field schema settings', () => {
.locator('div.ant-formily-item-control-content-component');
await expect(cell).toHaveCSS('background-color', 'rgb(163, 79, 204)');
});
test('text align', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(commonFormViewPage).waitForInit();
await mockRecord('general', { singleLineText: 'asdfcedg' });
await nocoPage.goto();
await page.getByText('asdfcedg', { exact: true }).hover();
await page.getByLabel('block-item-CollectionField-general-details-general.singleLineText-singleLineText').hover();
await page
.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-general-general.singleLineText', {
exact: true,
})
.click();
await page.getByRole('menuitem', { name: 'Style' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
await page.getByTestId('select-linkage-properties').click();
await page.getByText('Text Align', { exact: true }).click();
await page.getByTestId('select-single').click();
await page.getByRole('option', { name: 'right' }).click();
await page.getByRole('button', { name: 'OK' }).click();
const cell = page
.getByLabel('block-item-CollectionField-general-details-general.singleLineText-singleLineText')
.locator('div.ant-formily-item-control-content-component');
await expect(cell).toHaveCSS('text-align', 'right');
});
});

View File

@ -99,7 +99,7 @@ describe('FieldSettingsFormItem', () => {
describe('menu list', () => {
describe('edit mode', () => {
it('common field', async () => {
it.skip('common field', async () => {
await renderSettings(commonFieldOptions());
await checkSettings(
@ -213,7 +213,7 @@ describe('FieldSettingsFormItem', () => {
});
describe('read pretty mode', () => {
it('common field', async () => {
it.skip('common field', async () => {
await renderReadPrettySettings(commonFieldOptions());
await checkSettings(
@ -406,7 +406,7 @@ describe('FieldSettingsFormItem', () => {
]);
});
test('Set default value', async () => {
test.skip('Set default value', async () => {
await renderSettings(commonFieldOptions());
const newValue = 'new test';
@ -432,7 +432,7 @@ describe('FieldSettingsFormItem', () => {
]);
});
test('Pattern', async () => {
test.skip('Pattern', async () => {
await renderSettings(associationFieldOptions());
await checkSettings([
@ -464,7 +464,7 @@ describe('FieldSettingsFormItem', () => {
]);
});
test('EditValidationRules', async () => {
test.skip('EditValidationRules', async () => {
await renderSingleSettings(commonFieldOptions(true));
await checkSettings([
{

View File

@ -8,6 +8,7 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useDataBlockProps } from '../../../../data-source';
const commonOptions = {
title: '{{t("Configure actions")}}',
@ -25,6 +26,10 @@ const commonOptions = {
name: 'customRequest',
title: '{{t("Custom request")}}',
Component: 'CustomRequestInitializer',
useVisible() {
const { type } = useDataBlockProps();
return type !== 'publicForm';
},
},
],
};

View File

@ -8,7 +8,6 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: "{{t('Configure actions')}}",
@ -73,6 +72,14 @@ const commonOptions = {
},
},
},
{
name: 'customRequest',
title: '{{t("Custom request")}}',
Component: 'CustomRequestInitializer',
schema: {
'x-action': 'customize:table:request:global',
},
},
],
};

View File

@ -8,7 +8,6 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
@ -74,6 +73,14 @@ const commonOptions = {
},
},
},
{
name: 'customRequest',
title: '{{t("Custom request")}}',
Component: 'CustomRequestInitializer',
schema: {
'x-action': 'customize:table:request:global',
},
},
],
};

View File

@ -90,6 +90,14 @@ const commonOptions = {
return collection.tree && treeTable;
},
},
{
name: 'customRequest',
title: '{{t("Custom request")}}',
Component: 'CustomRequestInitializer',
schema: {
'x-action': 'customize:table:request:global',
},
},
],
};

View File

@ -57,4 +57,29 @@ test.describe('view', () => {
const bgColor = await cell.evaluate((el) => getComputedStyle(el.parentElement).backgroundColor);
expect(bgColor).toContain('163, 79, 204');
});
test('text align', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(oneTableBlockWithIntegerAndIDColumn).waitForInit();
await mockRecord('general', { integer: '423' });
await nocoPage.goto();
await page.getByText('integer', { exact: true }).hover();
await page
.getByRole('button', {
name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-general',
})
.click();
await page.getByRole('menuitem', { name: 'Style' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
await page.getByTestId('select-linkage-properties').click();
await page.getByText('Text Align', { exact: true }).click();
await page.getByTestId('select-single').click();
await page.getByRole('option', { name: 'right' }).click();
await page.getByRole('button', { name: 'OK' }).click();
const cell = page.getByRole('button', { name: '423' });
const textAlign = await cell.evaluate((el) => getComputedStyle(el.parentElement).textAlign);
expect(textAlign).toContain('right');
});
});

View File

@ -16,7 +16,6 @@ import { findFilterTargets } from '../../../../../block-provider/hooks';
import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider';
import { mergeFilter } from '../../../../../filter-provider/utils';
import { removeNullCondition } from '../../../../../schema-component';
import { useCollection } from '../../../../../data-source';
export const useTableBlockProps = () => {
const field = useField<ArrayField>();
@ -56,11 +55,12 @@ export const useTableBlockProps = () => {
loading: ctx?.service?.loading,
showIndex: ctx.showIndex,
dragSort: ctx.dragSort && ctx.dragSortBy,
rowKey: ctx.rowKey || 'id',
rowKey: ctx.rowKey || fieldSchema?.['x-component-props']?.rowKey || 'id',
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
onRowSelectionChange: useCallback((selectedRowKeys) => {
onRowSelectionChange: useCallback((selectedRowKeys, selectedRowData) => {
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx.field.data.selectedRowData = selectedRowData;
ctx?.field?.onRowSelect?.(selectedRowKeys);
}, []),
onRowDragEnd: useCallback(

View File

@ -7,22 +7,22 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { ISchema } from '@formily/json-schema';
import { useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useApp, useSchemaToolbar } from '../../../../application';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollectionManager_deprecated } from '../../../../collection-manager';
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { useCollection } from '../../../../data-source';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { useDesignable } from '../../../../schema-component';
import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
import { isPatternDisabled } from '../../../../schema-settings/isPatternDisabled';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
export const tableColumnSettings = new SchemaSettings({
name: 'fieldSettings:TableColumn',

View File

@ -13,11 +13,11 @@ import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source';
import { useCollectionManager } from '../../../../data-source/collection/CollectionManagerProvider';
import { useCompile, useDesignable } from '../../../../schema-component';
import { SchemaSettingsDefaultSortingRules } from '../../../../schema-settings';
import { SchemaSettingsDefaultSortingRules, SchemaSettingsDefaultValue } from '../../../../schema-settings';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
export const filterCollapseItemFieldSettings = new SchemaSettings({
name: 'fieldSettings:FilterCollapseItem',
@ -135,6 +135,12 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({
},
};
},
useVisible() {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const collectionField = collection.getField(fieldSchema['name']);
return !!collectionField?.target;
},
},
{
name: 'setDefaultSortingRules',
@ -150,6 +156,12 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({
name: collectionField?.target,
};
},
useVisible() {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const collectionField = collection.getField(fieldSchema['name']);
return !!collectionField?.target;
},
},
{
name: 'titleField',
@ -197,8 +209,20 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({
onChange: onTitleFieldChange,
};
},
useVisible() {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const collectionField = collection.getField(fieldSchema['name']);
return !!collectionField?.target;
},
},
fieldComponentSettingsItem,
{
name: 'setDefaultValue',
Component: SchemaSettingsDefaultValue,
componentProps: {
hideVariableButton: true,
},
} as any,
];
},
},

View File

@ -0,0 +1,357 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { expect, test } from '@nocobase/test/e2e';
import { ellipsis } from './templates';
test.describe('ellipsis', () => {
test('Input & Input.URL & Input.TextArea & Input.JSON & RichText & Markdown & MarkdownVditor', async ({
page,
mockPage,
mockRecord,
}) => {
const nocoPage = await mockPage(ellipsis).waitForInit();
await mockRecord('testEllipsis', {
input: '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ这是一段很长的输入文本用于测试省略功能。',
inputURL: 'https://www.nocobase.com/docs/welcome/introduction/getting-started/installation/docker-compose',
inputTextArea:
'1234567890abcdefghijklmnopqrstuvwxyz用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果\n第二行文本这里有更多的内容\n第三行文本继续添加更多文字\n第四行文本确保内容足够长\n第五行文本用于测试多行文本的省略效果',
inputJSON: `{
"99999": "其他",
"F3134": "软件销售",
"I3007": "人工智能基础软件开发",
"I3008": "人工智能应用软件开发",
"I3014": "数字文化创意软件开发",
"I3027": "信息技术咨询服务",
"I3034": "计算机系统服务",
"P1029": "业务培训(不含教育培训、职业技能培训等需取得许可的培训)"
}`,
richText:
'用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果<h1>NocoBase简介</h1><p>1234567890abcdefghijklmnopqrstuvwxyz</p><p>这是第二段落介绍NocoBase的主要特性</p><p>这是第三段落讨论NocoBase的应用场景</p><ul><li>企业内部系统</li><li>工作流管理</li><li>数据分析平台</li></ul>',
markdown:
'用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果# NocoBase开源无代码平台\n\n1234567890abcdefghijklmnopqrstuvwxyz\n\n## 为什么选择NocoBase\n\n- 快速开发\n- 灵活定制\n- 开源免费\n\n### 核心功能\n\n1. 数据模型设计\n2. 界面配置\n3. 工作流引擎\n4. 权限管理\n\n> NocoBase让每个人都能轻松构建自己的软件系统',
markdownVditor:
'用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果# Vditor强大的Markdown编辑器\n\n1234567890abcdefghijklmnopqrstuvwxyz\n\n> Vditor是一个强大的Markdown编辑器支持所见即所得、即时渲染和分屏预览等模式\n\n## 主要特性\n\n- 支持多种编辑模式\n- 丰富的快捷键\n- 自定义主题\n\n```js\nconsole.log("Vditor是NocoBase默认的Markdown编辑器");\n```\n\n更多信息请访问[Vditor官网](https://b3log.org/vditor/)',
type: '1',
});
await nocoPage.goto();
// 1. Table -------------------------------------------------------------------------------------------------------
await page.getByRole('button', { name: 'input', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByRole('button', { name: 'inputURL', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByRole('button', { name: 'inputTextArea', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByRole('button', { name: 'inputJSON', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByRole('button', { name: 'richText', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByRole('button', { name: 'markdown', exact: true }).hover();
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
// 2. Details -------------------------------------------------------------------------------------------------------
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.input-input').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputURL-inputURL').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputTextArea-').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputJSON-inputJSON').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.richText-richText').hover();
await page
.getByRole('button', {
name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText',
exact: true,
})
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.markdown-markdown').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
// 3. List -------------------------------------------------------------------------------------------------------
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.input-input')
.hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputURL-inputURL')
.hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputTextArea-')
.hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputJSON-inputJSON')
.hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.richText-richText')
.hover();
await page
.getByRole('button', {
name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText',
exact: true,
})
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page
.getByLabel('block-item-CardItem-testEllipsis-list')
.getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.markdown-markdown')
.hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
// 4. GridCard -------------------------------------------------------------------------------------------------------
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.input-input').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputURL-inputURL').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputURL-inputURL').hover();
await page
.getByRole('button', {
name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputURL',
exact: true,
})
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputTextArea-').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputJSON-').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.richText-richText').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.markdown-markdown').hover();
await page
.getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' })
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
// 5. Kanban -------------------------------------------------------------------------------------------------------
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.input-input').hover();
await page
.getByTestId('card-1')
.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.input', {
exact: true,
})
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputURL-inputURL').hover();
await page
.getByTestId('card-1')
.getByLabel(
'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputURL',
{
exact: true,
},
)
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputTextArea-').hover();
await page
.getByTestId('card-1')
.getByLabel(
'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputTextArea',
{
exact: true,
},
)
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputJSON-').hover();
await page
.getByTestId('card-1')
.getByLabel(
'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputJSON',
{
exact: true,
},
)
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.richText-richText').hover();
await page
.getByTestId('card-1')
.getByLabel(
'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText',
{
exact: true,
},
)
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
await page.mouse.move(600, 0);
await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.markdown-markdown').hover();
await page
.getByTestId('card-1')
.getByLabel(
'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.markdown',
{
exact: true,
},
)
.hover();
await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check();
await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true });
});
});

File diff suppressed because it is too large Load Diff

View File

@ -29,3 +29,21 @@ export const datePickerComponentFieldSettings = new SchemaSettings({
},
],
});
export const rangePickerPickerComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:DatePicker.RangePicker',
items: [
{
name: 'dateDisplayFormat',
Component: SchemaSettingsDateFormat as any,
useComponentProps() {
const schema = useFieldSchema();
const { fieldSchema: tableColumnSchema } = useColumnSchema();
const fieldSchema = tableColumnSchema || schema;
return {
fieldSchema,
};
},
},
],
});

View File

@ -0,0 +1,15 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const inputJSONSettings = new SchemaSettings({
name: 'fieldSettings:component:Input.JSON',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,16 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const inputTextAreaSettings = new SchemaSettings({
name: 'fieldSettings:component:Input.TextArea',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,16 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const inputURLSettings = new SchemaSettings({
name: 'fieldSettings:component:Input.URL',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,64 @@
/**
* 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 { useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { useDesignable } from '../../../../schema-component/hooks/useDesignable';
export const ellipsisSettingsItem: SchemaSettingsItemType = {
name: 'ellipsis',
type: 'switch',
useComponentProps() {
const { fieldSchema: tableFieldSchema, filedInstanceList } = useColumnSchema();
const fieldSchema = useFieldSchema();
const formField = useField();
const { dn } = useDesignable();
const { t } = useTranslation();
const schema = tableFieldSchema || fieldSchema;
const hidden = tableFieldSchema
? filedInstanceList[0]
? !filedInstanceList[0].readPretty
: !tableFieldSchema['x-read-pretty']
: !formField.readPretty;
return {
title: t('Ellipsis overflow content'),
checked: !!schema['x-component-props']?.ellipsis,
hidden,
onChange: async (checked) => {
await dn.emit('patch', {
schema: {
'x-uid': schema['x-uid'],
'x-component-props': {
...schema['x-component-props'],
ellipsis: checked,
},
},
});
if (tableFieldSchema && filedInstanceList) {
filedInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked;
});
} else {
formField.componentProps.ellipsis = checked;
}
},
};
},
};
export const inputComponentSettings = new SchemaSettings({
name: 'fieldSettings:component:Input',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,16 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const markdownSettings = new SchemaSettings({
name: 'fieldSettings:component:Markdown',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,16 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const markdownVditorSettings = new SchemaSettings({
name: 'fieldSettings:component:MarkdownVditor',
items: [ellipsisSettingsItem],
});

View File

@ -0,0 +1,16 @@
/**
* 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 { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
export const richTextSettings = new SchemaSettings({
name: 'fieldSettings:component:RichText',
items: [ellipsisSettingsItem],
});

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