mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-29 18:58:26 +08:00
chore: data source api (#4588)
* refactor: support api datasource * refactor: support api datasource * refactor: support api datasource * feat: url support preview (#4559) * feat: url support preview * feat: add settings for Input.Preview * refactor: refactor CollectionField.tsx to use dynamic component * Revert "refactor: refactor CollectionField.tsx to use dynamic component" This reverts commit 37719eb28e5866762459da3b269288340a21b661. * test: add e2e test * refactor(e2e): extract template --------- Co-authored-by: Zeke Zhang <958414905@qq.com> * fix(map): amap reset (#4574) * chore: volta node version * chore: data source api * refactor: support api datasource * chore: data source api * chore: data source api * chore: field options * refactor: support api data source * refactor: support api data source * refactor: support api data source * refactor: support api data source * refactor: support api data source * refactor: support api data source * refactor: support api data source * fix: load data source fields * refactor: support api data source * feat(data-vi): support for using url params and current role variables (#4586) * feat(data-vi): support for using url params and current role variable * fix: bug * fix(variable): should remove through collection field (#4590) * fix: style issues for gridCard in mobile client (#4593) * fix: style issiues for gridCard in mobile client * fix: bug * fix: bug * fix: bug * fix: style issues for gridCard in mobile client (#4599) * fix: style issiues for gridCard in mobile client * fix: bug * fix: bug * fix: bug * fix: style improve * chore: update lerna * chore(versions): 😊 publish v1.0.1-alpha.1 * chore: update changelog * chore: fix typo (#4589) * feat(plugin-workflow-smtp-mailer): add new plugin for sending email in workflow (#4584) * feat(plugin-workflow-smtp-mailer): add new plugin for sending email in workflow * refactor(plugin-workflow-mailer): change plugin name and locales * fix(plugin-workflow-mailer): fix parameters * fix(preset): add new plugin to preset * fix(plugin-workflow-mailer): fix locale namespace * fix: iframe block loses height when set to default (#4602) * fix: iframe block loses height when set to default * refactor: local improve * feat(client): allow JSON5 value in Form Input of type JSONTextArea (#4600) * feat(client): allow json5 value in form-item type json component * test: fix tests * chore: set json5 default as false * chore: add demo --------- Co-authored-by: xilesun <2013xile@gmail.com> * fix: remove grid wrap (#4612) * refactor(plugin-workflow): change variable getter from collection fields (#4567) * refactor(plugin-workflow): change variable getter from collection fields * fix(plugin-workflow): fix import * chore(plugin-workflow-action-trigger): remove unused import * fix(plugin-workflow): fix collection field in workflow variable * refactor(plugin-workflow-manual): avoid tslint error * fix(client): fix text wrap in variable input (#4605) * fix(client): fix text wrap in variable input * fix(client): revert css * feat(tree-block): support filtering child nodes (#4603) * feat(tree-block): support filtering child nodes * test: add list test * test: remove only * fix: use isValidFilter * fix(export): export button remaining in loading state after cancel (#4615) * chore: rebase * chore: collection option * fix: update data source fields * chore: console.log * refactor: support api data source * fix: data source test * fix: sync field in data source * chore: unavailableActions in collection option * chore: unavailableActions in data source collections * chore: file collection unavailableActions * fix: test * chore: unavailableActions in actionInitializers * fix: bug * fix: destroy action * chore: unavailableActions in plugin actionInitializers * fix: view * chore: unavailableActions in actionInitializers * fix: missing removeCollection * chore: test name * fix: bug * fix: bug * chore: test * refactor: availableTypes * refactor: availableTypes * chore: datasource options * refactor: get current data source * refactor: code improve * fix: update collection with fields * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * chore: datasource logger * chore: export Schema * refactor: rawTextArea * refactor: loadFilterTargetKeys in external data source * chore: unavailableFunctions * refactor: support unAvailableFunctions * refactor: support unAvailableFunctions * refactor: support unAvailableFunctions * refactor: support unAvailableFunctions * refactor: support unAvailableFunctions * revert: unavailableFunctions * refactor: code improve * fix: test * chore: operation * chore: operation * fix: input support json field * refactor: blockInitializers support unavailableActions * chore: availableActions * chore: availableActions * refactor: support availableActions * refactor: support availableActions * chore: magic model test case * fix: unixTimestamp support integer * fix: disassociate * fix: input readPretty * fix: resolve error when opening modal via URL * style: input readPretty style improve * chore: support simple pagination * chore: skip test * refactor: details support pagination * refactor: details support pagination * chore: useActionAvailable * fix: bug * test: fix test * fix: detailsBlockInitializer useActionAvailable * chore: fix unit test * refactor: pagination * refactor: pagination * test: fix test --------- Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Zeke Zhang <958414905@qq.com> Co-authored-by: YANG QIA <2013xile@gmail.com> Co-authored-by: Katherine <shunai.tang@hand-china.com> Co-authored-by: GitHub Actions Bot <actions@github.com> Co-authored-by: Junyi <mytharcher@users.noreply.github.com> Co-authored-by: David Fecke <david.fecke@eyecook.net>
This commit is contained in:
parent
b1e68dac9c
commit
dd08a1f5c7
@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({
|
||||
'x-component': 'Hello',
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -81,9 +81,9 @@ const myInitializer = new SchemaInitializer({
|
||||
'x-component': 'Hello',
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
|
@ -14,7 +14,7 @@ import { CollectionManagerProvider } from '../data-source/collection/CollectionM
|
||||
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
||||
import { useCollectionHistory } from './CollectionHistoryProvider';
|
||||
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
|
||||
import { CollectionCategroriesContext } from './context';
|
||||
import { CollectionCategoriesContext } from './context';
|
||||
import { CollectionManagerOptions } from './types';
|
||||
|
||||
/**
|
||||
@ -62,16 +62,16 @@ export const RemoteCollectionManagerProvider = (props: any) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<CollectionCategroriesProvider service={result} refreshCategory={refreshCategory}>
|
||||
<CollectionCategoriesProvider service={result} refreshCategory={refreshCategory}>
|
||||
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
|
||||
</CollectionCategroriesProvider>
|
||||
</CollectionCategoriesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionCategroriesProvider = (props) => {
|
||||
export const CollectionCategoriesProvider = (props) => {
|
||||
const { service, refreshCategory } = props;
|
||||
return (
|
||||
<CollectionCategroriesContext.Provider
|
||||
<CollectionCategoriesContext.Provider
|
||||
value={{
|
||||
data: service?.data?.data,
|
||||
refresh: refreshCategory,
|
||||
@ -79,6 +79,6 @@ export const CollectionCategroriesProvider = (props) => {
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</CollectionCategroriesContext.Provider>
|
||||
</CollectionCategoriesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -35,3 +35,4 @@ export * from './mixins/InheritanceCollectionMixin';
|
||||
export * from './sub-table';
|
||||
export * from './CollectionHistoryProvider';
|
||||
export * from './utils';
|
||||
export { UnSupportFields } from './templates/components/UnSupportFields';
|
||||
|
@ -26,7 +26,7 @@ export class CheckboxGroupFieldInterface extends CollectionFieldInterface {
|
||||
'x-component': 'Checkbox.Group',
|
||||
},
|
||||
};
|
||||
availableTypes = ['array'];
|
||||
availableTypes = ['array', 'json'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -27,7 +27,7 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
|
||||
},
|
||||
},
|
||||
};
|
||||
availableTypes = ['date', 'dateOnly'];
|
||||
availableTypes = ['date', 'dateOnly', 'string'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -26,7 +26,7 @@ export class MarkdownFieldInterface extends CollectionFieldInterface {
|
||||
'x-component': 'Markdown',
|
||||
},
|
||||
};
|
||||
availableTypes = ['text', 'json'];
|
||||
availableTypes = ['text', 'json', 'string'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -29,7 +29,7 @@ export class MultipleSelectFieldInterface extends CollectionFieldInterface {
|
||||
enum: [],
|
||||
},
|
||||
};
|
||||
availableTypes = ['array'];
|
||||
availableTypes = ['array', 'json'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -26,7 +26,7 @@ export class RichTextFieldInterface extends CollectionFieldInterface {
|
||||
'x-component': 'RichText',
|
||||
},
|
||||
};
|
||||
availableTypes = ['text', 'json'];
|
||||
availableTypes = ['text', 'json', 'string'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -26,7 +26,7 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
};
|
||||
availableTypes = ['text', 'json'];
|
||||
availableTypes = ['text', 'json', 'string'];
|
||||
hasDefaultValue = true;
|
||||
properties = {
|
||||
...defaultProps,
|
||||
|
@ -27,19 +27,26 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
||||
const { action, params = {}, record, requestService, requestOptions } = options;
|
||||
|
||||
const service = useMemo(() => {
|
||||
return requestService
|
||||
? requestService
|
||||
: (customParams) => {
|
||||
if (record) return Promise.resolve({ data: record });
|
||||
if (!action) {
|
||||
throw new Error(
|
||||
`[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`,
|
||||
);
|
||||
}
|
||||
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
|
||||
return (
|
||||
requestService ||
|
||||
((customParams) => {
|
||||
if (record) return Promise.resolve({ data: record });
|
||||
if (!action) {
|
||||
throw new Error(`[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`);
|
||||
}
|
||||
|
||||
return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data);
|
||||
};
|
||||
// fix https://nocobase.height.app/T-4876/description
|
||||
if (action === 'get' && _.isNil(params.filterByTk)) {
|
||||
return console.warn(
|
||||
'[nocobase]: The "filterByTk" parameter is missing in the "DataBlockRequestProvider" component',
|
||||
);
|
||||
}
|
||||
|
||||
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
|
||||
|
||||
return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data);
|
||||
})
|
||||
);
|
||||
}, [resource, action, JSON.stringify(params), JSON.stringify(record), requestService]);
|
||||
|
||||
const request = useRequest<T>(service, {
|
||||
|
@ -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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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: [
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -16,6 +16,7 @@ import { findFilterTargets } from '../../../../../block-provider/hooks';
|
||||
import { useFilterBlock } from '../../../../../filter-provider/FilterProvider';
|
||||
import { mergeFilter } from '../../../../../filter-provider/utils';
|
||||
import { removeNullCondition } from '../../../../../schema-component';
|
||||
import { useCollection } from '../../../../../data-source';
|
||||
|
||||
export const useTableBlockProps = () => {
|
||||
const field = useField<ArrayField>();
|
||||
@ -25,7 +26,6 @@ export const useTableBlockProps = () => {
|
||||
const { getDataBlocks } = useFilterBlock();
|
||||
const isLoading = ctx?.service?.loading;
|
||||
const params = useMemo(() => ctx?.service?.params, [JSON.stringify(ctx?.service?.params)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
const serviceResponse = ctx?.service?.data;
|
||||
|
@ -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',
|
||||
|
@ -9,3 +9,4 @@
|
||||
|
||||
export * from './data-blocks/details-multi';
|
||||
export * from './data-blocks/details-single';
|
||||
export * from './useActionAvailable';
|
||||
|
@ -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;
|
||||
};
|
@ -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',
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { Empty } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { useDataBlockRequest } from '../../../data-source';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { FormV2 } from '../form-v2';
|
||||
@ -20,9 +21,14 @@ export type DetailsProps = FormProps;
|
||||
export const Details = withDynamicSchemaProps(
|
||||
(props: DetailsProps) => {
|
||||
const request = useDataBlockRequest();
|
||||
|
||||
const schema = useFieldSchema();
|
||||
if (!request?.loading && _.isEmpty(request?.data?.data)) {
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
return (
|
||||
<>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<RecursionField schema={schema.properties.pagination} name="pagination" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <FormV2 {...props} />;
|
||||
|
@ -64,6 +64,7 @@ const schema: ISchema = {
|
||||
collection: 'users',
|
||||
resource: 'users',
|
||||
action: 'get',
|
||||
filterByTk: 1,
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
|
@ -40,6 +40,7 @@ const schema: ISchema = {
|
||||
collection: 'users',
|
||||
resource: 'users',
|
||||
action: 'get',
|
||||
filterByTk: 1,
|
||||
},
|
||||
properties: {
|
||||
form: {
|
||||
|
@ -46,10 +46,12 @@ ReadPretty.Input = (props: InputReadPrettyProps) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const compile = useCompile();
|
||||
return (
|
||||
<div className={cls(prefixCls, props.className)} style={props.style}>
|
||||
<div className={cls(prefixCls, props.className)} style={{ ...props.style, overflowWrap: 'break-word' }}>
|
||||
{props.addonBefore}
|
||||
{props.prefix}
|
||||
<EllipsisWithTooltip ellipsis={props.ellipsis}>{compile(props.value)}</EllipsisWithTooltip>
|
||||
<EllipsisWithTooltip ellipsis={props.ellipsis}>
|
||||
{props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value)}
|
||||
</EllipsisWithTooltip>
|
||||
{props.suffix}
|
||||
{props.addonAfter}
|
||||
</div>
|
||||
|
@ -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) => {
|
||||
|
@ -10,8 +10,6 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Button, Input } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { VariableSelect } from './VariableSelect';
|
||||
|
||||
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
|
||||
@ -27,9 +25,9 @@ function setNativeInputValue(input, value) {
|
||||
|
||||
export function RawTextArea(props): JSX.Element {
|
||||
const inputRef = useRef<any>(null);
|
||||
const { changeOnSelect, component: Component = Input.TextArea, ...others } = props;
|
||||
const scope = typeof props.scope === 'function' ? props.scope() : props.scope;
|
||||
const [options, setOptions] = useState(scope ? scope : []);
|
||||
const { changeOnSelect, component: Component = Input.TextArea, fieldNames, scope, ...others } = props;
|
||||
const dataScope = typeof scope === 'function' ? scope() : scope;
|
||||
const [options, setOptions] = useState(dataScope ? dataScope : []);
|
||||
|
||||
function onInsert(selected) {
|
||||
if (!inputRef.current) {
|
||||
@ -77,7 +75,7 @@ export function RawTextArea(props): JSX.Element {
|
||||
background-color: transparent;
|
||||
`
|
||||
}
|
||||
fieldNames={props.fieldNames}
|
||||
fieldNames={fieldNames}
|
||||
options={options}
|
||||
setOptions={setOptions}
|
||||
onInsert={onInsert}
|
||||
|
@ -20,6 +20,7 @@ export interface ISchemaComponentContext {
|
||||
setDesignable?: (value: boolean) => void;
|
||||
SchemaField?: React.FC<ISchemaFieldProps>;
|
||||
distributed?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ISchemaComponentProvider {
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -83,6 +83,6 @@ describe('Collection Manager', () => {
|
||||
|
||||
const UsersCollection = collectionManager.getCollection('users');
|
||||
|
||||
expect(UsersCollection.repository).toBe(MockRepository);
|
||||
expect(UsersCollection.repository).toBeInstanceOf(MockRepository);
|
||||
});
|
||||
});
|
||||
|
@ -16,13 +16,24 @@ import {
|
||||
IRepository,
|
||||
MergeOptions,
|
||||
} from './types';
|
||||
import { DataSource } from './data-source';
|
||||
import { Repository } from './repository';
|
||||
|
||||
export class CollectionManager implements ICollectionManager {
|
||||
public dataSource: DataSource;
|
||||
protected collections = new Map<string, ICollection>();
|
||||
protected repositories = new Map<string, IRepository>();
|
||||
protected models = new Map<string, any>();
|
||||
|
||||
constructor(options = {}) {}
|
||||
constructor(options: any = {}) {
|
||||
if (options.dataSource) {
|
||||
this.dataSource = options.dataSource;
|
||||
}
|
||||
|
||||
this.registerRepositories({
|
||||
Repository: Repository,
|
||||
});
|
||||
}
|
||||
|
||||
/* istanbul ignore next -- @preserve */
|
||||
getRegisteredFieldType(type) {}
|
||||
@ -103,6 +114,10 @@ export class CollectionManager implements ICollectionManager {
|
||||
|
||||
async sync() {}
|
||||
|
||||
removeCollection(name: string): void {
|
||||
this.collections.delete(name);
|
||||
}
|
||||
|
||||
protected newCollection(options): ICollection {
|
||||
// @ts-ignore
|
||||
return new Collection(options, this);
|
||||
|
@ -9,29 +9,30 @@
|
||||
|
||||
import { CollectionOptions, ICollection, ICollectionManager, IField, IRepository } from './types';
|
||||
import { default as lodash } from 'lodash';
|
||||
import merge from 'deepmerge';
|
||||
import { CollectionField } from './collection-field';
|
||||
|
||||
export class Collection implements ICollection {
|
||||
repository: IRepository;
|
||||
fields: Map<string, IField> = new Map<string, IField>();
|
||||
|
||||
constructor(
|
||||
protected options: CollectionOptions,
|
||||
protected collectionManager: ICollectionManager,
|
||||
) {
|
||||
constructor(protected options: CollectionOptions, protected collectionManager: ICollectionManager) {
|
||||
this.setRepository(options.repository);
|
||||
|
||||
if (options.fields) {
|
||||
this.setFields(options.fields);
|
||||
}
|
||||
}
|
||||
|
||||
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||
let newOptions = lodash.cloneDeep(options);
|
||||
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||
const newOptions = {
|
||||
...this.options,
|
||||
...lodash.cloneDeep(options),
|
||||
};
|
||||
|
||||
this.options = newOptions;
|
||||
|
||||
this.setFields(newOptions.fields || []);
|
||||
|
||||
if (options.repository) {
|
||||
this.setRepository(options.repository);
|
||||
}
|
||||
@ -40,6 +41,11 @@ export class Collection implements ICollection {
|
||||
}
|
||||
|
||||
setFields(fields: any[]) {
|
||||
const fieldNames = this.fields.keys();
|
||||
for (const fieldName of fieldNames) {
|
||||
this.removeField(fieldName);
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
this.setField(field.name, field);
|
||||
}
|
||||
@ -64,6 +70,7 @@ export class Collection implements ICollection {
|
||||
}
|
||||
|
||||
protected setRepository(repository: any) {
|
||||
this.repository = this.collectionManager.getRegisteredRepository(repository || 'Repository');
|
||||
const RepositoryClass = this.collectionManager.getRegisteredRepository(repository || 'Repository');
|
||||
this.repository = new RepositoryClass(this);
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,10 @@ export abstract class DataSource extends EventEmitter {
|
||||
return new ResourceManager(options);
|
||||
}
|
||||
|
||||
publicOptions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async load(options: any = {}) {}
|
||||
async close() {}
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -91,6 +91,8 @@ export class SequelizeCollectionManager implements ICollectionManager {
|
||||
return this.db.getCollection(name);
|
||||
}
|
||||
|
||||
removeCollection(name: string) {}
|
||||
|
||||
getCollections() {
|
||||
const collectionsFilter = this.collectionsFilter();
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
export type CollectionOptions = {
|
||||
name: string;
|
||||
repository?: string;
|
||||
filterTargetKey?: string;
|
||||
fields: any[];
|
||||
[key: string]: any;
|
||||
};
|
||||
@ -24,8 +25,8 @@ export type FieldOptions = {
|
||||
uiSchema?: any;
|
||||
possibleTypes?: string[];
|
||||
defaultValue?: any;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
primaryKey?: boolean;
|
||||
unique?: boolean;
|
||||
allowNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
[key: string]: any;
|
||||
@ -33,6 +34,7 @@ export type FieldOptions = {
|
||||
|
||||
export interface IField {
|
||||
options: FieldOptions;
|
||||
|
||||
isRelationField(): boolean;
|
||||
}
|
||||
|
||||
@ -44,6 +46,7 @@ export interface IFieldInterface {
|
||||
options: FieldOptions;
|
||||
|
||||
toString(value: any, ctx?: any): string;
|
||||
|
||||
toValue(str: string, ctx?: any): any;
|
||||
}
|
||||
|
||||
@ -63,6 +66,9 @@ export interface ICollection {
|
||||
getField(name: string): IField;
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
unavailableActions?: () => string[];
|
||||
availableActions?: () => string[];
|
||||
}
|
||||
|
||||
export interface IModel {
|
||||
@ -106,9 +112,9 @@ export interface ICollectionManager {
|
||||
|
||||
registerModels(models: Record<string, any>): void;
|
||||
|
||||
registerRepositories(repositories: Record<string, any>): void;
|
||||
registerRepositories(repositories: Record<string, new (collection: ICollection) => IRepository>): void;
|
||||
|
||||
getRegisteredRepository(key: string): IRepository;
|
||||
getRegisteredRepository(key: string): new (collection: ICollection) => IRepository;
|
||||
|
||||
defineCollection(options: CollectionOptions): ICollection;
|
||||
|
||||
@ -120,6 +126,8 @@ export interface ICollectionManager {
|
||||
|
||||
getCollections(): Array<ICollection>;
|
||||
|
||||
removeCollection(name: string): void;
|
||||
|
||||
getRepository(name: string, sourceId?: string | number): IRepository;
|
||||
|
||||
sync(): Promise<void>;
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -21,6 +21,14 @@ export class ViewCollection extends Collection {
|
||||
return true;
|
||||
}
|
||||
|
||||
unavailableActions(): Array<string> {
|
||||
if (this.options.writableView) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ['create', 'update', 'destroy'];
|
||||
}
|
||||
|
||||
protected sequelizeModelOptions(): any {
|
||||
const modelOptions = super.sequelizeModelOptions();
|
||||
modelOptions.tableName = this.options.viewName || this.options.name;
|
||||
|
@ -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;
|
||||
|
@ -36,3 +36,4 @@ export * from './url';
|
||||
export * from './i18n';
|
||||
|
||||
export { dayjs, lodash };
|
||||
export { Schema } from '@formily/json-schema';
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -7,7 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaInitializer, SchemaInitializerItemType, useCollection_deprecated } from '@nocobase/client';
|
||||
import {
|
||||
SchemaInitializer,
|
||||
SchemaInitializerItemType,
|
||||
useCollection_deprecated,
|
||||
useActionAvailable,
|
||||
} from '@nocobase/client';
|
||||
import { generateNTemplate } from '../../../locale';
|
||||
|
||||
export const deleteEventActionInitializer: SchemaInitializerItemType<any> = {
|
||||
@ -52,10 +57,7 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
useVisible() {
|
||||
const collection = useCollection_deprecated();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
useVisible: () => useActionAvailable('update'),
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
@ -65,10 +67,7 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
useVisible: function useVisible() {
|
||||
const collection = useCollection_deprecated();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
useVisible: () => useActionAvailable('destroy'),
|
||||
},
|
||||
deleteEventActionInitializer,
|
||||
],
|
||||
@ -157,19 +156,12 @@ export const CalendarFormActionInitializers = new SchemaInitializer({
|
||||
triggerWorkflows: [],
|
||||
},
|
||||
},
|
||||
useVisible() {
|
||||
const collection = useCollection_deprecated();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
useVisible: () => useActionAvailable('update'),
|
||||
},
|
||||
{
|
||||
name: 'customRequest',
|
||||
title: generateNTemplate('Custom request'),
|
||||
Component: 'CustomRequestInitializer',
|
||||
useVisible() {
|
||||
const collection = useCollection_deprecated();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -24,6 +24,10 @@ export class SQLCollection extends Collection {
|
||||
return true;
|
||||
}
|
||||
|
||||
unavailableActions(): Array<string> {
|
||||
return ['create', 'update', 'destroy'];
|
||||
}
|
||||
|
||||
public collectionSchema() {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { useForm, useField } from '@formily/react';
|
||||
import { action } from '@formily/reactive';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import React, { useContext, useMemo, useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
@ -20,7 +20,7 @@ import {
|
||||
SchemaComponent,
|
||||
SchemaComponentContext,
|
||||
useCompile,
|
||||
CollectionCategroriesContext,
|
||||
CollectionCategoriesContext,
|
||||
useCollectionManager_deprecated,
|
||||
useCancelAction,
|
||||
AddSubFieldAction,
|
||||
@ -33,7 +33,6 @@ import {
|
||||
import { message } from 'antd';
|
||||
import { getCollectionSchema } from './schema/collections';
|
||||
import { CollectionFields } from './CollectionFields';
|
||||
import { EditCollection } from './EditCollectionAction';
|
||||
import { DataSourceContext } from '../../DatabaseConnectionProvider';
|
||||
|
||||
/**
|
||||
@ -95,9 +94,8 @@ export const ConfigurationTable = () => {
|
||||
const ds = useDataSourceManager();
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const { name } = useParams();
|
||||
const data = useContext(CollectionCategroriesContext);
|
||||
const data = useContext(CollectionCategoriesContext);
|
||||
const api = useAPIClient();
|
||||
const resource = api.resource('dbViews');
|
||||
const compile = useCompile();
|
||||
const loadCategories = async () => {
|
||||
return data.data.map((item: any) => ({
|
||||
@ -106,18 +104,6 @@ export const ConfigurationTable = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const loadDBViews = async () => {
|
||||
return resource.list().then(({ data }) => {
|
||||
return data?.data?.map((item: any) => {
|
||||
const schema = item.schema;
|
||||
return {
|
||||
label: schema ? `${schema}.${compile(item.name)}` : item.name,
|
||||
value: schema ? `${schema}_${item.name}` : item.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const loadStorages = async () => {
|
||||
return api
|
||||
.resource('storages')
|
||||
@ -165,12 +151,43 @@ export const ConfigurationTable = () => {
|
||||
const collectionSchema = useMemo(() => {
|
||||
return getCollectionSchema(name);
|
||||
}, [name]);
|
||||
|
||||
const resource = api.resource('dataSources', name);
|
||||
const [dataSourceData, setDataSourceData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
resource
|
||||
.get({
|
||||
filterByTk: name,
|
||||
})
|
||||
.then((data) => {
|
||||
setDataSourceData(data?.data);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
const loadFilterTargetKeys = async (field) => {
|
||||
const { fields } = field.form.values;
|
||||
return Promise.resolve({
|
||||
data: fields,
|
||||
}).then(({ data }) => {
|
||||
return data?.map((item: any) => {
|
||||
return {
|
||||
label: compile(item.uiSchema?.title) || item.name,
|
||||
value: item.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: false, dataSourceData }}>
|
||||
<SchemaComponent
|
||||
schema={collectionSchema}
|
||||
components={{
|
||||
EditCollection,
|
||||
AddSubFieldAction,
|
||||
EditSubFieldAction,
|
||||
FieldSummary,
|
||||
@ -183,8 +200,8 @@ export const ConfigurationTable = () => {
|
||||
useBulkDestroySubField,
|
||||
useSelectedRowKeys,
|
||||
useAsyncDataSource,
|
||||
loadFilterTargetKeys,
|
||||
loadCategories,
|
||||
loadDBViews,
|
||||
loadStorages,
|
||||
useNewId,
|
||||
useCancelAction,
|
||||
|
@ -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;
|
||||
|
@ -22,7 +22,7 @@ const ReadPretty = (props) => {
|
||||
} = useCollectionRecord() as any;
|
||||
return (
|
||||
<div className={cls(prefixCls, props.className)} style={props.style}>
|
||||
{name !== tableName ? (
|
||||
{name !== tableName && tableName ? (
|
||||
<>
|
||||
{name} <span style={{ color: 'GrayText' }}>({tableName})</span>
|
||||
</>
|
||||
|
@ -16,8 +16,6 @@ import {
|
||||
AddCollectionAction,
|
||||
AddCollectionField,
|
||||
AddFieldAction,
|
||||
EditCollection,
|
||||
EditCollectionAction,
|
||||
EditCollectionField,
|
||||
EditFieldAction,
|
||||
OverridingCollectionField,
|
||||
@ -29,10 +27,14 @@ import {
|
||||
SyncSQLFieldsAction,
|
||||
DeleteCollection,
|
||||
DeleteCollectionAction,
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategoriesProvider,
|
||||
usePlugin,
|
||||
} from '@nocobase/client';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ConfigurationTable } from './ConfigurationTable';
|
||||
import { ConfigurationTabs } from './ConfigurationTabs';
|
||||
import PluginDatabaseConnectionsClient from '../../';
|
||||
import { EditCollection } from './EditCollectionAction';
|
||||
|
||||
const schema2: ISchema = {
|
||||
type: 'object',
|
||||
@ -44,20 +46,23 @@ const schema2: ISchema = {
|
||||
};
|
||||
|
||||
export const CollectionManagerPage = () => {
|
||||
const plugin = usePlugin(PluginDatabaseConnectionsClient);
|
||||
const location = useLocation();
|
||||
const dataSourceType = new URLSearchParams(location.search).get('type');
|
||||
const type = dataSourceType && plugin.types.get(dataSourceType);
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema2}
|
||||
components={{
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategoriesProvider,
|
||||
ConfigurationTable,
|
||||
ConfigurationTabs,
|
||||
AddFieldAction,
|
||||
AddCollectionField,
|
||||
AddCollection,
|
||||
AddCollection: type?.AddCollection || AddCollection,
|
||||
AddCollectionAction,
|
||||
EditCollection,
|
||||
EditCollectionAction,
|
||||
DeleteCollection,
|
||||
EditCollection: type?.EditCollection || EditCollection,
|
||||
DeleteCollection: type?.DeleteCollection || DeleteCollection,
|
||||
DeleteCollectionAction,
|
||||
EditFieldAction,
|
||||
EditCollectionField,
|
||||
@ -69,6 +74,12 @@ export const CollectionManagerPage = () => {
|
||||
SyncFieldsActionCom,
|
||||
SyncSQLFieldsAction,
|
||||
}}
|
||||
scope={{
|
||||
allowCollectionDeletion: !!type?.allowCollectionDeletion,
|
||||
disabledConfigureFields: type?.disabledConfigureFields,
|
||||
disableAddFields: type?.disableAddFields,
|
||||
allowCollectionCreate: !!type?.allowCollectionCreate,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -125,6 +125,7 @@ export const fieldsTableSchema: ISchema = {
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
'x-hidden': '{{ disableAddFields }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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")}}',
|
||||
|
@ -103,6 +103,7 @@ export const EditDatabaseConnectionAction = () => {
|
||||
'x-component-props': {
|
||||
useAction: '{{ useTestConnectionAction }}',
|
||||
},
|
||||
'x-hidden': type?.disableTestConnection,
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
SyncSQLFieldsAction,
|
||||
DeleteCollection,
|
||||
DeleteCollectionAction,
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategoriesProvider,
|
||||
} from '@nocobase/client';
|
||||
import {
|
||||
AddCategory,
|
||||
@ -55,7 +55,7 @@ export const MainDataSourceManager = () => {
|
||||
<SchemaComponent
|
||||
schema={schema2}
|
||||
components={{
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategoriesProvider,
|
||||
ConfigurationTable,
|
||||
ConfigurationTabs,
|
||||
AddFieldAction,
|
||||
|
@ -38,7 +38,7 @@ export const ViewDatabaseConnectionAction = () => {
|
||||
style={{ padding: '0px' }}
|
||||
disabled={!record.enabled}
|
||||
onClick={() => {
|
||||
navigate(getConnectionCollectionPath(record.key));
|
||||
navigate(getConnectionCollectionPath(record));
|
||||
}}
|
||||
role="button"
|
||||
aria-label={`${record?.key}-Configure`}
|
||||
|
@ -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}`;
|
||||
};
|
||||
|
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createMockServer, MockServer, waitSecond } from '@nocobase/test';
|
||||
import { CollectionManager, DataSource } from '@nocobase/data-source-manager';
|
||||
import { HasManyRepository } from '@nocobase/database';
|
||||
|
||||
describe('data source collection', () => {
|
||||
let app: MockServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createMockServer({
|
||||
plugins: ['nocobase', 'data-source-manager'],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should update collection fields', async () => {
|
||||
class MockDataSource extends DataSource {
|
||||
static testConnection(options?: any): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
await waitSecond(1000);
|
||||
}
|
||||
|
||||
createCollectionManager(options?: any): any {
|
||||
return new CollectionManager(options);
|
||||
}
|
||||
}
|
||||
|
||||
app.dataSourceManager.factory.register('mock', MockDataSource);
|
||||
|
||||
await app.db.getRepository('dataSources').create({
|
||||
values: {
|
||||
key: 'mockInstance1',
|
||||
type: 'mock',
|
||||
displayName: 'Mock',
|
||||
options: {},
|
||||
},
|
||||
});
|
||||
|
||||
await waitSecond(2000);
|
||||
|
||||
const dataSource = app.dataSourceManager.get('mockInstance1');
|
||||
const collectionInDb = await app.db.getRepository('dataSourcesCollections').create({
|
||||
values: {
|
||||
name: 'test',
|
||||
title: 'Test',
|
||||
dataSourceKey: 'mockInstance1',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
type: 'integer',
|
||||
},
|
||||
],
|
||||
},
|
||||
updateAssociationValues: ['fields'],
|
||||
});
|
||||
|
||||
// should get collection from collection manager
|
||||
const collectionManager = dataSource.collectionManager;
|
||||
const collection = collectionManager.getCollection('test');
|
||||
expect(collection).toBeTruthy();
|
||||
expect(collection.getField('name')).toBeTruthy();
|
||||
expect(collection.getField('age')).toBeTruthy();
|
||||
|
||||
const collectionInJson = collectionInDb.toJSON();
|
||||
|
||||
// set name field ui schema
|
||||
collectionInJson.fields.forEach((field: any) => {
|
||||
if (field.name === 'name') {
|
||||
field.uiSchema = {
|
||||
title: 'Name',
|
||||
'x-component': 'Input',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// push a new fields
|
||||
collectionInJson.fields.push({
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
});
|
||||
|
||||
// update collection with fields
|
||||
await app.db.getRepository<HasManyRepository>('dataSources.collections', 'mockInstance1').update({
|
||||
filterByTk: 'test',
|
||||
values: collectionInJson,
|
||||
updateAssociationValues: ['fields'],
|
||||
});
|
||||
|
||||
// get collection from collection manager again
|
||||
const collection2 = collectionManager.getCollection('test');
|
||||
expect(collection2).toBeTruthy();
|
||||
|
||||
expect(collection2.getField('name')).toBeTruthy();
|
||||
expect(collection2.getField('age')).toBeTruthy();
|
||||
expect(collection2.getField('email')).toBeTruthy();
|
||||
|
||||
expect(collection2.getField('name').options.uiSchema).toEqual({
|
||||
title: 'Name',
|
||||
'x-component': 'Input',
|
||||
});
|
||||
|
||||
const collectionInDb2 = await app.db.getRepository('dataSourcesCollections').findOne({
|
||||
filter: {
|
||||
name: 'test',
|
||||
},
|
||||
appends: ['fields'],
|
||||
});
|
||||
|
||||
const collectionInJson2 = collectionInDb2.toJSON();
|
||||
// it should remove field in update
|
||||
collectionInJson2.fields = collectionInJson2.fields.filter((field: any) => field.name !== 'age');
|
||||
|
||||
await app.db.getRepository<HasManyRepository>('dataSources.collections', 'mockInstance1').update({
|
||||
filterByTk: 'test',
|
||||
values: collectionInJson2,
|
||||
updateAssociationValues: ['fields'],
|
||||
});
|
||||
|
||||
const collection3 = collectionManager.getCollection('test');
|
||||
expect(collection3).toBeTruthy();
|
||||
expect(collection3.getField('name')).toBeTruthy();
|
||||
expect(collection3.getField('age')).toBeFalsy();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -48,6 +48,7 @@ export default defineCollection({
|
||||
name: 'collections',
|
||||
target: 'dataSourcesCollections',
|
||||
foreignKey: 'dataSourceKey',
|
||||
targetKey: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -19,8 +19,8 @@ import { SchemaOptionsContext } from '@formily/react';
|
||||
import {
|
||||
APIClientProvider,
|
||||
ApplicationContext,
|
||||
CollectionCategroriesContext,
|
||||
CollectionCategroriesProvider,
|
||||
CollectionCategoriesContext,
|
||||
CollectionCategoriesProvider,
|
||||
CurrentAppInfoContext,
|
||||
DataSourceApplicationProvider,
|
||||
SchemaComponent,
|
||||
@ -390,7 +390,7 @@ export const GraphDrawPage = React.memo(() => {
|
||||
const {
|
||||
data: { database },
|
||||
} = currentAppInfo;
|
||||
const categoryCtx = useContext(CollectionCategroriesContext);
|
||||
const categoryCtx = useContext(CollectionCategoriesContext);
|
||||
const scope = { ...options?.scope };
|
||||
const components = { ...options?.components };
|
||||
const saveGraphPositionAction = async (data) => {
|
||||
@ -520,7 +520,7 @@ export const GraphDrawPage = React.memo(() => {
|
||||
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSource?.key}>
|
||||
<APIClientProvider apiClient={api}>
|
||||
<SchemaComponentOptions inherit scope={scope} components={components}>
|
||||
<CollectionCategroriesProvider {...categoryCtx}>
|
||||
<CollectionCategoriesProvider {...categoryCtx}>
|
||||
{/* TODO: 因为画布中的卡片是一次性注册进 Graph 的,这里的 theme 是存在闭包里的,因此当主题动态变更时,并不会触发卡片的重新渲染 */}
|
||||
<ConfigProvider theme={theme as any}>
|
||||
<div style={{ height: 'auto' }}>
|
||||
@ -531,7 +531,7 @@ export const GraphDrawPage = React.memo(() => {
|
||||
</App>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</CollectionCategroriesProvider>
|
||||
</CollectionCategoriesProvider>
|
||||
</SchemaComponentOptions>
|
||||
</APIClientProvider>
|
||||
</DataSourceApplicationProvider>
|
||||
|
@ -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();
|
||||
|
@ -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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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 = {
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user