mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
cae5d68211
@ -2,9 +2,7 @@
|
|||||||
"version": "1.5.0-beta.33",
|
"version": "1.5.0-beta.33",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": [
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
"--ignore-engines"
|
|
||||||
],
|
|
||||||
"command": {
|
"command": {
|
||||||
"version": {
|
"version": {
|
||||||
"forcePublish": true,
|
"forcePublish": true,
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
||||||
import { DatePickerProvider, ActionBarProvider } from '../schema-component';
|
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
|
||||||
import { DefaultValueProvider } from '../schema-settings';
|
import { DefaultValueProvider } from '../schema-settings';
|
||||||
import { CollectOperators } from './CollectOperators';
|
import { CollectOperators } from './CollectOperators';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
|
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
||||||
|
|
||||||
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||||
const filedSchema = useFieldSchema();
|
const filedSchema = useFieldSchema();
|
||||||
@ -21,6 +22,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
|||||||
const deprecatedOperators = filedSchema['x-filter-operators'] || {};
|
const deprecatedOperators = filedSchema['x-filter-operators'] || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SchemaComponentOptions components={{ CollectionField: FilterCollectionField }}>
|
||||||
<CollectOperators defaultOperators={deprecatedOperators}>
|
<CollectOperators defaultOperators={deprecatedOperators}>
|
||||||
<DatePickerProvider value={{ utc: false }}>
|
<DatePickerProvider value={{ utc: false }}>
|
||||||
<ActionBarProvider
|
<ActionBarProvider
|
||||||
@ -38,5 +40,6 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
|||||||
</ActionBarProvider>
|
</ActionBarProvider>
|
||||||
</DatePickerProvider>
|
</DatePickerProvider>
|
||||||
</CollectOperators>
|
</CollectOperators>
|
||||||
|
</SchemaComponentOptions>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -164,8 +164,49 @@ export const time = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const boolean = [
|
export const boolean = [
|
||||||
{ label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true },
|
{
|
||||||
{ label: '{{t("No")}}', value: '$isFalsy', noValue: true },
|
label: '{{t("Yes")}}',
|
||||||
|
value: '$isTruly',
|
||||||
|
selected: true,
|
||||||
|
noValue: true,
|
||||||
|
schema: {
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '{{t("Yes")}}',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("No")}}',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("No")}}',
|
||||||
|
value: '$isFalsy',
|
||||||
|
noValue: true,
|
||||||
|
schema: {
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '{{t("Yes")}}',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("No")}}',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const tableoid = [
|
export const tableoid = [
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Field } from '@formily/core';
|
||||||
|
import { connect, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { untracked } from '@formily/reactive';
|
||||||
|
import { merge } from '@formily/shared';
|
||||||
|
import { concat } from 'lodash';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||||
|
import { useDynamicComponentProps } from '../../../hoc/withDynamicSchemaProps';
|
||||||
|
import { ErrorFallback, useCompile, useComponent } from '../../../schema-component';
|
||||||
|
import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
|
import { useCollectionManager_deprecated } from '../../../';
|
||||||
|
import {
|
||||||
|
CollectionFieldProvider,
|
||||||
|
useCollectionField,
|
||||||
|
} from '../../../data-source/collection-field/CollectionFieldProvider';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
component: any;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFieldProps = (field: Field, key: string, value: any) => {
|
||||||
|
untracked(() => {
|
||||||
|
if (field[key] === undefined) {
|
||||||
|
field[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => {
|
||||||
|
if (typeof fieldSchema['required'] === 'undefined') {
|
||||||
|
field.required = !!uiSchema['required'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: 初步适配
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
|
||||||
|
const compile = useCompile();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { getInterface } = useCollectionManager_deprecated();
|
||||||
|
const { uiSchema: uiSchemaOrigin, defaultValue, interface: collectionInterface } = useCollectionField();
|
||||||
|
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
|
||||||
|
const targetInterface = getInterface(collectionInterface);
|
||||||
|
const operator = targetInterface?.filterable?.operators?.find(
|
||||||
|
(v, index) => v.value === fieldSchema['x-filter-operator'] || index === 0,
|
||||||
|
);
|
||||||
|
const Component = useComponent(
|
||||||
|
operator?.schema?.['x-component'] ||
|
||||||
|
fieldSchema['x-component-props']?.['component'] ||
|
||||||
|
uiSchemaOrigin?.['x-component'] ||
|
||||||
|
'Input',
|
||||||
|
);
|
||||||
|
const ctx = useFormBlockContext();
|
||||||
|
const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props);
|
||||||
|
// TODO: 初步适配
|
||||||
|
useEffect(() => {
|
||||||
|
if (!uiSchemaOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uiSchema = compile(uiSchemaOrigin);
|
||||||
|
setFieldProps(field, 'content', uiSchema['x-content']);
|
||||||
|
setFieldProps(field, 'title', uiSchema.title);
|
||||||
|
setFieldProps(field, 'description', uiSchema.description);
|
||||||
|
if (ctx?.form) {
|
||||||
|
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
|
||||||
|
defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
|
||||||
|
const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []);
|
||||||
|
field.validator = concatSchema;
|
||||||
|
}
|
||||||
|
if (fieldSchema['x-disabled'] === true) {
|
||||||
|
field.disabled = true;
|
||||||
|
}
|
||||||
|
if (fieldSchema['x-read-pretty'] === true) {
|
||||||
|
field.readPretty = true;
|
||||||
|
}
|
||||||
|
setRequired(field, fieldSchema, uiSchema);
|
||||||
|
// @ts-ignore
|
||||||
|
field.dataSource = uiSchema.enum;
|
||||||
|
const originalProps =
|
||||||
|
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
|
||||||
|
|
||||||
|
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
|
||||||
|
}, [uiSchemaOrigin]);
|
||||||
|
|
||||||
|
if (!uiSchemaOrigin) return null;
|
||||||
|
|
||||||
|
return <Component {...props} {...dynamicProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterCollectionField = connect((props) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField<Field>();
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
|
||||||
|
<CollectionFieldProvider name={fieldSchema.name}>
|
||||||
|
<FilterCollectionFieldInternalField {...props} />
|
||||||
|
</CollectionFieldProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FilterCollectionField.displayName = 'FilterCollectionField';
|
@ -443,7 +443,13 @@ export const filterSelectComponentFieldSettings = new SchemaSettings({
|
|||||||
return isSelectFieldMode && !isFieldReadPretty;
|
return isSelectFieldMode && !isFieldReadPretty;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getAllowMultiple({ title: 'Allow multiple selection' }),
|
{
|
||||||
|
...getAllowMultiple({ title: 'Allow multiple selection' }),
|
||||||
|
useVisible() {
|
||||||
|
const field = useField();
|
||||||
|
return field.componentProps.multiple !== false;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...titleField,
|
...titleField,
|
||||||
useVisible: useIsAssociationField,
|
useVisible: useIsAssociationField,
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useForm } from '@formily/react';
|
||||||
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||||
import { Collection } from '../../../data-source/collection/Collection';
|
import { Collection } from '../../../data-source/collection/Collection';
|
||||||
import { useCollection } from '../../../data-source/collection/CollectionProvider';
|
import { useCollection } from '../../../data-source/collection/CollectionProvider';
|
||||||
|
@ -13,17 +13,18 @@ import { useActionContext } from '.';
|
|||||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
|
import { ActionDrawer } from './Action.Drawer';
|
||||||
|
|
||||||
const PopupLevelContext = React.createContext(0);
|
const PopupLevelContext = React.createContext(0);
|
||||||
|
|
||||||
export const ActionContainer: ComposedActionDrawer = observer(
|
export const ActionContainer: ComposedActionDrawer = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext();
|
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {};
|
||||||
const { openMode = defaultOpenMode } = useActionContext();
|
const { openMode = defaultOpenMode } = useActionContext();
|
||||||
const popupLevel = React.useContext(PopupLevelContext);
|
const popupLevel = React.useContext(PopupLevelContext);
|
||||||
const currentLevel = popupLevel + 1;
|
const currentLevel = popupLevel + 1;
|
||||||
|
|
||||||
const Component = getComponentByOpenMode(openMode);
|
const Component = getComponentByOpenMode(openMode) || ActionDrawer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupLevelContext.Provider value={currentLevel}>
|
<PopupLevelContext.Provider value={currentLevel}>
|
||||||
|
@ -28,6 +28,7 @@ import { isVariable } from '../../../variables/utils/isVariable';
|
|||||||
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
||||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||||
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||||
|
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
|
|
||||||
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||||
addMode?: 'quickAdd' | 'modalAdd';
|
addMode?: 'quickAdd' | 'modalAdd';
|
||||||
@ -160,6 +161,7 @@ const InternalAssociationSelect = observer(
|
|||||||
{addMode === 'modalAdd' && (
|
{addMode === 'modalAdd' && (
|
||||||
<SchemaComponentContext.Provider value={{ ...schemaComponentCtxValue, draggable: false }}>
|
<SchemaComponentContext.Provider value={{ ...schemaComponentCtxValue, draggable: false }}>
|
||||||
<RecordProvider isNew={true} record={null} parent={recordData}>
|
<RecordProvider isNew={true} record={null} parent={recordData}>
|
||||||
|
<VariablePopupRecordProvider>
|
||||||
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
|
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
|
||||||
<ClearCollectionFieldContext>
|
<ClearCollectionFieldContext>
|
||||||
<NocoBaseRecursionField
|
<NocoBaseRecursionField
|
||||||
@ -171,6 +173,7 @@ const InternalAssociationSelect = observer(
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ClearCollectionFieldContext>
|
</ClearCollectionFieldContext>
|
||||||
|
</VariablePopupRecordProvider>
|
||||||
</RecordProvider>
|
</RecordProvider>
|
||||||
</SchemaComponentContext.Provider>
|
</SchemaComponentContext.Provider>
|
||||||
)}
|
)}
|
||||||
|
@ -17,6 +17,7 @@ import React from 'react';
|
|||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
||||||
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
||||||
|
import { useCompile } from '../../';
|
||||||
|
|
||||||
export type SelectProps<
|
export type SelectProps<
|
||||||
ValueType = any,
|
ValueType = any,
|
||||||
@ -120,6 +121,7 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
|
|||||||
const InternalSelect = connect(
|
const InternalSelect = connect(
|
||||||
(props: SelectProps) => {
|
(props: SelectProps) => {
|
||||||
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
|
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
|
||||||
|
const compile = useCompile();
|
||||||
let mode: any = props.multiple ? 'multiple' : props.mode;
|
let mode: any = props.multiple ? 'multiple' : props.mode;
|
||||||
if (mode && !['multiple', 'tags'].includes(mode)) {
|
if (mode && !['multiple', 'tags'].includes(mode)) {
|
||||||
mode = undefined;
|
mode = undefined;
|
||||||
@ -172,7 +174,7 @@ const InternalSelect = connect(
|
|||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
{...others}
|
{...compile(others)}
|
||||||
onChange={(changed) => {
|
onChange={(changed) => {
|
||||||
props.onChange?.(changed === undefined ? null : changed);
|
props.onChange?.(changed === undefined ? null : changed);
|
||||||
}}
|
}}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { Schema } from '@formily/json-schema';
|
import { Schema } from '@formily/json-schema';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
||||||
import { useCollection } from '../../../data-source';
|
import { useCollection, useCollectionField } from '../../../data-source';
|
||||||
import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider';
|
import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||||
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
|
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
|
||||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||||
@ -59,12 +59,14 @@ export const useCurrentParentRecordContext = () => {
|
|||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const { isInSubForm, isInSubTable } = useFlag() || {};
|
const { isInSubForm, isInSubTable } = useFlag() || {};
|
||||||
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
|
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
|
||||||
|
const associationCollectionField = useCollectionField();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table)是在 DataBlockProvider 之前解析 filter 的,
|
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table)是在 DataBlockProvider 之前解析 filter 的,
|
||||||
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record,所以在后面加了 record?.data 来防止这种情况
|
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record,所以在后面加了 record?.data 来防止这种情况
|
||||||
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
|
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
|
||||||
shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable,
|
shouldDisplayCurrentParentRecord:
|
||||||
|
!!record?.parentRecord?.data && !isInSubForm && !isInSubTable && associationCollectionField,
|
||||||
// 在后面加上 collection?.name 的原因如上面的变量一样
|
// 在后面加上 collection?.name 的原因如上面的变量一样
|
||||||
collectionName: parentCollectionName || collection?.name,
|
collectionName: parentCollectionName || collection?.name,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -7,14 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { omit } from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Transactionable } from 'sequelize/types';
|
import { Transactionable } from 'sequelize/types';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
|
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
|
||||||
import { FindOptions } from '../repository';
|
import { FindOptions } from '../repository';
|
||||||
import { MultipleRelationRepository } from './multiple-relation-repository';
|
import { MultipleRelationRepository } from '../relation-repository/multiple-relation-repository';
|
||||||
import Database from '../database';
|
import Database from '../database';
|
||||||
import { Model } from '../model';
|
import { Model } from '../model';
|
||||||
|
import { UpdateAssociationOptions } from '../update-associations';
|
||||||
|
|
||||||
const transaction = transactionWrapperBuilder(function () {
|
const transaction = transactionWrapperBuilder(function () {
|
||||||
return this.collection.model.sequelize.transaction();
|
return this.collection.model.sequelize.transaction();
|
||||||
@ -68,6 +69,21 @@ export class BelongsToArrayAssociation {
|
|||||||
on: this.db.sequelize.literal(`${left}=any(${right})`),
|
on: this.db.sequelize.literal(`${left}=any(${right})`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(instance: Model, value: any, options: UpdateAssociationOptions = {}) {
|
||||||
|
// @ts-ignore
|
||||||
|
await instance.update(
|
||||||
|
{
|
||||||
|
[this.as]: value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
[this.as]: value,
|
||||||
|
},
|
||||||
|
transaction: options?.transaction,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BelongsToArrayRepository extends MultipleRelationRepository {
|
export class BelongsToArrayRepository extends MultipleRelationRepository {
|
||||||
@ -101,7 +117,7 @@ export class BelongsToArrayRepository extends MultipleRelationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findOptions = {
|
const findOptions = {
|
||||||
...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
..._.omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
||||||
filter: {
|
filter: {
|
||||||
$and: [options.filter || {}, addFilter],
|
$and: [options.filter || {}, addFilter],
|
||||||
},
|
},
|
@ -13,7 +13,7 @@ import { ModelStatic } from 'sequelize';
|
|||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import { BelongsToArrayAssociation } from './relation-repository/belongs-to-array-repository';
|
import { BelongsToArrayAssociation } from './belongs-to-array/belongs-to-array-repository';
|
||||||
|
|
||||||
const debug = require('debug')('noco-database');
|
const debug = require('debug')('noco-database');
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export * from './relation-repository/belongs-to-repository';
|
|||||||
export * from './relation-repository/hasmany-repository';
|
export * from './relation-repository/hasmany-repository';
|
||||||
export * from './relation-repository/multiple-relation-repository';
|
export * from './relation-repository/multiple-relation-repository';
|
||||||
export * from './relation-repository/single-relation-repository';
|
export * from './relation-repository/single-relation-repository';
|
||||||
export * from './relation-repository/belongs-to-array-repository';
|
export * from './belongs-to-array/belongs-to-array-repository';
|
||||||
export * from './repository';
|
export * from './repository';
|
||||||
export * from './update-associations';
|
export * from './update-associations';
|
||||||
export { snakeCase } from './utils';
|
export { snakeCase } from './utils';
|
||||||
|
@ -10,7 +10,26 @@
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
$isFalsy() {
|
$isFalsy(value) {
|
||||||
|
if (value === true || value === 'true') {
|
||||||
|
return {
|
||||||
|
[Op.or]: {
|
||||||
|
[Op.is]: null,
|
||||||
|
[Op.eq]: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[Op.eq]: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$isTruly(value) {
|
||||||
|
if (value === true || value === 'true') {
|
||||||
|
return {
|
||||||
|
[Op.eq]: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
[Op.or]: {
|
[Op.or]: {
|
||||||
[Op.is]: null,
|
[Op.is]: null,
|
||||||
@ -18,10 +37,4 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
$isTruly() {
|
|
||||||
return {
|
|
||||||
[Op.eq]: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as Record<string, any>;
|
} as Record<string, any>;
|
||||||
|
@ -13,6 +13,7 @@ import { Model } from '../model';
|
|||||||
import { FindOptions, TargetKey, UpdateOptions } from './types';
|
import { FindOptions, TargetKey, UpdateOptions } from './types';
|
||||||
import { updateModelByValues } from '../update-associations';
|
import { updateModelByValues } from '../update-associations';
|
||||||
import { RelationRepository, transaction } from './relation-repository';
|
import { RelationRepository, transaction } from './relation-repository';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
interface SetOption extends Transactionable {
|
interface SetOption extends Transactionable {
|
||||||
tk?: TargetKey;
|
tk?: TargetKey;
|
||||||
@ -100,7 +101,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await updateModelByValues(target, options?.values, {
|
await updateModelByValues(target, options?.values, {
|
||||||
...options,
|
...lodash.omit(options, 'values'),
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import FilterParser from './filter-parser';
|
|||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import operators from './operators';
|
import operators from './operators';
|
||||||
import { OptionsParser } from './options-parser';
|
import { OptionsParser } from './options-parser';
|
||||||
import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository';
|
import { BelongsToArrayRepository } from './belongs-to-array/belongs-to-array-repository';
|
||||||
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
|
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
|
||||||
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
|
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
|
||||||
import { HasManyRepository } from './relation-repository/hasmany-repository';
|
import { HasManyRepository } from './relation-repository/hasmany-repository';
|
||||||
|
@ -22,18 +22,7 @@ import { Model } from './model';
|
|||||||
import { UpdateGuard } from './update-guard';
|
import { UpdateGuard } from './update-guard';
|
||||||
import { TargetKey } from './repository';
|
import { TargetKey } from './repository';
|
||||||
import Database from './database';
|
import Database from './database';
|
||||||
|
import { getKeysByPrefix, isStringOrNumber, isUndefinedOrNull } from './utils';
|
||||||
function isUndefinedOrNull(value: any) {
|
|
||||||
return typeof value === 'undefined' || value === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStringOrNumber(value: any) {
|
|
||||||
return typeof value === 'string' || typeof value === 'number';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKeysByPrefix(keys: string[], prefix: string) {
|
|
||||||
return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function modelAssociations(instance: Model) {
|
export function modelAssociations(instance: Model) {
|
||||||
return (<typeof Model>instance.constructor).associations;
|
return (<typeof Model>instance.constructor).associations;
|
||||||
@ -51,7 +40,12 @@ export function belongsToManyAssociations(instance: Model): Array<BelongsToMany>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modelAssociationByKey(instance: Model, key: string): Association {
|
export function modelAssociationByKey(
|
||||||
|
instance: Model,
|
||||||
|
key: string,
|
||||||
|
): Association & {
|
||||||
|
update?: (instance: Model, value: any, options: UpdateAssociationOptions) => Promise<any>;
|
||||||
|
} {
|
||||||
return modelAssociations(instance)[key] as Association;
|
return modelAssociations(instance)[key] as Association;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +65,7 @@ interface UpdateOptions extends Transactionable {
|
|||||||
sourceModel?: Model;
|
sourceModel?: Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateAssociationOptions extends Transactionable, Hookable {
|
export interface UpdateAssociationOptions extends Transactionable, Hookable {
|
||||||
updateAssociationValues?: string[];
|
updateAssociationValues?: string[];
|
||||||
sourceModel?: Model;
|
sourceModel?: Model;
|
||||||
context?: any;
|
context?: any;
|
||||||
@ -243,6 +237,10 @@ export async function updateAssociation(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (association.update) {
|
||||||
|
return association.update(instance, value, options);
|
||||||
|
}
|
||||||
|
|
||||||
switch (association.associationType) {
|
switch (association.associationType) {
|
||||||
case 'HasOne':
|
case 'HasOne':
|
||||||
case 'BelongsTo':
|
case 'BelongsTo':
|
||||||
|
@ -116,3 +116,15 @@ export function percent2float(value: string) {
|
|||||||
const v = parseInt('1' + '0'.repeat(repeat));
|
const v = parseInt('1' + '0'.repeat(repeat));
|
||||||
return (parseFloat(value) * v) / (100 * v);
|
return (parseFloat(value) * v) / (100 * v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUndefinedOrNull(value: any) {
|
||||||
|
return typeof value === 'undefined' || value === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStringOrNumber(value: any) {
|
||||||
|
return typeof value === 'string' || typeof value === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeysByPrefix(keys: string[], prefix: string) {
|
||||||
|
return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
|
||||||
|
}
|
||||||
|
@ -12515,7 +12515,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
|
|||||||
'x-use-decorator-props': 'useFormItemProps',
|
'x-use-decorator-props': 'useFormItemProps',
|
||||||
'x-collection-field': 'general.oneToOneBelongsTo',
|
'x-collection-field': 'general.oneToOneBelongsTo',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
multiple: false,
|
multiple: true,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'id',
|
label: 'id',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
@ -12562,7 +12562,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
|
|||||||
'x-use-decorator-props': 'useFormItemProps',
|
'x-use-decorator-props': 'useFormItemProps',
|
||||||
'x-collection-field': 'general.oneToOneHasOne',
|
'x-collection-field': 'general.oneToOneHasOne',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
multiple: false,
|
multiple: true,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'id',
|
label: 'id',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
@ -12656,7 +12656,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
|
|||||||
'x-use-decorator-props': 'useFormItemProps',
|
'x-use-decorator-props': 'useFormItemProps',
|
||||||
'x-collection-field': 'general.manyToOne',
|
'x-collection-field': 'general.manyToOne',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
multiple: false,
|
multiple: true,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'id',
|
label: 'id',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
|
@ -113,7 +113,7 @@ describe('issues', () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('update m2m array field in single realtion collection', async () => {
|
test('update m2m array field in single relation collection, a.b', async () => {
|
||||||
await db.getRepository('collections').create({
|
await db.getRepository('collections').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
@ -216,4 +216,112 @@ describe('issues', () => {
|
|||||||
}
|
}
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('update m2m array field in single relation collection', async () => {
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
type: 'belongsToArray',
|
||||||
|
foreignKey: 'tag_ids',
|
||||||
|
target: 'tags',
|
||||||
|
targetKey: 'id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'projects',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
type: 'belongsTo',
|
||||||
|
foreignKey: 'user_id',
|
||||||
|
target: 'users',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
await db.getRepository('collections').load();
|
||||||
|
await db.sync();
|
||||||
|
await db.getRepository('tags').create({
|
||||||
|
values: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
|
||||||
|
});
|
||||||
|
await db.getRepository('users').create({
|
||||||
|
values: { id: 1, username: 'a' },
|
||||||
|
});
|
||||||
|
let user = await db.getRepository('users').findOne({
|
||||||
|
filterByTk: 1,
|
||||||
|
});
|
||||||
|
expect(user.tag_ids).toEqual(null);
|
||||||
|
await db.getRepository('projects').create({
|
||||||
|
values: { id: 1, title: 'p1', user_id: 1 },
|
||||||
|
});
|
||||||
|
const res = await agent.resource('projects').update({
|
||||||
|
filterByTk: 1,
|
||||||
|
updateAssociationValues: ['users', 'users.tags'],
|
||||||
|
values: {
|
||||||
|
users: {
|
||||||
|
id: 1,
|
||||||
|
tags: [
|
||||||
|
{ id: 1, title: 'a' },
|
||||||
|
{ id: 2, title: 'b' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
user = await db.getRepository('users').findOne({
|
||||||
|
filterByTk: 1,
|
||||||
|
});
|
||||||
|
if (db.sequelize.getDialect() === 'postgres') {
|
||||||
|
expect(user.tag_ids).toMatchObject(['1', '2']);
|
||||||
|
} else {
|
||||||
|
expect(user.tag_ids).toMatchObject([1, 2]);
|
||||||
|
}
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -239,11 +239,7 @@ describe('m2m array api, bigInt targetKey', () => {
|
|||||||
tags: [{ id: 1 }, { id: 3 }],
|
tags: [{ id: 1 }, { id: 3 }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (db.sequelize.getDialect() === 'postgres') {
|
|
||||||
expect(user.tag_ids).toMatchObject(['1', '3']);
|
|
||||||
} else {
|
|
||||||
expect(user.tag_ids).toMatchObject([1, 3]);
|
expect(user.tag_ids).toMatchObject([1, 3]);
|
||||||
}
|
|
||||||
const user2 = await db.getRepository('users').create({
|
const user2 = await db.getRepository('users').create({
|
||||||
values: {
|
values: {
|
||||||
id: 4,
|
id: 4,
|
||||||
@ -251,11 +247,7 @@ describe('m2m array api, bigInt targetKey', () => {
|
|||||||
tags: [1, 3],
|
tags: [1, 3],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (db.sequelize.getDialect() === 'postgres') {
|
|
||||||
expect(user2.tag_ids).toMatchObject(['1', '3']);
|
|
||||||
} else {
|
|
||||||
expect(user2.tag_ids).toMatchObject([1, 3]);
|
expect(user2.tag_ids).toMatchObject([1, 3]);
|
||||||
}
|
|
||||||
const user3 = await db.getRepository('users').create({
|
const user3 = await db.getRepository('users').create({
|
||||||
values: {
|
values: {
|
||||||
id: 5,
|
id: 5,
|
||||||
@ -263,11 +255,7 @@ describe('m2m array api, bigInt targetKey', () => {
|
|||||||
tags: { id: 1 },
|
tags: { id: 1 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (db.sequelize.getDialect() === 'postgres') {
|
|
||||||
expect(user3.tag_ids).toMatchObject(['1']);
|
|
||||||
} else {
|
|
||||||
expect(user3.tag_ids).toMatchObject([1]);
|
expect(user3.tag_ids).toMatchObject([1]);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create target when creating belongsToArray', async () => {
|
it('should create target when creating belongsToArray', async () => {
|
||||||
|
@ -54,17 +54,6 @@ export class BelongsToArrayField extends RelationField {
|
|||||||
model.set(foreignKey, tks);
|
model.set(foreignKey, tks);
|
||||||
};
|
};
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init();
|
|
||||||
const { name, ...opts } = this.options;
|
|
||||||
this.collection.model.associations[name] = new BelongsToArrayAssociation({
|
|
||||||
db: this.database,
|
|
||||||
source: this.collection.model,
|
|
||||||
as: name,
|
|
||||||
...opts,
|
|
||||||
}) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkTargetCollection() {
|
checkTargetCollection() {
|
||||||
const { target } = this.options;
|
const { target } = this.options;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -115,6 +104,13 @@ export class BelongsToArrayField extends RelationField {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.checkAssociationKeys();
|
this.checkAssociationKeys();
|
||||||
|
const { name, ...opts } = this.options;
|
||||||
|
this.collection.model.associations[name] = new BelongsToArrayAssociation({
|
||||||
|
db: this.database,
|
||||||
|
source: this.collection.model,
|
||||||
|
as: name,
|
||||||
|
...opts,
|
||||||
|
}) as any;
|
||||||
this.on('beforeSave', this.setForeignKeyArray);
|
this.on('beforeSave', this.setForeignKeyArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user