mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
cae5d68211
@ -2,9 +2,7 @@
|
||||
"version": "1.5.0-beta.33",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -10,10 +10,11 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
||||
import { DatePickerProvider, ActionBarProvider } from '../schema-component';
|
||||
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
|
||||
import { DefaultValueProvider } from '../schema-settings';
|
||||
import { CollectOperators } from './CollectOperators';
|
||||
import { FormBlockProvider } from './FormBlockProvider';
|
||||
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
||||
|
||||
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const filedSchema = useFieldSchema();
|
||||
@ -21,22 +22,24 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||
const deprecatedOperators = filedSchema['x-filter-operators'] || {};
|
||||
|
||||
return (
|
||||
<CollectOperators defaultOperators={deprecatedOperators}>
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
<ActionBarProvider
|
||||
forceProps={{
|
||||
style: {
|
||||
overflowX: 'auto',
|
||||
maxWidth: '100%',
|
||||
float: 'right',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
||||
</DefaultValueProvider>
|
||||
</ActionBarProvider>
|
||||
</DatePickerProvider>
|
||||
</CollectOperators>
|
||||
<SchemaComponentOptions components={{ CollectionField: FilterCollectionField }}>
|
||||
<CollectOperators defaultOperators={deprecatedOperators}>
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
<ActionBarProvider
|
||||
forceProps={{
|
||||
style: {
|
||||
overflowX: 'auto',
|
||||
maxWidth: '100%',
|
||||
float: 'right',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
||||
</DefaultValueProvider>
|
||||
</ActionBarProvider>
|
||||
</DatePickerProvider>
|
||||
</CollectOperators>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
});
|
||||
|
@ -164,8 +164,49 @@ export const time = [
|
||||
];
|
||||
|
||||
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 = [
|
||||
|
@ -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;
|
||||
},
|
||||
},
|
||||
getAllowMultiple({ title: 'Allow multiple selection' }),
|
||||
{
|
||||
...getAllowMultiple({ title: 'Allow multiple selection' }),
|
||||
useVisible() {
|
||||
const field = useField();
|
||||
return field.componentProps.multiple !== false;
|
||||
},
|
||||
},
|
||||
{
|
||||
...titleField,
|
||||
useVisible: useIsAssociationField,
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||
import { Collection } from '../../../data-source/collection/Collection';
|
||||
import { useCollection } from '../../../data-source/collection/CollectionProvider';
|
||||
|
@ -13,17 +13,18 @@ import { useActionContext } from '.';
|
||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
import { ActionDrawer } from './Action.Drawer';
|
||||
|
||||
const PopupLevelContext = React.createContext(0);
|
||||
|
||||
export const ActionContainer: ComposedActionDrawer = observer(
|
||||
(props: any) => {
|
||||
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext();
|
||||
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {};
|
||||
const { openMode = defaultOpenMode } = useActionContext();
|
||||
const popupLevel = React.useContext(PopupLevelContext);
|
||||
const currentLevel = popupLevel + 1;
|
||||
|
||||
const Component = getComponentByOpenMode(openMode);
|
||||
const Component = getComponentByOpenMode(openMode) || ActionDrawer;
|
||||
|
||||
return (
|
||||
<PopupLevelContext.Provider value={currentLevel}>
|
||||
|
@ -28,6 +28,7 @@ import { isVariable } from '../../../variables/utils/isVariable';
|
||||
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
||||
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||
addMode?: 'quickAdd' | 'modalAdd';
|
||||
@ -160,17 +161,19 @@ const InternalAssociationSelect = observer(
|
||||
{addMode === 'modalAdd' && (
|
||||
<SchemaComponentContext.Provider value={{ ...schemaComponentCtxValue, draggable: false }}>
|
||||
<RecordProvider isNew={true} record={null} parent={recordData}>
|
||||
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
|
||||
<ClearCollectionFieldContext>
|
||||
<NocoBaseRecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'Action';
|
||||
}}
|
||||
/>
|
||||
</ClearCollectionFieldContext>
|
||||
<VariablePopupRecordProvider>
|
||||
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
|
||||
<ClearCollectionFieldContext>
|
||||
<NocoBaseRecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'Action';
|
||||
}}
|
||||
/>
|
||||
</ClearCollectionFieldContext>
|
||||
</VariablePopupRecordProvider>
|
||||
</RecordProvider>
|
||||
</SchemaComponentContext.Provider>
|
||||
)}
|
||||
|
@ -17,6 +17,7 @@ import React from 'react';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
||||
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
||||
import { useCompile } from '../../';
|
||||
|
||||
export type SelectProps<
|
||||
ValueType = any,
|
||||
@ -120,6 +121,7 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
|
||||
const InternalSelect = connect(
|
||||
(props: SelectProps) => {
|
||||
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
|
||||
const compile = useCompile();
|
||||
let mode: any = props.multiple ? 'multiple' : props.mode;
|
||||
if (mode && !['multiple', 'tags'].includes(mode)) {
|
||||
mode = undefined;
|
||||
@ -172,7 +174,7 @@ const InternalSelect = connect(
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
{...others}
|
||||
{...compile(others)}
|
||||
onChange={(changed) => {
|
||||
props.onChange?.(changed === undefined ? null : changed);
|
||||
}}
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { Schema } from '@formily/json-schema';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { useParentCollection } from '../../../data-source/collection/AssociationProvider';
|
||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||
@ -59,12 +59,14 @@ export const useCurrentParentRecordContext = () => {
|
||||
const collection = useCollection();
|
||||
const { isInSubForm, isInSubTable } = useFlag() || {};
|
||||
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
|
||||
const associationCollectionField = useCollectionField();
|
||||
|
||||
return {
|
||||
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table)是在 DataBlockProvider 之前解析 filter 的,
|
||||
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record,所以在后面加了 record?.data 来防止这种情况
|
||||
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
|
||||
shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable,
|
||||
shouldDisplayCurrentParentRecord:
|
||||
!!record?.parentRecord?.data && !isInSubForm && !isInSubTable && associationCollectionField,
|
||||
// 在后面加上 collection?.name 的原因如上面的变量一样
|
||||
collectionName: parentCollectionName || collection?.name,
|
||||
dataSource,
|
||||
|
@ -7,14 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import { Transactionable } from 'sequelize/types';
|
||||
import { Collection } from '../collection';
|
||||
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
|
||||
import { FindOptions } from '../repository';
|
||||
import { MultipleRelationRepository } from './multiple-relation-repository';
|
||||
import { MultipleRelationRepository } from '../relation-repository/multiple-relation-repository';
|
||||
import Database from '../database';
|
||||
import { Model } from '../model';
|
||||
import { UpdateAssociationOptions } from '../update-associations';
|
||||
|
||||
const transaction = transactionWrapperBuilder(function () {
|
||||
return this.collection.model.sequelize.transaction();
|
||||
@ -68,6 +69,21 @@ export class BelongsToArrayAssociation {
|
||||
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 {
|
||||
@ -101,7 +117,7 @@ export class BelongsToArrayRepository extends MultipleRelationRepository {
|
||||
}
|
||||
|
||||
const findOptions = {
|
||||
...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
||||
..._.omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
||||
filter: {
|
||||
$and: [options.filter || {}, addFilter],
|
||||
},
|
@ -13,7 +13,7 @@ import { ModelStatic } from 'sequelize';
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
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');
|
||||
|
||||
|
@ -44,7 +44,7 @@ export * from './relation-repository/belongs-to-repository';
|
||||
export * from './relation-repository/hasmany-repository';
|
||||
export * from './relation-repository/multiple-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 './update-associations';
|
||||
export { snakeCase } from './utils';
|
||||
|
@ -10,7 +10,26 @@
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
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 {
|
||||
[Op.or]: {
|
||||
[Op.is]: null,
|
||||
@ -18,10 +37,4 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
$isTruly() {
|
||||
return {
|
||||
[Op.eq]: true,
|
||||
};
|
||||
},
|
||||
} as Record<string, any>;
|
||||
|
@ -13,6 +13,7 @@ import { Model } from '../model';
|
||||
import { FindOptions, TargetKey, UpdateOptions } from './types';
|
||||
import { updateModelByValues } from '../update-associations';
|
||||
import { RelationRepository, transaction } from './relation-repository';
|
||||
import lodash from 'lodash';
|
||||
|
||||
interface SetOption extends Transactionable {
|
||||
tk?: TargetKey;
|
||||
@ -100,7 +101,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
||||
}
|
||||
|
||||
await updateModelByValues(target, options?.values, {
|
||||
...options,
|
||||
...lodash.omit(options, 'values'),
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
@ -38,7 +38,7 @@ import FilterParser from './filter-parser';
|
||||
import { Model } from './model';
|
||||
import operators from './operators';
|
||||
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 { BelongsToRepository } from './relation-repository/belongs-to-repository';
|
||||
import { HasManyRepository } from './relation-repository/hasmany-repository';
|
||||
|
@ -22,18 +22,7 @@ import { Model } from './model';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { TargetKey } from './repository';
|
||||
import Database from './database';
|
||||
|
||||
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));
|
||||
}
|
||||
import { getKeysByPrefix, isStringOrNumber, isUndefinedOrNull } from './utils';
|
||||
|
||||
export function modelAssociations(instance: Model) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -71,7 +65,7 @@ interface UpdateOptions extends Transactionable {
|
||||
sourceModel?: Model;
|
||||
}
|
||||
|
||||
interface UpdateAssociationOptions extends Transactionable, Hookable {
|
||||
export interface UpdateAssociationOptions extends Transactionable, Hookable {
|
||||
updateAssociationValues?: string[];
|
||||
sourceModel?: Model;
|
||||
context?: any;
|
||||
@ -243,6 +237,10 @@ export async function updateAssociation(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (association.update) {
|
||||
return association.update(instance, value, options);
|
||||
}
|
||||
|
||||
switch (association.associationType) {
|
||||
case 'HasOne':
|
||||
case 'BelongsTo':
|
||||
|
@ -116,3 +116,15 @@ export function percent2float(value: string) {
|
||||
const v = parseInt('1' + '0'.repeat(repeat));
|
||||
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-collection-field': 'general.oneToOneBelongsTo',
|
||||
'x-component-props': {
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
@ -12562,7 +12562,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
|
||||
'x-use-decorator-props': 'useFormItemProps',
|
||||
'x-collection-field': 'general.oneToOneHasOne',
|
||||
'x-component-props': {
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
@ -12656,7 +12656,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
|
||||
'x-use-decorator-props': 'useFormItemProps',
|
||||
'x-collection-field': 'general.manyToOne',
|
||||
'x-component-props': {
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
|
@ -113,7 +113,7 @@ describe('issues', () => {
|
||||
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({
|
||||
values: {
|
||||
name: 'tags',
|
||||
@ -216,4 +216,112 @@ describe('issues', () => {
|
||||
}
|
||||
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 }],
|
||||
},
|
||||
});
|
||||
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({
|
||||
values: {
|
||||
id: 4,
|
||||
@ -251,11 +247,7 @@ describe('m2m array api, bigInt targetKey', () => {
|
||||
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({
|
||||
values: {
|
||||
id: 5,
|
||||
@ -263,11 +255,7 @@ describe('m2m array api, bigInt targetKey', () => {
|
||||
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 () => {
|
||||
|
@ -54,17 +54,6 @@ export class BelongsToArrayField extends RelationField {
|
||||
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() {
|
||||
const { target } = this.options;
|
||||
if (!target) {
|
||||
@ -115,6 +104,13 @@ export class BelongsToArrayField extends RelationField {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user