diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx index f16bad4b58..d598131411 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx @@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({ 'x-component': 'Hello', }); }, - } + }; }, - } + }, ], }); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx index f16bad4b58..d598131411 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-action-modal-2.tsx @@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({ 'x-component': 'Hello', }); }, - } + }; }, - } + }, ], }); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 4b38942736..0cd84c974f 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -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, diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 611c241239..81a0fcdfcb 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -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 ( - + - + ); }; -export const CollectionCategroriesProvider = (props) => { +export const CollectionCategoriesProvider = (props) => { const { service, refreshCategory } = props; return ( - { }} > {props.children} - + ); }; diff --git a/packages/core/client/src/collection-manager/context.ts b/packages/core/client/src/collection-manager/context.ts index 395f238cb1..9af16f1ea8 100644 --- a/packages/core/client/src/collection-manager/context.ts +++ b/packages/core/client/src/collection-manager/context.ts @@ -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'; diff --git a/packages/core/client/src/collection-manager/index.tsx b/packages/core/client/src/collection-manager/index.tsx index 0ccf781a45..fbb3756f76 100644 --- a/packages/core/client/src/collection-manager/index.tsx +++ b/packages/core/client/src/collection-manager/index.tsx @@ -35,3 +35,4 @@ export * from './mixins/InheritanceCollectionMixin'; export * from './sub-table'; export * from './CollectionHistoryProvider'; export * from './utils'; +export { UnSupportFields } from './templates/components/UnSupportFields'; diff --git a/packages/core/client/src/collection-manager/interfaces/checkboxGroup.ts b/packages/core/client/src/collection-manager/interfaces/checkboxGroup.ts index 47969049af..847f0f9d29 100644 --- a/packages/core/client/src/collection-manager/interfaces/checkboxGroup.ts +++ b/packages/core/client/src/collection-manager/interfaces/checkboxGroup.ts @@ -26,7 +26,7 @@ export class CheckboxGroupFieldInterface extends CollectionFieldInterface { 'x-component': 'Checkbox.Group', }, }; - availableTypes = ['array']; + availableTypes = ['array', 'json']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/datetime.ts b/packages/core/client/src/collection-manager/interfaces/datetime.ts index 7790d2a5e6..615d11e9ea 100644 --- a/packages/core/client/src/collection-manager/interfaces/datetime.ts +++ b/packages/core/client/src/collection-manager/interfaces/datetime.ts @@ -27,7 +27,7 @@ export class DatetimeFieldInterface extends CollectionFieldInterface { }, }, }; - availableTypes = ['date', 'dateOnly']; + availableTypes = ['date', 'dateOnly', 'string']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/markdown.ts b/packages/core/client/src/collection-manager/interfaces/markdown.ts index 2985dc5ce0..9b39068839 100644 --- a/packages/core/client/src/collection-manager/interfaces/markdown.ts +++ b/packages/core/client/src/collection-manager/interfaces/markdown.ts @@ -26,7 +26,7 @@ export class MarkdownFieldInterface extends CollectionFieldInterface { 'x-component': 'Markdown', }, }; - availableTypes = ['text', 'json']; + availableTypes = ['text', 'json', 'string']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/multipleSelect.ts b/packages/core/client/src/collection-manager/interfaces/multipleSelect.ts index 1ae229685b..cf7d786168 100644 --- a/packages/core/client/src/collection-manager/interfaces/multipleSelect.ts +++ b/packages/core/client/src/collection-manager/interfaces/multipleSelect.ts @@ -29,7 +29,7 @@ export class MultipleSelectFieldInterface extends CollectionFieldInterface { enum: [], }, }; - availableTypes = ['array']; + availableTypes = ['array', 'json']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/richText.ts b/packages/core/client/src/collection-manager/interfaces/richText.ts index 51e138c92b..26a11dce3e 100644 --- a/packages/core/client/src/collection-manager/interfaces/richText.ts +++ b/packages/core/client/src/collection-manager/interfaces/richText.ts @@ -26,7 +26,7 @@ export class RichTextFieldInterface extends CollectionFieldInterface { 'x-component': 'RichText', }, }; - availableTypes = ['text', 'json']; + availableTypes = ['text', 'json', 'string']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/textarea.ts b/packages/core/client/src/collection-manager/interfaces/textarea.ts index 942a3e4f47..26785ed0fb 100644 --- a/packages/core/client/src/collection-manager/interfaces/textarea.ts +++ b/packages/core/client/src/collection-manager/interfaces/textarea.ts @@ -26,7 +26,7 @@ export class TextareaFieldInterface extends CollectionFieldInterface { 'x-component': 'Input.TextArea', }, }; - availableTypes = ['text', 'json']; + availableTypes = ['text', 'json', 'string']; hasDefaultValue = true; properties = { ...defaultProps, diff --git a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx index 70ac5d4426..8b68467865 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx @@ -27,19 +27,26 @@ function useCurrentRequest(options: Omit) { 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(service, { diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx index b6ad3b6254..8e5852f3c9 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx @@ -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'), }, ], }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx index 0b24f5f8bc..41e4208e9f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx index 90d19b3451..d47dc645fd 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts index c723a185d5..885297e018 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts @@ -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: [ diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx index f9b7abc6bf..04c7ed8f53 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx index cc1226b9c1..cbc1ac8418 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts index b983227239..5d17d78e1d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx index e9bdb6323f..7f81f0a2e0 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts index 4a55f13856..1cf878c705 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx index ab94c2edb6..f353c29039 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx index b7f101b795..b8e8027ab9 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx @@ -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; }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx index 7c54943b45..63abe76d2f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx @@ -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(); @@ -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; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx index b74e0d1c67..f8404cd9fa 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -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', diff --git a/packages/core/client/src/modules/blocks/index.ts b/packages/core/client/src/modules/blocks/index.ts index b5912b21c5..6266fcd2ee 100644 --- a/packages/core/client/src/modules/blocks/index.ts +++ b/packages/core/client/src/modules/blocks/index.ts @@ -9,3 +9,4 @@ export * from './data-blocks/details-multi'; export * from './data-blocks/details-single'; +export * from './useActionAvailable'; diff --git a/packages/core/client/src/modules/blocks/useActionAvailable.ts b/packages/core/client/src/modules/blocks/useActionAvailable.ts new file mode 100644 index 0000000000..9384ddea1c --- /dev/null +++ b/packages/core/client/src/modules/blocks/useActionAvailable.ts @@ -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; +}; diff --git a/packages/core/client/src/modules/page/BlockInitializers.tsx b/packages/core/client/src/modules/page/BlockInitializers.tsx index 35c97fa06d..de6f33eba3 100644 --- a/packages/core/client/src/modules/page/BlockInitializers.tsx +++ b/packages/core/client/src/modules/page/BlockInitializers.tsx @@ -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', diff --git a/packages/core/client/src/schema-component/antd/details/Details.tsx b/packages/core/client/src/schema-component/antd/details/Details.tsx index 13e09f5d29..a691c04524 100644 --- a/packages/core/client/src/schema-component/antd/details/Details.tsx +++ b/packages/core/client/src/schema-component/antd/details/Details.tsx @@ -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 ; + return ( + <> + + + + ); } return ; diff --git a/packages/core/client/src/schema-component/antd/form-v2/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/form-v2/demos/demo2.tsx index 1384f49d04..ca56f6e598 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/demos/demo2.tsx @@ -64,6 +64,7 @@ const schema: ISchema = { collection: 'users', resource: 'users', action: 'get', + filterByTk: 1, }, properties: { form: { diff --git a/packages/core/client/src/schema-component/antd/form-v2/demos/demo3.tsx b/packages/core/client/src/schema-component/antd/form-v2/demos/demo3.tsx index 162bfef2cb..9bb2998a19 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/demos/demo3.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/demos/demo3.tsx @@ -40,6 +40,7 @@ const schema: ISchema = { collection: 'users', resource: 'users', action: 'get', + filterByTk: 1, }, properties: { form: { diff --git a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx index 15956c5fe9..17452276f4 100644 --- a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx @@ -46,10 +46,12 @@ ReadPretty.Input = (props: InputReadPrettyProps) => { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); return ( -
+
{props.addonBefore} {props.prefix} - {compile(props.value)} + + {props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value)} + {props.suffix} {props.addonAfter}
diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 3420aa46a2..4101b58393 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -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) => { diff --git a/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx b/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx index c217871d48..258a1ef51c 100644 --- a/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx +++ b/packages/core/client/src/schema-component/antd/variable/RawTextArea.tsx @@ -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(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} diff --git a/packages/core/client/src/schema-component/types.ts b/packages/core/client/src/schema-component/types.ts index a203604b18..ccd17e3757 100644 --- a/packages/core/client/src/schema-component/types.ts +++ b/packages/core/client/src/schema-component/types.ts @@ -20,6 +20,7 @@ export interface ISchemaComponentContext { setDesignable?: (value: boolean) => void; SchemaField?: React.FC; distributed?: boolean; + [key: string]: any; } export interface ISchemaComponentProvider { diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index ffe6211a97..c56c7e4042 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -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', diff --git a/packages/core/client/src/schema-settings/setDefaultSortingRulesSchemaSettingsItem.tsx b/packages/core/client/src/schema-settings/setDefaultSortingRulesSchemaSettingsItem.tsx index a870bfc627..fb6952c4d6 100644 --- a/packages/core/client/src/schema-settings/setDefaultSortingRulesSchemaSettingsItem.tsx +++ b/packages/core/client/src/schema-settings/setDefaultSortingRulesSchemaSettingsItem.tsx @@ -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; }, }; diff --git a/packages/core/client/src/schema-settings/setTheDataScopeSchemaSettingsItem.tsx b/packages/core/client/src/schema-settings/setTheDataScopeSchemaSettingsItem.tsx index 936954b2b8..5db1aefc62 100644 --- a/packages/core/client/src/schema-settings/setTheDataScopeSchemaSettingsItem.tsx +++ b/packages/core/client/src/schema-settings/setTheDataScopeSchemaSettingsItem.tsx @@ -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', diff --git a/packages/core/data-source-manager/src/__tests__/collection-manager.test.ts b/packages/core/data-source-manager/src/__tests__/collection-manager.test.ts index 6e57e01454..2ed2327660 100644 --- a/packages/core/data-source-manager/src/__tests__/collection-manager.test.ts +++ b/packages/core/data-source-manager/src/__tests__/collection-manager.test.ts @@ -83,6 +83,6 @@ describe('Collection Manager', () => { const UsersCollection = collectionManager.getCollection('users'); - expect(UsersCollection.repository).toBe(MockRepository); + expect(UsersCollection.repository).toBeInstanceOf(MockRepository); }); }); diff --git a/packages/core/data-source-manager/src/collection-manager.ts b/packages/core/data-source-manager/src/collection-manager.ts index 7f29a55178..30d39b8471 100644 --- a/packages/core/data-source-manager/src/collection-manager.ts +++ b/packages/core/data-source-manager/src/collection-manager.ts @@ -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(); protected repositories = new Map(); protected models = new Map(); - 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); diff --git a/packages/core/data-source-manager/src/collection.ts b/packages/core/data-source-manager/src/collection.ts index 01c6615283..51e633fd17 100644 --- a/packages/core/data-source-manager/src/collection.ts +++ b/packages/core/data-source-manager/src/collection.ts @@ -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 = new Map(); - 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); } } diff --git a/packages/core/data-source-manager/src/data-source.ts b/packages/core/data-source-manager/src/data-source.ts index b6762cc0a5..84ce8bdb29 100644 --- a/packages/core/data-source-manager/src/data-source.ts +++ b/packages/core/data-source-manager/src/data-source.ts @@ -87,6 +87,10 @@ export abstract class DataSource extends EventEmitter { return new ResourceManager(options); } + publicOptions() { + return null; + } + async load(options: any = {}) {} async close() {} diff --git a/packages/core/data-source-manager/src/index.ts b/packages/core/data-source-manager/src/index.ts index 0b9abf563a..defaf540f8 100644 --- a/packages/core/data-source-manager/src/index.ts +++ b/packages/core/data-source-manager/src/index.ts @@ -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'; diff --git a/packages/core/data-source-manager/src/sequelize-collection-manager.ts b/packages/core/data-source-manager/src/sequelize-collection-manager.ts index eff1e5390b..26537716f0 100644 --- a/packages/core/data-source-manager/src/sequelize-collection-manager.ts +++ b/packages/core/data-source-manager/src/sequelize-collection-manager.ts @@ -91,6 +91,8 @@ export class SequelizeCollectionManager implements ICollectionManager { return this.db.getCollection(name); } + removeCollection(name: string) {} + getCollections() { const collectionsFilter = this.collectionsFilter(); diff --git a/packages/core/data-source-manager/src/types.ts b/packages/core/data-source-manager/src/types.ts index f9a6364fcc..f5bf5da697 100644 --- a/packages/core/data-source-manager/src/types.ts +++ b/packages/core/data-source-manager/src/types.ts @@ -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): void; - registerRepositories(repositories: Record): void; + registerRepositories(repositories: Record 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; + removeCollection(name: string): void; + getRepository(name: string, sourceId?: string | number): IRepository; sync(): Promise; diff --git a/packages/core/database/src/__tests__/filter-match.test.ts b/packages/core/database/src/__tests__/filter-match.test.ts index 3d2097cc34..f2f4b58362 100644 --- a/packages/core/database/src/__tests__/filter-match.test.ts +++ b/packages/core/database/src/__tests__/filter-match.test.ts @@ -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(), + ); + }); }); diff --git a/packages/core/database/src/__tests__/magic-attribute-model.test.ts b/packages/core/database/src/__tests__/magic-attribute-model.test.ts index 78592ed796..696c595f0e 100644 --- a/packages/core/database/src/__tests__/magic-attribute-model.test.ts +++ b/packages/core/database/src/__tests__/magic-attribute-model.test.ts @@ -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 }); diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index 688b5c8a18..8b22f153ca 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -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 { diff --git a/packages/core/database/src/filter-match.ts b/packages/core/database/src/filter-match.ts index 816064b45f..258f0e27e4 100644 --- a/packages/core/database/src/filter-match.ts +++ b/packages/core/database/src/filter-match.ts @@ -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, diff --git a/packages/core/database/src/view-collection.ts b/packages/core/database/src/view-collection.ts index 0e09c43b99..1654423f66 100644 --- a/packages/core/database/src/view-collection.ts +++ b/packages/core/database/src/view-collection.ts @@ -21,6 +21,14 @@ export class ViewCollection extends Collection { return true; } + unavailableActions(): Array { + if (this.options.writableView) { + return []; + } + + return ['create', 'update', 'destroy']; + } + protected sequelizeModelOptions(): any { const modelOptions = super.sequelizeModelOptions(); modelOptions.tableName = this.options.viewName || this.options.name; diff --git a/packages/core/server/src/plugin-manager/plugin-manager.ts b/packages/core/server/src/plugin-manager/plugin-manager.ts index 34f5f4001d..5fcf31a6e4 100644 --- a/packages/core/server/src/plugin-manager/plugin-manager.ts +++ b/packages/core/server/src/plugin-manager/plugin-manager.ts @@ -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; diff --git a/packages/core/utils/src/index.ts b/packages/core/utils/src/index.ts index 0189b23696..7f20a0f15b 100644 --- a/packages/core/utils/src/index.ts +++ b/packages/core/utils/src/index.ts @@ -36,3 +36,4 @@ export * from './url'; export * from './i18n'; export { dayjs, lodash }; +export { Schema } from '@formily/json-schema'; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/index.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/index.tsx index bde1e87542..c1f52a22bb 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/index.tsx @@ -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); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/index.tsx b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/index.tsx index a73ed3c2d0..755dfbebfe 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/index.tsx @@ -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); diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/index.ts b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/index.ts index eba297dc34..882b100e64 100644 --- a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/index.ts @@ -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); diff --git a/packages/plugins/@nocobase/plugin-action-export/src/client/index.ts b/packages/plugins/@nocobase/plugin-action-export/src/client/index.ts index 9dbd00b52b..aa14c0f8c0 100644 --- a/packages/plugins/@nocobase/plugin-action-export/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-action-export/src/client/index.ts @@ -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'); diff --git a/packages/plugins/@nocobase/plugin-action-import/src/client/index.ts b/packages/plugins/@nocobase/plugin-action-import/src/client/index.ts index d7c908f1c6..25a89e0419 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/client/index.ts @@ -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'); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarActionInitializers.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarActionInitializers.tsx index 6bd779ec11..78dca46f84 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarActionInitializers.tsx @@ -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'), }, ], }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarFormActionInitializers.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarFormActionInitializers.tsx index 48e1ff9998..dd99d48fa4 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarFormActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/initializers/CalendarFormActionInitializers.tsx @@ -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 = { @@ -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'; - }, }, ], }, diff --git a/packages/plugins/@nocobase/plugin-collection-sql/src/server/sql-collection/sql-collection.ts b/packages/plugins/@nocobase/plugin-collection-sql/src/server/sql-collection/sql-collection.ts index 9c6e807378..b5cb001d7c 100644 --- a/packages/plugins/@nocobase/plugin-collection-sql/src/server/sql-collection/sql-collection.ts +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/server/sql-collection/sql-collection.ts @@ -24,6 +24,10 @@ export class SQLCollection extends Collection { return true; } + unavailableActions(): Array { + return ['create', 'update', 'destroy']; + } + public collectionSchema() { return undefined; } diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts index 317595a6f2..c4833ab77c 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts @@ -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()); + }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/models/collection.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/models/collection.ts index f65c77f4fb..18efa02366 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/models/collection.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/models/collection.ts @@ -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; } diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx index 57fac41ec4..832af9de63 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx @@ -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 ( - + { useBulkDestroySubField, useSelectedRowKeys, useAsyncDataSource, + loadFilterTargetKeys, loadCategories, - loadDBViews, loadStorages, useNewId, useCancelAction, diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTabs.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTabs.tsx index eb5526253f..d9201a9ca3 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTabs.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTabs.tsx @@ -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; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx index 1499dc21b7..e22b5b56a9 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx @@ -22,7 +22,7 @@ const ReadPretty = (props) => { } = useCollectionRecord() as any; return (
- {name !== tableName ? ( + {name !== tableName && tableName ? ( <> {name} ({tableName}) diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx index 992227d220..df322cc4f7 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx @@ -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 ( { SyncFieldsActionCom, SyncSQLFieldsAction, }} + scope={{ + allowCollectionDeletion: !!type?.allowCollectionDeletion, + disabledConfigureFields: type?.disabledConfigureFields, + disableAddFields: type?.disableAddFields, + allowCollectionCreate: !!type?.allowCollectionCreate, + }} /> ); }; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collectionFields.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collectionFields.ts index 682a4d981a..0d21b5d5bf 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collectionFields.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collectionFields.ts @@ -125,6 +125,7 @@ export const fieldsTableSchema: ISchema = { 'x-component-props': { type: 'primary', }, + 'x-hidden': '{{ disableAddFields }}', }, }, }, diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts index fcc411cddd..14f1251d51 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts @@ -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', diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CreateDatabaseConnectAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CreateDatabaseConnectAction.tsx index 9a5699b694..976152e2c8 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CreateDatabaseConnectAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CreateDatabaseConnectAction.tsx @@ -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")}}', diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/EditDatabaseConnectionAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/EditDatabaseConnectionAction.tsx index fd823a6d8f..7787737a18 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/EditDatabaseConnectionAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/EditDatabaseConnectionAction.tsx @@ -103,6 +103,7 @@ export const EditDatabaseConnectionAction = () => { 'x-component-props': { useAction: '{{ useTestConnectionAction }}', }, + 'x-hidden': type?.disableTestConnection, }, submit: { title: '{{t("Submit")}}', diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/AddCategoryAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/AddCategoryAction.tsx index 7ec913b944..8fb5ea77e5 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/AddCategoryAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/AddCategoryAction.tsx @@ -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() { diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx index 6ed2d5f809..1d68668885 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx @@ -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(); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTabs.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTabs.tsx index 97db883dbe..75335e9bb4 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTabs.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTabs.tsx @@ -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); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/EditCategoryAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/EditCategoryAction.tsx index 8537143283..060d1f6e50 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/EditCategoryAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/EditCategoryAction.tsx @@ -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(); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/index.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/index.tsx index 2ddfa6464c..6b9207ce7d 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/index.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/index.tsx @@ -29,7 +29,7 @@ import { SyncSQLFieldsAction, DeleteCollection, DeleteCollectionAction, - CollectionCategroriesProvider, + CollectionCategoriesProvider, } from '@nocobase/client'; import { AddCategory, @@ -55,7 +55,7 @@ export const MainDataSourceManager = () => { { style={{ padding: '0px' }} disabled={!record.enabled} onClick={() => { - navigate(getConnectionCollectionPath(record.key)); + navigate(getConnectionCollectionPath(record)); }} role="button" aria-label={`${record?.key}-Configure`} diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/constant.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/constant.ts index 0dd1cfef08..c37046c5f7 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/constant.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/constant.ts @@ -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}`; +}; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-source-collections.test.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-source-collections.test.ts new file mode 100644 index 0000000000..8d54da7a73 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-source-collections.test.ts @@ -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 { + return Promise.resolve(true); + } + + async load(): Promise { + 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('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('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(); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-sources.test.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-sources.test.ts index d51e95d77b..1812ab8d28 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-sources.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/data-sources.test.ts @@ -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(); }); }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/collections/data-sources.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/collections/data-sources.ts index 33abc24c6c..50aadf8154 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/collections/data-sources.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/collections/data-sources.ts @@ -48,6 +48,7 @@ export default defineCollection({ name: 'collections', target: 'dataSourcesCollections', foreignKey: 'dataSourceKey', + targetKey: 'name', }, { type: 'hasMany', diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-collection-model.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-collection-model.ts index c8b1a77085..a24f469091 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-collection-model.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-collection-model.ts @@ -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; } } diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-field-model.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-field-model.ts index 3427ce48bf..c4b3ca74e3 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-field-model.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/models/data-sources-field-model.ts @@ -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); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts index 0a86c9a987..8df2e0677f 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts @@ -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')); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections-fields.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections-fields.ts index 2870f71893..61a9aff450 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections-fields.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections-fields.ts @@ -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(); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections.ts index 4abbfde784..095875b335 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/resourcers/data-sources-collections.ts @@ -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(); diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttActionInitializers.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttActionInitializers.tsx index 307209f044..594c53a0fc 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttActionInitializers.tsx @@ -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; }, diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphDrawPage.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphDrawPage.tsx index b7d2d31f5a..22e9d40ba3 100644 --- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphDrawPage.tsx +++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphDrawPage.tsx @@ -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(() => { - + {/* TODO: 因为画布中的卡片是一次性注册进 Graph 的,这里的 theme 是存在闭包里的,因此当主题动态变更时,并不会触发卡片的重新渲染 */}
@@ -531,7 +531,7 @@ export const GraphDrawPage = React.memo(() => {
-
+
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx index 100917b445..6597f054d2 100644 --- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx +++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx @@ -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(); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanActionInitializers.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanActionInitializers.tsx index cec4eedc41..1a86f8475e 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanActionInitializers.tsx @@ -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'), }, ], }; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx index 7c6234e657..7956afbd5e 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx @@ -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', diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts index 7852e5511a..92cb14d3a0 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts @@ -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 = { diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts index b9194292d3..caea2b38eb 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts @@ -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', diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts index d943ecb244..e61ecde6ff 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts @@ -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', diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts index 2365983be5..b24aa23400 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts @@ -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', diff --git a/packages/plugins/@nocobase/plugin-workflow-mailer/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-mailer/src/server/__tests__/instruction.test.ts new file mode 100644 index 0000000000..e5c8b883e4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-mailer/src/server/__tests__/instruction.test.ts @@ -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); + }); + }); +});