mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 01:56:16 +00:00
Merge branch 'main' into T-4256
This commit is contained in:
commit
cd056e700a
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,22 +7,37 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
<!-- Note: Please do not clear the contents of the issue template. Items marked with * are required. Issues not filled out according to the template will be closed. -->
|
||||
<!-- 注意:请不要将 issue 模板内容清空,带 * 的项目为必填项,没有按照模板填写的issue将被关闭。-->
|
||||
|
||||
## * Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## Version
|
||||
## * Environment
|
||||
|
||||
<!-- NocoBase version that you are using. -->
|
||||
<!-- Please view it by clicking on the ? icon in the upper right corner of the NocoBase navigation bar. -->
|
||||
- NocoBase version:
|
||||
|
||||
## How To Reproduce
|
||||
<!-- [e.g. PostgreSQL 12, MySQL 8.x, SQLite] -->
|
||||
- Database type and version:
|
||||
|
||||
<!-- [e.g. MacOS, Windows] -->
|
||||
- OS:
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
<!-- Docker, Create-nocobase-app, Git source code -->
|
||||
- Deployment Methods:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
<!-- If using Docker for deployment, please provide. [e.g. nocobase/nocobase:latest] -->
|
||||
- Docker image version:
|
||||
|
||||
<!-- If using Create-nocobase-app or Git source code for deployment, please provide. -->
|
||||
- NodeJS version:
|
||||
|
||||
|
||||
## * How To Reproduce
|
||||
|
||||
<!-- Please describe the reproduction process in as much detail as possible. -->
|
||||
|
||||
## Expected behavior
|
||||
|
||||
@ -32,18 +47,6 @@ Steps to reproduce the behavior:
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
## Desktop (please complete the following information)
|
||||
## Logs
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome v102, safari]
|
||||
|
||||
## Smartphone (please complete the following information)
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
<!-- If it's an API error, please provide the relevant server logs. -->
|
||||
|
@ -50,4 +50,49 @@ describe('update or create', () => {
|
||||
expect(post).not.toBeNull();
|
||||
expect(post['title']).toEqual('t1');
|
||||
});
|
||||
|
||||
test('update or create with empty values', async () => {
|
||||
await Post.repository.create({ values: { title: 't1' } });
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.updateOrCreate({
|
||||
values: {},
|
||||
filterKeys: ['title'],
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
const post = await Post.repository.findOne({
|
||||
filter: {
|
||||
'title.$empty': true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(post).not.toBeNull();
|
||||
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.updateOrCreate({
|
||||
values: {},
|
||||
filterKeys: ['title'],
|
||||
});
|
||||
|
||||
expect(
|
||||
await Post.repository.count({
|
||||
filter: {
|
||||
'title.$empty': true,
|
||||
},
|
||||
}),
|
||||
).toEqual(1);
|
||||
|
||||
expect(
|
||||
await Post.repository.count({
|
||||
filter: {
|
||||
title: 't1',
|
||||
},
|
||||
}),
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
@ -40,6 +40,7 @@ export default defineConfig({
|
||||
window['__webpack_public_path__'] = '{{env.APP_PUBLIC_PATH}}';
|
||||
window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';
|
||||
window['__nocobase_api_base_url__'] = '{{env.API_BASE_URL}}';
|
||||
window['__nocobase_api_client_storage_prefix__'] = '{{env.API_CLIENT_STORAGE_PREFIX}}';
|
||||
window['__nocobase_ws_url__'] = '{{env.WS_URL}}';
|
||||
window['__nocobase_ws_path__'] = '{{env.WS_PATH}}';
|
||||
`,
|
||||
|
@ -1,3 +1,12 @@
|
||||
/**
|
||||
* 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 { Application } from '@nocobase/client';
|
||||
import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client';
|
||||
import devDynamicImport from '../.plugins/index';
|
||||
@ -5,6 +14,10 @@ import devDynamicImport from '../.plugins/index';
|
||||
export const app = new Application({
|
||||
apiClient: {
|
||||
// @ts-ignore
|
||||
storagePrefix:
|
||||
// @ts-ignore
|
||||
window['__nocobase_api_client_storage_prefix__'] || process.env.API_CLIENT_STORAGE_PREFIX || 'NOCOBASE_',
|
||||
// @ts-ignore
|
||||
baseURL: window['__nocobase_api_base_url__'] || process.env.API_BASE_URL || '/api/',
|
||||
},
|
||||
// @ts-ignore
|
||||
|
@ -286,6 +286,7 @@ function buildIndexHtml(force = false) {
|
||||
const data = fs.readFileSync(tpl, 'utf-8');
|
||||
const replacedData = data
|
||||
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
|
||||
.replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX)
|
||||
.replace(/\{\{env.API_BASE_URL\}\}/g, process.env.API_BASE_URL || process.env.API_BASE_PATH)
|
||||
.replace(/\{\{env.WS_URL\}\}/g, process.env.WEBSOCKET_URL || '')
|
||||
.replace(/\{\{env.WS_PATH\}\}/g, process.env.WS_PATH)
|
||||
@ -301,6 +302,7 @@ exports.initEnv = function initEnv() {
|
||||
APP_KEY: 'test-jwt-secret',
|
||||
APP_PORT: 13000,
|
||||
API_BASE_PATH: '/api/',
|
||||
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
|
||||
DB_DIALECT: 'sqlite',
|
||||
DB_STORAGE: 'storage/db/nocobase.sqlite',
|
||||
DB_TIMEZONE: '+00:00',
|
||||
|
@ -134,6 +134,9 @@ export class Application {
|
||||
this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this);
|
||||
this.addRoutes();
|
||||
this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
|
||||
this.i18n.on('languageChanged', (lng) => {
|
||||
this.apiClient.auth.locale = lng;
|
||||
});
|
||||
}
|
||||
|
||||
private initRequireJs() {
|
||||
|
@ -30,7 +30,7 @@ i18n
|
||||
// .use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: localStorage.getItem('NOCOBASE_LOCALE') || 'en-US',
|
||||
lng: 'en-US',
|
||||
// debug: true,
|
||||
defaultNS: 'client',
|
||||
// fallbackNS: 'client',
|
||||
@ -47,7 +47,3 @@ i18n
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
});
|
||||
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
localStorage.setItem('NOCOBASE_LOCALE', lng);
|
||||
});
|
||||
|
@ -204,6 +204,37 @@ describe('array field operator', function () {
|
||||
expect(filter3[0].get('name')).toEqual(t2.get('name'));
|
||||
});
|
||||
|
||||
test('$anyOf with association with same column name', async () => {
|
||||
const Tag = db.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'array', name: 'type' },
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
});
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
tableName: 'posts_table',
|
||||
fields: [
|
||||
{ type: 'array', name: 'type' },
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'tags',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync({ force: true });
|
||||
|
||||
await Post.repository.find({
|
||||
filter: {
|
||||
'type.$anyOf': ['aa'],
|
||||
'tags.name': 't1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// fix https://nocobase.height.app/T-2803
|
||||
test('$anyOf with string', async () => {
|
||||
const filter3 = await Test.repository.find({
|
||||
|
@ -191,8 +191,11 @@ describe('create', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(u1.name).toEqual('u1');
|
||||
|
||||
const group = await u1.get('group');
|
||||
|
||||
expect(group.name).toEqual('g1');
|
||||
|
||||
const u2 = await User.repository.firstOrCreate({
|
||||
|
@ -33,15 +33,18 @@ const getFieldName = (ctx) => {
|
||||
|
||||
const model = getModelFromAssociationPath();
|
||||
|
||||
let columnPrefix = model.name;
|
||||
|
||||
if (model.rawAttributes[fieldName]) {
|
||||
columnName = model.rawAttributes[fieldName].field || fieldName;
|
||||
}
|
||||
|
||||
if (associationPath.length > 0) {
|
||||
const association = associationPath.join('->');
|
||||
columnName = `${association}.${columnName}`;
|
||||
columnPrefix = association;
|
||||
}
|
||||
|
||||
columnName = `${columnPrefix}.${columnName}`;
|
||||
return columnName;
|
||||
};
|
||||
|
||||
|
@ -242,11 +242,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
this.model = collection.model;
|
||||
}
|
||||
|
||||
public static valuesToFilter(values: Values, filterKeys: Array<string>) {
|
||||
const filterAnd = [];
|
||||
const flattedValues = flatten(values);
|
||||
|
||||
const keyWithOutArrayIndex = (key) => {
|
||||
public static valuesToFilter(values: Values = {}, filterKeys: Array<string>) {
|
||||
const removeArrayIndexInKey = (key) => {
|
||||
const chunks = key.split('.');
|
||||
return chunks
|
||||
.filter((chunk) => {
|
||||
@ -255,29 +252,36 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
.join('.');
|
||||
};
|
||||
|
||||
for (const filterKey of filterKeys) {
|
||||
let filterValue;
|
||||
const filterAnd = [];
|
||||
const flattedValues = flatten(values);
|
||||
const flattedValuesObject = {};
|
||||
|
||||
for (const flattedKey of Object.keys(flattedValues)) {
|
||||
const flattedKeyWithoutIndex = keyWithOutArrayIndex(flattedKey);
|
||||
|
||||
if (flattedKeyWithoutIndex === filterKey) {
|
||||
if (filterValue) {
|
||||
if (Array.isArray(filterValue)) {
|
||||
filterValue.push(flattedValues[flattedKey]);
|
||||
} else {
|
||||
filterValue = [filterValue, flattedValues[flattedKey]];
|
||||
}
|
||||
} else {
|
||||
filterValue = flattedValues[flattedKey];
|
||||
}
|
||||
for (const key in flattedValues) {
|
||||
const keyWithoutArrayIndex = removeArrayIndexInKey(key);
|
||||
if (flattedValuesObject[keyWithoutArrayIndex]) {
|
||||
if (!Array.isArray(flattedValuesObject[keyWithoutArrayIndex])) {
|
||||
flattedValuesObject[keyWithoutArrayIndex] = [flattedValuesObject[keyWithoutArrayIndex]];
|
||||
}
|
||||
|
||||
flattedValuesObject[keyWithoutArrayIndex].push(flattedValues[key]);
|
||||
} else {
|
||||
flattedValuesObject[keyWithoutArrayIndex] = [flattedValues[key]];
|
||||
}
|
||||
}
|
||||
|
||||
for (const filterKey of filterKeys) {
|
||||
const filterValue = flattedValuesObject[filterKey]
|
||||
? flattedValuesObject[filterKey]
|
||||
: lodash.get(values, filterKey);
|
||||
|
||||
if (filterValue) {
|
||||
filterAnd.push({
|
||||
[filterKey]: filterValue,
|
||||
});
|
||||
} else {
|
||||
filterAnd.push({
|
||||
[filterKey]: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ const path = require('path');
|
||||
console.log('VERSION: ', packageJson.version);
|
||||
|
||||
function getUmiConfig() {
|
||||
const { APP_PORT, API_BASE_URL, APP_PUBLIC_PATH } = process.env;
|
||||
const { APP_PORT, API_BASE_URL, API_CLIENT_STORAGE_PREFIX, APP_PUBLIC_PATH } = process.env;
|
||||
const API_BASE_PATH = process.env.API_BASE_PATH || '/api/';
|
||||
const PROXY_TARGET_URL = process.env.PROXY_TARGET_URL || `http://127.0.0.1:${APP_PORT}`;
|
||||
const LOCAL_STORAGE_BASE_URL = 'storage/uploads/';
|
||||
@ -40,6 +40,7 @@ function getUmiConfig() {
|
||||
'process.env.APP_PUBLIC_PATH': process.env.APP_PUBLIC_PATH,
|
||||
'process.env.WS_PATH': process.env.WS_PATH,
|
||||
'process.env.API_BASE_URL': API_BASE_URL || API_BASE_PATH,
|
||||
'process.env.API_CLIENT_STORAGE_PREFIX': API_CLIENT_STORAGE_PREFIX,
|
||||
'process.env.APP_ENV': process.env.APP_ENV,
|
||||
'process.env.VERSION': packageJson.version,
|
||||
'process.env.WEBSOCKET_URL': process.env.WEBSOCKET_URL,
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
|
||||
import qs from 'qs';
|
||||
import getSubAppName from './getSubAppName';
|
||||
|
||||
export interface ActionParams {
|
||||
filterByTk?: any;
|
||||
@ -32,13 +31,29 @@ export type IResource = {
|
||||
export class Auth {
|
||||
protected api: APIClient;
|
||||
|
||||
protected KEYS = {
|
||||
locale: 'NOCOBASE_LOCALE',
|
||||
role: 'NOCOBASE_ROLE',
|
||||
token: 'NOCOBASE_TOKEN',
|
||||
authenticator: 'NOCOBASE_AUTH',
|
||||
theme: 'NOCOBASE_THEME',
|
||||
};
|
||||
get storagePrefix() {
|
||||
return this.api.storagePrefix;
|
||||
}
|
||||
|
||||
get KEYS() {
|
||||
const defaults = {
|
||||
locale: this.storagePrefix + 'LOCALE',
|
||||
role: this.storagePrefix + 'ROLE',
|
||||
token: this.storagePrefix + 'TOKEN',
|
||||
authenticator: this.storagePrefix + 'AUTH',
|
||||
theme: this.storagePrefix + 'THEME',
|
||||
};
|
||||
|
||||
if (this.api['app']) {
|
||||
const appName = this.api['app']?.getName?.();
|
||||
if (appName) {
|
||||
defaults['role'] = `${appName.toUpperCase()}_` + defaults['role'];
|
||||
defaults['locale'] = `${appName.toUpperCase()}_` + defaults['locale'];
|
||||
}
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
protected options = {
|
||||
locale: null,
|
||||
@ -49,22 +64,9 @@ export class Auth {
|
||||
|
||||
constructor(api: APIClient) {
|
||||
this.api = api;
|
||||
this.initKeys();
|
||||
this.api.axios.interceptors.request.use(this.middleware.bind(this));
|
||||
}
|
||||
|
||||
initKeys() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const appName = getSubAppName(this.api['app'] ? this.api['app'].getPublicPath() : '/');
|
||||
if (!appName) {
|
||||
return;
|
||||
}
|
||||
this.KEYS['role'] = `${appName.toUpperCase()}_` + this.KEYS['role'];
|
||||
this.KEYS['locale'] = `${appName.toUpperCase()}_` + this.KEYS['locale'];
|
||||
}
|
||||
|
||||
get locale() {
|
||||
return this.getLocale();
|
||||
}
|
||||
@ -266,6 +268,7 @@ export class MemoryStorage extends Storage {
|
||||
interface ExtendedOptions {
|
||||
authClass?: any;
|
||||
storageClass?: any;
|
||||
storagePrefix?: string;
|
||||
}
|
||||
|
||||
export type APIClientOptions = AxiosInstance | (AxiosRequestConfig & ExtendedOptions);
|
||||
@ -274,6 +277,7 @@ export class APIClient {
|
||||
axios: AxiosInstance;
|
||||
auth: Auth;
|
||||
storage: Storage;
|
||||
storagePrefix = 'NOCOBASE_';
|
||||
|
||||
getHeaders() {
|
||||
const headers = {};
|
||||
@ -296,7 +300,8 @@ export class APIClient {
|
||||
if (typeof instance === 'function') {
|
||||
this.axios = instance;
|
||||
} else {
|
||||
const { authClass, storageClass, ...others } = instance || {};
|
||||
const { authClass, storageClass, storagePrefix = 'NOCOBASE_', ...others } = instance || {};
|
||||
this.storagePrefix = storagePrefix;
|
||||
this.axios = axios.create(others);
|
||||
this.initStorage(storageClass);
|
||||
if (authClass) {
|
||||
|
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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 { MockServer } from '@nocobase/test';
|
||||
import { createApp } from '../index';
|
||||
import { Database, MigrationContext } from '@nocobase/database';
|
||||
import Migrator from '../../migrations/20240517101001-fix-target-option-in-tree-collection';
|
||||
|
||||
describe('fix target option in tree collection', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp({});
|
||||
|
||||
db = app.db;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should remove target options', async () => {
|
||||
await app.db.getRepository('collections').create({
|
||||
values: {
|
||||
logging: true,
|
||||
autoGenId: true,
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
updatedAt: true,
|
||||
updatedBy: true,
|
||||
fields: [
|
||||
{
|
||||
interface: 'integer',
|
||||
name: 'parentId',
|
||||
type: 'bigInt',
|
||||
isForeignKey: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: '{{t("Parent ID")}}',
|
||||
'x-component': 'InputNumber',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
target: 'treeTests',
|
||||
},
|
||||
{
|
||||
interface: 'm2o',
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
foreignKey: 'parentId',
|
||||
treeParent: true,
|
||||
onDelete: 'CASCADE',
|
||||
uiSchema: {
|
||||
title: '{{t("Parent")}}',
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } },
|
||||
},
|
||||
target: 'treeTests',
|
||||
},
|
||||
{
|
||||
interface: 'o2m',
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
foreignKey: 'parentId',
|
||||
treeChildren: true,
|
||||
onDelete: 'CASCADE',
|
||||
uiSchema: {
|
||||
title: '{{t("Children")}}',
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } },
|
||||
},
|
||||
target: 'treeTests',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: '{{t("ID")}}',
|
||||
'x-component': 'InputNumber',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
interface: 'integer',
|
||||
target: 'treeTests',
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
interface: 'createdAt',
|
||||
type: 'date',
|
||||
field: 'createdAt',
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
title: '{{t("Created at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'createdBy',
|
||||
interface: 'createdBy',
|
||||
type: 'belongsTo',
|
||||
target: 'users',
|
||||
foreignKey: 'createdById',
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: '{{t("Created by")}}',
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } },
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
field: 'updatedAt',
|
||||
name: 'updatedAt',
|
||||
interface: 'updatedAt',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Last updated at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
target: 'users',
|
||||
foreignKey: 'updatedById',
|
||||
name: 'updatedBy',
|
||||
interface: 'updatedBy',
|
||||
uiSchema: {
|
||||
type: 'object',
|
||||
title: '{{t("Last updated by")}}',
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } },
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'treeTests',
|
||||
template: 'tree',
|
||||
view: false,
|
||||
tree: 'adjacencyList',
|
||||
title: 'treeTests',
|
||||
},
|
||||
});
|
||||
|
||||
const idFieldBeforeMigrate = await app.db.getRepository('fields').findOne({
|
||||
filter: {
|
||||
collectionName: 'treeTests',
|
||||
name: 'id',
|
||||
},
|
||||
});
|
||||
|
||||
expect(idFieldBeforeMigrate.get('target')).toBe('treeTests');
|
||||
|
||||
const migration = new Migrator({ db } as MigrationContext);
|
||||
migration.context.app = app;
|
||||
await migration.up();
|
||||
|
||||
const idField = await app.db.getRepository('fields').findOne({
|
||||
filter: {
|
||||
collectionName: 'treeTests',
|
||||
name: 'id',
|
||||
},
|
||||
});
|
||||
|
||||
expect(idField.get('target')).toBeUndefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { Migration } from '@nocobase/server';
|
||||
|
||||
export default class extends Migration {
|
||||
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
|
||||
appVersion = '<1.0.0-alpha.15';
|
||||
|
||||
async up() {
|
||||
const treeCollections = await this.app.db.getRepository('collections').find({
|
||||
appends: ['fields'],
|
||||
filter: {
|
||||
'options.tree': 'adjacencyList',
|
||||
},
|
||||
});
|
||||
|
||||
for (const treeCollection of treeCollections) {
|
||||
const fields = treeCollection.get('fields');
|
||||
|
||||
for (const field of fields) {
|
||||
if (!['belongsTo', 'hasMany', 'belongsToMany', 'hasOne'].includes(field.get('type')) && field.get('target')) {
|
||||
field.set('target', undefined);
|
||||
await field.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -219,13 +219,14 @@ const CurrentFields = (props) => {
|
||||
const InheritFields = (props) => {
|
||||
const compile = useCompile();
|
||||
const { getInterface } = useCollectionManager_deprecated();
|
||||
const { resource, targetKey } = props.collectionResource || {};
|
||||
const { targetKey } = props.collectionResource || {};
|
||||
const parentRecord = useRecord();
|
||||
const [loadingRecord, setLoadingRecord] = React.useState(null);
|
||||
const { t } = useTranslation();
|
||||
const { refreshCM, isTitleField } = useCollectionManager_deprecated();
|
||||
const { [targetKey]: filterByTk, titleField, name } = parentRecord;
|
||||
const ctx = useContext(CollectionListContext);
|
||||
const api = useAPIClient();
|
||||
|
||||
const columns: TableColumnProps<any>[] = [
|
||||
{
|
||||
@ -246,20 +247,19 @@ const InheritFields = (props) => {
|
||||
dataIndex: 'titleField',
|
||||
title: t('Title field'),
|
||||
render(_, record) {
|
||||
const handleChange = (checked) => {
|
||||
const handleChange = async (checked) => {
|
||||
setLoadingRecord(record);
|
||||
resource
|
||||
.update({ filterByTk, values: { titleField: checked ? record.name : 'id' } })
|
||||
.then(async () => {
|
||||
await props.refreshAsync();
|
||||
setLoadingRecord(null);
|
||||
refreshCM();
|
||||
ctx?.refresh?.();
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoadingRecord(null);
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
await api.request({
|
||||
url: `collections:update?filterByTk=${filterByTk}`,
|
||||
method: 'post',
|
||||
data: { titleField: checked ? record.name : 'id' },
|
||||
});
|
||||
message.success(t('Saved successfully'));
|
||||
await props.refreshAsync();
|
||||
setLoadingRecord(null);
|
||||
refreshCM();
|
||||
ctx?.refresh?.();
|
||||
};
|
||||
|
||||
return isTitleField(record) ? (
|
||||
|
@ -90,6 +90,9 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
||||
if (!options || !Array.isArray(options)) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => parseEnum(field, v));
|
||||
}
|
||||
const option = options.find((option) => option.value === value);
|
||||
return Schema.compile(option?.label || value, scope);
|
||||
};
|
||||
@ -104,6 +107,7 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
||||
switch (field.interface) {
|
||||
case 'select':
|
||||
case 'radioGroup':
|
||||
case 'multipleSelect':
|
||||
processed[key] = parseEnum(field, value);
|
||||
break;
|
||||
default:
|
||||
|
Loading…
Reference in New Issue
Block a user