chore: data source api (#4588)

* refactor: support api datasource

* refactor: support api datasource

* refactor: support api datasource

* feat: url support preview (#4559)

* feat: url support preview

* feat: add settings for Input.Preview

* refactor: refactor CollectionField.tsx to use dynamic component

* Revert "refactor: refactor CollectionField.tsx to use dynamic component"

This reverts commit 37719eb28e5866762459da3b269288340a21b661.

* test: add e2e test

* refactor(e2e): extract template

---------

Co-authored-by: Zeke Zhang <958414905@qq.com>

* fix(map): amap reset (#4574)

* chore: volta node version

* chore: data source api

* refactor: support api datasource

* chore: data source api

* chore: data source api

* chore: field options

* refactor: support api data source

* refactor: support api data source

* refactor: support api data source

* refactor: support api data source

* refactor: support api data source

* refactor: support api data source

* refactor: support api data source

* fix: load data source fields

* refactor: support api data source

* feat(data-vi): support for using url params and current role variables (#4586)

* feat(data-vi): support for using url params and current role variable

* fix: bug

* fix(variable): should remove through collection field (#4590)

* fix: style issues for gridCard in mobile client (#4593)

* fix: style issiues for gridCard in mobile client

* fix: bug

* fix: bug

* fix: bug

* fix: style issues for gridCard in mobile client (#4599)

* fix: style issiues for gridCard in mobile client

* fix: bug

* fix: bug

* fix: bug

* fix: style improve

* chore: update lerna

* chore(versions): 😊 publish v1.0.1-alpha.1

* chore: update changelog

* chore: fix typo (#4589)

* feat(plugin-workflow-smtp-mailer): add new plugin for sending email in workflow (#4584)

* feat(plugin-workflow-smtp-mailer): add new plugin for sending email in workflow

* refactor(plugin-workflow-mailer): change plugin name and locales

* fix(plugin-workflow-mailer): fix parameters

* fix(preset): add new plugin to preset

* fix(plugin-workflow-mailer): fix locale namespace

* fix: iframe block loses height when set to default (#4602)

* fix: iframe block loses height when set to default

* refactor: local improve

* feat(client): allow JSON5 value in Form Input of type JSONTextArea (#4600)

* feat(client): allow json5 value in form-item type json component

* test: fix tests

* chore: set json5 default as false

* chore: add demo

---------

Co-authored-by: xilesun <2013xile@gmail.com>

* fix: remove grid wrap (#4612)

* refactor(plugin-workflow): change variable getter from collection fields (#4567)

* refactor(plugin-workflow): change variable getter from collection fields

* fix(plugin-workflow): fix import

* chore(plugin-workflow-action-trigger): remove unused import

* fix(plugin-workflow): fix collection field in workflow variable

* refactor(plugin-workflow-manual): avoid tslint error

* fix(client): fix text wrap in variable input (#4605)

* fix(client): fix text wrap in variable input

* fix(client): revert css

* feat(tree-block): support filtering child nodes (#4603)

* feat(tree-block): support filtering child nodes

* test: add list test

* test: remove only

* fix: use isValidFilter

* fix(export): export button remaining in loading state after cancel (#4615)

* chore: rebase

* chore: collection option

* fix: update data source fields

* chore: console.log

* refactor: support api data source

* fix: data source test

* fix: sync field in data source

* chore: unavailableActions in collection option

* chore: unavailableActions in data source collections

* chore: file collection unavailableActions

* fix: test

* chore: unavailableActions  in actionInitializers

* fix: bug

* fix: destroy action

* chore: unavailableActions in plugin  actionInitializers

* fix: view

* chore: unavailableActions  in actionInitializers

* fix: missing removeCollection

* chore: test name

* fix: bug

* fix: bug

* chore: test

* refactor: availableTypes

* refactor: availableTypes

* chore: datasource options

* refactor: get current data source

* refactor: code improve

* fix: update collection with fields

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* chore: datasource logger

* chore: export Schema

* refactor: rawTextArea

* refactor: loadFilterTargetKeys in external data source

* chore: unavailableFunctions

* refactor:  support unAvailableFunctions

* refactor:  support unAvailableFunctions

* refactor:  support unAvailableFunctions

* refactor:  support unAvailableFunctions

* refactor:  support unAvailableFunctions

* revert: unavailableFunctions

* refactor: code improve

* fix: test

* chore: operation

* chore: operation

* fix: input support json field

* refactor: blockInitializers support unavailableActions

* chore: availableActions

* chore: availableActions

* refactor: support availableActions

* refactor: support availableActions

* chore: magic model test case

* fix: unixTimestamp support integer

* fix: disassociate

* fix: input readPretty

* fix: resolve error when opening modal via URL

* style: input readPretty style improve

* chore: support simple pagination

* chore: skip test

* refactor: details support pagination

* refactor: details support pagination

* chore: useActionAvailable

* fix: bug

* test: fix test

* fix: detailsBlockInitializer useActionAvailable

* chore: fix unit test

* refactor: pagination

* refactor: pagination

* test: fix test

---------

Co-authored-by: katherinehhh <katherine_15995@163.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: Zeke Zhang <958414905@qq.com>
Co-authored-by: YANG QIA <2013xile@gmail.com>
Co-authored-by: Katherine <shunai.tang@hand-china.com>
Co-authored-by: GitHub Actions Bot <actions@github.com>
Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
Co-authored-by: David Fecke <david.fecke@eyecook.net>
This commit is contained in:
ChengLei Shao 2024-07-19 22:26:27 +08:00 committed by GitHub
parent b1e68dac9c
commit dd08a1f5c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
96 changed files with 1442 additions and 377 deletions

View File

@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({
'x-component': 'Hello',
});
},
}
};
},
}
},
],
});

View File

@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({
'x-component': 'Hello',
});
},
}
};
},
}
},
],
});

View File

