refactor(association field): association field support tag field mode (#2251)
* refactor: association field support tag field mode * refactor: association field support tag field mode * refactor: locale improve * refactor: color field * refactor: locale improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: colorPicker refactor * refactor: colorPicker refactor * fix: color field failed to config defaultValue * style: style improve * refactor: code improve * fix: merge bug * refactor: color field defaultValue --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									8f1d0d80af
								
							
						
					
					
						commit
						18bf6e507e
					
				@ -0,0 +1,26 @@
 | 
			
		||||
import { defaultProps, operators } from './properties';
 | 
			
		||||
import { IField } from './types';
 | 
			
		||||
 | 
			
		||||
export const color: IField = {
 | 
			
		||||
  name: 'color',
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  group: 'basic',
 | 
			
		||||
  order: 10,
 | 
			
		||||
  title: '{{t("Color")}}',
 | 
			
		||||
  default: {
 | 
			
		||||
    type: 'string',
 | 
			
		||||
    uiSchema: {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      'x-component': 'ColorPicker',
 | 
			
		||||
      default: '#1677FF',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  availableTypes: ['string'],
 | 
			
		||||
  hasDefaultValue: true,
 | 
			
		||||
  properties: {
 | 
			
		||||
    ...defaultProps,
 | 
			
		||||
  },
 | 
			
		||||
  filterable: {
 | 
			
		||||
    operators: operators.string,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@ -2,6 +2,7 @@ export * from './checkbox';
 | 
			
		||||
export * from './checkboxGroup';
 | 
			
		||||
export * from './chinaRegion';
 | 
			
		||||
export * from './collection';
 | 
			
		||||
export * from './color';
 | 
			
		||||
export * from './createdAt';
 | 
			
		||||
export * from './createdBy';
 | 
			
		||||
export * from './datetime';
 | 
			
		||||
 | 
			
		||||
@ -711,6 +711,8 @@ export default {
 | 
			
		||||
  "Date display format":"Date display format",
 | 
			
		||||
  "Assign  data scope for the template":"Assign  data scope for the template",
 | 
			
		||||
  "Table selected records":"Table selected records",
 | 
			
		||||
  "Tag":"Tag",
 | 
			
		||||
  "Tag color field":"Tag color field",
 | 
			
		||||
  "Sync successfully":"Sync successfully",
 | 
			
		||||
  "Sync from form fields":"Sync from form fields",
 | 
			
		||||
  "Select all":"Select all"
 | 
			
		||||
 | 
			
		||||
@ -621,6 +621,8 @@ export default {
 | 
			
		||||
  "Allow add new, update and delete actions":"削除変更操作の許可",
 | 
			
		||||
  "Date display format":"日付表示形式",
 | 
			
		||||
  "Assign  data scope for the template":"テンプレートのデータ範囲の指定",
 | 
			
		||||
  "Tag":"タブ",
 | 
			
		||||
  "Tag color field":"ラベルの色フィールド",
 | 
			
		||||
  "Sync successfully":"同期成功",
 | 
			
		||||
  "Sync from form fields":"フォームフィールドの同期",
 | 
			
		||||
  "Select all":"すべて選択"
 | 
			
		||||
 | 
			
		||||
@ -796,6 +796,8 @@ export default {
 | 
			
		||||
  "Date display format":"日期显示格式",
 | 
			
		||||
  "Assign  data scope for the template":"为模板指定数据范围",
 | 
			
		||||
  "Table selected records":"表格中选中的记录",
 | 
			
		||||
  "Tag":"标签",
 | 
			
		||||
  "Tag color field":"标签颜色字段",
 | 
			
		||||
  "Sync successfully":"同步成功",
 | 
			
		||||
  "Sync from form fields":"同步表单字段",
 | 
			
		||||
  "Select all":"全选"
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,123 @@
 | 
			
		||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { toArr } from '@formily/shared';
 | 
			
		||||
import flat from 'flat';
 | 
			
		||||
import React, { Fragment, useRef, useState } from 'react';
 | 
			
		||||
import { useDesignable } from '../../';
 | 
			
		||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
 | 
			
		||||
import { CollectionProvider } from '../../../collection-manager';
 | 
			
		||||
import { RecordProvider, useRecord } from '../../../record-provider';
 | 
			
		||||
import { FormProvider } from '../../core';
 | 
			
		||||
import { useCompile } from '../../hooks';
 | 
			
		||||
import { ActionContextProvider, useActionContext } from '../action';
 | 
			
		||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
 | 
			
		||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
 | 
			
		||||
import schema from './schema';
 | 
			
		||||
import { getTabFormatValue, useLabelUiSchema } from './util';
 | 
			
		||||
 | 
			
		||||
interface IEllipsisWithTooltipRef {
 | 
			
		||||
  setPopoverVisible: (boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const toValue = (value, placeholder) => {
 | 
			
		||||
  if (value === null || value === undefined) {
 | 
			
		||||
    return placeholder;
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
};
 | 
			
		||||
export const ReadPrettyInternalTag: React.FC = observer(
 | 
			
		||||
  (props: any) => {
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
    const recordCtx = useRecord();
 | 
			
		||||
    const { enableLink, tagColorField } = fieldSchema['x-component-props'];
 | 
			
		||||
    // value 做了转换,但 props.value 和原来 useField().value 的值不一致
 | 
			
		||||
    const field = useField();
 | 
			
		||||
    const fieldNames = useFieldNames(props);
 | 
			
		||||
    const [visible, setVisible] = useState(false);
 | 
			
		||||
    const insertViewer = useInsertSchema('Viewer');
 | 
			
		||||
    const { options: collectionField } = useAssociationFieldContext();
 | 
			
		||||
    const [record, setRecord] = useState({});
 | 
			
		||||
    const compile = useCompile();
 | 
			
		||||
    const { designable } = useDesignable();
 | 
			
		||||
    const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
 | 
			
		||||
    const { snapshot } = useActionContext();
 | 
			
		||||
    const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
 | 
			
		||||
    const tagColor = flat(recordCtx)[`${fieldSchema.name}.${tagColorField}`];
 | 
			
		||||
    const renderRecords = () =>
 | 
			
		||||
      toArr(props.value).map((record, index, arr) => {
 | 
			
		||||
        const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
 | 
			
		||||
        const text = getTabFormatValue(compile(labelUiSchema), val, tagColor);
 | 
			
		||||
        return (
 | 
			
		||||
          <Fragment key={`${record.id}_${index}`}>
 | 
			
		||||
            <span>
 | 
			
		||||
              {snapshot ? (
 | 
			
		||||
                text
 | 
			
		||||
              ) : enableLink !== false ? (
 | 
			
		||||
                <a
 | 
			
		||||
                  onClick={(e) => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    if (designable) {
 | 
			
		||||
                      insertViewer(schema.Viewer);
 | 
			
		||||
                    }
 | 
			
		||||
                    setVisible(true);
 | 
			
		||||
                    setRecord(record);
 | 
			
		||||
                    ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {text}
 | 
			
		||||
                </a>
 | 
			
		||||
              ) : (
 | 
			
		||||
                text
 | 
			
		||||
              )}
 | 
			
		||||
            </span>
 | 
			
		||||
            {index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
 | 
			
		||||
          </Fragment>
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    const renderWithoutTableFieldResourceProvider = () => (
 | 
			
		||||
      <WithoutTableFieldResource.Provider value={true}>
 | 
			
		||||
        <FormProvider>
 | 
			
		||||
          <RecursionField
 | 
			
		||||
            schema={fieldSchema}
 | 
			
		||||
            onlyRenderProperties
 | 
			
		||||
            basePath={field.address}
 | 
			
		||||
            filterProperties={(s) => {
 | 
			
		||||
              return s['x-component'] === 'AssociationField.Viewer';
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </FormProvider>
 | 
			
		||||
      </WithoutTableFieldResource.Provider>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderRecordProvider = () => {
 | 
			
		||||
      const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
 | 
			
		||||
 | 
			
		||||
      return collectionFieldNames && collectionFieldNames.length > 2 ? (
 | 
			
		||||
        <RecordProvider record={recordCtx[collectionFieldNames[1]]}>
 | 
			
		||||
          <RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
 | 
			
		||||
        </RecordProvider>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
 | 
			
		||||
          <CollectionProvider name={collectionField?.target ?? collectionField?.targetCollection}>
 | 
			
		||||
            <EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
 | 
			
		||||
              {renderRecords()}
 | 
			
		||||
            </EllipsisWithTooltip>
 | 
			
		||||
            <ActionContextProvider
 | 
			
		||||
              value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
 | 
			
		||||
            >
 | 
			
		||||
              {renderRecordProvider()}
 | 
			
		||||
            </ActionContextProvider>
 | 
			
		||||
          </CollectionProvider>
 | 
			
		||||
        </BlockAssociationContext.Provider>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'ReadPrettyInternalTag' },
 | 
			
		||||
);
 | 
			
		||||
@ -1,11 +1,12 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useField, observer } from '@formily/react';
 | 
			
		||||
import { observer } from '@formily/react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { AssociationFieldProvider } from './AssociationFieldProvider';
 | 
			
		||||
import { InternalNester } from './InternalNester';
 | 
			
		||||
import { ReadPrettyInternalViewer } from './InternalViewer';
 | 
			
		||||
import { InternalSubTable } from './InternalSubTable';
 | 
			
		||||
import { FileManageReadPretty } from './FileManager';
 | 
			
		||||
import { useAssociationFieldContext } from './hooks';
 | 
			
		||||
import { InternalNester } from './InternalNester';
 | 
			
		||||
import { InternalSubTable } from './InternalSubTable';
 | 
			
		||||
import { ReadPrettyInternalTag } from './InternalTag';
 | 
			
		||||
import { ReadPrettyInternalViewer } from './InternalViewer';
 | 
			
		||||
 | 
			
		||||
const ReadPrettyAssociationField = observer(
 | 
			
		||||
  (props: any) => {
 | 
			
		||||
@ -14,6 +15,7 @@ const ReadPrettyAssociationField = observer(
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
 | 
			
		||||
        {currentMode === 'Tag' && <ReadPrettyInternalTag {...props} />}
 | 
			
		||||
        {currentMode === 'Nester' && <InternalNester {...props} />}
 | 
			
		||||
        {currentMode === 'SubTable' && <InternalSubTable {...props} />}
 | 
			
		||||
        {currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,28 @@ export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag =
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getTabFormatValue = (labelUiSchema: ISchema, value: any, tagColor): any => {
 | 
			
		||||
  const options = labelUiSchema?.enum;
 | 
			
		||||
  if (Array.isArray(options) && value) {
 | 
			
		||||
    const values = toArr(value).map((val) => {
 | 
			
		||||
      const opt: any = options.find((option: any) => option.value === val);
 | 
			
		||||
      return React.createElement(Tag, { color: tagColor||opt?.color }, opt?.label);
 | 
			
		||||
    });
 | 
			
		||||
    return values;
 | 
			
		||||
  }
 | 
			
		||||
  switch (labelUiSchema?.['x-component']) {
 | 
			
		||||
    case 'DatePicker':
 | 
			
		||||
      return React.createElement(
 | 
			
		||||
        Tag,
 | 
			
		||||
        { color: tagColor },
 | 
			
		||||
        getDatePickerLabels({ ...labelUiSchema?.['x-component-props'], value }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      return React.createElement(Tag, { color: tagColor }, value);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function flatData(data) {
 | 
			
		||||
  const newArr = [];
 | 
			
		||||
  for (let i = 0; i < data.length; i++) {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
 | 
			
		||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
 | 
			
		||||
import { ColorPicker as AntdColorPicker } from 'antd';
 | 
			
		||||
import cls from 'classnames';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
export const ColorPicker = connect(
 | 
			
		||||
  (props) => {
 | 
			
		||||
    const { value, onChange, ...others } = props;
 | 
			
		||||
    return (
 | 
			
		||||
      <AntdColorPicker
 | 
			
		||||
        value={value}
 | 
			
		||||
        trigger="hover"
 | 
			
		||||
        {...others}
 | 
			
		||||
        destroyTooltipOnHide
 | 
			
		||||
        getPopupContainer={(current) => current}
 | 
			
		||||
        presets={[
 | 
			
		||||
          {
 | 
			
		||||
            label: 'Recommended',
 | 
			
		||||
            colors: [
 | 
			
		||||
              '#8BBB11',
 | 
			
		||||
              '#52C41A',
 | 
			
		||||
              '#13A8A8',
 | 
			
		||||
              '#1677FF',
 | 
			
		||||
              '#F5222D',
 | 
			
		||||
              '#FADB14',
 | 
			
		||||
              '#FA8C164D',
 | 
			
		||||
              '#FADB144D',
 | 
			
		||||
              '#52C41A4D',
 | 
			
		||||
              '#1677FF4D',
 | 
			
		||||
              '#2F54EB4D',
 | 
			
		||||
              '#722ED14D',
 | 
			
		||||
              '#EB2F964D',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        onChange={(color) => onChange(color.toHexString())}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  mapProps((props, field) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...props,
 | 
			
		||||
    };
 | 
			
		||||
  }),
 | 
			
		||||
  mapReadPretty((props) => {
 | 
			
		||||
    const prefixCls = usePrefixCls('description-color-picker', props);
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={cls(
 | 
			
		||||
          prefixCls,
 | 
			
		||||
          css`
 | 
			
		||||
            display: inline-flex;
 | 
			
		||||
            .ant-color-picker-trigger-disabled {
 | 
			
		||||
              cursor: default;
 | 
			
		||||
            }
 | 
			
		||||
          `,
 | 
			
		||||
          props.className,
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <AntdColorPicker disabled value={props.value} size="small" {...props} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default ColorPicker;
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
 | 
			
		||||
import type { ColorPickerProps as AntdColorPickerProps } from 'antd/es/color-picker';
 | 
			
		||||
import cls from 'classnames';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
type Composed = {
 | 
			
		||||
  ColorPicker: React.FC<AntdColorPickerProps>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ReadPretty: Composed = () => null;
 | 
			
		||||
 | 
			
		||||
ReadPretty.ColorPicker = function ColorPicker(props: any) {
 | 
			
		||||
  const prefixCls = usePrefixCls('description-color-picker', props);
 | 
			
		||||
 | 
			
		||||
  if (!props.value) {
 | 
			
		||||
    return <div></div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={cls(prefixCls, props.className)}>
 | 
			
		||||
      <ColorPicker showText disabled value={props.value} size='small'/>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
/**
 | 
			
		||||
 * title: ColorPicker
 | 
			
		||||
 */
 | 
			
		||||
import { FormItem } from '@formily/antd-v5';
 | 
			
		||||
import { ColorPicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
const schema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  properties: {
 | 
			
		||||
    input: {
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      title: `Editable`,
 | 
			
		||||
      'x-decorator': 'FormItem',
 | 
			
		||||
      'x-component': 'ColorPicker',
 | 
			
		||||
      'x-reactions': {
 | 
			
		||||
        target: '*(read1,read2)',
 | 
			
		||||
        fulfill: {
 | 
			
		||||
          state: {
 | 
			
		||||
            value: '{{$self.value}}',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    read1: {
 | 
			
		||||
      type: 'boolean',
 | 
			
		||||
      title: `Read pretty`,
 | 
			
		||||
      'x-read-pretty': true,
 | 
			
		||||
      'x-decorator': 'FormItem',
 | 
			
		||||
      'x-component': 'ColorPicker',
 | 
			
		||||
    },
 | 
			
		||||
    read2: {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      title: `Value`,
 | 
			
		||||
      'x-read-pretty': true,
 | 
			
		||||
      'x-decorator': 'FormItem',
 | 
			
		||||
      'x-component': 'Input',
 | 
			
		||||
      'x-component-props': {},
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaComponentProvider components={{ Input, ColorPicker, FormItem }}>
 | 
			
		||||
      <SchemaComponent schema={schema} />
 | 
			
		||||
    </SchemaComponentProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
---
 | 
			
		||||
group:
 | 
			
		||||
  title: Schema Components
 | 
			
		||||
  order: 3
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# ColorPicker
 | 
			
		||||
 | 
			
		||||
## Examples
 | 
			
		||||
 | 
			
		||||
### Basic
 | 
			
		||||
 | 
			
		||||
<code src="./demos/demo1.tsx"></code>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export * from './ColorPicker';
 | 
			
		||||
@ -0,0 +1,158 @@
 | 
			
		||||
import { dayjs, getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
 | 
			
		||||
import type { Dayjs } from 'dayjs';
 | 
			
		||||
 | 
			
		||||
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
 | 
			
		||||
  if (!dayjs.isDayjs(value)) return value;
 | 
			
		||||
  if (timezone === 'local') {
 | 
			
		||||
    const offset = new Date().getTimezoneOffset();
 | 
			
		||||
    return dayjs(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (picker === 'year') {
 | 
			
		||||
    return value.format('YYYY') + '-01-01T00:00:00.000Z';
 | 
			
		||||
  }
 | 
			
		||||
  if (picker === 'month') {
 | 
			
		||||
    return value.format('YYYY-MM') + '-01T00:00:00.000Z';
 | 
			
		||||
  }
 | 
			
		||||
  if (picker === 'quarter') {
 | 
			
		||||
    return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
 | 
			
		||||
  }
 | 
			
		||||
  if (picker === 'week') {
 | 
			
		||||
    return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
 | 
			
		||||
  }
 | 
			
		||||
  return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toGmtByPicker = (value: Dayjs, picker?: any) => {
 | 
			
		||||
  if (!value || !dayjs.isDayjs(value)) {
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  return toStringByPicker(value, picker, 'gmt');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toLocalByPicker = (value: Dayjs, picker?: any) => {
 | 
			
		||||
  if (!value || !dayjs.isDayjs(value)) {
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  return toStringByPicker(value, picker, 'local');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface Moment2strOptions {
 | 
			
		||||
  showTime?: boolean;
 | 
			
		||||
  gmt?: boolean;
 | 
			
		||||
  utc?: boolean;
 | 
			
		||||
  picker?: 'year' | 'month' | 'week' | 'quarter';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
 | 
			
		||||
  const { showTime, gmt, picker, utc = true } = options;
 | 
			
		||||
  if (!value) {
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  if (!utc) {
 | 
			
		||||
    const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
 | 
			
		||||
    return value.format(format);
 | 
			
		||||
  }
 | 
			
		||||
  if (showTime) {
 | 
			
		||||
    return gmt ? toGmt(value) : toLocal(value);
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof gmt === 'boolean') {
 | 
			
		||||
    return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
 | 
			
		||||
  }
 | 
			
		||||
  return toGmtByPicker(value, picker);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mapDatePicker = function () {
 | 
			
		||||
  return (props: any) => {
 | 
			
		||||
    const format = getDefaultFormat(props) as any;
 | 
			
		||||
    const onChange = props.onChange;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      ...props,
 | 
			
		||||
      format: format,
 | 
			
		||||
      value: str2moment(props.value, props),
 | 
			
		||||
      onChange: (value: Dayjs | null) => {
 | 
			
		||||
        if (onChange) {
 | 
			
		||||
          if (!props.showTime && value) {
 | 
			
		||||
            value = value.startOf('day');
 | 
			
		||||
          }
 | 
			
		||||
          onChange(moment2str(value, props));
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mapRangePicker = function () {
 | 
			
		||||
  return (props: any) => {
 | 
			
		||||
    const format = getDefaultFormat(props) as any;
 | 
			
		||||
    const onChange = props.onChange;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      ...props,
 | 
			
		||||
      format: format,
 | 
			
		||||
      value: str2moment(props.value, props),
 | 
			
		||||
      onChange: (value: Dayjs[]) => {
 | 
			
		||||
        if (onChange) {
 | 
			
		||||
          onChange(
 | 
			
		||||
            value
 | 
			
		||||
              ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
 | 
			
		||||
              : [],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    } as any;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
 | 
			
		||||
  const { showTime } = options;
 | 
			
		||||
  if (showTime) {
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  return value.startOf('day');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
 | 
			
		||||
  const { showTime } = options;
 | 
			
		||||
  if (showTime) {
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  return value.endOf('day');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getStart = (offset: any, unit: any) => {
 | 
			
		||||
  return dayjs()
 | 
			
		||||
    .add(offset, unit === 'isoWeek' ? 'week' : unit)
 | 
			
		||||
    .startOf(unit);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getEnd = (offset: any, unit: any) => {
 | 
			
		||||
  return dayjs()
 | 
			
		||||
    .add(offset, unit === 'isoWeek' ? 'week' : unit)
 | 
			
		||||
    .endOf(unit);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDateRanges = () => {
 | 
			
		||||
  return {
 | 
			
		||||
    today: () => [getStart(0, 'day'), getEnd(0, 'day')],
 | 
			
		||||
    lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
 | 
			
		||||
    thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
 | 
			
		||||
    nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
 | 
			
		||||
    lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
 | 
			
		||||
    thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
 | 
			
		||||
    nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
 | 
			
		||||
    lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
 | 
			
		||||
    thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
 | 
			
		||||
    nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
 | 
			
		||||
    lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
 | 
			
		||||
    thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
 | 
			
		||||
    nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
 | 
			
		||||
    last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
 | 
			
		||||
    next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
 | 
			
		||||
    last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
 | 
			
		||||
    next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
 | 
			
		||||
    last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
 | 
			
		||||
    next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -28,6 +28,7 @@ import { BlockItem } from '../block-item';
 | 
			
		||||
import { removeNullCondition } from '../filter';
 | 
			
		||||
import { HTMLEncode } from '../input/shared';
 | 
			
		||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
 | 
			
		||||
import { useColorFields } from '../table-v2/Table.Column.Designer';
 | 
			
		||||
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
 | 
			
		||||
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
 | 
			
		||||
 | 
			
		||||
@ -183,6 +184,7 @@ FormItem.Designer = function Designer() {
 | 
			
		||||
      value: field?.name,
 | 
			
		||||
      label: compile(field?.uiSchema?.title) || field?.name,
 | 
			
		||||
    }));
 | 
			
		||||
  const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
 | 
			
		||||
 | 
			
		||||
  let readOnlyMode = 'editable';
 | 
			
		||||
  if (fieldSchema['x-disabled'] === true) {
 | 
			
		||||
@ -529,10 +531,6 @@ FormItem.Designer = function Designer() {
 | 
			
		||||
            schema['x-component-props'] = fieldSchema['x-component-props'];
 | 
			
		||||
            field.componentProps = field.componentProps || {};
 | 
			
		||||
            field.componentProps.mode = mode;
 | 
			
		||||
            // if (mode === 'Nester') {
 | 
			
		||||
            //   const initValue = ['hasMany', 'belongsToMany'].includes(collectionField?.type) ? [{}] : {};
 | 
			
		||||
            //   field.value = field.value || initValue;
 | 
			
		||||
            // }
 | 
			
		||||
            dn.emit('patch', {
 | 
			
		||||
              schema,
 | 
			
		||||
            });
 | 
			
		||||
@ -811,6 +809,29 @@ FormItem.Designer = function Designer() {
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
 | 
			
		||||
 | 
			
		||||
      {isAssociationField && ['Tag'].includes(fieldMode) && (
 | 
			
		||||
        <SchemaSettings.SelectItem
 | 
			
		||||
          key="title-field"
 | 
			
		||||
          title={t('Tag color field')}
 | 
			
		||||
          options={colorFieldOptions}
 | 
			
		||||
          value={field?.componentProps?.tagColorField}
 | 
			
		||||
          onChange={(tagColorField) => {
 | 
			
		||||
            const schema = {
 | 
			
		||||
              ['x-uid']: fieldSchema['x-uid'],
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
 | 
			
		||||
            fieldSchema['x-component-props']['tagColorField'] = tagColorField;
 | 
			
		||||
            schema['x-component-props'] = fieldSchema['x-component-props'];
 | 
			
		||||
            field.componentProps.tagColorField = tagColorField;
 | 
			
		||||
            dn.emit('patch', {
 | 
			
		||||
              schema,
 | 
			
		||||
            });
 | 
			
		||||
            dn.refresh();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {collectionField && <SchemaSettings.Divider />}
 | 
			
		||||
      <SchemaSettings.Remove
 | 
			
		||||
        key="remove"
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ export * from './card-item';
 | 
			
		||||
export * from './cascader';
 | 
			
		||||
export * from './checkbox';
 | 
			
		||||
export * from './collection-select';
 | 
			
		||||
export * from './color-picker';
 | 
			
		||||
export * from './color-select';
 | 
			
		||||
export * from './cron';
 | 
			
		||||
export * from './date-picker';
 | 
			
		||||
@ -49,4 +50,5 @@ export * from './time-picker';
 | 
			
		||||
export * from './tree-select';
 | 
			
		||||
export * from './upload';
 | 
			
		||||
export * from './variable';
 | 
			
		||||
 | 
			
		||||
import './index.less';
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,22 @@ const useLabelFields = (collectionName?: any) => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useColorFields = (collectionName?: any) => {
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { getCollectionFields } = useCollectionManager();
 | 
			
		||||
  if (!collectionName) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  const targetFields = getCollectionFields(collectionName);
 | 
			
		||||
  return targetFields
 | 
			
		||||
    ?.filter?.((field) => field?.interface === 'color')
 | 
			
		||||
    ?.map?.((field) => {
 | 
			
		||||
      return {
 | 
			
		||||
        value: field.name,
 | 
			
		||||
        label: compile(field?.uiSchema?.title || field.name),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
export const TableColumnDesigner = (props) => {
 | 
			
		||||
  const { uiSchema, fieldSchema, collectionField } = props;
 | 
			
		||||
  const { getInterface, getCollection } = useCollectionManager();
 | 
			
		||||
@ -37,6 +53,7 @@ export const TableColumnDesigner = (props) => {
 | 
			
		||||
  const fieldNames =
 | 
			
		||||
    fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames'];
 | 
			
		||||
  const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection);
 | 
			
		||||
  const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
 | 
			
		||||
  const intefaceCfg = getInterface(collectionField?.interface);
 | 
			
		||||
  const targetCollection = getCollection(collectionField?.target);
 | 
			
		||||
  const isFileField = isFileCollection(targetCollection);
 | 
			
		||||
@ -45,6 +62,7 @@ export const TableColumnDesigner = (props) => {
 | 
			
		||||
  const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
 | 
			
		||||
  const dataSource = useCollectionFilterOptions(collectionField?.target);
 | 
			
		||||
  const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
 | 
			
		||||
  const fieldMode = fieldSchema?.['x-component-props']?.['mode'] || 'Select';
 | 
			
		||||
  let readOnlyMode = 'editable';
 | 
			
		||||
  if (fieldSchema['x-disabled'] === true) {
 | 
			
		||||
    readOnlyMode = 'readonly';
 | 
			
		||||
@ -232,6 +250,55 @@ export const TableColumnDesigner = (props) => {
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {readOnlyMode === 'read-pretty' &&
 | 
			
		||||
        ['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot'].includes(collectionField?.interface) && (
 | 
			
		||||
          <SchemaSettings.SelectItem
 | 
			
		||||
            key="field-mode"
 | 
			
		||||
            title={t('Field component')}
 | 
			
		||||
            options={[
 | 
			
		||||
              { label: t('Title'), value: 'Select' },
 | 
			
		||||
              { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
            ]}
 | 
			
		||||
            value={fieldMode}
 | 
			
		||||
            onChange={(mode) => {
 | 
			
		||||
              const schema = {
 | 
			
		||||
                ['x-uid']: fieldSchema['x-uid'],
 | 
			
		||||
              };
 | 
			
		||||
              fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
 | 
			
		||||
              fieldSchema['x-component-props']['mode'] = mode;
 | 
			
		||||
              schema['x-component-props'] = fieldSchema['x-component-props'];
 | 
			
		||||
              field.componentProps = field.componentProps || {};
 | 
			
		||||
              field.componentProps.mode = mode;
 | 
			
		||||
              dn.emit('patch', {
 | 
			
		||||
                schema,
 | 
			
		||||
              });
 | 
			
		||||
              dn.refresh();
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
      {['Tag'].includes(fieldMode) && (
 | 
			
		||||
        <SchemaSettings.SelectItem
 | 
			
		||||
          key="title-field"
 | 
			
		||||
          title={t('Tag color field')}
 | 
			
		||||
          options={colorFieldOptions}
 | 
			
		||||
          value={fieldSchema?.['x-component-props']?.tagColorField}
 | 
			
		||||
          onChange={(tagColorField) => {
 | 
			
		||||
            const schema = {
 | 
			
		||||
              ['x-uid']: fieldSchema['x-uid'],
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
 | 
			
		||||
            fieldSchema['x-component-props']['tagColorField'] = tagColorField;
 | 
			
		||||
            schema['x-component-props'] = fieldSchema['x-component-props'];
 | 
			
		||||
            field.componentProps.tagColorField = tagColorField;
 | 
			
		||||
            dn.emit('patch', {
 | 
			
		||||
              schema,
 | 
			
		||||
            });
 | 
			
		||||
            dn.refresh();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty'] && (
 | 
			
		||||
        <SchemaSettings.SwitchItem
 | 
			
		||||
 | 
			
		||||
@ -297,7 +297,6 @@ export const Table: any = observer(
 | 
			
		||||
                    console.warn('move cancel');
 | 
			
		||||
                    return;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  const fromIndex = e.active?.data.current?.sortable?.index;
 | 
			
		||||
                  const toIndex = e.over?.data.current?.sortable?.index;
 | 
			
		||||
                  const from = field.value[fromIndex];
 | 
			
		||||
@ -324,6 +323,11 @@ export const Table: any = observer(
 | 
			
		||||
                  .nb-read-pretty-input-number {
 | 
			
		||||
                    text-align: right;
 | 
			
		||||
                  }
 | 
			
		||||
                  .ant-color-picker-trigger{
 | 
			
		||||
                    position:absolute;
 | 
			
		||||
                    top:50%;
 | 
			
		||||
                    transform: translateY(-50%);
 | 
			
		||||
                  }
 | 
			
		||||
                `,
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ export const useFieldModeOptions = () => {
 | 
			
		||||
        ? [
 | 
			
		||||
            { label: t('Title'), value: 'Select' },
 | 
			
		||||
            { label: t('File manager'), value: 'FileManager' },
 | 
			
		||||
            { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
          ]
 | 
			
		||||
        : [
 | 
			
		||||
            { label: t('Select'), value: 'Select' },
 | 
			
		||||
@ -37,6 +38,7 @@ export const useFieldModeOptions = () => {
 | 
			
		||||
        return isReadPretty
 | 
			
		||||
          ? [
 | 
			
		||||
              { label: t('Title'), value: 'Select' },
 | 
			
		||||
              { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
              { label: t('Sub-table'), value: 'SubTable' },
 | 
			
		||||
              { label: t('Sub-details'), value: 'Nester' },
 | 
			
		||||
            ]
 | 
			
		||||
@ -50,6 +52,7 @@ export const useFieldModeOptions = () => {
 | 
			
		||||
        return isReadPretty
 | 
			
		||||
          ? [
 | 
			
		||||
              { label: t('Title'), value: 'Select' },
 | 
			
		||||
              { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
              { label: t('Sub-details'), value: 'Nester' },
 | 
			
		||||
              { label: t('Sub-table'), value: 'SubTable' },
 | 
			
		||||
            ]
 | 
			
		||||
@ -64,6 +67,7 @@ export const useFieldModeOptions = () => {
 | 
			
		||||
        return isReadPretty
 | 
			
		||||
          ? [
 | 
			
		||||
              { label: t('Title'), value: 'Select' },
 | 
			
		||||
              { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
              { label: t('Sub-details'), value: 'Nester' },
 | 
			
		||||
            ]
 | 
			
		||||
          : [
 | 
			
		||||
@ -76,6 +80,7 @@ export const useFieldModeOptions = () => {
 | 
			
		||||
        return isReadPretty
 | 
			
		||||
          ? [
 | 
			
		||||
              { label: t('Title'), value: 'Select' },
 | 
			
		||||
              { label: t('Tag'), value: 'Tag' },
 | 
			
		||||
              { label: t('Sub-details'), value: 'Nester' },
 | 
			
		||||
            ]
 | 
			
		||||
          : [
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5';
 | 
			
		||||
import { Field, GeneralField, createForm } from '@formily/core';
 | 
			
		||||
import { createForm, Field, GeneralField } from '@formily/core';
 | 
			
		||||
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import { error } from '@nocobase/utils/client';
 | 
			
		||||
@ -21,32 +21,32 @@ import {
 | 
			
		||||
} from 'antd';
 | 
			
		||||
import _, { cloneDeep } from 'lodash';
 | 
			
		||||
import React, {
 | 
			
		||||
  ReactNode,
 | 
			
		||||
  createContext,
 | 
			
		||||
  ReactNode,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useContext,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useState,
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  useTransition as useReactTransition,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { createPortal } from 'react-dom';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import {
 | 
			
		||||
  APIClientProvider,
 | 
			
		||||
  ActionContextProvider,
 | 
			
		||||
  APIClientProvider,
 | 
			
		||||
  CollectionFieldOptions,
 | 
			
		||||
  CollectionManagerContext,
 | 
			
		||||
  CollectionProvider,
 | 
			
		||||
  createDesignable,
 | 
			
		||||
  Designable,
 | 
			
		||||
  findFormBlock,
 | 
			
		||||
  FormDialog,
 | 
			
		||||
  FormProvider,
 | 
			
		||||
  RemoteSchemaComponent,
 | 
			
		||||
  SchemaComponent,
 | 
			
		||||
  SchemaComponentContext,
 | 
			
		||||
  SchemaComponentOptions,
 | 
			
		||||
  createDesignable,
 | 
			
		||||
  findFormBlock,
 | 
			
		||||
  useAPIClient,
 | 
			
		||||
  useBlockRequestContext,
 | 
			
		||||
  useCollection,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user