mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 06:46:38 +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. -->
|
<!-- 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:
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
<!-- [e.g. MacOS, Windows] -->
|
||||||
|
- OS:
|
||||||
|
|
||||||
1. Go to '...'
|
<!-- Docker, Create-nocobase-app, Git source code -->
|
||||||
2. Click on '....'
|
- Deployment Methods:
|
||||||
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
|
## Expected behavior
|
||||||
|
|
||||||
@ -32,18 +47,6 @@ Steps to reproduce the behavior:
|
|||||||
|
|
||||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||||
|
|
||||||
## Desktop (please complete the following information)
|
## Logs
|
||||||
|
|
||||||
- OS: [e.g. iOS]
|
<!-- If it's an API error, please provide the relevant server logs. -->
|
||||||
- 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. -->
|
|
||||||
|
@ -50,4 +50,49 @@ describe('update or create', () => {
|
|||||||
expect(post).not.toBeNull();
|
expect(post).not.toBeNull();
|
||||||
expect(post['title']).toEqual('t1');
|
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['__webpack_public_path__'] = '{{env.APP_PUBLIC_PATH}}';
|
||||||
window['__nocobase_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_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_url__'] = '{{env.WS_URL}}';
|
||||||
window['__nocobase_ws_path__'] = '{{env.WS_PATH}}';
|
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 { Application } from '@nocobase/client';
|
||||||
import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client';
|
import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client';
|
||||||
import devDynamicImport from '../.plugins/index';
|
import devDynamicImport from '../.plugins/index';
|
||||||
@ -5,6 +14,10 @@ import devDynamicImport from '../.plugins/index';
|
|||||||
export const app = new Application({
|
export const app = new Application({
|
||||||
apiClient: {
|
apiClient: {
|
||||||
// @ts-ignore
|
// @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/',
|
baseURL: window['__nocobase_api_base_url__'] || process.env.API_BASE_URL || '/api/',
|
||||||
},
|
},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -286,6 +286,7 @@ function buildIndexHtml(force = false) {
|
|||||||
const data = fs.readFileSync(tpl, 'utf-8');
|
const data = fs.readFileSync(tpl, 'utf-8');
|
||||||
const replacedData = data
|
const replacedData = data
|
||||||
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
|
.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.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_URL\}\}/g, process.env.WEBSOCKET_URL || '')
|
||||||
.replace(/\{\{env.WS_PATH\}\}/g, process.env.WS_PATH)
|
.replace(/\{\{env.WS_PATH\}\}/g, process.env.WS_PATH)
|
||||||
@ -301,6 +302,7 @@ exports.initEnv = function initEnv() {
|
|||||||
APP_KEY: 'test-jwt-secret',
|
APP_KEY: 'test-jwt-secret',
|
||||||
APP_PORT: 13000,
|
APP_PORT: 13000,
|
||||||
API_BASE_PATH: '/api/',
|
API_BASE_PATH: '/api/',
|
||||||
|
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
|
||||||
DB_DIALECT: 'sqlite',
|
DB_DIALECT: 'sqlite',
|
||||||
DB_STORAGE: 'storage/db/nocobase.sqlite',
|
DB_STORAGE: 'storage/db/nocobase.sqlite',
|
||||||
DB_TIMEZONE: '+00:00',
|
DB_TIMEZONE: '+00:00',
|
||||||
|
@ -134,6 +134,9 @@ export class Application {
|
|||||||
this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this);
|
this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this);
|
||||||
this.addRoutes();
|
this.addRoutes();
|
||||||
this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
|
this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
|
||||||
|
this.i18n.on('languageChanged', (lng) => {
|
||||||
|
this.apiClient.auth.locale = lng;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private initRequireJs() {
|
private initRequireJs() {
|
||||||
|
@ -30,7 +30,7 @@ i18n
|
|||||||
// .use(Backend)
|
// .use(Backend)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
lng: localStorage.getItem('NOCOBASE_LOCALE') || 'en-US',
|
lng: 'en-US',
|
||||||
// debug: true,
|
// debug: true,
|
||||||
defaultNS: 'client',
|
defaultNS: 'client',
|
||||||
// fallbackNS: 'client',
|
// fallbackNS: 'client',
|
||||||
@ -47,7 +47,3 @@ i18n
|
|||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
nsSeparator: 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'));
|
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
|
// fix https://nocobase.height.app/T-2803
|
||||||
test('$anyOf with string', async () => {
|
test('$anyOf with string', async () => {
|
||||||
const filter3 = await Test.repository.find({
|
const filter3 = await Test.repository.find({
|
||||||
|
@ -191,8 +191,11 @@ describe('create', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(u1.name).toEqual('u1');
|
expect(u1.name).toEqual('u1');
|
||||||
|
|
||||||
const group = await u1.get('group');
|
const group = await u1.get('group');
|
||||||
|
|
||||||
expect(group.name).toEqual('g1');
|
expect(group.name).toEqual('g1');
|
||||||
|
|
||||||
const u2 = await User.repository.firstOrCreate({
|
const u2 = await User.repository.firstOrCreate({
|
||||||
|
@ -33,15 +33,18 @@ const getFieldName = (ctx) => {
|
|||||||
|
|
||||||
const model = getModelFromAssociationPath();
|
const model = getModelFromAssociationPath();
|
||||||
|
|
||||||
|
let columnPrefix = model.name;
|
||||||
|
|
||||||
if (model.rawAttributes[fieldName]) {
|
if (model.rawAttributes[fieldName]) {
|
||||||
columnName = model.rawAttributes[fieldName].field || fieldName;
|
columnName = model.rawAttributes[fieldName].field || fieldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (associationPath.length > 0) {
|
if (associationPath.length > 0) {
|
||||||
const association = associationPath.join('->');
|
const association = associationPath.join('->');
|
||||||
columnName = `${association}.${columnName}`;
|
columnPrefix = association;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
columnName = `${columnPrefix}.${columnName}`;
|
||||||
return columnName;
|
return columnName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -242,11 +242,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
this.model = collection.model;
|
this.model = collection.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static valuesToFilter(values: Values, filterKeys: Array<string>) {
|
public static valuesToFilter(values: Values = {}, filterKeys: Array<string>) {
|
||||||
const filterAnd = [];
|
const removeArrayIndexInKey = (key) => {
|
||||||
const flattedValues = flatten(values);
|
|
||||||
|
|
||||||
const keyWithOutArrayIndex = (key) => {
|
|
||||||
const chunks = key.split('.');
|
const chunks = key.split('.');
|
||||||
return chunks
|
return chunks
|
||||||
.filter((chunk) => {
|
.filter((chunk) => {
|
||||||
@ -255,29 +252,36 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
.join('.');
|
.join('.');
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const filterKey of filterKeys) {
|
const filterAnd = [];
|
||||||
let filterValue;
|
const flattedValues = flatten(values);
|
||||||
|
const flattedValuesObject = {};
|
||||||
|
|
||||||
for (const flattedKey of Object.keys(flattedValues)) {
|
for (const key in flattedValues) {
|
||||||
const flattedKeyWithoutIndex = keyWithOutArrayIndex(flattedKey);
|
const keyWithoutArrayIndex = removeArrayIndexInKey(key);
|
||||||
|
if (flattedValuesObject[keyWithoutArrayIndex]) {
|
||||||
if (flattedKeyWithoutIndex === filterKey) {
|
if (!Array.isArray(flattedValuesObject[keyWithoutArrayIndex])) {
|
||||||
if (filterValue) {
|
flattedValuesObject[keyWithoutArrayIndex] = [flattedValuesObject[keyWithoutArrayIndex]];
|
||||||
if (Array.isArray(filterValue)) {
|
|
||||||
filterValue.push(flattedValues[flattedKey]);
|
|
||||||
} else {
|
|
||||||
filterValue = [filterValue, flattedValues[flattedKey]];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filterValue = flattedValues[flattedKey];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (filterValue) {
|
||||||
filterAnd.push({
|
filterAnd.push({
|
||||||
[filterKey]: filterValue,
|
[filterKey]: filterValue,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
filterAnd.push({
|
||||||
|
[filterKey]: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const path = require('path');
|
|||||||
console.log('VERSION: ', packageJson.version);
|
console.log('VERSION: ', packageJson.version);
|
||||||
|
|
||||||
function getUmiConfig() {
|
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 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 PROXY_TARGET_URL = process.env.PROXY_TARGET_URL || `http://127.0.0.1:${APP_PORT}`;
|
||||||
const LOCAL_STORAGE_BASE_URL = 'storage/uploads/';
|
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.APP_PUBLIC_PATH': process.env.APP_PUBLIC_PATH,
|
||||||
'process.env.WS_PATH': process.env.WS_PATH,
|
'process.env.WS_PATH': process.env.WS_PATH,
|
||||||
'process.env.API_BASE_URL': API_BASE_URL || API_BASE_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.APP_ENV': process.env.APP_ENV,
|
||||||
'process.env.VERSION': packageJson.version,
|
'process.env.VERSION': packageJson.version,
|
||||||
'process.env.WEBSOCKET_URL': process.env.WEBSOCKET_URL,
|
'process.env.WEBSOCKET_URL': process.env.WEBSOCKET_URL,
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import getSubAppName from './getSubAppName';
|
|
||||||
|
|
||||||
export interface ActionParams {
|
export interface ActionParams {
|
||||||
filterByTk?: any;
|
filterByTk?: any;
|
||||||
@ -32,13 +31,29 @@ export type IResource = {
|
|||||||
export class Auth {
|
export class Auth {
|
||||||
protected api: APIClient;
|
protected api: APIClient;
|
||||||
|
|
||||||
protected KEYS = {
|
get storagePrefix() {
|
||||||
locale: 'NOCOBASE_LOCALE',
|
return this.api.storagePrefix;
|
||||||
role: 'NOCOBASE_ROLE',
|
}
|
||||||
token: 'NOCOBASE_TOKEN',
|
|
||||||
authenticator: 'NOCOBASE_AUTH',
|
get KEYS() {
|
||||||
theme: 'NOCOBASE_THEME',
|
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 = {
|
protected options = {
|
||||||
locale: null,
|
locale: null,
|
||||||
@ -49,22 +64,9 @@ export class Auth {
|
|||||||
|
|
||||||
constructor(api: APIClient) {
|
constructor(api: APIClient) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.initKeys();
|
|
||||||
this.api.axios.interceptors.request.use(this.middleware.bind(this));
|
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() {
|
get locale() {
|
||||||
return this.getLocale();
|
return this.getLocale();
|
||||||
}
|
}
|
||||||
@ -266,6 +268,7 @@ export class MemoryStorage extends Storage {
|
|||||||
interface ExtendedOptions {
|
interface ExtendedOptions {
|
||||||
authClass?: any;
|
authClass?: any;
|
||||||
storageClass?: any;
|
storageClass?: any;
|
||||||
|
storagePrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type APIClientOptions = AxiosInstance | (AxiosRequestConfig & ExtendedOptions);
|
export type APIClientOptions = AxiosInstance | (AxiosRequestConfig & ExtendedOptions);
|
||||||
@ -274,6 +277,7 @@ export class APIClient {
|
|||||||
axios: AxiosInstance;
|
axios: AxiosInstance;
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
|
storagePrefix = 'NOCOBASE_';
|
||||||
|
|
||||||
getHeaders() {
|
getHeaders() {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@ -296,7 +300,8 @@ export class APIClient {
|
|||||||
if (typeof instance === 'function') {
|
if (typeof instance === 'function') {
|
||||||
this.axios = instance;
|
this.axios = instance;
|
||||||
} else {
|
} else {
|
||||||
const { authClass, storageClass, ...others } = instance || {};
|
const { authClass, storageClass, storagePrefix = 'NOCOBASE_', ...others } = instance || {};
|
||||||
|
this.storagePrefix = storagePrefix;
|
||||||
this.axios = axios.create(others);
|
this.axios = axios.create(others);
|
||||||
this.initStorage(storageClass);
|
this.initStorage(storageClass);
|
||||||
if (authClass) {
|
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 InheritFields = (props) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getInterface } = useCollectionManager_deprecated();
|
const { getInterface } = useCollectionManager_deprecated();
|
||||||
const { resource, targetKey } = props.collectionResource || {};
|
const { targetKey } = props.collectionResource || {};
|
||||||
const parentRecord = useRecord();
|
const parentRecord = useRecord();
|
||||||
const [loadingRecord, setLoadingRecord] = React.useState(null);
|
const [loadingRecord, setLoadingRecord] = React.useState(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { refreshCM, isTitleField } = useCollectionManager_deprecated();
|
const { refreshCM, isTitleField } = useCollectionManager_deprecated();
|
||||||
const { [targetKey]: filterByTk, titleField, name } = parentRecord;
|
const { [targetKey]: filterByTk, titleField, name } = parentRecord;
|
||||||
const ctx = useContext(CollectionListContext);
|
const ctx = useContext(CollectionListContext);
|
||||||
|
const api = useAPIClient();
|
||||||
|
|
||||||
const columns: TableColumnProps<any>[] = [
|
const columns: TableColumnProps<any>[] = [
|
||||||
{
|
{
|
||||||
@ -246,20 +247,19 @@ const InheritFields = (props) => {
|
|||||||
dataIndex: 'titleField',
|
dataIndex: 'titleField',
|
||||||
title: t('Title field'),
|
title: t('Title field'),
|
||||||
render(_, record) {
|
render(_, record) {
|
||||||
const handleChange = (checked) => {
|
const handleChange = async (checked) => {
|
||||||
setLoadingRecord(record);
|
setLoadingRecord(record);
|
||||||
resource
|
|
||||||
.update({ filterByTk, values: { titleField: checked ? record.name : 'id' } })
|
await api.request({
|
||||||
.then(async () => {
|
url: `collections:update?filterByTk=${filterByTk}`,
|
||||||
await props.refreshAsync();
|
method: 'post',
|
||||||
setLoadingRecord(null);
|
data: { titleField: checked ? record.name : 'id' },
|
||||||
refreshCM();
|
});
|
||||||
ctx?.refresh?.();
|
message.success(t('Saved successfully'));
|
||||||
})
|
await props.refreshAsync();
|
||||||
.catch((err) => {
|
setLoadingRecord(null);
|
||||||
setLoadingRecord(null);
|
refreshCM();
|
||||||
console.error(err);
|
ctx?.refresh?.();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return isTitleField(record) ? (
|
return isTitleField(record) ? (
|
||||||
|
@ -90,6 +90,9 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
|||||||
if (!options || !Array.isArray(options)) {
|
if (!options || !Array.isArray(options)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((v) => parseEnum(field, v));
|
||||||
|
}
|
||||||
const option = options.find((option) => option.value === value);
|
const option = options.find((option) => option.value === value);
|
||||||
return Schema.compile(option?.label || value, scope);
|
return Schema.compile(option?.label || value, scope);
|
||||||
};
|
};
|
||||||
@ -104,6 +107,7 @@ export const processData = (selectedFields: FieldOption[], data: any[], scope: a
|
|||||||
switch (field.interface) {
|
switch (field.interface) {
|
||||||
case 'select':
|
case 'select':
|
||||||
case 'radioGroup':
|
case 'radioGroup':
|
||||||
|
case 'multipleSelect':
|
||||||
processed[key] = parseEnum(field, value);
|
processed[key] = parseEnum(field, value);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
Loading…
Reference in New Issue
Block a user