@ -21,6 +21,7 @@ import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next';
import { NavigateFunction } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import { css } from '@emotion/css';
import {
AssociationFilter,
useCollection,
@ -1056,13 +1057,13 @@ export const useDetailPrintActionProps = () => {
const printHandler = useReactToPrint({
content: () => formBlockRef.current,
pageStyle: `@media print {
* {
margin: 0;
}
:not(.ant-formily-item-control-content-component) > div.ant-formily-layout>div:first-child {
overflow: hidden; height: 0;
}
}`,
* {
margin: 0;
}
:not(.ant-formily-item-control-content-component) > div.ant-formily-layout>div:first-child {
overflow: hidden; height: 0;
}
}`,
});
return {
async onClick() {
@ -1110,6 +1111,31 @@ export const useRefreshActionProps = () => {
export const useDetailsPaginationProps = () => {
const ctx = useDetailsBlockContext();
const count = ctx.service?.data?.meta?.count || 0;
const current = ctx.service?.data?.meta?.page;
if (!count && current) {
return {
simple: true,
current: ctx.service?.data?.meta?.page || 1,
pageSize: 1,
showSizeChanger: false,
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });
},
style: {
marginTop: 24,
textAlign: 'center',
},
showTotal: false,
showTitle: false,
total: ctx.service?.data?.data?.length ? 1 * current + 1 : 1 * current,
className: css`
.ant-pagination-simple-pager {
display: none !important;
}
`,
};
}
return {
simple: true,
hidden: count <= 1,

View File

@ -14,7 +14,7 @@ import { CollectionManagerProvider } from '../data-source/collection/CollectionM
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
import { useCollectionHistory } from './CollectionHistoryProvider';
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
import { CollectionCategroriesContext } from './context';
import { CollectionCategoriesContext } from './context';
import { CollectionManagerOptions } from './types';
/**
@ -62,16 +62,16 @@ export const RemoteCollectionManagerProvider = (props: any) => {
}
return (
<CollectionCategroriesProvider service={result} refreshCategory={refreshCategory}>
<CollectionCategoriesProvider service={result} refreshCategory={refreshCategory}>
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
</CollectionCategroriesProvider>
</CollectionCategoriesProvider>
);
};
export const CollectionCategroriesProvider = (props) => {
export const CollectionCategoriesProvider = (props) => {
const { service, refreshCategory } = props;
return (
<CollectionCategroriesContext.Provider
<CollectionCategoriesContext.Provider
value={{
data: service?.data?.data,
refresh: refreshCategory,
@ -79,6 +79,6 @@ export const CollectionCategroriesProvider = (props) => {
}}
>
{props.children}
</CollectionCategroriesContext.Provider>
</CollectionCategoriesContext.Provider>
);
};

View File

@ -9,5 +9,5 @@
import { createContext } from 'react';
export const CollectionCategroriesContext = createContext({ data: [], refresh: () => {} });
CollectionCategroriesContext.displayName = 'CollectionCategroriesContext';
export const CollectionCategoriesContext = createContext({ data: [], refresh: () => {} });
CollectionCategoriesContext.displayName = 'CollectionCategoriesContext';

View File

@ -35,3 +35,4 @@ export * from './mixins/InheritanceCollectionMixin';
export * from './sub-table';
export * from './CollectionHistoryProvider';
export * from './utils';
export { UnSupportFields } from './templates/components/UnSupportFields';

View File

@ -26,7 +26,7 @@ export class CheckboxGroupFieldInterface extends CollectionFieldInterface {
'x-component': 'Checkbox.Group',
},
};
availableTypes = ['array'];
availableTypes = ['array', 'json'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -27,7 +27,7 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
},
},
};
availableTypes = ['date', 'dateOnly'];
availableTypes = ['date', 'dateOnly', 'string'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -26,7 +26,7 @@ export class MarkdownFieldInterface extends CollectionFieldInterface {
'x-component': 'Markdown',
},
};
availableTypes = ['text', 'json'];
availableTypes = ['text', 'json', 'string'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -29,7 +29,7 @@ export class MultipleSelectFieldInterface extends CollectionFieldInterface {
enum: [],
},
};
availableTypes = ['array'];
availableTypes = ['array', 'json'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -26,7 +26,7 @@ export class RichTextFieldInterface extends CollectionFieldInterface {
'x-component': 'RichText',
},
};
availableTypes = ['text', 'json'];
availableTypes = ['text', 'json', 'string'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -26,7 +26,7 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
'x-component': 'Input.TextArea',
},
};
availableTypes = ['text', 'json'];
availableTypes = ['text', 'json', 'string'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -27,19 +27,26 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
const { action, params = {}, record, requestService, requestOptions } = options;
const service = useMemo(() => {
return requestService
? requestService
: (customParams) => {
if (record) return Promise.resolve({ data: record });
if (!action) {
throw new Error(
`[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`,
);
}
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
return (
requestService ||
((customParams) => {
if (record) return Promise.resolve({ data: record });
if (!action) {
throw new Error(`[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`);
}
return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data);
};
// fix https://nocobase.height.app/T-4876/description
if (action === 'get' && _.isNil(params.filterByTk)) {
return console.warn(
'[nocobase]: The "filterByTk" parameter is missing in the "DataBlockRequestProvider" component',
);
}
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data);
})
);
}, [resource, action, JSON.stringify(params), JSON.stringify(record), requestService]);
const request = useRequest<T>(service, {

View File

@ -7,8 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useCollection } from '../../../../data-source';
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
@ -32,6 +33,7 @@ const commonOptions = {
type: 'primary',
},
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'delete',
@ -41,6 +43,7 @@ const commonOptions = {
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
},
useVisible: () => useActionAvailable('destroy'),
},
],
},

View File

@ -8,12 +8,8 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager/hooks/useCollection_deprecated';
const useVisibleCollection = () => {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
};
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: '{{t("Configure actions")}}',
@ -33,7 +29,7 @@ const commonOptions = {
type: 'primary',
},
},
useVisible: useVisibleCollection,
useVisible: () => useActionAvailable('update'),
},
{
title: '{{t("Delete")}}',
@ -43,7 +39,7 @@ const commonOptions = {
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
},
useVisible: useVisibleCollection,
useVisible: () => useActionAvailable('destroy'),
},
{
name: 'popup',
@ -64,13 +60,12 @@ const commonOptions = {
'x-component': 'Action',
};
},
useVisible: useVisibleCollection,
useVisible: () => useActionAvailable('update'),
},
{
name: 'customRequest',
title: '{{t("Custom request")}}',
Component: 'CustomRequestInitializer',
useVisible: useVisibleCollection,
},
{
name: 'link',

View File

@ -8,8 +8,8 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: "{{t('Configure actions')}}",
icon: 'SettingOutlined',
@ -36,10 +36,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView;
},
useVisible: () => useActionAvailable('create'),
},
{
name: 'refresh',
@ -61,15 +58,13 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('import'),
},
{
name: 'export',
title: "{{t('Export')}}",
Component: 'ExportActionInitializer',
useVisible: () => useActionAvailable('export'),
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',

View File

@ -22,6 +22,8 @@ import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSetti
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow';
import { useCollection } from '../../../../data-source';
export const gridCardBlockSettings = new SchemaSettings({
name: 'blockSettings:gridCard',
items: [

View File

@ -8,8 +8,8 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
@ -24,6 +24,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible: () => useActionAvailable('get'),
},
{
name: 'edit',
@ -35,10 +36,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'delete',
@ -50,10 +48,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible() {
const collection = useCollection_deprecated();
return collection.template !== 'sql';
},
useVisible: () => useActionAvailable('destroy'),
},
{
name: 'popup',
@ -69,10 +64,7 @@ const commonOptions = {
name: 'update-record',
title: '{{t("Update record")}}',
Component: 'UpdateRecordActionInitializer',
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'customRequest',
@ -81,10 +73,6 @@ const commonOptions = {
schema: {
'x-action': 'customize:table:request',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
},
{
name: 'link',

View File

@ -8,7 +8,8 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: "{{t('Configure actions')}}",
@ -36,14 +37,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
useVisible: () => useActionAvailable('create'),
},
{
name: 'refresh',
@ -65,15 +59,13 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('import'),
},
{
name: 'export',
title: "{{t('Export')}}",
Component: 'ExportActionInitializer',
useVisible: () => useActionAvailable('export'),
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',

View File

@ -20,6 +20,7 @@ import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/Schema
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { useCollection } from '../../../../data-source';
export const listBlockSettings = new SchemaSettings({
name: 'blockSettings:list',

View File

@ -8,7 +8,8 @@
*/
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: '{{t("Configure actions")}}',
@ -24,6 +25,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible: () => useActionAvailable('get'),
},
{
name: 'edit',
@ -35,10 +37,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'delete',
@ -50,10 +49,7 @@ const commonOptions = {
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
useVisible() {
const collection = useCollection_deprecated();
return collection.template !== 'sql';
},
useVisible: () => useActionAvailable('destroy'),
},
{
name: 'popup',
@ -69,10 +65,7 @@ const commonOptions = {
name: 'updateRecord',
title: '{{t("Update record")}}',
Component: 'UpdateRecordActionInitializer',
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'customRequest',
@ -81,10 +74,6 @@ const commonOptions = {
schema: {
'x-action': 'customize:table:request',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
},
{
name: 'link',

View File

@ -23,6 +23,7 @@ import {
import { removeNullCondition, useDesignable } from '../../../../schema-component';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { setDataLoadingModeSettingsItem, useDataLoadingMode } from '../details-multi/setDataLoadingModeSettingsItem';
import { useCollection } from '../../../../data-source';
export const tableSelectorBlockSettings = new SchemaSettings({
name: 'blockSettings:tableSelector',

View File

@ -16,12 +16,12 @@ import { useAPIClient } from '../../../../api-client';
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { SchemaInitializerActionModal } from '../../../../application/schema-initializer/components/SchemaInitializerActionModal';
import { SchemaInitializerItem } from '../../../../application/schema-initializer/components/SchemaInitializerItem';
import { useCollection_deprecated } from '../../../../collection-manager';
import { SelectWithTitle } from '../../../../common/SelectWithTitle';
import { useDataBlockProps } from '../../../../data-source';
import { createDesignable, useDesignable } from '../../../../schema-component';
import { useGetAriaLabelOfDesigner } from '../../../../schema-settings/hooks/useGetAriaLabelOfDesigner';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
export const Resizable = () => {
const { t } = useTranslation();
const { dn } = useDesignable();
@ -161,6 +161,7 @@ const commonOptions = {
'x-action': 'view',
'x-decorator': 'ACLActionProvider',
},
useVisible: () => useActionAvailable('get'),
},
{
type: 'item',
@ -172,10 +173,7 @@ const commonOptions = {
'x-action': 'update',
'x-decorator': 'ACLActionProvider',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
type: 'item',
@ -187,10 +185,7 @@ const commonOptions = {
'x-action': 'destroy',
'x-decorator': 'ACLActionProvider',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('destroy'),
},
{
type: 'item',
@ -200,17 +195,20 @@ const commonOptions = {
schema: {
'x-component': 'Action.Link',
'x-action': 'disassociate',
'x-acl-action': 'destroy',
'x-acl-action': 'update',
'x-decorator': 'ACLActionProvider',
},
useVisible() {
const props = useDataBlockProps();
const collection = useCollection_deprecated();
return (
!!props?.association &&
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'sql'
);
const collection = useCollection() || ({} as any);
const { unavailableActions, availableActions } = collection?.options || {};
if (availableActions) {
return !!props?.association && availableActions.includes?.('update');
}
if (unavailableActions) {
return !!props?.association && !unavailableActions?.includes?.('update');
}
return true;
},
},
{
@ -225,7 +223,7 @@ const commonOptions = {
},
useVisible() {
const fieldSchema = useFieldSchema();
const collection = useCollection_deprecated();
const collection = useCollection();
const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {};
return collection.tree && treeTable;
},
@ -241,10 +239,7 @@ const commonOptions = {
title: '{{t("Update record")}}',
name: 'updateRecord',
Component: 'UpdateRecordActionInitializer',
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'customRequest',
@ -253,10 +248,6 @@ const commonOptions = {
schema: {
'x-action': 'customize:table:request',
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
},
{
name: 'link',

View File

@ -9,8 +9,8 @@
import { useFieldSchema } from '@formily/react';
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCollection_deprecated } from '../../../../collection-manager/hooks/useCollection_deprecated';
import { useCollection } from '../../../../data-source';
import { useActionAvailable } from '../../useActionAvailable';
const commonOptions = {
title: "{{t('Configure actions')}}",
icon: 'SettingOutlined',
@ -27,6 +27,7 @@ const commonOptions = {
'x-align': 'left',
},
},
{
type: 'item',
title: "{{t('Add new')}}",
@ -39,10 +40,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView;
},
useVisible: () => useActionAvailable('create'),
},
{
type: 'item',
@ -53,10 +51,7 @@ const commonOptions = {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
},
useVisible() {
const collection = useCollection_deprecated();
return !['view', 'sql'].includes(collection.template) || collection?.writableView;
},
useVisible: () => useActionAvailable('destroyMany'),
},
{
type: 'item',
@ -90,7 +85,7 @@ const commonOptions = {
},
useVisible() {
const schema = useFieldSchema();
const collection = useCollection_deprecated();
const collection = useCollection();
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
return collection.tree && treeTable;
},

View File

@ -16,6 +16,7 @@ import { findFilterTargets } from '../../../../../block-provider/hooks';
import { useFilterBlock } from '../../../../../filter-provider/FilterProvider';
import { mergeFilter } from '../../../../../filter-provider/utils';
import { removeNullCondition } from '../../../../../schema-component';
import { useCollection } from '../../../../../data-source';
export const useTableBlockProps = () => {
const field = useField<ArrayField>();
@ -25,7 +26,6 @@ export const useTableBlockProps = () => {
const { getDataBlocks } = useFilterBlock();
const isLoading = ctx?.service?.loading;
const params = useMemo(() => ctx?.service?.params, [JSON.stringify(ctx?.service?.params)]);
useEffect(() => {
if (!isLoading) {
const serviceResponse = ctx?.service?.data;

View File

@ -24,6 +24,7 @@ import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettin
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { useCollection } from '../../../../data-source';
export const tableBlockSettings = new SchemaSettings({
name: 'blockSettings:table',

View File

@ -9,3 +9,4 @@
export * from './data-blocks/details-multi';
export * from './data-blocks/details-single';
export * from './useActionAvailable';

View File

@ -0,0 +1,22 @@
/**
* 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 { useCollection } from '../../data-source';
export const useActionAvailable = (actionKey) => {
const collection = useCollection() || ({} as any);
const { unavailableActions, availableActions } = collection?.options || {};
if (availableActions) {
return availableActions?.includes?.(actionKey);
}
if (unavailableActions) {
return !unavailableActions?.includes?.(actionKey);
}
return true;
};

View File

@ -28,6 +28,19 @@ const commonOptions = {
name: 'form',
title: '{{t("Form")}}',
Component: 'FormBlockInitializer',
useComponentProps: () => {
const filterCollections = ({ collection }) => {
const { unavailableActions, availableActions } = collection?.options || {};
if (availableActions) {
return availableActions.includes?.('create');
}
if (unavailableActions) {
return !unavailableActions?.includes?.('create');
}
return true;
};
return { filterCollections };
},
},
{
name: 'details',

View File

@ -10,6 +10,7 @@
import { Empty } from 'antd';
import _ from 'lodash';
import React from 'react';
import { RecursionField, useFieldSchema } from '@formily/react';
import { useDataBlockRequest } from '../../../data-source';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { FormV2 } from '../form-v2';
@ -20,9 +21,14 @@ export type DetailsProps = FormProps;
export const Details = withDynamicSchemaProps(
(props: DetailsProps) => {
const request = useDataBlockRequest();
const schema = useFieldSchema();
if (!request?.loading && _.isEmpty(request?.data?.data)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
<RecursionField schema={schema.properties.pagination} name="pagination" />
</>
);
}
return <FormV2 {...props} />;

View File

@ -64,6 +64,7 @@ const schema: ISchema = {
collection: 'users',
resource: 'users',
action: 'get',
filterByTk: 1,
},
properties: {
form: {

View File

@ -40,6 +40,7 @@ const schema: ISchema = {
collection: 'users',
resource: 'users',
action: 'get',
filterByTk: 1,
},
properties: {
form: {

View File

@ -46,10 +46,12 @@ ReadPretty.Input = (props: InputReadPrettyProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const compile = useCompile();
return (
<div className={cls(prefixCls, props.className)} style={props.style}>
<div className={cls(prefixCls, props.className)} style={{ ...props.style, overflowWrap: 'break-word' }}>
{props.addonBefore}
{props.prefix}
<EllipsisWithTooltip ellipsis={props.ellipsis}>{compile(props.value)}</EllipsisWithTooltip>
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value)}
</EllipsisWithTooltip>
{props.suffix}
{props.addonAfter}
</div>

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { DeleteOutlined, MenuOutlined } from '@ant-design/icons';
import { DeleteOutlined, MenuOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import { TinyColor } from '@ctrl/tinycolor';
import { SortableContext, SortableContextProps, useSortable } from '@dnd-kit/sortable';
import { css } from '@emotion/css';
@ -261,21 +261,42 @@ const TableIndex = (props) => {
const usePaginationProps = (pagination1, pagination2) => {
const { t } = useTranslation();
const field: any = useField();
const pagination = useMemo(
() => ({ ...pagination1, ...pagination2 }),
[JSON.stringify({ ...pagination1, ...pagination2 })],
);
const showTotal = useCallback((total) => t('Total {{count}} items', { count: total }), [t]);
const result = useMemo(
() => ({
showTotal,
showSizeChanger: true,
...pagination,
}),
[pagination, t, showTotal],
const { total: totalCount, current, pageSize } = pagination || {};
const showTotal = useCallback(
(total) => {
return t('Total {{count}} items', { count: total });
},
[t, totalCount],
);
const result = useMemo(() => {
if (totalCount) {
return {
showTotal,
showSizeChanger: true,
...pagination,
};
} else {
return {
showTotal: false,
simple: { readOnly: true },
showTitle: false,
showSizeChanger: true,
hideOnSinglePage: false,
...pagination,
total: field.value?.length < pageSize ? pageSize * current : pageSize * current + 1,
className: css`
.ant-pagination-simple-pager {
display: none !important;
}
`,
};
}
}, [pagination, t, showTotal]);
if (pagination2 === false) {
return false;
@ -499,12 +520,12 @@ export const Table: any = withDynamicSchemaProps(
[rowKey, defaultRowKey],
);
const dataSourceKeys = field?.value?.map(getRowKey);
const dataSourceKeys = field?.value?.map?.(getRowKey);
const memoizedDataSourceKeys = useMemo(() => dataSourceKeys, [JSON.stringify(dataSourceKeys)]);
const dataSource = useMemo(
() => [...(field?.value || [])].filter(Boolean),
[field?.value, field?.value?.length, memoizedDataSourceKeys],
);
const dataSource = useMemo(() => {
const value = Array.isArray(field?.value) ? field.value : [];
return value.filter(Boolean);
}, [field?.value, field?.value?.length, memoizedDataSourceKeys]);
const bodyWrapperComponent = useMemo(() => {
return (props) => {

View File

@ -10,8 +10,6 @@
import React, { useRef, useState } from 'react';
import { css } from '@emotion/css';
import { Button, Input } from 'antd';
import { cloneDeep } from 'lodash';
import { VariableSelect } from './VariableSelect';
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
@ -27,9 +25,9 @@ function setNativeInputValue(input, value) {
export function RawTextArea(props): JSX.Element {
const inputRef = useRef<any>(null);
const { changeOnSelect, component: Component = Input.TextArea, ...others } = props;
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
const [options, setOptions] = useState(scope ? scope : []);
const { changeOnSelect, component: Component = Input.TextArea, fieldNames, scope, ...others } = props;
const dataScope = typeof scope === 'function' ? scope() : scope;
const [options, setOptions] = useState(dataScope ? dataScope : []);
function onInsert(selected) {
if (!inputRef.current) {
@ -77,7 +75,7 @@ export function RawTextArea(props): JSX.Element {
background-color: transparent;
`
}
fieldNames={props.fieldNames}
fieldNames={fieldNames}
options={options}
setOptions={setOptions}
onInsert={onInsert}

View File

@ -20,6 +20,7 @@ export interface ISchemaComponentContext {
setDesignable?: (value: boolean) => void;
SchemaField?: React.FC<ISchemaFieldProps>;
distributed?: boolean;
[key: string]: any;
}
export interface ISchemaComponentProvider {

View File

@ -22,6 +22,7 @@ import {
useCreateEditFormBlock,
useCreateFormBlock,
useCreateTableBlock,
useActionAvailable,
} from '../..';
import { CompatibleSchemaInitializer } from '../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer';
@ -145,9 +146,7 @@ function useRecordBlocks() {
showAssociationFields: true,
};
},
useVisible() {
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'createForm',

View File

@ -15,6 +15,7 @@ import { useTableBlockContext } from '../block-provider';
import { useCollection_deprecated, useSortFields } from '../collection-manager';
import { useDesignable } from '../schema-component';
import { SchemaSettingsItemType } from '../application';
import { useCollection } from '../data-source';
export const setDefaultSortingRulesSchemaSettingsItem: SchemaSettingsItemType = {
name: 'SetDefaultSortingRules',
@ -134,7 +135,6 @@ export const setDefaultSortingRulesSchemaSettingsItem: SchemaSettingsItemType =
useVisible() {
const field = useField();
const { dragSort } = field.decoratorProps;
return !dragSort;
},
};

View File

@ -14,6 +14,7 @@ import { useFormBlockContext, useTableBlockContext } from '../block-provider';
import { useCollection_deprecated } from '../collection-manager';
import { useDesignable, removeNullCondition } from '../schema-component';
import { SchemaSettingsDataScope } from './SchemaSettingsDataScope';
import { useCollection } from '../data-source';
export const setTheDataScopeSchemaSettingsItem: SchemaSettingsItemType = {
name: 'SetTheDataScope',

View File

@ -83,6 +83,6 @@ describe('Collection Manager', () => {
const UsersCollection = collectionManager.getCollection('users');
expect(UsersCollection.repository).toBe(MockRepository);
expect(UsersCollection.repository).toBeInstanceOf(MockRepository);
});
});

View File

@ -16,13 +16,24 @@ import {
IRepository,
MergeOptions,
} from './types';
import { DataSource } from './data-source';
import { Repository } from './repository';
export class CollectionManager implements ICollectionManager {
public dataSource: DataSource;
protected collections = new Map<string, ICollection>();
protected repositories = new Map<string, IRepository>();
protected models = new Map<string, any>();
constructor(options = {}) {}
constructor(options: any = {}) {
if (options.dataSource) {
this.dataSource = options.dataSource;
}
this.registerRepositories({
Repository: Repository,
});
}
/* istanbul ignore next -- @preserve */
getRegisteredFieldType(type) {}
@ -103,6 +114,10 @@ export class CollectionManager implements ICollectionManager {
async sync() {}
removeCollection(name: string): void {
this.collections.delete(name);
}
protected newCollection(options): ICollection {
// @ts-ignore
return new Collection(options, this);

View File

@ -9,29 +9,30 @@
import { CollectionOptions, ICollection, ICollectionManager, IField, IRepository } from './types';
import { default as lodash } from 'lodash';
import merge from 'deepmerge';
import { CollectionField } from './collection-field';
export class Collection implements ICollection {
repository: IRepository;
fields: Map<string, IField> = new Map<string, IField>();
constructor(
protected options: CollectionOptions,
protected collectionManager: ICollectionManager,
) {
constructor(protected options: CollectionOptions, protected collectionManager: ICollectionManager) {
this.setRepository(options.repository);
if (options.fields) {
this.setFields(options.fields);
}
}
updateOptions(options: CollectionOptions, mergeOptions?: any) {
let newOptions = lodash.cloneDeep(options);
newOptions = merge(this.options, newOptions, mergeOptions);
const newOptions = {
...this.options,
...lodash.cloneDeep(options),
};
this.options = newOptions;
this.setFields(newOptions.fields || []);
if (options.repository) {
this.setRepository(options.repository);
}
@ -40,6 +41,11 @@ export class Collection implements ICollection {
}
setFields(fields: any[]) {
const fieldNames = this.fields.keys();
for (const fieldName of fieldNames) {
this.removeField(fieldName);
}
for (const field of fields) {
this.setField(field.name, field);
}
@ -64,6 +70,7 @@ export class Collection implements ICollection {
}
protected setRepository(repository: any) {
this.repository = this.collectionManager.getRegisteredRepository(repository || 'Repository');
const RepositoryClass = this.collectionManager.getRegisteredRepository(repository || 'Repository');
this.repository = new RepositoryClass(this);
}
}

View File

@ -87,6 +87,10 @@ export abstract class DataSource extends EventEmitter {
return new ResourceManager(options);
}
publicOptions() {
return null;
}
async load(options: any = {}) {}
async close() {}

View File

@ -8,11 +8,11 @@
*/
export * from './collection-manager';
export * from './collection';
export * from './data-source';
export * from './data-source-manager';
export * from './sequelize-collection-manager';
export * from './sequelize-data-source';
export * from './load-default-actions';
export * from './types';

View File

@ -91,6 +91,8 @@ export class SequelizeCollectionManager implements ICollectionManager {
return this.db.getCollection(name);
}
removeCollection(name: string) {}
getCollections() {
const collectionsFilter = this.collectionsFilter();

View File

@ -10,6 +10,7 @@
export type CollectionOptions = {
name: string;
repository?: string;
filterTargetKey?: string;
fields: any[];
[key: string]: any;
};
@ -24,8 +25,8 @@ export type FieldOptions = {
uiSchema?: any;
possibleTypes?: string[];
defaultValue?: any;
primaryKey: boolean;
unique: boolean;
primaryKey?: boolean;
unique?: boolean;
allowNull?: boolean;
autoIncrement?: boolean;
[key: string]: any;
@ -33,6 +34,7 @@ export type FieldOptions = {
export interface IField {
options: FieldOptions;
isRelationField(): boolean;
}
@ -44,6 +46,7 @@ export interface IFieldInterface {
options: FieldOptions;
toString(value: any, ctx?: any): string;
toValue(str: string, ctx?: any): any;
}
@ -63,6 +66,9 @@ export interface ICollection {
getField(name: string): IField;
[key: string]: any;
unavailableActions?: () => string[];
availableActions?: () => string[];
}
export interface IModel {
@ -106,9 +112,9 @@ export interface ICollectionManager {
registerModels(models: Record<string, any>): void;
registerRepositories(repositories: Record<string, any>): void;
registerRepositories(repositories: Record<string, new (collection: ICollection) => IRepository>): void;
getRegisteredRepository(key: string): IRepository;
getRegisteredRepository(key: string): new (collection: ICollection) => IRepository;
defineCollection(options: CollectionOptions): ICollection;
@ -120,6 +126,8 @@ export interface ICollectionManager {
getCollections(): Array<ICollection>;
removeCollection(name: string): void;
getRepository(name: string, sourceId?: string | number): IRepository;
sync(): Promise<void>;

View File

@ -61,4 +61,38 @@ describe('filterMatch', () => {
}),
).toBeFalsy();
});
test('filter by array operation', () => {
expect(
expect(
filterMatch(
{
tags: ['tag1', 'tag2'],
},
{
tags: {
$match: 'tag1',
},
},
),
).toBeTruthy(),
);
});
test('filter by date operation', () => {
expect(
expect(
filterMatch(
{
createdAt: '2013-02-08T09:30:26.123Z',
},
{
createdAt: {
$dateOn: '2013-02-08',
},
},
),
).toBeTruthy(),
);
});
});

View File

@ -22,6 +22,51 @@ describe('magic-attribute-model', () => {
await db.close();
});
it.skip('should update with magic attribute', async () => {
db.registerModels({ MagicAttributeModel });
const Test = db.collection({
name: 'tests',
model: 'MagicAttributeModel',
fields: [
{ type: 'string', name: 'title' },
{ type: 'json', name: 'options' },
],
});
await db.sync();
const record0 = await Test.repository.create({
values: {
title: 'xxx',
other: 'a',
actions: {
list: {
a: 'b',
c: 'd',
},
get: {
a: 'b',
c: 'd',
},
},
},
});
await record0.update({
title: 'xxx',
other: 'b',
actions: {
list: {
a: 'b',
c: 'd',
},
},
});
const data = record0.toJSON();
expect(data['actions']['get']).toBeUndefined();
});
it('case 0', async () => {
db.registerModels({ MagicAttributeModel });

View File

@ -839,6 +839,14 @@ export class Collection<
return false;
}
unavailableActions() {
if (this.options.template === 'file') {
return ['create', 'update', 'destroy'];
}
return [];
}
protected sequelizeModelOptions() {
const { name } = this.options;
return {

View File

@ -7,17 +7,34 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { filter } from 'mathjs';
import moment from 'moment';
export function filterMatch(model, where) {
if (where.filter !== undefined) {
where = filter;
}
// Create an object that maps operator names to functions
const operatorFunctions = {
// string
$eq: (value, condition) => value === condition,
$not: (value, condition) => !filterMatch(model, condition),
$includes: (value, condition) => value.includes(condition),
$notIncludes: (value, condition) => !value.includes(condition),
$empty: (value) =>
value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0),
$notEmpty: (value) =>
(value !== null && value !== undefined && value !== '') || (Array.isArray(value) && value.length > 0),
// array
$match: (value, condition) => value.some((item) => filterMatch(item, condition)),
$notMatch: (value, condition) => !value.some((item) => filterMatch(item, condition)),
$anyOf: (value, condition) => value.some((item) => condition.includes(item)),
$noneOf: (value, condition) => !value.some((item) => condition.includes(item)),
// datetime
$dateOn: (value, condition) => moment(value).isSame(condition, 'day'),
$dateNotOn: (value, condition) => !moment(value).isSame(condition, 'day'),
$dateBefore: (value, condition) => moment(value).isBefore(condition, 'day'),
$dateAfter: (value, condition) => moment(value).isAfter(condition, 'day'),
$dateNotBefore: (value, condition) => !moment(value).isBefore(condition, 'day'),
$dateNotAfter: (value, condition) => !moment(value).isAfter(condition, 'day'),
$gt: (value, condition) => value > condition,
$gte: (value, condition) => value >= condition,
$lt: (value, condition) => value < condition,

View File

@ -21,6 +21,14 @@ export class ViewCollection extends Collection {
return true;
}
unavailableActions(): Array<string> {
if (this.options.writableView) {
return [];
}
return ['create', 'update', 'destroy'];
}
protected sequelizeModelOptions(): any {
const modelOptions = super.sequelizeModelOptions();
modelOptions.tableName = this.options.viewName || this.options.name;

View File

@ -198,6 +198,7 @@ export class PluginManager {
if (typeof pluginName === 'string') {
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName);
this.clearCache(packageName);
return await importModule(packageName);
} else {
return pluginName;

View File

@ -36,3 +36,4 @@ export * from './url';
export * from './i18n';
export { dayjs, lodash };
export { Schema } from '@formily/json-schema';

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Plugin, useCollection_deprecated } from '@nocobase/client';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { bulkEditActionSettings, deprecatedBulkEditActionSettings } from './BulkEditAction.Settings';
import { BulkEditActionInitializer } from './BulkEditActionInitializer';
import {
@ -54,14 +54,7 @@ export class PluginActionBulkEditClient extends Plugin {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
useVisible: () => useActionAvailable('updateMany'),
};
this.app.schemaInitializerManager.addItem('table:configureActions', 'customize.bulkEdit', initializerData);

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Plugin, useCollection_deprecated } from '@nocobase/client';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { bulkUpdateActionSettings, deprecatedBulkUpdateActionSettings } from './BulkUpdateAction.Settings';
import { BulkUpdateActionInitializer } from './BulkUpdateActionInitializer';
import { CustomizeActionInitializer } from './CustomizeActionInitializer';
@ -24,14 +24,7 @@ export class PluginActionBulkUpdateClient extends Plugin {
title: '{{t("Bulk update")}}',
Component: BulkUpdateActionInitializer,
name: 'bulkUpdate',
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
useVisible: () => useActionAvailable('updateMany'),
};
this.app.schemaInitializerManager.addItem('table:configureActions', 'customize.bulkUpdate', initializerData);

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Plugin, useCollection_deprecated } from '@nocobase/client';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { DuplicateAction } from './DuplicateAction';
import { deprecatedDuplicateActionSettings, duplicateActionSettings } from './DuplicateAction.Settings';
import { DuplicateActionInitializer } from './DuplicateActionInitializer';
@ -23,29 +23,6 @@ export class PluginActionDuplicateClient extends Plugin {
this.app.schemaSettingsManager.add(deprecatedDuplicateActionSettings);
this.app.schemaSettingsManager.add(duplicateActionSettings);
const initializerData = {
title: '{{t("Duplicate")}}',
Component: 'DuplicateActionInitializer',
schema: {
'x-component': 'Action',
'x-action': 'duplicate',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:duplicate',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
type: 'primary',
},
},
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
};
const initializerTableData = {
title: '{{t("Duplicate")}}',
Component: 'DuplicateActionInitializer',
@ -59,14 +36,7 @@ export class PluginActionDuplicateClient extends Plugin {
type: 'primary',
},
},
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
useVisible: () => useActionAvailable('create'),
};
this.app.schemaInitializerManager.addItem('table:configureItemActions', 'actions.duplicate', initializerTableData);

View File

@ -11,7 +11,7 @@ export * from './ExportActionInitializer';
export * from './ExportDesigner';
export * from './ExportPluginProvider';
export * from './useExportAction';
import { Plugin } from '@nocobase/client';
import { Plugin, useCollection, useActionAvailable } from '@nocobase/client';
import { ExportPluginProvider } from './ExportPluginProvider';
import { exportActionSchemaSettings } from './schemaSettings';
@ -29,6 +29,7 @@ export class PluginActionExportClient extends Plugin {
skipScopeCheck: true,
},
},
useVisible: () => useActionAvailable('export'),
};
const tableActionInitializers = this.app.schemaInitializerManager.get('table:configureActions');

View File

@ -15,7 +15,7 @@ export * from './ImportDesigner';
export * from './ImportPluginProvider';
export * from './useImportAction';
import { Plugin, useCollection_deprecated } from '@nocobase/client';
import { Plugin, useActionAvailable } from '@nocobase/client';
import { ImportPluginProvider } from './ImportPluginProvider';
import { importActionSchemaSettings } from './schemaSettings';
@ -34,14 +34,7 @@ export class PluginActionImportClient extends Plugin {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'file' &&
collection.template !== 'sql'
);
},
useVisible: () => useActionAvailable('import'),
};
const tableActionInitializers = this.app.schemaInitializerManager.get('table:configureActions');

View File

@ -10,7 +10,7 @@
import {
CompatibleSchemaInitializer,
InitializerWithSwitch,
useCollection_deprecated,
useActionAvailable,
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
@ -90,10 +90,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('create'),
},
],
};

View File

@ -7,7 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { SchemaInitializer, SchemaInitializerItemType, useCollection_deprecated } from '@nocobase/client';
import {
SchemaInitializer,
SchemaInitializerItemType,
useCollection_deprecated,
useActionAvailable,
} from '@nocobase/client';
import { generateNTemplate } from '../../../locale';
export const deleteEventActionInitializer: SchemaInitializerItemType<any> = {
@ -52,10 +57,7 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
type: 'primary',
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'delete',
@ -65,10 +67,7 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
},
useVisible: function useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('destroy'),
},
deleteEventActionInitializer,
],
@ -157,19 +156,12 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
triggerWorkflows: [],
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
useVisible: () => useActionAvailable('update'),
},
{
name: 'customRequest',
title: generateNTemplate('Custom request'),
Component: 'CustomRequestInitializer',
useVisible() {
const collection = useCollection_deprecated();
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
},
},
],
},

View File

@ -24,6 +24,10 @@ export class SQLCollection extends Collection {
return true;
}
unavailableActions(): Array<string> {
return ['create', 'update', 'destroy'];
}
public collectionSchema() {
return undefined;
}

View File

@ -582,4 +582,13 @@ describe('collections repository', () => {
),
).not.toBeDefined();
});
it('should get collection list with unavailableActions', async () => {
const response = await app.agent().resource('collections').list();
expect(response.statusCode).toBe(200);
const data = response.body.data;
const firstCollection = data[0];
const collectionInMemory = app.db.getCollection(firstCollection.name);
expect(firstCollection.unavailableActions).toEqual(collectionInMemory.unavailableActions());
});
});

