feat: kanban& gantt&bulk edit& bulk update& duplicate& print action pluggable (#3019)
* fix: bug * test: kanban test * test: gantt block test * test: gantt test * test: gantt test * test: gantt test * refactor: convert parameters to destructured object * test: add tests choices * test: add tests media * test: add tests for datetime in creating form * feat(plugin-mock-collection): generate faker time * test: add tests for datetime in editing form * test: add tests for datetime in details form * test: gantt block test * test: gantt test * test: gantt test * test: gantt test * fix: bug * feat: improve code * test: add tests for relation fields * refactor: style improve * fix: rename SchemaSettings * fix: type bug * refactor: useDesinger() * fix: bug * fix: bug * fix: build tip * refactor: kanban setting * refactor: gantt setting * refactor: bulk update setting * refactor: bulk update setting * refactor: bulk edit settings * refactor: duplicate settings * refactor: print action setting * refactor: duplicate settings * refactor: bulk update refactor * refactor: bulk update refactor * fix: designableState * fix: bug * fix: designable * refactor: bulk update * fix: designable * test: add tests for relation fields * test: add tests for relation fields * test: add tests for relation fields * feat: client api doc * test: add tests for relation fields * test: avoid errors * test: make tests passing * fix: bug * test: gantt test * test: make tests passing * test: add tests for advanced fields * test: increase e2e timeout-minutes to 60 * fix: bug * fix: improve code * feat: add schema initailizer component demos * test: make tests passing * fix: schema settings demos * feat: shallowMerge & deepMerge * test: reduce number of tests * test: make tests passing * feat: updates * fix: add Initializer Internal * demos: useSchemaSettingsRender * test: make tests passing * test: make tests passing * test: bulk update test * refactor: useSchemaToolbar * fix: improve docs * fix: bug * test: duplicate action test * test: duplicate action test * test: duplicate action test * test: duplicate action test * chore: upgrade dumi theme * test: make tests passing * test: add tests for linkage rules * test: add test for form data templates * test: add tests for default value * test: reduce number of tests * refactor: bulk edit setting * test: bulk edit action test * test: bulk edit action test * fix: dn.deepMerge * fix: bug * test: bulk edit action test * test: kanban test * refactor: bulk update version * refactor: print action version * fix: bug * fix: toolbar * fix: docs ssr * test: add tests for system fields * test: add tests for actions * fix: bug * test: add tests for lazy loading of variables * test: make testing more stable * test: bulk edit test * test: bulk update action test * fix: update docs * test: print test * test: kanban test * test: kanban test * refactor: duplicate version * fix: merge bug * refactor: bulk update * fix: merge bug * refactor: bulk edit * refactor: import * refactor: export action * refactor: code improve * refactor: kanban code improve * fix: merge bug * refactor: readPrettyFormActionInitializers * test: print action test * test: print action test * refactor: print action * test: action test * refactor: gantt date change * style: gantt style improve * test: gantt &bulkUpdate test * test: bulk update test * test: duplicate action test * test: print action test * test: bulk edit action test * test: gantt block test * test: kanban block test * test: test file name * test: test file name * test: bulk update action tst * refactor: rename test file name * refactor: bulk update action * refactor: gantt test * fix: block template * fix: block template * fix: useFieldModeOptions * refactor: code improve * fix: merge bug --------- Co-authored-by: dream2023 <1098626505@qq.com> Co-authored-by: Rain <958414905@qq.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
823a112435
commit
b1610e6994
@ -7,9 +7,7 @@ import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvid
|
||||
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
|
||||
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
|
||||
import { GanttBlockProvider, useGanttBlockProps } from './GanttBlockProvider';
|
||||
import * as bp from './hooks';
|
||||
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
|
||||
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
|
||||
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
|
||||
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
||||
@ -19,7 +17,6 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
GanttBlockProvider,
|
||||
CalendarBlockProvider,
|
||||
TableFieldProvider,
|
||||
TableBlockProvider,
|
||||
@ -28,7 +25,6 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
FilterFormBlockProvider,
|
||||
FormFieldProvider,
|
||||
DetailsBlockProvider,
|
||||
KanbanBlockProvider,
|
||||
RecordLink,
|
||||
}}
|
||||
scope={{
|
||||
@ -43,8 +39,6 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
useTableFieldProps,
|
||||
useTableBlockProps,
|
||||
useTableSelectorProps,
|
||||
useKanbanBlockProps,
|
||||
useGanttBlockProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
@ -60,7 +54,6 @@ export class BlockSchemaComponentPlugin extends Plugin {
|
||||
|
||||
addComponents() {
|
||||
this.app.addComponents({
|
||||
GanttBlockProvider,
|
||||
CalendarBlockProvider,
|
||||
TableFieldProvider,
|
||||
TableBlockProvider,
|
||||
@ -69,7 +62,6 @@ export class BlockSchemaComponentPlugin extends Plugin {
|
||||
FilterFormBlockProvider,
|
||||
FormFieldProvider,
|
||||
DetailsBlockProvider,
|
||||
KanbanBlockProvider,
|
||||
RecordLink,
|
||||
});
|
||||
}
|
||||
@ -87,8 +79,6 @@ export class BlockSchemaComponentPlugin extends Plugin {
|
||||
useTableFieldProps,
|
||||
useTableBlockProps,
|
||||
useTableSelectorProps,
|
||||
useKanbanBlockProps,
|
||||
useGanttBlockProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { TemplateBlockProvider } from './TemplateBlockProvider';
|
||||
export const FormBlockContext = createContext<any>({});
|
||||
|
||||
const InternalFormBlockProvider = (props) => {
|
||||
const ctx = useFormBlockContext();
|
||||
const { action, readPretty, params, association } = props;
|
||||
const field = useField();
|
||||
const form = useMemo(
|
||||
@ -28,6 +29,7 @@ const InternalFormBlockProvider = (props) => {
|
||||
const record = useRecord();
|
||||
const formBlockValue = useMemo(() => {
|
||||
return {
|
||||
...ctx,
|
||||
params,
|
||||
action,
|
||||
form,
|
||||
|
@ -3,12 +3,10 @@ export * from './BlockSchemaComponentProvider';
|
||||
export * from './CalendarBlockProvider';
|
||||
export * from './FilterFormBlockProvider';
|
||||
export * from './FormBlockProvider';
|
||||
export * from './KanbanBlockProvider';
|
||||
export * from './SharedFilterProvider';
|
||||
export * from './TableBlockProvider';
|
||||
export * from './TableFieldProvider';
|
||||
export * from './TableSelectorProvider';
|
||||
export * from './FormFieldProvider';
|
||||
export * from './GanttBlockProvider';
|
||||
export * from './SharedFilterProvider';
|
||||
export * from './hooks';
|
||||
|
@ -1,157 +0,0 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => {
|
||||
return css`
|
||||
.react-kanban-board {
|
||||
height: 100%;
|
||||
// padding: 5px;
|
||||
// margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
box-sizing: border-box;
|
||||
max-width: 250px;
|
||||
min-width: 250px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.react-kanban-card-skeleton {
|
||||
box-sizing: border-box;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
height: 70vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 12px;
|
||||
> div {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
+ div {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card--dragging {
|
||||
box-shadow: 2px 2px grey;
|
||||
}
|
||||
|
||||
.react-kanban-card__description {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card__title {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.react-kanban-column {
|
||||
background-color: ${token.colorFillQuaternary};
|
||||
margin-right: 15px;
|
||||
padding-bottom: 15px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form {
|
||||
box-sizing: border-box;
|
||||
max-width: 250px;
|
||||
min-width: 250px;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin-bottom: 7px;
|
||||
input {
|
||||
border: 0px;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__description {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 45%;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
border-color: #cccccc;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button {
|
||||
border: 2px dashed #eee;
|
||||
height: 132px;
|
||||
margin: 5px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
});
|
@ -20,7 +20,6 @@ export * from './application';
|
||||
export * from './async-data-provider';
|
||||
export * from './auth';
|
||||
export * from './block-provider';
|
||||
export * from './board';
|
||||
export * from './china-region';
|
||||
export * from './collection-manager';
|
||||
export * from './css-variable';
|
||||
@ -47,3 +46,5 @@ export * from './style';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
export * from './variables';
|
||||
export * from './flag-provider';
|
||||
export * from './testUtils';
|
||||
|
@ -675,7 +675,6 @@
|
||||
"Changed to": "Mudou para",
|
||||
"Clear": "Limpar",
|
||||
"Add attach": "Adicionar anexo",
|
||||
"Please select the records to be updated": "Por favor, selecione os registros a serem atualizados",
|
||||
"Selector": "Seletor",
|
||||
"Inner": "Interno",
|
||||
"Search and select collection": "Pesquisar e selecionar coleção",
|
||||
|
@ -15,12 +15,7 @@ import { RemoteDocumentTitlePlugin } from '../document-title';
|
||||
import { PinnedListPlugin } from '../plugin-manager';
|
||||
import { PMPlugin } from '../pm';
|
||||
import { AdminLayoutPlugin, AuthLayout, RouteSchemaComponent } from '../route-switch';
|
||||
import {
|
||||
AntdSchemaComponentPlugin,
|
||||
KanbanPlugin,
|
||||
SchemaComponentPlugin,
|
||||
menuItemInitializer,
|
||||
} from '../schema-component';
|
||||
import { AntdSchemaComponentPlugin, SchemaComponentPlugin, menuItemInitializer } from '../schema-component';
|
||||
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||
import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
||||
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||
@ -317,7 +312,6 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
||||
}
|
||||
async addPlugins() {
|
||||
await this.app.pm.add(AssociationFilterPlugin);
|
||||
await this.app.pm.add(KanbanPlugin);
|
||||
await this.app.pm.add(LocalePlugin, { name: 'builtin-locale' });
|
||||
await this.app.pm.add(AdminLayoutPlugin, { name: 'admin-layout' });
|
||||
await this.app.pm.add(SystemSettingsPlugin, { name: 'system-setting' });
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { ArrayTable } from '@formily/antd-v5';
|
||||
import { onFieldInputValueChange, onFieldValueChange } from '@formily/core';
|
||||
import { onFieldValueChange } from '@formily/core';
|
||||
import { ISchema, connect, mapProps, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
|
||||
import { isValid, uid } from '@formily/shared';
|
||||
import { Alert, Tree as AntdTree, ModalProps } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RemoteSelect, useCompile, useDesignable } from '../..';
|
||||
import { useApp } from '../../../application';
|
||||
@ -13,10 +12,8 @@ import { SchemaSettingOptions, SchemaSettings } from '../../../application/schem
|
||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||
import { CollectionOptions, useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { FlagProvider } from '../../../flag-provider';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
|
||||
import { useSyncFromForm } from '../../../schema-settings/DataTemplates/utils';
|
||||
import { GeneralSchemaDesigner } from '../../../schema-settings/GeneralSchemaDesigner';
|
||||
import {
|
||||
SchemaSettingsActionModalItem,
|
||||
@ -44,27 +41,10 @@ const Tree = connect(
|
||||
field.value = checkedKeys;
|
||||
};
|
||||
field.onCheck = onCheck;
|
||||
const form = useForm();
|
||||
return {
|
||||
...props,
|
||||
checkedKeys,
|
||||
onCheck,
|
||||
treeData: props?.treeData.map((v: any) => {
|
||||
if (form.values.duplicateMode === 'quickDulicate') {
|
||||
const children = v?.children?.map((k) => {
|
||||
return {
|
||||
...k,
|
||||
disabled: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...v,
|
||||
disabled: false,
|
||||
children,
|
||||
};
|
||||
}
|
||||
return v;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -269,264 +249,6 @@ const getAllkeys = (data, result) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
function DuplicationMode() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name } = useCollection();
|
||||
const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(name);
|
||||
const duplicateValues = cloneDeep(fieldSchema['x-component-props'].duplicateFields || []);
|
||||
const record = useRecord();
|
||||
const syncCallBack = useCallback((treeData, selectFields, form) => {
|
||||
form.query('duplicateFields').take((f) => {
|
||||
f.componentProps.treeData = treeData;
|
||||
f.componentProps.defaultCheckedKeys = selectFields;
|
||||
f.setInitialValue(selectFields);
|
||||
f?.onCheck(selectFields);
|
||||
form.setValues({ ...form.values, treeData });
|
||||
});
|
||||
}, []);
|
||||
const useSelectAllFields = (form) => {
|
||||
return {
|
||||
async run() {
|
||||
form.query('duplicateFields').take((f) => {
|
||||
const selectFields = getAllkeys(f.componentProps.treeData, []);
|
||||
f.componentProps.defaultCheckedKeys = selectFields;
|
||||
f.setInitialValue(selectFields);
|
||||
f?.onCheck(selectFields);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
const useUnSelectAllFields = (form) => {
|
||||
return {
|
||||
async run() {
|
||||
form.query('duplicateFields').take((f) => {
|
||||
f.componentProps.defaultCheckedKeys = [];
|
||||
f.setInitialValue([]);
|
||||
f?.onCheck([]);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Duplicate mode')}
|
||||
components={{ Tree }}
|
||||
scope={{
|
||||
getEnableFieldTree,
|
||||
collectionName: fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name,
|
||||
currentCollection: record?.__collection || name,
|
||||
getOnLoadData,
|
||||
getOnCheck,
|
||||
treeData: fieldSchema['x-component-props']?.treeData,
|
||||
duplicateValues,
|
||||
onFieldInputValueChange,
|
||||
}}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Duplicate mode'),
|
||||
properties: {
|
||||
duplicateMode: {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
title: t('Duplicate mode'),
|
||||
default: fieldSchema['x-component-props']?.duplicateMode || 'quickDulicate',
|
||||
enum: [
|
||||
{ value: 'quickDulicate', label: '{{t("Direct duplicate")}}' },
|
||||
{ value: 'continueduplicate', label: '{{t("Copy into the form and continue to fill in")}}' },
|
||||
],
|
||||
},
|
||||
collection: {
|
||||
type: 'string',
|
||||
title: '{{ t("Target collection") }}',
|
||||
required: true,
|
||||
description: t('If collection inherits, choose inherited collections as templates'),
|
||||
default: '{{ collectionName }}',
|
||||
'x-display': collectionList.length > 1 ? 'visible' : 'hidden',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
options: collectionList,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
disabled: `{{ $deps[0]==="quickDulicate" }}`,
|
||||
value: `{{ $deps[0]==="quickDulicate"? currentCollection:collectionName }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
syncFromForm: {
|
||||
type: 'void',
|
||||
title: '{{ t("Sync from form fields") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200 },
|
||||
useAction: () => {
|
||||
const formSchema = useMemo(() => findFormBlock(fieldSchema), [fieldSchema]);
|
||||
return useSyncFromForm(
|
||||
formSchema,
|
||||
fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name,
|
||||
syncCallBack,
|
||||
);
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{ $deps[0]!=="quickDulicate" }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
selectAll: {
|
||||
type: 'void',
|
||||
title: '{{ t("Select all") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{ $deps[0]==="quickDulicate"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200 },
|
||||
useAction: () => {
|
||||
const from = useForm();
|
||||
return useSelectAllFields(from);
|
||||
},
|
||||
},
|
||||
},
|
||||
unselectAll: {
|
||||
type: 'void',
|
||||
title: '{{ t("UnSelect all") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.duplicateMode', '.duplicateFields'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{ $deps[0]==="quickDulicate"&&$form.getValuesIn('duplicateFields').length>0 }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: { float: 'right', position: 'relative', zIndex: 1200, marginRight: '10px' },
|
||||
useAction: () => {
|
||||
const from = useForm();
|
||||
return useUnSelectAllFields(from);
|
||||
},
|
||||
},
|
||||
},
|
||||
duplicateFields: {
|
||||
type: 'array',
|
||||
title: '{{ t("Data fields") }}',
|
||||
required: true,
|
||||
description: t('Only the selected fields will be used as the initialization data for the form'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': Tree,
|
||||
'x-component-props': {
|
||||
defaultCheckedKeys: duplicateValues,
|
||||
treeData: [],
|
||||
checkable: true,
|
||||
checkStrictly: true,
|
||||
selectable: false,
|
||||
loadData: '{{ getOnLoadData($self) }}',
|
||||
onCheck: '{{ getOnCheck($self) }}',
|
||||
rootStyle: {
|
||||
padding: '8px 0',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '2px',
|
||||
maxHeight: '30vh',
|
||||
overflow: 'auto',
|
||||
margin: '2px 0',
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.collection', '.duplicateMode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
disabled: '{{ !$deps[0] }}',
|
||||
componentProps: {
|
||||
treeData: '{{ getEnableFieldTree($deps[0], $self,treeData) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ duplicateMode, collection, duplicateFields, treeData }) => {
|
||||
const fields = Array.isArray(duplicateFields) ? duplicateFields : duplicateFields.checked || [];
|
||||
field.componentProps.duplicateMode = duplicateMode;
|
||||
field.componentProps.duplicateFields = fields;
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
fieldSchema['x-component-props'].duplicateMode = duplicateMode;
|
||||
fieldSchema['x-component-props'].duplicateFields = fields;
|
||||
fieldSchema['x-component-props'].duplicateCollection = collection;
|
||||
fieldSchema['x-component-props'].treeData = treeData || field.componentProps?.treeData;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-component-props': {
|
||||
...fieldSchema['x-component-props'],
|
||||
},
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateMode() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
return (
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Data will be updated')}
|
||||
options={[
|
||||
{ label: t('Selected'), value: 'selected' },
|
||||
{ label: t('All'), value: 'all' },
|
||||
]}
|
||||
value={fieldSchema?.['x-action-settings']?.['updateMode']}
|
||||
onChange={(value) => {
|
||||
fieldSchema['x-action-settings']['updateMode'] = value;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-action-settings': fieldSchema['x-action-settings'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AssignedFieldValues() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
@ -630,7 +352,6 @@ function SkipValidation() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AfterSuccess() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
@ -699,7 +420,6 @@ function AfterSuccess() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveButton(
|
||||
props: {
|
||||
onConfirmOk?: ModalProps['onOk'];
|
||||
@ -955,15 +675,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duplicationMode',
|
||||
Component: DuplicationMode,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isDuplicateAction = fieldSchema['x-action'] === 'duplicate';
|
||||
return isDuplicateAction;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
@ -984,17 +695,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updateMode',
|
||||
Component: UpdateMode,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(
|
||||
fieldSchema['x-action'],
|
||||
);
|
||||
return isUpdateModePopupAction;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'assignFieldValues',
|
||||
Component: AssignedFieldValues,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer } from '@formily/react';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, ActionContextProvider } from '../action/context';
|
||||
@ -16,6 +16,8 @@ export const InternaPopoverNester = observer(
|
||||
const { options } = useAssociationFieldContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const schema = useFieldSchema();
|
||||
schema['x-component-props'].enableLink = false;
|
||||
const ref = useRef();
|
||||
const nesterProps = {
|
||||
...props,
|
||||
@ -37,10 +39,6 @@ export const InternaPopoverNester = observer(
|
||||
<InternalNester {...nesterProps} />
|
||||
</div>
|
||||
);
|
||||
const titleProps = {
|
||||
...props,
|
||||
enableLink: true,
|
||||
};
|
||||
const getContainer = () => ref.current;
|
||||
const ctx = useContext(ActionContext);
|
||||
const modalProps = {
|
||||
@ -69,7 +67,7 @@ export const InternaPopoverNester = observer(
|
||||
max-width: 95%;
|
||||
`}
|
||||
>
|
||||
<ReadPrettyInternalViewer {...titleProps} />
|
||||
<ReadPrettyInternalViewer {...props} />
|
||||
</div>
|
||||
<EditOutlined style={{ display: 'inline-flex', margin: '5px' }} />
|
||||
</span>
|
||||
|
@ -67,7 +67,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
<span>
|
||||
{snapshot ? (
|
||||
text
|
||||
) : enableLink !== false && !props.enableLink ? (
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
@ -7,10 +7,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { findFormBlock } from '../../../block-provider';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { useDuplicatefieldsContext } from '../../../schema-initializer/components';
|
||||
import { compatibleDataId } from '../../../schema-settings/DataTemplates/FormDataTemplates';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { RemoteSelect } from '../remote-select';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
|
||||
export interface ITemplate {
|
||||
config?: {
|
||||
@ -38,10 +38,11 @@ export interface ITemplate {
|
||||
const useDataTemplates = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const data = useDuplicatefieldsContext();
|
||||
const { duplicateData } = useFormBlockContext();
|
||||
const { getCollectionJoinField } = useCollectionManager();
|
||||
if (data) {
|
||||
return data;
|
||||
console.log(useFormBlockContext());
|
||||
if (duplicateData) {
|
||||
return duplicateData;
|
||||
}
|
||||
const { items = [], display = true } = findDataTemplates(fieldSchema);
|
||||
// 过滤掉已经被删除的字段
|
||||
|
@ -11,4 +11,6 @@ FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
||||
FormV2.Templates = Templates;
|
||||
|
||||
export * from './FormField';
|
||||
export * from './Templates';
|
||||
|
||||
export { FormV2, DetailsDesigner };
|
||||
|
@ -1,184 +0,0 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile, useDesignable } from '../..';
|
||||
import { useFormBlockContext, useGanttBlockContext } from '../../../block-provider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsDataScope,
|
||||
SchemaSettingsDivider,
|
||||
SchemaSettingsRemove,
|
||||
SchemaSettingsSelectItem,
|
||||
SchemaSettingsTemplate,
|
||||
} from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
|
||||
const useOptions = (type = 'string') => {
|
||||
const compile = useCompile();
|
||||
const { fields } = useCollection();
|
||||
const options = fields
|
||||
?.filter((field) => field.type === type)
|
||||
?.map((field) => {
|
||||
return {
|
||||
value: field.name,
|
||||
label: compile(field?.uiSchema?.title),
|
||||
};
|
||||
});
|
||||
return options;
|
||||
};
|
||||
|
||||
export const GanttDesigner = () => {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { form } = useFormBlockContext();
|
||||
const { name, title } = useCollection();
|
||||
const { service } = useGanttBlockContext();
|
||||
const { dn } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const template = useSchemaTemplate();
|
||||
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Title field')}
|
||||
value={fieldNames.title}
|
||||
options={useOptions('string')}
|
||||
onChange={(title) => {
|
||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||
fieldNames['title'] = title;
|
||||
field.decoratorProps.params = fieldNames;
|
||||
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||
// Select切换option后value未按照预期切换,固增加以下代码
|
||||
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||
service.refresh();
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Time scale')}
|
||||
value={fieldNames.range || 'day'}
|
||||
options={[
|
||||
{ label: compile('{{t("Hour")}}'), value: 'hour', color: 'orange' },
|
||||
{ label: compile('{{t("Quarter of day")}}'), value: 'quarterDay', color: 'default' },
|
||||
{ label: compile('{{t("Half of day")}}'), value: 'halfDay', color: 'blue' },
|
||||
{ label: compile('{{t("Day")}}'), value: 'day', color: 'yellow' },
|
||||
{ label: compile('{{t("Week")}}'), value: 'week', color: 'pule' },
|
||||
{ label: compile('{{t("Month")}}'), value: 'month', color: 'green' },
|
||||
{ label: compile('{{t("QuarterYear")}}'), value: 'quarterYear', color: 'red' },
|
||||
{ label: compile('{{t("Year")}}'), value: 'year', color: 'green' },
|
||||
]}
|
||||
onChange={(range) => {
|
||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||
fieldNames['range'] = range;
|
||||
field.decoratorProps.params = fieldNames;
|
||||
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||
// Select切换option后value未按照预期切换,固增加以下代码
|
||||
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||
service.refresh();
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Start date field')}
|
||||
value={fieldNames.start}
|
||||
options={useOptions('date')}
|
||||
onChange={(start) => {
|
||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||
fieldNames['start'] = start;
|
||||
field.decoratorProps.fieldNames = fieldNames;
|
||||
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||
service.refresh();
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('End date field')}
|
||||
value={fieldNames.end}
|
||||
options={useOptions('date')}
|
||||
onChange={(end) => {
|
||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||
fieldNames['end'] = end;
|
||||
field.decoratorProps.fieldNames = fieldNames;
|
||||
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||
service.refresh();
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Progress field')}
|
||||
value={fieldNames.progress}
|
||||
options={useOptions('float')}
|
||||
onChange={(progress) => {
|
||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||
fieldNames['progress'] = progress;
|
||||
field.decoratorProps.fieldNames = fieldNames;
|
||||
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||
service.refresh();
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsDataScope
|
||||
collectionName={name}
|
||||
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
|
||||
form={form}
|
||||
onSubmit={({ filter }) => {
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
service.run({ ...service?.params?.[0], filter });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': field.decoratorProps,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsTemplate componentName={'Gantt'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
describe('gantt', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<App1 />);
|
||||
});
|
||||
});
|
@ -1,52 +0,0 @@
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { genStyleHook } from '../../../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-gantt-calendar', (token) => {
|
||||
const { componentCls } = token;
|
||||
const colorFillAlterSolid = new TinyColor(token.colorFillAlter)
|
||||
.onBackground(token.colorBgContainer)
|
||||
.toHexShortString();
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.calendarBottomText': {
|
||||
textAnchor: 'middle',
|
||||
fill: token.colorText,
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
'.calendarTopTick': {
|
||||
stroke: token.colorBorderSecondary,
|
||||
strokeWidth: 0,
|
||||
},
|
||||
|
||||
'.calendarTopText': {
|
||||
textAnchor: 'middle',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
fontWeight: 500,
|
||||
fill: token.colorText,
|
||||
},
|
||||
|
||||
'.calendarHeader': {
|
||||
color: token.colorText,
|
||||
fill: colorFillAlterSolid,
|
||||
strokeWidth: 1.4,
|
||||
background: colorFillAlterSolid,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
@ -1,40 +0,0 @@
|
||||
import { genStyleHook } from '../../../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-gantt', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.ganttVerticalContainer': {
|
||||
overflow: 'hidden',
|
||||
fontSize: '0',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
width: '100%',
|
||||
borderLeft: `2px solid ${token.colorBorderSecondary}`,
|
||||
|
||||
'.ganttHeader': { borderBottom: `1px solid ${token.colorBorderSecondary}`, fontWeight: 700 },
|
||||
'.ganttBody': { borderBottom: `1px solid ${token.colorBorderSecondary}` },
|
||||
},
|
||||
|
||||
'.horizontalContainer': {
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
'.wrapper': {
|
||||
display: 'flex',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
listStyle: 'none',
|
||||
outline: 'none',
|
||||
position: 'relative',
|
||||
'.gantt-horizontal-scoll': { display: 'none' },
|
||||
'&:hover': { '.gantt-horizontal-scoll': { display: 'block' } },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
@ -1,30 +0,0 @@
|
||||
import { genStyleHook } from '../../../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-grid-body', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.gridRow': {
|
||||
fill: token.colorBgContainer,
|
||||
},
|
||||
|
||||
'.gridHeightRow': {
|
||||
fill: '#e6f7ff',
|
||||
borderColor: token.colorBorder,
|
||||
},
|
||||
|
||||
'.gridRowLine': {
|
||||
stroke: token.colorBorderSecondary,
|
||||
strokeWidth: 0,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
},
|
||||
|
||||
'.gridTick': {
|
||||
stroke: token.colorBorderSecondary,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
@ -1,91 +0,0 @@
|
||||
import { genStyleHook } from '../../../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-grid-other', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'&.scrollWrapper': {
|
||||
overflow: 'auto',
|
||||
position: 'absolute',
|
||||
bottom: '-4px',
|
||||
maxWidth: '100%',
|
||||
scrollbarWidth: 'thin',
|
||||
height: '1.2rem',
|
||||
'&::-webkit-scrollbar': { width: 8, height: 8 },
|
||||
'&::-webkit-scrollbar-corner': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-track': { background: 'var(--colorBgScrollTrack)' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'var(--colorBgScrollBar)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: 'var(--colorBgScrollBarHover)',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:active': {
|
||||
background: 'var(--colorBgScrollBarActive)',
|
||||
},
|
||||
},
|
||||
|
||||
'.horizontalScroll': {
|
||||
height: 1,
|
||||
},
|
||||
|
||||
'&.tooltipDefaultContainer': {
|
||||
padding: '12px',
|
||||
backgroundColor: token.colorBgElevated,
|
||||
backgroundClip: 'padding-box',
|
||||
borderRadius: token.borderRadius,
|
||||
boxShadow: token.boxShadow,
|
||||
b: { display: 'block', marginBottom: token.marginXS },
|
||||
|
||||
'.tooltipDefaultContainerParagraph': {
|
||||
fontSize: token.fontSizeSM,
|
||||
marginBottom: token.marginXXS + (token.marginXS - token.marginXXS),
|
||||
color: token.colorText,
|
||||
},
|
||||
},
|
||||
|
||||
'&.tooltipDetailsContainer': {
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
pointerEvents: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
},
|
||||
|
||||
'&.tooltipDetailsContainerHidden': {
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
'&.verticalScroll': {
|
||||
overflow: 'hidden auto',
|
||||
width: '1rem',
|
||||
flexShrink: 0,
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: 8, height: 8 },
|
||||
'&::-webkit-scrollbar-corner': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-track': { background: 'var(--colorBgScrollTrack)' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'var(--colorBgScrollBar)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: 'var(--colorBgScrollBarHover)',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:active': {
|
||||
background: 'var(--colorBgScrollBarActive)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
@ -1,48 +0,0 @@
|
||||
import { genStyleHook } from '../../../__builtins__';
|
||||
|
||||
const useStyles = genStyleHook('nb-gantt-task-item', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.barLabel': {
|
||||
fill: token.colorTextLightSolid,
|
||||
textAnchor: 'middle',
|
||||
fontWeight: 400,
|
||||
dominantBaseline: 'central',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
'.projectLabel': {
|
||||
fill: '#130d0d',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.9em',
|
||||
dominantBaseline: 'central',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
'.barLabelOutside': {
|
||||
fill: token.colorTextLabel,
|
||||
textAnchor: 'start',
|
||||
WebkitTouchCallout: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
@ -1,15 +0,0 @@
|
||||
import { ActionBar } from '../action';
|
||||
import { Gantt } from './components/gantt/gantt';
|
||||
import { GanttDesigner } from './Gantt.Designer';
|
||||
import { ViewMode } from './types/public-types';
|
||||
import { Event } from './components/gantt/Event';
|
||||
|
||||
Gantt.ActionBar = ActionBar;
|
||||
|
||||
Gantt.ViewMode = ViewMode;
|
||||
Gantt.Designer = GanttDesigner;
|
||||
Gantt.Event = Event;
|
||||
|
||||
// const GanttV2 = Gantt;
|
||||
|
||||
export { Gantt };
|
@ -21,13 +21,11 @@ export * from './form-dialog';
|
||||
export * from './form-item';
|
||||
export * from './form-v2';
|
||||
export * from './g2plot';
|
||||
export * from './gantt';
|
||||
export * from './grid';
|
||||
export * from './grid-card';
|
||||
export * from './icon-picker';
|
||||
export * from './input';
|
||||
export * from './input-number';
|
||||
export * from './kanban';
|
||||
export * from './list';
|
||||
export * from './markdown';
|
||||
export * from './menu';
|
||||
|
@ -1,62 +0,0 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useFormBlockContext, useKanbanBlockContext } from '../../../block-provider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsDataScope,
|
||||
SchemaSettingsDivider,
|
||||
SchemaSettingsRemove,
|
||||
SchemaSettingsTemplate,
|
||||
} from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FixedBlockDesignerItem } from '../page';
|
||||
|
||||
export const KanbanDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { form } = useFormBlockContext();
|
||||
const { service } = useKanbanBlockContext();
|
||||
const { dn } = useDesignable();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const template = useSchemaTemplate();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
<SchemaSettingsDataScope
|
||||
collectionName={name}
|
||||
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
|
||||
form={form}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
service.run({ ...service.params?.[0], filter });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FixedBlockDesignerItem />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsTemplate componentName={'Kanban'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from 'testUtils';
|
||||
import App1 from '../demos/demo1';
|
||||
|
||||
describe('Kanban', () => {
|
||||
it('should render correctly', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
await waitFor(() => {
|
||||
// 每列卡片的状态
|
||||
expect(screen.getByText(/未开始/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/进行中/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/测试中/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/已完成/i)).toBeInTheDocument();
|
||||
|
||||
// 第一个卡片的内容
|
||||
expect(screen.getByText(/子表格字段缺少标题/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/见附件,在此位置显示标题,包括表单和详情/i)).toBeInTheDocument();
|
||||
|
||||
// 最后一个卡片的内容
|
||||
expect(screen.getByText(/新增 uischema:clearancestor action api/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,89 +0,0 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import {
|
||||
AntdSchemaComponentProvider,
|
||||
APIClientProvider,
|
||||
BlockSchemaComponentProvider,
|
||||
CollectionManagerProvider,
|
||||
CurrentUserProvider,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../testUtils';
|
||||
import collections from './collections';
|
||||
import data from './data';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
||||
|
||||
mockRequest.onGet('/t_j6omof6tza8:list').reply(async (config) => {
|
||||
await sleep(200);
|
||||
return [200, data];
|
||||
});
|
||||
mockRequest.onGet('/auth:check').reply(() => {
|
||||
return [200, { data: {} }];
|
||||
});
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
block: {
|
||||
type: 'void',
|
||||
'x-decorator': 'KanbanBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 't_j6omof6tza8',
|
||||
resource: 't_j6omof6tza8',
|
||||
action: 'list',
|
||||
groupField: 'f_hpmvdltzs6m',
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
kanban: {
|
||||
type: 'array',
|
||||
'x-component': 'Kanban',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useKanbanBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
card: {
|
||||
type: 'void',
|
||||
'x-component': 'Kanban.Card',
|
||||
properties: {
|
||||
f_g8j5jvalqh0: {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
f_tegyd222bcc: {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<CurrentUserProvider>
|
||||
<SchemaComponentProvider>
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<AntdSchemaComponentProvider>
|
||||
<BlockSchemaComponentProvider>
|
||||
<SchemaComponent schema={schema} />
|
||||
</BlockSchemaComponentProvider>
|
||||
</AntdSchemaComponentProvider>
|
||||
</CollectionManagerProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CurrentUserProvider>
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { Plugin } from '../../../application/Plugin';
|
||||
import { Action } from '../action';
|
||||
import { Kanban } from './Kanban';
|
||||
import { KanbanCard } from './Kanban.Card';
|
||||
import { KanbanCardDesigner, kanbanCardInitializers } from './Kanban.Card.Designer';
|
||||
import { KanbanCardDesignerTitleSwitch } from './Kanban.Card.Designer.TitleSwitch';
|
||||
import { KanbanCardViewer } from './Kanban.CardViewer';
|
||||
import { KanbanDesigner } from './Kanban.Designer';
|
||||
|
||||
Kanban.Card = KanbanCard;
|
||||
Kanban.CardAdder = Action;
|
||||
Kanban.CardViewer = KanbanCardViewer;
|
||||
Kanban.Card.Designer = KanbanCardDesigner;
|
||||
Kanban.Card.Designer.TitleSwitch = KanbanCardDesignerTitleSwitch;
|
||||
Kanban.Designer = KanbanDesigner;
|
||||
|
||||
const KanbanV2 = Kanban;
|
||||
|
||||
class KanbanPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.schemaInitializerManager.add(kanbanCardInitializers);
|
||||
}
|
||||
}
|
||||
|
||||
export { Kanban, KanbanV2, KanbanPlugin };
|
@ -1,25 +0,0 @@
|
||||
import { genStyleHook } from './../__builtins__';
|
||||
|
||||
export const useStyles = genStyleHook('nb-kanban', (token) => {
|
||||
const { componentCls } = token;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
'> .ant-spin-container': { height: '100%' },
|
||||
|
||||
'.react-kanban-board': {
|
||||
'.nb-block-item': {
|
||||
'.ant-formily-item-control .ant-space-item': {
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
},
|
||||
'& .ant-formily-item-label': { color: '#8c8c8c', fontWeight: 'normal' },
|
||||
},
|
||||
'.kanban-no-label': { '.ant-formily-item-label': { display: 'none' } },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
@ -9,7 +9,7 @@ export const useFieldModeOptions = (props?) => {
|
||||
const fieldSchema = props?.fieldSchema || currentFieldSchema;
|
||||
const field = useField();
|
||||
const form = useForm();
|
||||
const isReadPretty = field.readPretty && form.readPretty;
|
||||
const isReadPretty = field.readPretty || form.readPretty;
|
||||
const isSubTableField = props?.fieldSchema;
|
||||
const { getField } = useCollection();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
|
@ -42,16 +42,6 @@ export const blockInitializers = new SchemaInitializer({
|
||||
title: '{{t("Calendar")}}',
|
||||
Component: 'CalendarBlockInitializer',
|
||||
},
|
||||
{
|
||||
name: 'kanban',
|
||||
title: '{{t("Kanban")}}',
|
||||
Component: 'KanbanBlockInitializer',
|
||||
},
|
||||
{
|
||||
name: 'gantt',
|
||||
title: '{{t("Gantt")}}',
|
||||
Component: 'GanttBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -57,14 +57,6 @@ export const calendarFormActionInitializers = new SchemaInitializer({
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'print',
|
||||
title: '{{t("Print")}}',
|
||||
Component: 'PrintActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -35,19 +35,6 @@ export const detailsActionInitializers = new SchemaInitializer({
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duplicate',
|
||||
title: '{{t("Duplicate")}}',
|
||||
Component: 'DuplicateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
'x-action': 'duplicate',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -42,28 +42,6 @@ export const readPrettyFormActionInitializers = new SchemaInitializer({
|
||||
},
|
||||
useVisible: useVisibleCollection,
|
||||
},
|
||||
{
|
||||
title: '{{t("Duplicate")}}',
|
||||
name: 'duplicate',
|
||||
Component: 'DuplicateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
'x-action': 'duplicate',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
useVisible: useVisibleCollection,
|
||||
},
|
||||
{
|
||||
title: '{{t("Print")}}',
|
||||
name: 'print',
|
||||
Component: 'PrintActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -168,21 +168,6 @@ export const tableActionColumnInitializers = new SchemaInitializer({
|
||||
return collection.tree && treeTable !== false;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Duplicate")}}',
|
||||
name: 'duplicate',
|
||||
Component: 'DuplicateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'duplicate',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -94,52 +94,6 @@ export const tableActionInitializers = new SchemaInitializer({
|
||||
name: 'customize',
|
||||
title: '{{t("Customize")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Bulk update")}}',
|
||||
Component: 'CustomizeActionInitializer',
|
||||
name: 'bulkUpdate',
|
||||
schema: {
|
||||
type: 'void',
|
||||
title: '{{ t("Bulk update") }}',
|
||||
'x-component': 'Action',
|
||||
'x-align': 'right',
|
||||
'x-acl-action': 'update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-action': 'customize:bulkUpdate',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
assignedValues: {},
|
||||
updateMode: 'selected',
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Updated successfully")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
icon: 'EditOutlined',
|
||||
useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
name: 'bulkEdit',
|
||||
Component: 'CustomizeBulkEditActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'update',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Add record")}}',
|
||||
|
@ -1,18 +1,14 @@
|
||||
export * from './BlockInitializers';
|
||||
export * from './BulkEditFormItemInitializers';
|
||||
export * from './CalendarActionInitializers';
|
||||
export * from './CalendarFormActionInitializers';
|
||||
export * from './CreateFormBlockInitializers';
|
||||
export * from './CreateFormBulkEditBlockInitializers';
|
||||
export * from './CusomeizeCreateFormBlockInitializers';
|
||||
export * from './CustomFormItemInitializers';
|
||||
export * from './DetailsActionInitializers';
|
||||
export * from './FilterFormActionInitializers';
|
||||
export * from './FormActionInitializers';
|
||||
export * from './FormItemInitializers';
|
||||
export * from './GanttActionInitializers';
|
||||
export * from './GridCardActionInitializers';
|
||||
export * from './KanbanActionInitializers';
|
||||
export * from './ListActionInitializers';
|
||||
export * from './ReadPrettyFormActionInitializers';
|
||||
export * from './ReadPrettyFormItemInitializers';
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './assigned-field';
|
||||
export * from './BulkEditField';
|
||||
export * from './CreateRecordAction';
|
||||
export * from './DuplicateAction'
|
||||
|
@ -16,26 +16,25 @@ export {
|
||||
useInheritsTableColumnInitializerFields,
|
||||
useRecordCollectionDataSourceItems,
|
||||
useTableColumnInitializerFields,
|
||||
useRemoveGridFormItem,
|
||||
useAssociatedFormItemInitializerFields,
|
||||
useFormItemInitializerFields,
|
||||
} from './utils';
|
||||
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import {
|
||||
bulkEditFormItemInitializers,
|
||||
calendarActionInitializers,
|
||||
calendarFormActionInitializers,
|
||||
createFormBlockInitializers,
|
||||
createFormBulkEditBlockInitializers,
|
||||
cusomeizeCreateFormBlockInitializers,
|
||||
customFormItemInitializers,
|
||||
filterFormActionInitializers,
|
||||
createFormActionInitializers,
|
||||
updateFormActionInitializers,
|
||||
bulkEditFormActionInitializers,
|
||||
ganttActionInitializers,
|
||||
filterFormItemInitializers,
|
||||
gridCardActionInitializers,
|
||||
gridCardItemActionInitializers,
|
||||
kanbanActionInitializers,
|
||||
listActionInitializers,
|
||||
listItemActionInitializers,
|
||||
recordBlockInitializers,
|
||||
@ -72,22 +71,19 @@ export class SchemaInitializerPlugin extends Plugin {
|
||||
this.app.schemaInitializerManager.add(detailsActionInitializers);
|
||||
this.app.schemaInitializerManager.add(readPrettyFormItemInitializers);
|
||||
this.app.schemaInitializerManager.add(readPrettyFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(bulkEditFormItemInitializers);
|
||||
this.app.schemaInitializerManager.add(calendarActionInitializers);
|
||||
this.app.schemaInitializerManager.add(calendarFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(createFormBlockInitializers);
|
||||
this.app.schemaInitializerManager.add(createFormBulkEditBlockInitializers);
|
||||
this.app.schemaInitializerManager.add(cusomeizeCreateFormBlockInitializers);
|
||||
this.app.schemaInitializerManager.add(customFormItemInitializers);
|
||||
this.app.schemaInitializerManager.add(filterFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(createFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(updateFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(bulkEditFormActionInitializers);
|
||||
this.app.schemaInitializerManager.add(ganttActionInitializers);
|
||||
this.app.schemaInitializerManager.add(filterFormItemInitializers);
|
||||
this.app.schemaInitializerManager.add(gridCardActionInitializers);
|
||||
this.app.schemaInitializerManager.add(gridCardItemActionInitializers);
|
||||
this.app.schemaInitializerManager.add(kanbanActionInitializers);
|
||||
|
||||
this.app.schemaInitializerManager.add(listActionInitializers);
|
||||
this.app.schemaInitializerManager.add(listItemActionInitializers);
|
||||
this.app.schemaInitializerManager.add(recordBlockInitializers);
|
||||
|
@ -13,30 +13,24 @@ export * from './CreateActionInitializer';
|
||||
export * from './CreateChildInitializer';
|
||||
export * from './CreateFilterActionInitializer';
|
||||
export * from './CreateFormBlockInitializer';
|
||||
export * from './CreateFormBulkEditBlockInitializer';
|
||||
export * from './CreateResetActionInitializer';
|
||||
export * from './CreateSubmitActionInitializer';
|
||||
export * from './CustomizeActionInitializer';
|
||||
export * from './CustomizeAddRecordActionInitializer';
|
||||
export * from './CustomizeBulkEditActionInitializer';
|
||||
export * from './DataBlockInitializer';
|
||||
export * from './DeleteEventActionInitializer';
|
||||
export * from './DestroyActionInitializer';
|
||||
export * from './DetailsBlockInitializer';
|
||||
export * from './DuplicateActionInitializer';
|
||||
export * from './ExpandActionInitializer';
|
||||
export * from './FilterActionInitializer';
|
||||
export * from './FilterCollapseBlockInitializer';
|
||||
export * from './FilterFormBlockInitializer';
|
||||
export * from './FormBlockInitializer';
|
||||
export * from './G2PlotInitializer';
|
||||
export * from './GanttBlockInitializer';
|
||||
export * from './GridCardBlockInitializer';
|
||||
export * from './InitializerWithSwitch';
|
||||
export * from './KanbanBlockInitializer';
|
||||
export * from './ListBlockInitializer';
|
||||
export * from './MarkdownBlockInitializer';
|
||||
export * from './PrintActionInitializer';
|
||||
export * from './RecordAssociationBlockInitializer';
|
||||
export * from './RecordAssociationGridCardBlockInitializer';
|
||||
export * from './RecordAssociationListBlockInitializer';
|
||||
|
@ -1592,242 +1592,6 @@ export const createCalendarBlockSchema = (options) => {
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const createGanttBlockSchema = (options) => {
|
||||
const { collection, resource, fieldNames, ...others } = options;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-acl-action': `${resource || collection}:list`,
|
||||
'x-decorator': 'GanttBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: collection,
|
||||
resource: resource || collection,
|
||||
action: 'list',
|
||||
fieldNames: {
|
||||
id: 'id',
|
||||
...fieldNames,
|
||||
},
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
...others,
|
||||
},
|
||||
'x-designer': 'Gantt.Designer',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Gantt',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGanttBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
toolBar: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
'x-initializer': 'GanttActionInitializers',
|
||||
properties: {},
|
||||
},
|
||||
table: {
|
||||
type: 'array',
|
||||
'x-decorator': 'div',
|
||||
'x-decorator-props': {
|
||||
style: {
|
||||
float: 'left',
|
||||
maxWidth: '35%',
|
||||
},
|
||||
},
|
||||
|
||||
'x-initializer': 'TableColumnInitializers',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
pagination: false,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'TableActionColumnInitializers',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
type: 'void',
|
||||
'x-component': 'Gantt.Event',
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
title: '{{ t("View record") }}',
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return schema;
|
||||
};
|
||||
export const createKanbanBlockSchema = (options) => {
|
||||
const { collection, resource, groupField, ...others } = options;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-acl-action': `${resource || collection}:list`,
|
||||
'x-decorator': 'KanbanBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: collection,
|
||||
resource: resource || collection,
|
||||
action: 'list',
|
||||
groupField,
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
...others,
|
||||
},
|
||||
'x-designer': 'Kanban.Designer',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-initializer': 'KanbanActionInitializers',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'array',
|
||||
'x-component': 'Kanban',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useKanbanBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
card: {
|
||||
type: 'void',
|
||||
'x-read-pretty': true,
|
||||
'x-label-disabled': true,
|
||||
'x-decorator': 'BlockItem',
|
||||
'x-component': 'Kanban.Card',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
'x-designer': 'Kanban.Card.Designer',
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': { dndContext: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
cardViewer: {
|
||||
type: 'void',
|
||||
title: '{{ t("View") }}',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Kanban.CardViewer',
|
||||
'x-action': 'view',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("View record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return schema;
|
||||
};
|
||||
|
||||
const getChildren = ({
|
||||
collections,
|
||||
getCollectionFields,
|
||||
|
@ -166,7 +166,7 @@ export const useSyncFromForm = (fieldSchema, collection?, callBack?) => {
|
||||
const formData = new Set([]);
|
||||
const selectFields = new Set([]);
|
||||
const getAssociationAppends = (schema, str) => {
|
||||
schema.reduceProperties((pre, s) => {
|
||||
schema?.reduceProperties?.((pre, s) => {
|
||||
const prefix = pre || str;
|
||||
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
|
||||
const isAssociationSubfield = s.name.includes('.');
|
||||
|
@ -1,6 +1,9 @@
|
||||
export * from './GeneralSchemaDesigner';
|
||||
export * from './SchemaSettings';
|
||||
export * from './hooks/useIsAllowToSetDefaultValue';
|
||||
export * from './hooks/useGetAriaLabelOfDesigner';
|
||||
export * from './DataTemplates/hooks/useCollectionState';
|
||||
export * from './DataTemplates/utils';
|
||||
|
||||
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
|
||||
|
||||
|
@ -3,5 +3,7 @@ export { default as useBuiltinVariables } from './hooks/useBuiltinVariables';
|
||||
export { default as useContextVariable } from './hooks/useContextVariable';
|
||||
export { default as useLocalVariables } from './hooks/useLocalVariables';
|
||||
export { default as useVariables } from './hooks/useVariables';
|
||||
export * from './utils/isVariable';
|
||||
export * from './utils/transformVariableValue';
|
||||
|
||||
export * from './constants';
|
||||
|
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
@ -0,0 +1 @@
|
||||
# @nocobase/plugin-action-bulk-edit
|
2
packages/plugins/@nocobase/plugin-action-bulk-edit/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-bulk-edit/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-action-bulk-edit",
|
||||
"version": "0.17.0-alpha.7",
|
||||
"main": "dist/server/index.js",
|
||||
"displayName": "Bulk edit action",
|
||||
"displayName.zh-CN": "批量编辑操作",
|
||||
"description": "Provides bulk edit action",
|
||||
"description.zh-CN": "提供批量编辑操作",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
}
|
||||
}
|
2
packages/plugins/@nocobase/plugin-action-bulk-edit/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-bulk-edit/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
SchemaSettings,
|
||||
useCompile,
|
||||
ActionDesigner,
|
||||
useSchemaToolbar,
|
||||
useDesignable,
|
||||
SchemaInitializerOpenModeSchemaItems,
|
||||
SchemaSettingsSelectItem,
|
||||
SchemaSettingsDivider,
|
||||
SchemaSettingsRemove,
|
||||
SchemaSettingsItemGroup,
|
||||
} from '@nocobase/client';
|
||||
import { ModalProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
|
||||
const MenuGroup = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const actionTitle = fieldSchema.title ? compile(fieldSchema.title) : '';
|
||||
return (
|
||||
<SchemaSettingsItemGroup title={`${t('Customize')} > ${actionTitle}`}>{props.children}</SchemaSettingsItemGroup>
|
||||
);
|
||||
};
|
||||
|
||||
function UpdateMode() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
return (
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Data will be updated')}
|
||||
options={[
|
||||
{ label: t('Selected'), value: 'selected' },
|
||||
{ label: t('All'), value: 'all' },
|
||||
]}
|
||||
value={fieldSchema?.['x-action-settings']?.['updateMode']}
|
||||
onChange={(value) => {
|
||||
fieldSchema['x-action-settings']['updateMode'] = value;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-action-settings': fieldSchema['x-action-settings'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveButton(
|
||||
props: {
|
||||
onConfirmOk?: ModalProps['onOk'];
|
||||
} = {},
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isDeletable = fieldSchema?.parent['x-component'] === 'CollectionField';
|
||||
return (
|
||||
!isDeletable && (
|
||||
<>
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={(s) => {
|
||||
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
|
||||
}}
|
||||
confirm={{
|
||||
title: t('Delete action'),
|
||||
onOk: props.onConfirmOk,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
const bulkEditactionSettings = new SchemaSettings({
|
||||
name: 'ActionSettings:customize:bulkEdit',
|
||||
items: [
|
||||
{
|
||||
name: 'Customize',
|
||||
Component: MenuGroup,
|
||||
children: [
|
||||
{
|
||||
name: 'editButton',
|
||||
Component: ActionDesigner.ButtonEditor,
|
||||
useComponentProps() {
|
||||
const { buttonEditorProps } = useSchemaToolbar();
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaInitializerOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isPopupAction = [
|
||||
'create',
|
||||
'update',
|
||||
'view',
|
||||
'customize:popup',
|
||||
'duplicate',
|
||||
'customize:create',
|
||||
].includes(fieldSchema['x-action'] || '');
|
||||
|
||||
return {
|
||||
openMode: isPopupAction,
|
||||
openSize: isPopupAction,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updateMode',
|
||||
Component: UpdateMode,
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
Component: RemoveButton as any,
|
||||
useComponentProps() {
|
||||
const { removeButtonProps } = useSchemaToolbar();
|
||||
return removeButtonProps;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export { bulkEditactionSettings };
|
@ -1,7 +1,7 @@
|
||||
import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer';
|
||||
import { gridRowColWrap, useCustomBulkEditFormItemInitializerFields } from '../utils';
|
||||
import { SchemaInitializer, gridRowColWrap } from '@nocobase/client';
|
||||
import { useCustomBulkEditFormItemInitializerFields } from './utils';
|
||||
|
||||
export const bulkEditFormItemInitializers = new SchemaInitializer({
|
||||
export const BulkEditFormItemInitializers = new SchemaInitializer({
|
||||
name: 'BulkEditFormItemInitializers',
|
||||
wrap: gridRowColWrap,
|
||||
icon: 'SettingOutlined',
|
@ -0,0 +1,17 @@
|
||||
import { SchemaComponentOptions } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { CustomizeBulkEditActionInitializer } from './CustomizeBulkEditActionInitializer';
|
||||
import { CreateFormBulkEditBlockInitializer } from './CreateFormBulkEditBlockInitializer';
|
||||
|
||||
export const BulkEditPluginProvider = (props: any) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
CustomizeBulkEditActionInitializer,
|
||||
CreateFormBulkEditBlockInitializer,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
@ -1,10 +1,15 @@
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { useBlockAssociationContext } from '../../block-provider';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||
import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
|
||||
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
|
||||
import {
|
||||
useBlockAssociationContext,
|
||||
useCollection,
|
||||
useSchemaTemplateManager,
|
||||
createFormBlockSchema,
|
||||
useRecordCollectionDataSourceItems,
|
||||
SchemaInitializerItem,
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
} from '@nocobase/client';
|
||||
|
||||
export const CreateFormBulkEditBlockInitializer = () => {
|
||||
const itemConfig = useSchemaInitializerItem();
|
@ -1,7 +1,6 @@
|
||||
import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer';
|
||||
import { gridRowColWrap } from '../utils';
|
||||
import { SchemaInitializer, gridRowColWrap } from '@nocobase/client';
|
||||
|
||||
export const createFormBulkEditBlockInitializers = new SchemaInitializer({
|
||||
export const CreateFormBulkEditBlockInitializers = new SchemaInitializer({
|
||||
name: 'CreateFormBulkEditBlockInitializers',
|
||||
wrap: gridRowColWrap,
|
||||
title: '{{t("Add block")}}',
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BlockInitializer } from './BlockInitializer';
|
||||
import { useSchemaInitializerItem } from '../../application';
|
||||
import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||
|
||||
export const CustomizeBulkEditActionInitializer = () => {
|
||||
const schema = {
|
@ -0,0 +1,22 @@
|
||||
import { expect, test, oneEmptyTableBlockWithActions } from '@nocobase/test/client';
|
||||
import { oneEmptyGantt } from './utils';
|
||||
|
||||
test.describe('TableActionInitializers & GanttActionInitializers & MapActionInitializers should add bulk edit action', () => {
|
||||
test('bulk edit in TableActionInitializers', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Customize right' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Bulk edit' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await expect(await page.getByLabel('Bulk edit')).toBeVisible();
|
||||
});
|
||||
test('bulk edit in GanttActionInitializers', async ({ page, mockPage, mockRecords }) => {
|
||||
const nocoPage = await mockPage(oneEmptyGantt).waitForInit();
|
||||
await mockRecords('general', 3);
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('schema-initializer-ActionBar-GanttActionInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Customize right' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Bulk edit' }).click();
|
||||
await expect(await page.getByLabel('Bulk edit')).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
import { expect, test } from '@nocobase/test/client';
|
||||
import { oneEmptyTableBlockWithCustomizeActions } from './utils';
|
||||
|
||||
test.describe('bulk edit action setting', () => {
|
||||
test('data will be updated && edit form configure', async ({ page, mockPage, mockRecords }) => {
|
||||
const nocoPage = await mockPage(oneEmptyTableBlockWithCustomizeActions).waitForInit();
|
||||
await mockRecords('general', 3);
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('Bulk edit').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action-Action.Designer-general').hover();
|
||||
//默认是选中的数据
|
||||
await expect(await page.getByTitle('Data will be updated').getByText('Selected')).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'Data will be updated' }).click();
|
||||
//切换为全部数据
|
||||
await page.getByRole('option', { name: 'All' }).click();
|
||||
//配置更新规则
|
||||
await page.getByLabel('Bulk edit').click();
|
||||
await page.getByLabel('schema-initializer-Grid-CreateFormBulkEditBlockInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByLabel('schema-initializer-Grid-BulkEditFormItemInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Single line text' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Start date time' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Percent' }).click();
|
||||
await page.getByLabel('block-item-BulkEditField-general-form-general.singleLineText').click();
|
||||
await page.getByText('Clear').click();
|
||||
await page.getByLabel('block-item-BulkEditField-general-form-general.startDatetime').click();
|
||||
await page.getByRole('option', { name: 'Remains the same' }).locator('div').click();
|
||||
await page.getByLabel('block-item-BulkEditField-general-form-general.percent').click();
|
||||
await page.getByRole('option', { name: 'Changed to' }).locator('div').click();
|
||||
await page.getByRole('spinbutton').click();
|
||||
await page.getByRole('spinbutton').fill('0');
|
||||
await page.getByLabel('schema-initializer-ActionBar-BulkEditFormActionInitializers-general').click();
|
||||
await page.getByRole('menuitem', { name: 'Submit' }).click();
|
||||
//更新提交的数据符合预期
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest((request) => request.url().includes('api/general:update')),
|
||||
page.getByLabel('action-Action-Submit-submit-general-form').click(),
|
||||
]);
|
||||
const postData = request.postDataJSON();
|
||||
//更新的数据符合预期
|
||||
expect(postData.singleLineText).toEqual(null);
|
||||
expect(postData.percent).toEqual(0);
|
||||
});
|
||||
});
|
@ -0,0 +1,534 @@
|
||||
import { CollectionSetting, PageConfig } from '@nocobase/test/client';
|
||||
|
||||
/**
|
||||
* 1. 创建一个名为 general 的 collection,其包含 时间、Percent 类型的字段
|
||||
*/
|
||||
export const generalWithDatetimeFields: CollectionSetting[] = [
|
||||
{
|
||||
name: 'general',
|
||||
title: 'General',
|
||||
fields: [
|
||||
{
|
||||
name: 'singleLineText',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
title: 'Single line text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'startDatetime',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'Start date time',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'endDatetime',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'End date time',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'f_t22o7loai3j',
|
||||
interface: 'integer',
|
||||
isForeignKey: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: 'f_t22o7loai3j',
|
||||
'x-component': 'InputNumber',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'manyToOne',
|
||||
interface: 'm2o',
|
||||
foreignKey: 'f_t22o7loai3j',
|
||||
uiSchema: {
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
multiple: false,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
title: 'Many to one',
|
||||
},
|
||||
target: 'users',
|
||||
targetKey: 'id',
|
||||
},
|
||||
{
|
||||
name: 'percent',
|
||||
type: 'float',
|
||||
interface: 'percent',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
step: '0.01',
|
||||
stringMode: true,
|
||||
addonAfter: '%',
|
||||
},
|
||||
'x-component': 'Percent',
|
||||
title: 'Percent',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const oneEmptyGantt: PageConfig = {
|
||||
collections: generalWithDatetimeFields,
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
jebhzap4dzi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
properties: {
|
||||
gl57m4hyewf: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
jm7n5dybw6t: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nsq0rdemz4i: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action': 'general:list',
|
||||
'x-decorator': 'GanttBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'general',
|
||||
resource: 'general',
|
||||
action: 'list',
|
||||
fieldNames: {
|
||||
id: 'id',
|
||||
start: 'startDatetime',
|
||||
range: 'day',
|
||||
title: 'singleLineText',
|
||||
end: 'endDatetime',
|
||||
},
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
},
|
||||
'x-designer': 'Gantt.Designer',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
zf07g7relim: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Gantt',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGanttBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
toolBar: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
'x-initializer': 'GanttActionInitializers',
|
||||
'x-uid': 'guwovmwt4c0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
table: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-decorator': 'div',
|
||||
'x-decorator-props': {
|
||||
style: {
|
||||
float: 'left',
|
||||
maxWidth: '35%',
|
||||
},
|
||||
},
|
||||
'x-initializer': 'TableColumnInitializers',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
pagination: false,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'TableActionColumnInitializers',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-uid': '9in7s3pymsd',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'rgpbsjwvq2h',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'g7qwompxaeo',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
detail: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Gantt.Event',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
title: '{{ t("View record") }}',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
'x-uid': 'gxtfjqzxbfu',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'zxwwx4358p1',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '1p0tnmzpsim',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'hcue8v3fwti',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'fg7cjxu4hxp',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-uid': 'rf47sf7k16z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tritqukkd86',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '3am2ctuhyka',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'p8pesbc7fiv',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ddm9fhkvrbw',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'vpk8sp3eap1',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个空的 Table 区块,并且有这些按钮:Bulk edit
|
||||
*/
|
||||
export const oneEmptyTableBlockWithCustomizeActions: PageConfig = {
|
||||
collections: generalWithDatetimeFields,
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-index': 1,
|
||||
properties: {
|
||||
'1lqiou007g2': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
'x-index': 1,
|
||||
properties: {
|
||||
'1m4gz110aaw': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
'695oy51236d': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
twtgsvrdmn1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'general:list',
|
||||
'x-decorator-props': {
|
||||
collection: 'general',
|
||||
resource: 'general',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
disableTemplate: false,
|
||||
},
|
||||
'x-designer': 'TableBlockDesigner',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'TableActionInitializers',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
yt5ua92rqvb: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Action',
|
||||
'x-action': 'customize:bulkEdit',
|
||||
'x-action-settings': {
|
||||
updateMode: 'selected',
|
||||
},
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'update',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializersForBulkEditFormBlock',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'CreateFormBulkEditBlockInitializers',
|
||||
'x-uid': '3gjmo60w4de',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '2q284n9l6w2',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 's07s8zva7id',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '0n86l5rtgpb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'k8sk9wa8sv6',
|
||||
'x-async': false,
|
||||
'x-index': 6,
|
||||
},
|
||||
},
|
||||
'x-uid': 'znrsshrlsna',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
'1xnl1d9j48o': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'TableColumnInitializers',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'TableActionColumnInitializers',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-uid': 'ijgo5usyzbp',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '5tnwpodzirq',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'k8t01z9qna3',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'c29q4s49svw',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '9pe6fpnq33f',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'j6g551r7tbp',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'mbw5vw7y3ea',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-uid': '4mbt7m7in1l',
|
||||
'x-async': true,
|
||||
},
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
import { Plugin, useCollection } from '@nocobase/client';
|
||||
import { BulkEditPluginProvider } from './BulkEditPluginProvider';
|
||||
import { BulkEditFormItemInitializers } from './BulkEditFormItemInitializers';
|
||||
import { CreateFormBulkEditBlockInitializers } from './CreateFormBulkEditBlockInitializers';
|
||||
import { bulkEditactionSettings } from './BulkEditAction.Settings';
|
||||
export class BulkEditPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(BulkEditPluginProvider);
|
||||
this.app.schemaSettingsManager.add(bulkEditactionSettings);
|
||||
this.app.schemaInitializerManager.add(BulkEditFormItemInitializers);
|
||||
this.app.schemaInitializerManager.add(CreateFormBulkEditBlockInitializers);
|
||||
|
||||
const initializerData = {
|
||||
type: 'item',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
name: 'bulkEdit',
|
||||
Component: 'CustomizeBulkEditActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action': 'customize:bulkEdit',
|
||||
'x-settings': 'ActionSettings:customize:bulkEdit',
|
||||
'x-acl-action': 'update',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
useVisible() {
|
||||
const collection = useCollection();
|
||||
return (
|
||||
(collection.template !== 'view' || collection?.writableView) &&
|
||||
collection.template !== 'file' &&
|
||||
collection.template !== 'sql'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const tableActionInitializers = this.app.schemaInitializerManager.get('TableActionInitializers');
|
||||
tableActionInitializers?.add('customize.bulkEdit', initializerData);
|
||||
this.app.schemaInitializerManager.addItem('GanttActionInitializers', 'customize.bulkEdit', initializerData);
|
||||
this.app.schemaInitializerManager.addItem('MapActionInitializers', 'customize.bulkEdit', initializerData);
|
||||
}
|
||||
}
|
||||
|
||||
export default BulkEditPlugin;
|
@ -0,0 +1,21 @@
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NAMESPACE = 'bulk-edit';
|
||||
|
||||
// i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
// i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
||||
}
|
||||
|
||||
export function useBulkEditTranslation() {
|
||||
return useTranslation(NAMESPACE, {
|
||||
nsMode: 'fallback',
|
||||
});
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import {
|
||||
useCollection,
|
||||
useCollectionManager,
|
||||
useRemoveGridFormItem,
|
||||
SchemaInitializerItemType,
|
||||
} from '@nocobase/client';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
|
||||
const { name, fields } = useCollection();
|
||||
const { getInterface, getCollection } = useCollectionManager();
|
||||
const form = useForm();
|
||||
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
||||
const remove = useRemoveGridFormItem();
|
||||
const filterFields = useMemo(
|
||||
() =>
|
||||
fields
|
||||
?.filter((field) => {
|
||||
return (
|
||||
field?.interface &&
|
||||
!field?.uiSchema?.['x-read-pretty'] &&
|
||||
field.interface !== 'snapshot' &&
|
||||
field.type !== 'sequence'
|
||||
);
|
||||
})
|
||||
.map((field) => {
|
||||
const interfaceConfig = getInterface(field.interface);
|
||||
const schema = {
|
||||
type: 'string',
|
||||
name: field.name,
|
||||
title: field?.uiSchema?.title || field.name,
|
||||
'x-designer': 'FormItem.Designer',
|
||||
'x-component': 'BulkEditField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field': `${name}.${field.name}`,
|
||||
};
|
||||
return {
|
||||
name: field?.uiSchema?.title || field.name,
|
||||
type: 'item',
|
||||
title: field?.uiSchema?.title || field.name,
|
||||
Component: 'CollectionFieldInitializer',
|
||||
remove: remove,
|
||||
schemaInitialize: (s) => {
|
||||
interfaceConfig?.schemaInitialize?.(s, {
|
||||
field,
|
||||
block,
|
||||
readPretty,
|
||||
targetCollection: getCollection(field.target),
|
||||
});
|
||||
},
|
||||
schema,
|
||||
} as SchemaInitializerItemType;
|
||||
}),
|
||||
[fields],
|
||||
);
|
||||
|
||||
return filterFields;
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './server';
|
||||
export { default } from './server';
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Bulk edit": "Bulk edit",
|
||||
"Data will be updated": "Data will be updated"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Bulk edit": "Edición en masa",
|
||||
"Data will be updated": "Se actualizarán los datos"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Bulk edit": "Edição em massa",
|
||||
"Data will be updated": "Os dados serão atualizados"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Bulk edit": "批量编辑",
|
||||
"Data will be updated": "更新的数据"
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
@ -0,0 +1,19 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
|
||||
export class PluginActionBulkEditServer extends Plugin {
|
||||
afterAdd() {}
|
||||
|
||||
beforeLoad() {}
|
||||
|
||||
async load() {}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginActionBulkEditServer;
|
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
@ -0,0 +1 @@
|
||||
# @nocobase/plugin-action-bulk-update
|
2
packages/plugins/@nocobase/plugin-action-bulk-update/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-bulk-update/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-action-bulk-update",
|
||||
"version":"0.17.0-alpha.7",
|
||||
"main": "dist/server/index.js",
|
||||
"displayName": "Bulk update action",
|
||||
"displayName.zh-CN": "批量更新操作",
|
||||
"description": "Provides bulk update action",
|
||||
"description.zh-CN": "提供批量更新操作",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
}
|
||||
}
|
2
packages/plugins/@nocobase/plugin-action-bulk-update/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-bulk-update/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,237 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import {
|
||||
SchemaSettings,
|
||||
ActionDesigner,
|
||||
useSchemaToolbar,
|
||||
useDesignable,
|
||||
useCompile,
|
||||
DefaultValueProvider,
|
||||
SchemaSettingsItemGroup,
|
||||
SchemaSettingsModalItem,
|
||||
SchemaSettingsActionModalItem,
|
||||
SchemaSettingsSelectItem,
|
||||
FlagProvider,
|
||||
} from '@nocobase/client';
|
||||
import { isValid, uid } from '@formily/shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useField, useFieldSchema, ISchema } from '@formily/react';
|
||||
|
||||
const MenuGroup = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const actionTitle = fieldSchema.title ? compile(fieldSchema.title) : '';
|
||||
|
||||
return (
|
||||
<SchemaSettingsItemGroup title={`${t('Customize')} > ${actionTitle}`}>{props.children}</SchemaSettingsItemGroup>
|
||||
);
|
||||
};
|
||||
|
||||
function UpdateMode() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
return (
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Data will be updated')}
|
||||
options={[
|
||||
{ label: t('Selected'), value: 'selected' },
|
||||
{ label: t('All'), value: 'all' },
|
||||
]}
|
||||
value={fieldSchema?.['x-action-settings']?.['updateMode']}
|
||||
onChange={(value) => {
|
||||
fieldSchema['x-action-settings']['updateMode'] = value;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-action-settings': fieldSchema['x-action-settings'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AfterSuccess() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
title={t('After successful submission')}
|
||||
initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('After successful submission'),
|
||||
properties: {
|
||||
successMessage: {
|
||||
title: t('Popup message'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
manualClose: {
|
||||
title: t('Popup close method'),
|
||||
enum: [
|
||||
{ label: t('Automatic close'), value: false },
|
||||
{ label: t('Manually close'), value: true },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {},
|
||||
},
|
||||
redirecting: {
|
||||
title: t('Then'),
|
||||
enum: [
|
||||
{ label: t('Stay on current page'), value: false },
|
||||
{ label: t('Redirect to'), value: true },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {},
|
||||
'x-reactions': {
|
||||
target: 'redirectTo',
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
redirectTo: {
|
||||
title: t('Link'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={(onSuccess) => {
|
||||
fieldSchema['x-action-settings']['onSuccess'] = onSuccess;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-action-settings': fieldSchema['x-action-settings'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function AssignedFieldValues() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const [initialSchema, setInitialSchema] = useState<ISchema>();
|
||||
useEffect(() => {
|
||||
const schemaUid = uid();
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-uid': schemaUid,
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'CustomFormItemInitializers',
|
||||
};
|
||||
setInitialSchema(schema);
|
||||
}, [field.address]);
|
||||
|
||||
const tips = {
|
||||
'customize:update': t(
|
||||
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
|
||||
),
|
||||
'customize:save': t(
|
||||
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
|
||||
),
|
||||
};
|
||||
const actionType = fieldSchema['x-action'] ?? '';
|
||||
const onSubmit = useCallback(
|
||||
(assignedValues) => {
|
||||
fieldSchema['x-action-settings']['assignedValues'] = assignedValues;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-action-settings': fieldSchema['x-action-settings'],
|
||||
},
|
||||
});
|
||||
},
|
||||
[dn, fieldSchema],
|
||||
);
|
||||
|
||||
return (
|
||||
<FlagProvider isInAssignFieldValues={true}>
|
||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||
<SchemaSettingsActionModalItem
|
||||
title={t('Assign field values')}
|
||||
maskClosable={false}
|
||||
initialSchema={initialSchema}
|
||||
initialValues={fieldSchema?.['x-action-settings']?.assignedValues}
|
||||
modalTip={tips[actionType]}
|
||||
uid={fieldSchema?.['x-action-settings']?.schemaUid}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</DefaultValueProvider>
|
||||
</FlagProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const bulkUpdateActionSettings = new SchemaSettings({
|
||||
name: 'ActionSettings:customize:bulkUpdate',
|
||||
items: [
|
||||
{
|
||||
name: 'Customize',
|
||||
Component: MenuGroup,
|
||||
children: [
|
||||
{
|
||||
name: 'editButton',
|
||||
Component: ActionDesigner.ButtonEditor,
|
||||
useComponentProps() {
|
||||
const { buttonEditorProps } = useSchemaToolbar();
|
||||
return buttonEditorProps;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'updateMode',
|
||||
Component: UpdateMode,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(
|
||||
fieldSchema['x-action'],
|
||||
);
|
||||
return isUpdateModePopupAction;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'assignFieldValues',
|
||||
Component: AssignedFieldValues,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return isValid(fieldSchema?.['x-action-settings']?.assignedValues);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'afterSuccess',
|
||||
Component: AfterSuccess,
|
||||
useVisible() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
sort: 100,
|
||||
Component: ActionDesigner.RemoveButton as any,
|
||||
useComponentProps() {
|
||||
const { removeButtonProps } = useSchemaToolbar();
|
||||
return removeButtonProps;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export { bulkUpdateActionSettings };
|
@ -0,0 +1,17 @@
|
||||
import { SchemaComponentOptions } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { CustomizeActionInitializer } from './CustomizeActionInitializer';
|
||||
import { useCustomizeBulkUpdateActionProps } from './utils';
|
||||
|
||||
export const BulkUpdatePluginProvider = (props: any) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
components={{ CustomizeActionInitializer }}
|
||||
scope={{
|
||||
useCustomizeBulkUpdateActionProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { BlockInitializer, useCollection, useSchemaInitializerItem } from '@nocobase/client';
|
||||
|
||||
export const CustomizeActionInitializer = () => {
|
||||
const collection = useCollection();
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
|
||||
const schema = {};
|
||||
if (collection && schema['x-acl-action']) {
|
||||
schema['x-acl-action'] = `${collection.name}:${schema['x-acl-action']}`;
|
||||
schema['x-decorator'] = 'ACLActionProvider';
|
||||
}
|
||||
|
||||
return <BlockInitializer {...itemConfig} schema={schema} item={itemConfig} />;
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { expect, test, oneEmptyTableBlockWithCustomizeActions } from '@nocobase/test/client';
|
||||
import { oneEmptyGantt } from './utils';
|
||||
|
||||
test.describe('TableActionInitializers & GanttActionInitializers & MapActionInitializers ', () => {
|
||||
test('TableActionInitializers should add bulk update', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithCustomizeActions).goto();
|
||||
await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Customize right' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Bulk update' }).click();
|
||||
await expect(page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table')).toBeVisible();
|
||||
});
|
||||
test('GanttActionInitializers should add bulk update', async ({ page, mockPage, mockRecords }) => {
|
||||
const nocoPage = await mockPage(oneEmptyGantt).waitForInit();
|
||||
await mockRecords('general', 3);
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('schema-initializer-ActionBar-GanttActionInitializers-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Customize right' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Bulk update' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await expect(page.getByRole('button', { name: 'Bulk update' })).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
import { expect, test } from '@nocobase/test/client';
|
||||
import { oneEmptyTableBlockWithCustomizeUpdate } from './utils';
|
||||
|
||||
test.describe('data will be updated && Assign field values && after successful submission', () => {
|
||||
test('data will be updated && Assign field values && after successful submission', async ({
|
||||
page,
|
||||
mockPage,
|
||||
mockRecords,
|
||||
}) => {
|
||||
await mockPage(oneEmptyTableBlockWithCustomizeUpdate).goto();
|
||||
await mockRecords('general', 2);
|
||||
await page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action-Action.Designer-general').hover();
|
||||
//默认是选中的数据
|
||||
await expect(await page.getByTitle('Data will be updated').getByText('Selected')).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'Data will be updated' }).click();
|
||||
//切换为全部数据
|
||||
await page.getByRole('option', { name: 'All' }).click();
|
||||
//字段赋值
|
||||
await page.getByRole('menuitem', { name: 'Assign field values' }).click();
|
||||
await page.getByLabel('schema-initializer-Grid-CustomFormItemInitializers-general').click();
|
||||
await page.getByRole('menuitem', { name: 'Single select' }).click();
|
||||
await page.getByTestId('select-single').click();
|
||||
await page.getByText('option3').click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action-Action.Designer-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'After successful submission' }).click();
|
||||
await page.getByLabel('Manually close').check();
|
||||
await page.getByLabel('Redirect to').check();
|
||||
await page.locator('input[type="text"]').click();
|
||||
await page.locator('input[type="text"]').fill('/admin/pm/list/local/');
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table').click();
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest((request) => request.url().includes('api/general:update')),
|
||||
page.getByRole('button', { name: 'OK' }).click(),
|
||||
]);
|
||||
const postData = request.postDataJSON();
|
||||
//更新的数据符合预期
|
||||
expect(postData.singleSelect).toEqual('option3');
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
//成功后跳转路由
|
||||
expect(page.url()).toContain('/admin/pm/list/local/');
|
||||
});
|
||||
});
|
@ -0,0 +1,529 @@
|
||||
import { CollectionSetting, PageConfig, generalWithM2oSingleSelect } from '@nocobase/test/client';
|
||||
|
||||
/**
|
||||
* 1. 创建一个名为 general 的 collection,其包含 时间、Percent 类型的字段
|
||||
*/
|
||||
export const generalWithDatetimeFields: CollectionSetting[] = [
|
||||
{
|
||||
name: 'general',
|
||||
title: 'General',
|
||||
fields: [
|
||||
{
|
||||
name: 'singleLineText',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
title: 'Single line text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'singleLineText2',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
title: 'Single line text2',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'startDatetime',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'Start date time',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'startDatetime2',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'Start date time2',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'endDatetime',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'End date time',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'endDatetime2',
|
||||
interface: 'datetime',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'DatePicker',
|
||||
title: 'End date time2',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'f_t22o7loai3j',
|
||||
interface: 'integer',
|
||||
isForeignKey: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: 'f_t22o7loai3j',
|
||||
'x-component': 'InputNumber',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'manyToOne',
|
||||
interface: 'm2o',
|
||||
foreignKey: 'f_t22o7loai3j',
|
||||
uiSchema: {
|
||||
'x-component': 'AssociationField',
|
||||
'x-component-props': {
|
||||
multiple: false,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
title: 'Many to one',
|
||||
},
|
||||
target: 'users',
|
||||
targetKey: 'id',
|
||||
},
|
||||
{
|
||||
name: 'percent',
|
||||
type: 'float',
|
||||
interface: 'percent',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
step: '0.01',
|
||||
stringMode: true,
|
||||
addonAfter: '%',
|
||||
},
|
||||
'x-component': 'Percent',
|
||||
title: 'Percent',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'percent2',
|
||||
type: 'float',
|
||||
interface: 'percent',
|
||||
uiSchema: {
|
||||
'x-component-props': {
|
||||
step: '0.01',
|
||||
stringMode: true,
|
||||
addonAfter: '%',
|
||||
},
|
||||
'x-component': 'Percent',
|
||||
title: 'Percent2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const oneEmptyGantt: PageConfig = {
|
||||
collections: generalWithDatetimeFields,
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
jebhzap4dzi: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
properties: {
|
||||
gl57m4hyewf: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
jm7n5dybw6t: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
nsq0rdemz4i: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action': 'general:list',
|
||||
'x-decorator': 'GanttBlockProvider',
|
||||
'x-decorator-props': {
|
||||
collection: 'general',
|
||||
resource: 'general',
|
||||
action: 'list',
|
||||
fieldNames: {
|
||||
id: 'id',
|
||||
start: 'startDatetime',
|
||||
range: 'day',
|
||||
title: 'singleLineText',
|
||||
end: 'endDatetime',
|
||||
},
|
||||
params: {
|
||||
paginate: false,
|
||||
},
|
||||
},
|
||||
'x-designer': 'Gantt.Designer',
|
||||
'x-component': 'CardItem',
|
||||
properties: {
|
||||
zf07g7relim: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Gantt',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGanttBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
toolBar: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
'x-initializer': 'GanttActionInitializers',
|
||||
'x-uid': 'guwovmwt4c0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
table: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-decorator': 'div',
|
||||
'x-decorator-props': {
|
||||
style: {
|
||||
float: 'left',
|
||||
maxWidth: '35%',
|
||||
},
|
||||
},
|
||||
'x-initializer': 'TableColumnInitializers',
|
||||
'x-component': 'TableV2',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
pagination: false,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-initializer': 'TableActionColumnInitializers',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-uid': '9in7s3pymsd',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'rgpbsjwvq2h',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'g7qwompxaeo',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
detail: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Gantt.Event',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
title: '{{ t("View record") }}',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
'x-uid': 'gxtfjqzxbfu',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'zxwwx4358p1',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '1p0tnmzpsim',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'hcue8v3fwti',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'fg7cjxu4hxp',
|
||||
'x-async': false,
|
||||
'x-index': 3,
|
||||
},
|
||||
},
|
||||
'x-uid': 'rf47sf7k16z',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tritqukkd86',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '3am2ctuhyka',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'p8pesbc7fiv',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ddm9fhkvrbw',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'vpk8sp3eap1',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
/**
|
||||
* 一个空的 Table 区块,并且有这些按钮:Bulk edit
|
||||
*/
|
||||
export const oneEmptyTableBlockWithCustomizeUpdate: PageConfig = {
|
||||
collections: generalWithM2oSingleSelect,
|
||||
pageSchema: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Page',
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
s409jhc9wzp: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'BlockInitializers',
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
btrf53i4lpi: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Grid.Row',
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
'6u5d4dju1zw': {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Grid.Col',
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
yxyx102howk: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-designer': 'TableBlockDesigner',
|
||||
'x-component': 'CardItem',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'general:list',
|
||||
'x-filter-targets': [],
|
||||
'x-decorator-props': {
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
dragSort: false,
|
||||
resource: 'general',
|
||||
showIndex: true,
|
||||
collection: 'general',
|
||||
disableTemplate: false,
|
||||
},
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'ActionBar',
|
||||
'x-initializer': 'TableActionInitializers',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
hsb2p0q5ftw: {
|
||||
type: 'void',
|
||||
title: '{{ t("Bulk update") }}',
|
||||
version: '2.0',
|
||||
'x-align': 'right',
|
||||
'x-action': 'customize:bulkUpdate',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-settings': 'ActionSettings:customize:bulkUpdate',
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'update',
|
||||
'x-action-settings': {
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Updated successfully")}}',
|
||||
},
|
||||
updateMode: 'selected',
|
||||
assignedValues: {},
|
||||
},
|
||||
'x-component-props': {
|
||||
icon: 'EditOutlined',
|
||||
useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||
},
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
_isJSONSchemaObject: true,
|
||||
'x-uid': '5nghpuc3f3x',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '8lq9zqwwkqd',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
dcblkvx1jj1: {
|
||||
type: 'array',
|
||||
version: '2.0',
|
||||
'x-component': 'TableV2',
|
||||
'x-initializer': 'TableColumnInitializers',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
useProps: '{{ useTableBlockProps }}',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
version: '2.0',
|
||||
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-initializer': 'TableActionColumnInitializers',
|
||||
'x-action-column': 'actions',
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Space',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
_isJSONSchemaObject: true,
|
||||
'x-uid': 'dqt7mgaazmh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'qp1nqbd1ccv',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '32v8q0beyo2',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 't785fa9uwcc',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'pebalqz6iw2',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'drll4zh4jq1',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '5fdz5g6qr6y',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'z47gi9qo5jn',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
import { Plugin, useCollection } from '@nocobase/client';
|
||||
import { BulkUpdatePluginProvider } from './BulkUpdatePluginProvider';
|
||||
import { bulkUpdateActionSettings } from './BulkUpdateAction.Settings';
|
||||
export class BulkUpdatePlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(BulkUpdatePluginProvider);
|
||||
this.app.schemaSettingsManager.add(bulkUpdateActionSettings);
|
||||
|
||||
const initializerData = {
|
||||
title: '{{t("Bulk update")}}',
|
||||
Component: 'CustomizeActionInitializer',
|
||||
name: 'bulkUpdate',
|
||||
schema: {
|
||||
type: 'void',
|
||||
title: '{{ t("Bulk update") }}',
|
||||
'x-component': 'Action',
|
||||
'x-align': 'right',
|
||||
'x-acl-action': 'update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-action': 'customize:bulkUpdate',
|
||||
'x-settings': 'ActionSettings:customize:bulkUpdate',
|
||||
'x-action-settings': {
|
||||
assignedValues: {},
|
||||
updateMode: 'selected',
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Updated successfully")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
icon: 'EditOutlined',
|
||||
useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||
},
|
||||
},
|
||||
useVisible() {
|
||||
const collection = useCollection();
|
||||
return (
|
||||
(collection.template !== 'view' || collection?.writableView) &&
|
||||
collection.template !== 'file' &&
|
||||
collection.template !== 'sql'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const tableActionInitializers = this.app.schemaInitializerManager.get('TableActionInitializers');
|
||||
tableActionInitializers?.add('customize.bulkUpdate', initializerData);
|
||||
this.app.schemaInitializerManager.addItem('GanttActionInitializers', 'customize.bulkUpdate', initializerData);
|
||||
this.app.schemaInitializerManager.addItem('MapActionInitializers', 'customize.bulkUpdate', initializerData);
|
||||
}
|
||||
}
|
||||
|
||||
export default BulkUpdatePlugin;
|
@ -0,0 +1,21 @@
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NAMESPACE = 'bulk-update';
|
||||
|
||||
// i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
// i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
||||
}
|
||||
|
||||
export function useBulkUpdateTranslation() {
|
||||
return useTranslation(NAMESPACE, {
|
||||
nsMode: 'fallback',
|
||||
});
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import {
|
||||
useBlockRequestContext,
|
||||
useTableBlockContext,
|
||||
useCompile,
|
||||
useRecord,
|
||||
useVariables,
|
||||
useCollection,
|
||||
useLocalVariables,
|
||||
isVariable,
|
||||
transformVariableValue,
|
||||
TableFieldResource,
|
||||
} from '@nocobase/client';
|
||||
import { isURL } from '@nocobase/utils/client';
|
||||
import { SchemaExpressionScopeContext, useFieldSchema, useField } from '@formily/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { App, message } from 'antd';
|
||||
import { useContext } from 'react';
|
||||
import { useBulkUpdateTranslation } from './locale';
|
||||
|
||||
export const useCustomizeBulkUpdateActionProps = () => {
|
||||
const { field, resource, __parent, service } = useBlockRequestContext();
|
||||
const expressionScope = useContext(SchemaExpressionScopeContext);
|
||||
const actionSchema = useFieldSchema();
|
||||
const tableBlockContext = useTableBlockContext();
|
||||
const { rowKey } = tableBlockContext;
|
||||
const selectedRecordKeys =
|
||||
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
||||
const navigate = useNavigate();
|
||||
const compile = useCompile();
|
||||
const { t } = useBulkUpdateTranslation();
|
||||
const actionField: any = useField();
|
||||
const { modal } = App.useApp();
|
||||
const variables = useVariables();
|
||||
const record = useRecord();
|
||||
const { name, getField } = useCollection();
|
||||
const localVariables = useLocalVariables({ currentRecord: { __parent: record, __collectionName: name } });
|
||||
|
||||
return {
|
||||
async onClick() {
|
||||
const {
|
||||
assignedValues: originalAssignedValues = {},
|
||||
onSuccess,
|
||||
updateMode,
|
||||
} = actionSchema?.['x-action-settings'] ?? {};
|
||||
actionField.data = field.data || {};
|
||||
actionField.data.loading = true;
|
||||
|
||||
const assignedValues = {};
|
||||
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
|
||||
const value = originalAssignedValues[key];
|
||||
const collectionField = getField(key);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!collectionField) {
|
||||
throw new Error(`useCustomizeBulkUpdateActionProps: field "${key}" not found in collection "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isVariable(value)) {
|
||||
const result = await variables?.parseVariable(value, localVariables);
|
||||
if (result) {
|
||||
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
|
||||
}
|
||||
} else if (value != null && value !== '') {
|
||||
assignedValues[key] = value;
|
||||
}
|
||||
});
|
||||
await Promise.all(waitList);
|
||||
|
||||
modal.confirm({
|
||||
title: t('Bulk update'),
|
||||
content: updateMode === 'selected' ? t('Update selected data?') : t('Update all data?'),
|
||||
async onOk() {
|
||||
const { filter } = service.params?.[0] ?? {};
|
||||
const updateData: { filter?: any; values: any; forceUpdate: boolean } = {
|
||||
values: { ...assignedValues },
|
||||
filter,
|
||||
forceUpdate: false,
|
||||
};
|
||||
if (updateMode === 'selected') {
|
||||
if (!selectedRecordKeys?.length) {
|
||||
message.error(t('Please select the records to be updated'));
|
||||
actionField.data.loading = false;
|
||||
return;
|
||||
}
|
||||
updateData.filter = { $and: [{ [rowKey || 'id']: { $in: selectedRecordKeys } }] };
|
||||
}
|
||||
if (!updateData.filter) {
|
||||
updateData.forceUpdate = true;
|
||||
}
|
||||
try {
|
||||
await resource.update(updateData);
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
} finally {
|
||||
actionField.data.loading = false;
|
||||
}
|
||||
service?.refresh?.();
|
||||
if (!(resource instanceof TableFieldResource)) {
|
||||
__parent?.service?.refresh?.();
|
||||
}
|
||||
if (!onSuccess?.successMessage) {
|
||||
return;
|
||||
}
|
||||
if (onSuccess?.manualClose) {
|
||||
modal.success({
|
||||
title: compile(onSuccess?.successMessage),
|
||||
onOk: async () => {
|
||||
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||
if (isURL(onSuccess.redirectTo)) {
|
||||
window.location.href = onSuccess.redirectTo;
|
||||
} else {
|
||||
navigate(onSuccess.redirectTo);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.success(compile(onSuccess?.successMessage));
|
||||
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||
if (isURL(onSuccess.redirectTo)) {
|
||||
window.location.href = onSuccess.redirectTo;
|
||||
} else {
|
||||
navigate(onSuccess.redirectTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async onCancel() {
|
||||
actionField.data.loading = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './server';
|
||||
export { default } from './server';
|
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"Bulk update": "Bulk update",
|
||||
"After successful bulk update": "After successful bulk update",
|
||||
"Please select the records to be updated": "Please select the records to be updated",
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"Bulk update": "Actualización en masa",
|
||||
"After successful bulk update": "Tras una actualización en masa correcta",
|
||||
"Please select the records to be updated": "Seleccione los registros que desea actualizar",
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"Bulk update": "Atualização em massa",
|
||||
"After successful bulk update": "Após a atualização em massa bem sucedida",
|
||||
"Please select the records to be updated": "Por favor, selecione os registros a serem atualizados",
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
'Bulk update': '批量更新',
|
||||
'After successful bulk update': '批量成功更新后',
|
||||
'Please select the records to be updated': '请选择要更新的记录',
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
@ -0,0 +1,19 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
|
||||
export class PluginActionBulkUpdateServer extends Plugin {
|
||||
afterAdd() {}
|
||||
|
||||
beforeLoad() {}
|
||||
|
||||
async load() {}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginActionBulkUpdateServer;
|
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
@ -0,0 +1 @@
|
||||
# @nocobase/plugin-action-duplicate
|
2
packages/plugins/@nocobase/plugin-action-duplicate/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-duplicate/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-action-duplicate",
|
||||
"version": "0.17.0-alpha.7",
|
||||
"main": "dist/server/index.js",
|
||||
"displayName": "Data duplicate",
|
||||
"displayName.zh-CN": "数据复制",
|
||||
"description": "Provides deplicate action",
|
||||
"description.zh-CN": "提供复制数据操作",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
}
|
||||
}
|
2
packages/plugins/@nocobase/plugin-action-duplicate/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-action-duplicate/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user