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:
katherinehhh 2023-12-16 21:59:33 +08:00 committed by GitHub
parent 823a112435
commit b1610e6994
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
250 changed files with 7177 additions and 5336 deletions

View File

@ -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,
});
}
}

View File

@ -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,

View File

@ -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';

View File

@ -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;
}
}
`;
});

View File

@ -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';

View File

@ -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",

View File

@ -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' });

View File

@ -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,

View File

@ -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>

View File

@ -67,7 +67,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
<span>
{snapshot ? (
text
) : enableLink !== false && !props.enableLink ? (
) : enableLink !== false ? (
<a
onClick={(e) => {
e.stopPropagation();

View File

@ -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);
// 过滤掉已经被删除的字段

View File

@ -11,4 +11,6 @@ FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
FormV2.Templates = Templates;
export * from './FormField';
export * from './Templates';
export { FormV2, DetailsDesigner };

View File

@ -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>
);
};

View File

@ -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 />);
});
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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();
});
});
});

View File

@ -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>
);
};

View File

@ -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 };

View File

@ -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' } },
},
},
};
});

View File

@ -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']);

View File

@ -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',
},
],
},
{

View File

@ -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',
},
},
],
},
{

View File

@ -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',
},
},
},
],
},
],

View File

@ -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',
},
},
],
},
{

View File

@ -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';
},
},
],
},
{

View File

@ -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")}}',

View File

@ -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';

View File

@ -1,4 +1,3 @@
export * from './assigned-field';
export * from './BulkEditField';
export * from './CreateRecordAction';
export * from './DuplicateAction'

View File

@ -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);

View File

@ -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';

View File

@ -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,

View File

@ -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('.');

View File

@ -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';

View File

@ -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';

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @nocobase/plugin-action-bulk-edit

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -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"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -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 };

View File

@ -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',

View File

@ -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>
);
};

View File

@ -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();

View File

@ -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")}}',

View File

@ -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 = {

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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,
},
};

View File

@ -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;

View File

@ -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',
});
}

View File

@ -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;
};

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,4 @@
{
"Bulk edit": "Bulk edit",
"Data will be updated": "Data will be updated"
}

View File

@ -0,0 +1,4 @@
{
"Bulk edit": "Edición en masa",
"Data will be updated": "Se actualizarán los datos"
}

View File

@ -0,0 +1,4 @@
{
"Bulk edit": "Edição em massa",
"Data will be updated": "Os dados serão atualizados"
}

View File

@ -0,0 +1,4 @@
{
"Bulk edit": "批量编辑",
"Data will be updated": "更新的数据"
}

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -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;

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @nocobase/plugin-action-bulk-update

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -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"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -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 };

View File

@ -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>
);
};

View File

@ -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} />;
};

View File

@ -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();
});
});

View File

@ -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/');
});
});

View File

@ -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,
},
};

View File

@ -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;

View File

@ -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',
});
}

View File

@ -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;
},
});
},
};
};

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -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",
};

View File

@ -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",
};

View File

@ -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",
};

View File

@ -0,0 +1,5 @@
export default {
'Bulk update': '批量更新',
'After successful bulk update': '批量成功更新后',
'Please select the records to be updated': '请选择要更新的记录',
};

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -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;

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @nocobase/plugin-action-duplicate

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -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"
}
}

View 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