View File

@ -26,10 +26,23 @@ export class CollectionModel extends MagicAttributeModel {
toJSON() {
const json = super.toJSON();
const collection = this.db.getCollection(json.name);
if (!json.filterTargetKey) {
const collection = this.db.getCollection(json.name);
json.filterTargetKey = collection?.filterTargetKey;
}
if (collection && collection.unavailableActions) {
json['unavailableActions'] = collection.unavailableActions();
}
// @ts-ignore
if (collection && collection.availableActions) {
// @ts-ignore
json['availableActions'] = collection.availableActions();
}
return json;
}

View File

@ -10,7 +10,7 @@
import { useForm, useField } from '@formily/react';
import { action } from '@formily/reactive';
import { uid } from '@formily/shared';
import React, { useContext, useMemo, useState } from 'react';
import React, { useContext, useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import {
@ -20,7 +20,7 @@ import {
SchemaComponent,
SchemaComponentContext,
useCompile,
CollectionCategroriesContext,
CollectionCategoriesContext,
useCollectionManager_deprecated,
useCancelAction,
AddSubFieldAction,
@ -33,7 +33,6 @@ import {
import { message } from 'antd';
import { getCollectionSchema } from './schema/collections';
import { CollectionFields } from './CollectionFields';
import { EditCollection } from './EditCollectionAction';
import { DataSourceContext } from '../../DatabaseConnectionProvider';
/**
@ -95,9 +94,8 @@ export const ConfigurationTable = () => {
const ds = useDataSourceManager();
const ctx = useContext(SchemaComponentContext);
const { name } = useParams();
const data = useContext(CollectionCategroriesContext);
const data = useContext(CollectionCategoriesContext);
const api = useAPIClient();
const resource = api.resource('dbViews');
const compile = useCompile();
const loadCategories = async () => {
return data.data.map((item: any) => ({
@ -106,18 +104,6 @@ export const ConfigurationTable = () => {
}));
};
const loadDBViews = async () => {
return resource.list().then(({ data }) => {
return data?.data?.map((item: any) => {
const schema = item.schema;
return {
label: schema ? `${schema}.${compile(item.name)}` : item.name,
value: schema ? `${schema}_${item.name}` : item.name,
};
});
});
};
const loadStorages = async () => {
return api
.resource('storages')
@ -165,12 +151,43 @@ export const ConfigurationTable = () => {
const collectionSchema = useMemo(() => {
return getCollectionSchema(name);
}, [name]);
const resource = api.resource('dataSources', name);
const [dataSourceData, setDataSourceData] = useState({});
useEffect(() => {
try {
// eslint-disable-next-line promise/catch-or-return
resource
.get({
filterByTk: name,
})
.then((data) => {
setDataSourceData(data?.data);
});
} catch (error) {
console.log(error);
}
}, [name]);
const loadFilterTargetKeys = async (field) => {
const { fields } = field.form.values;
return Promise.resolve({
data: fields,
}).then(({ data }) => {
return data?.map((item: any) => {
return {
label: compile(item.uiSchema?.title) || item.name,
value: item.name,
};
});
});
};
return (
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
<SchemaComponentContext.Provider value={{ ...ctx, designable: false, dataSourceData }}>
<SchemaComponent
schema={collectionSchema}
components={{
EditCollection,
AddSubFieldAction,
EditSubFieldAction,
FieldSummary,
@ -183,8 +200,8 @@ export const ConfigurationTable = () => {
useBulkDestroySubField,
useSelectedRowKeys,
useAsyncDataSource,
loadFilterTargetKeys,
loadCategories,
loadDBViews,
loadStorages,
useNewId,
useCancelAction,

View File

@ -18,7 +18,7 @@ import {
SchemaComponentOptions,
useCompile,
useResourceActionContext,
CollectionCategroriesContext,
CollectionCategoriesContext,
} from '@nocobase/client';
import { CollectionFields } from './CollectionFields';
import { CollectionName } from './components/CollectionName';
@ -37,7 +37,7 @@ const TabBar = ({ item }) => {
const DndProvider = observer(
(props) => {
const [activeTab, setActiveId] = useState(null);
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh } = useContext(CollectionCategoriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const onDragEnd = async (props: DragEndEvent) => {
@ -78,7 +78,7 @@ const DndProvider = observer(
);
export const ConfigurationTabs = () => {
const { t } = useTranslation();
const { data } = useContext(CollectionCategroriesContext);
const { data } = useContext(CollectionCategoriesContext);
const compile = useCompile();
if (!data) return null;

View File

@ -22,7 +22,7 @@ const ReadPretty = (props) => {
} = useCollectionRecord() as any;
return (
<div className={cls(prefixCls, props.className)} style={props.style}>
{name !== tableName ? (
{name !== tableName && tableName ? (
<>
{name} <span style={{ color: 'GrayText' }}>({tableName})</span>
</>

View File

@ -16,8 +16,6 @@ import {
AddCollectionAction,
AddCollectionField,
AddFieldAction,
EditCollection,
EditCollectionAction,
EditCollectionField,
EditFieldAction,
OverridingCollectionField,
@ -29,10 +27,14 @@ import {
SyncSQLFieldsAction,
DeleteCollection,
DeleteCollectionAction,
CollectionCategroriesProvider,
CollectionCategoriesProvider,
usePlugin,
} from '@nocobase/client';
import { useLocation } from 'react-router-dom';
import { ConfigurationTable } from './ConfigurationTable';
import { ConfigurationTabs } from './ConfigurationTabs';
import PluginDatabaseConnectionsClient from '../../';
import { EditCollection } from './EditCollectionAction';
const schema2: ISchema = {
type: 'object',
@ -44,20 +46,23 @@ const schema2: ISchema = {
};
export const CollectionManagerPage = () => {
const plugin = usePlugin(PluginDatabaseConnectionsClient);
const location = useLocation();
const dataSourceType = new URLSearchParams(location.search).get('type');
const type = dataSourceType && plugin.types.get(dataSourceType);
return (
<SchemaComponent
schema={schema2}
components={{
CollectionCategroriesProvider,
CollectionCategoriesProvider,
ConfigurationTable,
ConfigurationTabs,
AddFieldAction,
AddCollectionField,
AddCollection,
AddCollection: type?.AddCollection || AddCollection,
AddCollectionAction,
EditCollection,
EditCollectionAction,
DeleteCollection,
EditCollection: type?.EditCollection || EditCollection,
DeleteCollection: type?.DeleteCollection || DeleteCollection,
DeleteCollectionAction,
EditFieldAction,
EditCollectionField,
@ -69,6 +74,12 @@ export const CollectionManagerPage = () => {
SyncFieldsActionCom,
SyncSQLFieldsAction,
}}
scope={{
allowCollectionDeletion: !!type?.allowCollectionDeletion,
disabledConfigureFields: type?.disabledConfigureFields,
disableAddFields: type?.disableAddFields,
allowCollectionCreate: !!type?.allowCollectionCreate,
}}
/>
);
};

View File

@ -125,6 +125,7 @@ export const fieldsTableSchema: ISchema = {
'x-component-props': {
type: 'primary',
},
'x-hidden': '{{ disableAddFields }}',
},
},
},

View File

@ -169,7 +169,7 @@ export const collectionTableSchema: ISchema = {
role: 'button',
isBulk: true,
},
'x-visible': false,
'x-visible': '{{allowCollectionDeletion}}',
},
create: {
type: 'void',
@ -178,7 +178,7 @@ export const collectionTableSchema: ISchema = {
'x-component-props': {
type: 'primary',
},
'x-visible': false,
'x-visible': '{{allowCollectionCreate}}',
},
},
},
@ -299,6 +299,7 @@ export const collectionTableSchema: ISchema = {
},
},
},
'x-hidden': '{{disabledConfigureFields}}',
},
update: {
type: 'void',
@ -313,7 +314,7 @@ export const collectionTableSchema: ISchema = {
delete: {
type: 'void',
title: '{{ t("Delete") }}',
'x-visible': false,
'x-visible': '{{allowCollectionDeletion}}',
'x-component': 'DeleteCollection',
'x-component-props': {
role: 'button',

View File

@ -57,6 +57,7 @@ export const CreateDatabaseConnectAction = () => {
'x-decorator-props': {
initialValue: {
type: info.key,
key: `d_${uid()}`,
},
},
title: compile("{{t('Add new')}}") + ' - ' + compile(type.label),
@ -69,12 +70,13 @@ export const CreateDatabaseConnectAction = () => {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
testConnectiion: {
testConnection: {
title: `{{ t("Test Connection",{ ns: "${NAMESPACE}" }) }}`,
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTestConnectionAction }}',
},
'x-hidden': type?.disableTestConnection,
},
cancel: {
title: '{{t("Cancel")}}',

View File

@ -103,6 +103,7 @@ export const EditDatabaseConnectionAction = () => {
'x-component-props': {
useAction: '{{ useTestConnectionAction }}',
},
'x-hidden': type?.disableTestConnection,
},
submit: {
title: '{{t("Submit")}}',

View File

@ -18,7 +18,7 @@ import {
SchemaComponent,
useActionContext,
useCancelAction,
CollectionCategroriesContext,
CollectionCategoriesContext,
CollectionCategory,
CollectionTemplateTag,
} from '@nocobase/client';
@ -27,7 +27,7 @@ import { collectionCategorySchema } from './schemas/collections';
const useCreateCategry = () => {
const form = useForm();
const ctx = useActionContext();
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh } = useContext(CollectionCategoriesContext);
const api = useAPIClient();
return {
async run() {

View File

@ -25,7 +25,7 @@ import {
DataSourceContext_deprecated,
AddSubFieldAction,
EditSubFieldAction,
CollectionCategroriesContext,
CollectionCategoriesContext,
FieldSummary,
TemplateSummary,
useRequest,
@ -106,7 +106,7 @@ export const ConfigurationTable = () => {
data: { database },
} = useCurrentAppInfo();
const data = useContext(CollectionCategroriesContext);
const data = useContext(CollectionCategoriesContext);
const api = useAPIClient();
const resource = api.resource('dbViews');
const compile = useCompile();

View File

@ -30,7 +30,7 @@ import {
SchemaComponentOptions,
useCompile,
useResourceActionContext,
CollectionCategroriesContext,
CollectionCategoriesContext,
} from '@nocobase/client';
import { CollectionFields } from './CollectionFields';
import { collectionTableSchema } from './schemas/collections';
@ -93,7 +93,7 @@ const TabBar = ({ item }) => {
const DndProvider = observer(
(props) => {
const [activeTab, setActiveId] = useState(null);
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh } = useContext(CollectionCategoriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const onDragEnd = async (props: DragEndEvent) => {
@ -134,7 +134,7 @@ const DndProvider = observer(
);
export const ConfigurationTabs = () => {
const { t } = useTranslation();
const { data, refresh } = useContext(CollectionCategroriesContext);
const { data, refresh } = useContext(CollectionCategoriesContext);
const { refresh: refreshCM, run, defaultRequest, setState } = useResourceActionContext();
const [activeKey, setActiveKey] = useState({ tab: 'all' });
const [key, setKey] = useState(activeKey.tab);

View File

@ -21,14 +21,14 @@ import {
useCompile,
useResourceActionContext,
useCancelAction,
CollectionCategroriesContext,
CollectionCategoriesContext,
} from '@nocobase/client';
import { collectionCategoryEditSchema } from './schemas/collections';
const useEditCategry = () => {
const form = useForm();
const ctx = useActionContext();
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh } = useContext(CollectionCategoriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();

View File

@ -29,7 +29,7 @@ import {
SyncSQLFieldsAction,
DeleteCollection,
DeleteCollectionAction,
CollectionCategroriesProvider,
CollectionCategoriesProvider,
} from '@nocobase/client';
import {
AddCategory,
@ -55,7 +55,7 @@ export const MainDataSourceManager = () => {
<SchemaComponent
schema={schema2}
components={{
CollectionCategroriesProvider,
CollectionCategoriesProvider,
ConfigurationTable,
ConfigurationTabs,
AddFieldAction,

View File

@ -38,7 +38,7 @@ export const ViewDatabaseConnectionAction = () => {
style={{ padding: '0px' }}
disabled={!record.enabled}
onClick={() => {
navigate(getConnectionCollectionPath(record.key));
navigate(getConnectionCollectionPath(record));
}}
role="button"
aria-label={`${record?.key}-Configure`}

View File

@ -7,5 +7,6 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export const getConnectionCollectionPath = (name: string | number) =>
`/admin/settings/data-source-manager/${name}/collections`;
export const getConnectionCollectionPath = ({ key, type }: { key: string | number; type: string }) => {
return `/admin/settings/data-source-manager/${key}/collections?type=${type}`;
};

View File

@ -0,0 +1,142 @@
/**
* 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 { createMockServer, MockServer, waitSecond } from '@nocobase/test';
import { CollectionManager, DataSource } from '@nocobase/data-source-manager';
import { HasManyRepository } from '@nocobase/database';
describe('data source collection', () => {
let app: MockServer;
beforeEach(async () => {
app = await createMockServer({
plugins: ['nocobase', 'data-source-manager'],
});
});
afterEach(async () => {
await app.destroy();
});
it('should update collection fields', async () => {
class MockDataSource extends DataSource {
static testConnection(options?: any): Promise<boolean> {
return Promise.resolve(true);
}
async load(): Promise<void> {
await waitSecond(1000);
}
createCollectionManager(options?: any): any {
return new CollectionManager(options);
}
}
app.dataSourceManager.factory.register('mock', MockDataSource);
await app.db.getRepository('dataSources').create({
values: {
key: 'mockInstance1',
type: 'mock',
displayName: 'Mock',
options: {},
},
});
await waitSecond(2000);
const dataSource = app.dataSourceManager.get('mockInstance1');
const collectionInDb = await app.db.getRepository('dataSourcesCollections').create({
values: {
name: 'test',
title: 'Test',
dataSourceKey: 'mockInstance1',
fields: [
{
name: 'name',
type: 'string',
},
{
name: 'age',
type: 'integer',
},
],
},
updateAssociationValues: ['fields'],
});
// should get collection from collection manager
const collectionManager = dataSource.collectionManager;
const collection = collectionManager.getCollection('test');
expect(collection).toBeTruthy();
expect(collection.getField('name')).toBeTruthy();
expect(collection.getField('age')).toBeTruthy();
const collectionInJson = collectionInDb.toJSON();
// set name field ui schema
collectionInJson.fields.forEach((field: any) => {
if (field.name === 'name') {
field.uiSchema = {
title: 'Name',
'x-component': 'Input',
};
}
});
// push a new fields
collectionInJson.fields.push({
name: 'email',
type: 'string',
});
// update collection with fields
await app.db.getRepository<HasManyRepository>('dataSources.collections', 'mockInstance1').update({
filterByTk: 'test',
values: collectionInJson,
updateAssociationValues: ['fields'],
});
// get collection from collection manager again
const collection2 = collectionManager.getCollection('test');
expect(collection2).toBeTruthy();
expect(collection2.getField('name')).toBeTruthy();
expect(collection2.getField('age')).toBeTruthy();
expect(collection2.getField('email')).toBeTruthy();
expect(collection2.getField('name').options.uiSchema).toEqual({
title: 'Name',
'x-component': 'Input',
});
const collectionInDb2 = await app.db.getRepository('dataSourcesCollections').findOne({
filter: {
name: 'test',
},
appends: ['fields'],
});
const collectionInJson2 = collectionInDb2.toJSON();
// it should remove field in update
collectionInJson2.fields = collectionInJson2.fields.filter((field: any) => field.name !== 'age');
await app.db.getRepository<HasManyRepository>('dataSources.collections', 'mockInstance1').update({
filterByTk: 'test',
values: collectionInJson2,
updateAssociationValues: ['fields'],
});
const collection3 = collectionManager.getCollection('test');
expect(collection3).toBeTruthy();
expect(collection3.getField('name')).toBeTruthy();
expect(collection3.getField('age')).toBeFalsy();
});
});

View File

@ -400,7 +400,10 @@ describe('data source', async () => {
},
});
await waitSecond(1000);
await waitSecond(2000);
const dataSource = app.dataSourceManager.dataSources.get('mockInstance1');
expect(dataSource).toBeDefined();
});
it('should get data source collections', async () => {
@ -515,7 +518,53 @@ describe('data source', async () => {
expect(field.options.title).toBe('标题 Field');
});
it('should create collection field', async () => {
it('should update fields through collection', async () => {
const dataSource = app.dataSourceManager.dataSources.get('mockInstance1');
const collection = dataSource.collectionManager.getCollection('posts');
const updateResp = await app
.agent()
.resource('dataSources.collections', 'mockInstance1')
.update({
filterByTk: 'posts',
values: {
fields: [
{
type: 'string',
name: 'title',
uiSchema: {
test: 'value',
},
},
{
type: 'text',
name: 'content',
},
],
},
});
expect(updateResp.status).toBe(200);
const fieldsOptions = [...collection.fields.values()].map((f) => f.options);
// remove a field
const newFieldsOptions = fieldsOptions.filter((f) => f.name === 'title');
const updateResp2 = await app
.agent()
.resource('dataSources.collections', 'mockInstance1')
.update({
filterByTk: 'posts',
values: {
fields: newFieldsOptions,
},
});
expect(updateResp2.status).toBe(200);
expect(collection.getField('comments')).toBeFalsy();
});
it('should update collection with field', async () => {
const dataSource = app.dataSourceManager.dataSources.get('mockInstance1');
const collection = dataSource.collectionManager.getCollection('comments');
@ -549,6 +598,20 @@ describe('data source', async () => {
expect(destroyResp.status).toBe(200);
expect(collection.getField('post')).toBeFalsy();
// reload data source manager
const refreshResp = await app.agent().resource('dataSources').refresh({
filterByTk: 'mockInstance1',
});
expect(refreshResp.status).toBe(200);
expect(refreshResp.body.data.status).toBe('reloading');
await waitSecond(2000);
const dataSource2 = app.dataSourceManager.dataSources.get('mockInstance1');
const collection2 = dataSource2.collectionManager.getCollection('comments');
expect(collection2.getField('post')).toBeFalsy();
});
});
});

View File

@ -48,6 +48,7 @@ export default defineCollection({
name: 'collections',
target: 'dataSourcesCollections',
foreignKey: 'dataSourceKey',
targetKey: 'name',
},
{
type: 'hasMany',

View File

@ -7,17 +7,39 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { MagicAttributeModel } from '@nocobase/database';
import { MagicAttributeModel, Model } from '@nocobase/database';
import { Application } from '@nocobase/server';
import { Transaction } from 'sequelize';
export class DataSourcesCollectionModel extends MagicAttributeModel {
load(loadOptions: { app: Application }) {
const { app } = loadOptions;
async load(loadOptions: { app: Application; transaction: Transaction }) {
const { app, transaction } = loadOptions;
const collectionFields = await this.getFields({ transaction });
const collectionOptions = this.get();
collectionOptions.fields = collectionFields;
const dataSourceName = this.get('dataSourceKey');
const dataSource = app.dataSourceManager.dataSources.get(dataSourceName);
const collection = dataSource.collectionManager.getCollection(collectionOptions.name);
collection.updateOptions(collectionOptions);
if (collectionOptions.fields) {
collectionOptions.fields = collectionOptions.fields.map((field) => {
if (field instanceof Model) {
return field.get();
}
return field;
});
}
if (collection) {
collection.updateOptions(collectionOptions);
} else {
dataSource.collectionManager.defineCollection(collectionOptions);
}
return collection;
}
}

View File

@ -24,7 +24,6 @@ export class DataSourcesFieldModel extends MagicAttributeModel {
const dataSource = app.dataSourceManager.dataSources.get(dataSourceKey);
const collection = dataSource.collectionManager.getCollection(collectionName);
const oldField = collection.getField(name);
const newOptions = mergeOptions(oldField ? oldField.options : {}, options);
collection.setField(name, newOptions);

View File

@ -182,6 +182,11 @@ export class PluginDataSourceManagerServer extends Plugin {
isDBInstance: !!dataSource?.collectionManager.db,
};
const publicOptions = dataSource?.publicOptions();
if (publicOptions) {
item['options'] = publicOptions;
}
if (dataSourceStatus === 'loading-failed' || dataSourceStatus === 'reloading-failed') {
item['errorMessage'] = plugin.dataSourceErrors[dataSourceModel.get('key')].message;
}
@ -195,12 +200,24 @@ export class PluginDataSourceManagerServer extends Plugin {
item.collections = collections.map((collection) => {
const collectionOptions = collection.options;
const collectionInstance = dataSource.collectionManager.getCollection(collectionOptions.name);
const fields = [...collection.fields.values()].map((field) => field.options);
return {
const results = {
...collectionOptions,
fields,
};
if (collectionInstance && collectionInstance.availableActions) {
results['availableActions'] = collectionInstance.availableActions();
}
if (collectionInstance && collectionInstance.unavailableActions) {
results['unavailableActions'] = collectionInstance.unavailableActions();
}
return results;
});
}
@ -320,7 +337,27 @@ export class PluginDataSourceManagerServer extends Plugin {
name: 'dataSources',
});
this.app.db.on('dataSourcesFields.afterSave', async (model: DataSourcesFieldModel) => {
this.app.db.on('dataSourcesFields.beforeSave', async (model: DataSourcesFieldModel, options) => {
const { transaction } = options;
if (!model.get('collectionName') || !model.get('dataSourceKey')) {
const collectionKey = model.get('collectionKey');
if (!collectionKey) {
throw new Error('collectionKey is required');
}
const collection = await model.getCollection({ transaction });
model.set('collectionName', collection.get('name'));
model.set('dataSourceKey', collection.get('dataSourceKey'));
}
});
this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel) => {
const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey'));
dataSource.collectionManager.removeCollection(model.get('name'));
});
this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel) => {
model.load({
app: this.app,
});
@ -332,11 +369,15 @@ export class PluginDataSourceManagerServer extends Plugin {
});
});
this.app.db.on('dataSourcesCollections.afterSave', async (model: DataSourcesCollectionModel) => {
model.load({
app: this.app,
});
});
this.app.db.on(
'dataSourcesCollections.afterSaveWithAssociations',
async (model: DataSourcesCollectionModel, { transaction }) => {
await model.load({
app: this.app,
transaction,
});
},
);
this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel) => {
this.app.dataSourceManager.dataSources.delete(model.get('key'));

View File

@ -67,8 +67,13 @@ export default {
},
});
} else {
await fieldRecord.update({
...values,
await mainDb.getRepository('dataSourcesFields').update({
filter: {
name,
collectionName,
dataSourceKey,
},
values,
});
}
@ -76,6 +81,7 @@ export default {
.get(dataSourceKey)
.collectionManager.getCollection(collectionName)
.getField(name);
ctx.body = field.options;
await next();

View File

@ -86,11 +86,23 @@ export default {
},
});
} else {
await dataSourceCollectionRecord.update({
...params.values,
await ctx.db.getRepository('dataSourcesCollections').update({
filter: {
name: collectionName,
dataSourceKey,
},
values: params.values,
updateAssociationValues: ['fields'],
});
}
dataSourceCollectionRecord = await ctx.db.getRepository('dataSourcesCollections').findOne({
filter: {
name: collectionName,
dataSourceKey,
},
});
ctx.body = dataSourceCollectionRecord.toJSON();
await next();

View File

@ -8,7 +8,7 @@
*/
import { useFieldSchema } from '@formily/react';
import { CompatibleSchemaInitializer, useCollection_deprecated } from '@nocobase/client';
import { CompatibleSchemaInitializer, useActionAvailable, useCollection } from '@nocobase/client';
const commonOptions = {
title: "{{t('Configure actions')}}",
@ -38,10 +38,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView;
},
useVisible: () => useActionAvailable('create'),
},
{
type: 'item',
@ -52,10 +49,7 @@ const commonOptions = {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
},
useVisible() {
const collection = useCollection_deprecated();
return !['view', 'sql'].includes(collection.template) || collection?.writableView;
},
useVisible: () => useActionAvailable('destroyMany'),
},
{
type: 'item',
@ -73,9 +67,10 @@ const commonOptions = {
schema: {
'x-align': 'right',
},
useVisible() {
const schema = useFieldSchema();
const collection = useCollection_deprecated();
const collection = useCollection();
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
return collection.tree && treeTable;
},

View File

@ -19,8 +19,8 @@ import { SchemaOptionsContext } from '@formily/react';
import {
APIClientProvider,
ApplicationContext,
CollectionCategroriesContext,
CollectionCategroriesProvider,
CollectionCategoriesContext,
CollectionCategoriesProvider,
CurrentAppInfoContext,
DataSourceApplicationProvider,
SchemaComponent,
@ -390,7 +390,7 @@ export const GraphDrawPage = React.memo(() => {
const {
data: { database },
} = currentAppInfo;
const categoryCtx = useContext(CollectionCategroriesContext);
const categoryCtx = useContext(CollectionCategoriesContext);
const scope = { ...options?.scope };
const components = { ...options?.components };
const saveGraphPositionAction = async (data) => {
@ -520,7 +520,7 @@ export const GraphDrawPage = React.memo(() => {
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSource?.key}>
<APIClientProvider apiClient={api}>
<SchemaComponentOptions inherit scope={scope} components={components}>
<CollectionCategroriesProvider {...categoryCtx}>
<CollectionCategoriesProvider {...categoryCtx}>
{/* TODO: 因为画布中的卡片是一次性注册进 Graph 的,这里的 theme 是存在闭包里的,因此当主题动态变更时,并不会触发卡片的重新渲染 */}
<ConfigProvider theme={theme as any}>
<div style={{ height: 'auto' }}>
@ -531,7 +531,7 @@ export const GraphDrawPage = React.memo(() => {
</App>
</div>
</ConfigProvider>
</CollectionCategroriesProvider>
</CollectionCategoriesProvider>
</SchemaComponentOptions>
</APIClientProvider>
</DataSourceApplicationProvider>

View File

@ -12,7 +12,7 @@ import { css } from '@emotion/css';
import { SchemaOptionsContext } from '@formily/react';
import { uid } from '@formily/shared';
import {
CollectionCategroriesContext,
CollectionCategoriesContext,
CollectionProvider_deprecated,
SchemaComponent,
SchemaComponentProvider,
@ -387,7 +387,7 @@ const Entity: React.FC<{
data: { database },
} = useCurrentAppInfo();
const collectionData = useRef();
const categoryData = useContext(CollectionCategroriesContext);
const categoryData = useContext(CollectionCategoriesContext);
collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) };
const { category = [] } = item;
const compile = useCompile();

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CompatibleSchemaInitializer, useCollection_deprecated } from '@nocobase/client';
import { CompatibleSchemaInitializer, useCollection, useActionAvailable } from '@nocobase/client';
const commonOptions = {
title: "{{t('Configure actions')}}",
@ -35,10 +35,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return (collection as any).template !== 'view' || collection?.writableView;
},
useVisible: () => useActionAvailable('create'),
},
],
};

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CompatibleSchemaInitializer, useCollection_deprecated } from '@nocobase/client';
import { CompatibleSchemaInitializer, useActionAvailable } from '@nocobase/client';
const commonOptions = {
title: "{{t('Configure actions')}}",
@ -35,10 +35,7 @@ const commonOptions = {
skipScopeCheck: true,
},
},
useVisible() {
const collection = useCollection_deprecated();
return collection.template !== 'sql';
},
useVisible: () => useActionAvailable('create'),
},
{
name: 'refresh',

View File

@ -16,7 +16,7 @@ export class CircleFieldInterface extends CommonSchema {
group = 'map';
order = 3;
title = generateNTemplate('Circle');
availableTypes = ['circle'];
availableTypes = ['circle', 'json'];
description = generateNTemplate('Circle');
sortable = true;
default = {

View File

@ -17,7 +17,7 @@ export class LineStringFieldInterface extends CommonSchema {
order = 2;
title = generateNTemplate('Line');
description = generateNTemplate('Line');
availableTypes = ['lineString'];
availableTypes = ['lineString', 'json'];
sortable = true;
default = {
type: 'lineString',

View File

@ -17,7 +17,7 @@ export class PointFieldInterface extends CommonSchema {
order = 1;
title = generateNTemplate('Point');
description = generateNTemplate('Point');
availableTypes = ['point'];
availableTypes = ['point', 'json'];
sortable = true;
default = {
type: 'point',

View File

@ -17,7 +17,7 @@ export class PolygonFieldInterface extends CommonSchema {
order = 4;
title = generateNTemplate('Polygon');
description = generateNTemplate('Polygon');
availableTypes = ['polygon'];
availableTypes = ['polygon', 'json'];
sortable = true;
default = {
type: 'polygon',

View File

@ -0,0 +1,588 @@
/**
* 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 { Server } from 'http';
import jwt from 'jsonwebtoken';
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import Database from '@nocobase/database';
import { MockServer } from '@nocobase/test';
import PluginWorkflow, { Processor, EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
import { RequestConfig } from '../MailerInstruction';
const HOST = 'localhost';
class MockAPI {
app: Koa;
server: Server;
port: number;
get URL_DATA() {
return `http://${HOST}:${this.port}/api/data`;
}
get URL_400() {
return `http://${HOST}:${this.port}/api/400`;
}
get URL_400_MESSAGE() {
return `http://${HOST}:${this.port}/api/400_message`;
}
get URL_400_OBJECT() {
return `http://${HOST}:${this.port}/api/400_object`;
}
get URL_404() {
return `http://${HOST}:${this.port}/api/404`;
}
get URL_TIMEOUT() {
return `http://${HOST}:${this.port}/api/timeout`;
}
get URL_END() {
return `http://${HOST}:${this.port}/api/end`;
}
constructor() {
this.app = new Koa();
this.app.use(bodyParser());
this.app.use(async (ctx, next) => {
if (ctx.path === '/api/400') {
return ctx.throw(400);
}
if (ctx.path === '/api/400_message') {
return ctx.throw(400, 'bad request message');
}
if (ctx.path === '/api/400_object') {
ctx.body = { a: 1 };
ctx.status = 400;
return;
}
if (ctx.path === '/api/end') {
ctx.res.socket.end();
return;
}
if (ctx.path === '/api/timeout') {
await sleep(2000);
ctx.status = 204;
return;
}
if (ctx.path === '/api/data') {
await sleep(100);
ctx.body = {
meta: { title: ctx.query.title },
data: ctx.request.body,
};
}
await next();
});
}
async start() {
return new Promise((resolve) => {
this.server = this.app.listen(0, () => {
this.port = this.server.address()['port'];
resolve(true);
});
});
}
async close() {
return new Promise((resolve) => {
this.server.close(() => {
resolve(true);
});
});
}
}
describe('workflow > instructions > request', () => {
let app: MockServer;
let db: Database;
let PostRepo;
let PostCollection;
let ReplyRepo;
let WorkflowModel;
let workflow;
let api: MockAPI;
beforeEach(async () => {
api = new MockAPI();
api.start();
app = await getApp({
resourcer: {
prefix: '/api',
},
plugins: ['users', 'auth', 'workflow-request'],
});
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
PostCollection = db.getCollection('posts');
PostRepo = PostCollection.repository;
ReplyRepo = db.getCollection('replies').repository;
workflow = await WorkflowModel.create({
enabled: true,
type: 'collection',
config: {
mode: 1,
collection: 'posts',
},
});
});
afterEach(async () => {
await api.close();
await app.destroy();
});
describe('request static app routes', () => {
it('get data (legacy)', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'GET',
onlyData: true,
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result).toMatchObject({ meta: {}, data: {} });
});
it('get data', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'GET',
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result).toMatchObject({
data: { meta: {}, data: {} },
});
});
it('timeout', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_TIMEOUT,
method: 'GET',
timeout: 250,
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(1000);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.FAILED);
expect(job.result).toMatchObject({
code: 'ECONNABORTED',
name: 'Error',
status: null,
message: 'timeout of 250ms exceeded',
});
// NOTE: to wait for the response to finish and avoid non finished promise.
await sleep(1500);
});
it('ignoreFail', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_TIMEOUT,
method: 'GET',
timeout: 250,
ignoreFail: true,
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(1000);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result).toMatchObject({
code: 'ECONNABORTED',
name: 'Error',
status: null,
message: 'timeout of 250ms exceeded',
});
});
it('response 400 without body', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_400,
method: 'GET',
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.FAILED);
expect(job.result.status).toBe(400);
});
it('response 400 with text message', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_400_MESSAGE,
method: 'GET',
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.FAILED);
expect(job.result.status).toBe(400);
expect(job.result.data).toBe('bad request message');
});
it('response 400 with object', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_400_OBJECT,
method: 'GET',
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.FAILED);
expect(job.result.status).toBe(400);
expect(job.result.data).toEqual({ a: 1 });
});
it('response just end', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_END,
method: 'GET',
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.FAILED);
expect(job.result).toMatchObject({
code: 'ECONNRESET',
name: 'Error',
status: null,
message: 'socket hang up',
});
});
it('response 400 ignoreFail', async () => {
await workflow.createNode({
type: 'request',
config: {
url: api.URL_400,
method: 'GET',
timeout: 1000,
ignoreFail: true,
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.status).toBe(400);
});
it('request with data', async () => {
const n1 = await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'POST',
data: { title: '{{$context.data.title}}' },
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toEqual({ title: 't1' });
});
// TODO(bug): should not use ejs
it('request json data with multiple lines', async () => {
const n1 = await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'POST',
data: { title: '{{$context.data.title}}' },
} as RequestConfig,
});
const title = 't1\n\nline 2';
await PostRepo.create({
values: { title },
});
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toEqual({ title });
});
it.skip('request inside loop', async () => {
const n1 = await workflow.createNode({
type: 'loop',
config: {
target: 2,
},
});
const n2 = await workflow.createNode({
type: 'request',
upstreamId: n1.id,
branchIndex: 0,
config: {
url: api.URL_DATA,
method: 'GET',
},
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
expect(jobs.length).toBe(3);
expect(jobs.map((item) => item.status)).toEqual(Array(3).fill(JOB_STATUS.RESOLVED));
expect(jobs[0].result).toBe(2);
});
});
describe('contentType', () => {
it('no contentType as "application/json"', async () => {
const n1 = await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'POST',
data: { a: '{{$context.data.title}}' },
},
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toEqual({ a: 't1' });
});
it('contentType as "application/x-www-form-urlencoded"', async () => {
const n1 = await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'POST',
data: [
{ name: 'a', value: '{{$context.data.title}}' },
{ name: 'a', value: '&=1' },
],
contentType: 'application/x-www-form-urlencoded',
},
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toEqual({ a: ['t1', '&=1'] });
});
});
describe('invalid characters', () => {
it('\\n in header value should be trimed, and should not cause error', async () => {
const n1 = await workflow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'POST',
data: { a: '{{$context.data.title}}' },
headers: [{ name: 'Authorization', value: 'abc\n' }],
},
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toEqual({ a: 't1' });
});
});
describe('request db resource', () => {
it('request db resource', async () => {
const user = await db.getRepository('users').create({});
const token = jwt.sign(
{
userId: typeof user.id,
},
process.env.APP_KEY,
{
expiresIn: '1d',
},
);
const server = app.listen(12346, () => {});
await sleep(1000);
const n1 = await workflow.createNode({
type: 'request',
config: {
url: `http://localhost:12346/api/categories`,
method: 'POST',
headers: [{ name: 'Authorization', value: `Bearer ${token}` }],
} as RequestConfig,
});
await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const category = await db.getRepository('categories').findOne({});
const [execution] = await workflow.getExecutions();
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data.data).toMatchObject({});
server.close();
});
});
describe('sync request', () => {
let syncFlow;
beforeEach(async () => {
syncFlow = await WorkflowModel.create({
type: 'syncTrigger',
enabled: true,
});
});
it('sync trigger', async () => {
await syncFlow.createNode({
type: 'request',
config: {
url: api.URL_DATA,
method: 'GET',
} as RequestConfig,
});
const workflowPlugin = app.pm.get(PluginWorkflow) as PluginWorkflow;
const processor = (await workflowPlugin.trigger(syncFlow, { data: { title: 't1' } })) as Processor;
const [execution] = await syncFlow.getExecutions();
expect(processor.execution.id).toEqual(execution.id);
expect(processor.execution.status).toBe(execution.status);
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.data).toEqual({ meta: {}, data: {} });
});
it('ignoreFail', async () => {
await syncFlow.createNode({
type: 'request',
config: {
url: api.URL_404,
method: 'GET',
ignoreFail: true,
} as RequestConfig,
});
const workflowPlugin = app.pm.get(PluginWorkflow) as PluginWorkflow;
const processor = (await workflowPlugin.trigger(syncFlow, { data: { title: 't1' } })) as Processor;
const [execution] = await syncFlow.getExecutions();
const [job] = await execution.getJobs();
expect(job.status).toBe(JOB_STATUS.RESOLVED);
expect(job.result.status).toBe(404);
});
});
});