feat: support cron field (#1421)

* feat(calendar): replace cron component with CronSet

* fix: same exported

* fix: type incorrect

* fix: locale

* feat: upgrade react-js-cron and update some text

* fix: cron path

* fix: edge case

* feat: support FixedBlock
This commit is contained in:
Dunqing 2023-03-19 10:55:05 +08:00 committed by GitHub
parent 9ada0e37d6
commit 3cce8f75e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 166 additions and 34 deletions

View File

@ -38,7 +38,7 @@
"react-i18next": "^11.15.1", "react-i18next": "^11.15.1",
"react-iframe": "~1.8.5", "react-iframe": "~1.8.5",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-js-cron": "^1.4.0", "react-js-cron": "^3.1.0",
"react-quill": "^1.3.5", "react-quill": "^1.3.5",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-to-print": "^2.14.7", "react-to-print": "^2.14.7",

View File

@ -1,6 +1,7 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { useField } from '@formily/react'; import { useField } from '@formily/react';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useFixedSchema } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
export const CalendarBlockContext = createContext<any>({}); export const CalendarBlockContext = createContext<any>({});
@ -9,6 +10,7 @@ const InternalCalendarBlockProvider = (props) => {
const { fieldNames, showLunar } = props; const { fieldNames, showLunar } = props;
const field = useField(); const field = useField();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const fixedBlock = useFixedSchema()
// if (service.loading) { // if (service.loading) {
// return <Spin />; // return <Spin />;
// } // }
@ -20,6 +22,7 @@ const InternalCalendarBlockProvider = (props) => {
resource, resource,
fieldNames, fieldNames,
showLunar, showLunar,
fixedBlock,
}} }}
> >
{props.children} {props.children}
@ -50,5 +53,6 @@ export const useCalendarBlockProps = () => {
return { return {
fieldNames: ctx.fieldNames, fieldNames: ctx.fieldNames,
showLunar: ctx.showLunar, showLunar: ctx.showLunar,
fixedBlock: ctx.fixedBlock
}; };
}; };

View File

@ -24,8 +24,7 @@ const useGroupField = (props) => {
const InternalKanbanBlockProvider = (props) => { const InternalKanbanBlockProvider = (props) => {
const field = useField<any>(); const field = useField<any>();
const fieldSchema = useFieldSchema(); const fixedBlock = useFixedSchema();
useFixedSchema();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const groupField = useGroupField(props); const groupField = useGroupField(props);
if (!groupField) { if (!groupField) {
@ -45,7 +44,7 @@ const InternalKanbanBlockProvider = (props) => {
service, service,
resource, resource,
groupField, groupField,
fixedBlock: fieldSchema?.['x-decorator-props']?.fixedBlock, fixedBlock,
}} }}
> >
{props.children} {props.children}

View File

@ -18,13 +18,26 @@ export const calendar: ICollectionTemplate = {
type: 'string', type: 'string',
uiSchema: { uiSchema: {
type: 'string', type: 'string',
title: '{{t("Cron")}}', title: '{{t("Repeats")}}',
'x-component': 'Select', 'x-component': 'CronSet',
'x-component-props': 'allowClear',
enum: [ enum: [
{ value: '0 0 * * *', label: '每天' }, {
{ value: '0 0 ? * 1', label: '每个星期一' }, label: '{{t("Daily")}}',
{ value: '0 0 12 * * ?', label: '每天中午12点' }, value: '0 0 0 * * ?',
{ value: '0 0 10,14,16 * * ?', label: '每天上午10点,下午2点,4点 ' }, },
{
label: '{{t("Weekly")}}',
value: 'every_week',
},
{
label: '{{t("Monthly")}}',
value: 'every_month',
},
{
label: '{{t("Yearly")}}',
value: 'every_year',
},
], ],
}, },
interface: 'select', interface: 'select',

View File

@ -270,6 +270,7 @@ export default {
"Import": "Import", "Import": "Import",
"Export": "Export", "Export": "Export",
"Customize": "Customize", "Customize": "Customize",
"Custom": "Custom",
"Function": "Function", "Function": "Function",
"Popup form": "Popup form", "Popup form": "Popup form",
"Flexible popup": "Flexible popup", "Flexible popup": "Flexible popup",
@ -345,6 +346,11 @@ export default {
"Configure calendar": "Configure calendar", "Configure calendar": "Configure calendar",
"Title field": "Title field", "Title field": "Title field",
"Custom title": "Custom title", "Custom title": "Custom title",
"Daily": "Daily",
"Weekly": "Weekly",
"Monthly": "Monthly",
"Yearly": "Yearly",
"Repeats": "Repeats",
"Show lunar": "Show lunar", "Show lunar": "Show lunar",
"Start date field": "Start date field", "Start date field": "Start date field",
"End date field": "End date field", "End date field": "End date field",

View File

@ -286,6 +286,7 @@ export default {
"Import": "导入", "Import": "导入",
"Export": "导出", "Export": "导出",
"Customize": "自定义", "Customize": "自定义",
"Custom": "自定义",
"Function": "Function", "Function": "Function",
"Popup form": "Popup form", "Popup form": "Popup form",
"Flexible popup": "Flexible popup", "Flexible popup": "Flexible popup",
@ -367,6 +368,11 @@ export default {
"Allow linking to multiple records": "允许关联多条记录", "Allow linking to multiple records": "允许关联多条记录",
"Allow uploading multiple files": "允许上传多个文件", "Allow uploading multiple files": "允许上传多个文件",
"Daily": "每天",
"Weekly": "每周",
"Monthly": "每月",
"Yearly": "每年",
"Repeats": "重复",
"Configure calendar": "配置日历", "Configure calendar": "配置日历",
"Title field": "标题字段", "Title field": "标题字段",
"Custom title": "自定义标题", "Custom title": "自定义标题",

View File

@ -1,7 +1,7 @@
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..'; import { useCompile, useDesignable, useFixedBlockDesignerSetting } from '../..';
import { useCalendarBlockContext } from '../../../block-provider'; import { useCalendarBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks'; import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
@ -21,6 +21,8 @@ export const CalendarDesigner = () => {
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
@ -60,6 +62,7 @@ export const CalendarDesigner = () => {
dn.refresh(); dn.refresh();
}} }}
/> />
{fixedBlockDesignerSetting}
<SchemaSettings.CascaderItem <SchemaSettings.CascaderItem
title={t('Start date field')} title={t('Start date field')}
value={fieldNames.start} value={fieldNames.start}

View File

@ -191,7 +191,7 @@ const CalendarRecordViewer = (props) => {
}; };
export const Calendar: any = observer((props: any) => { export const Calendar: any = observer((props: any) => {
const { dataSource, fieldNames, showLunar } = useProps(props); const { dataSource, fieldNames, showLunar, fixedBlock } = useProps(props);
const [date, setDate] = useState<Date>(new Date()); const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<typeof Weeks[number]>('month'); const [view, setView] = useState<typeof Weeks[number]>('month');
const events = useEvents(dataSource, fieldNames, date, view); const events = useEvents(dataSource, fieldNames, date, view);
@ -211,7 +211,7 @@ export const Calendar: any = observer((props: any) => {
}, [showLunar]); }, [showLunar]);
return ( return (
<div style={{ height: 700 }}> <div style={{ height: fixedBlock ? '100%' : 700 }}>
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} /> <CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
<BigCalendar <BigCalendar
popup popup

View File

@ -411,7 +411,7 @@ button.rbc-input::-moz-focus-inner {
.rbc-overlay { .rbc-overlay {
position: absolute; position: absolute;
z-index: 5; z-index: 50;
margin-top: 5px; margin-top: 5px;
border-radius: 2px; border-radius: 2px;
// border: 1px solid #e5e5e5; // border: 1px solid #e5e5e5;

View File

@ -1,13 +1,13 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty } from '@formily/react';
import cronstrue from 'cronstrue'; import cronstrue from 'cronstrue';
import React from 'react'; import React, { useMemo } from 'react';
import { Cron as ReactCron, CronProps } from 'react-js-cron'; import { Cron as ReactCron, CronProps } from 'react-js-cron';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import 'react-js-cron/dist/styles.css';
import { useTranslation } from 'react-i18next';
type ComposedCron = React.FC<CronProps> & {}; const Input = (props: Omit<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
const Input = (props: Exclude<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
const { onChange, ...rest } = props; const { onChange, ...rest } = props;
return ( return (
<fieldset <fieldset
@ -39,16 +39,28 @@ const Input = (props: Exclude<CronProps, 'setValue'> & { onChange: (value: strin
const ReadPretty = (props) => { const ReadPretty = (props) => {
const api = useAPIClient(); const api = useAPIClient();
const locale = api.auth.getLocale(); const locale = api.auth.getLocale();
return props.value ? ( const value = useMemo(() => {
<span> try {
{cronstrue.toString(props.value, { return cronstrue.toString(props.value, {
locale, locale,
use24HourTimeFormat: true, use24HourTimeFormat: true,
})} })
} catch {
console.error(`The '${props.value}' is not a valid cron expression`)
return props.value
}
}, [props.value])
return props.value ? (
<span>
{value}
</span> </span>
) : null; ) : null;
}; };
export const Cron: ComposedCron = connect(Input, mapReadPretty(ReadPretty)); export const Cron = connect(Input, mapReadPretty(ReadPretty)) as unknown as typeof Input & {
ReadPretty;
};
Cron.ReadPretty = ReadPretty;
export default Cron; export default Cron;

View File

@ -0,0 +1,90 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Select, SelectProps } from 'antd';
import Cron from './Cron';
import { connect, mapReadPretty, useFieldSchema } from '@formily/react';
import { useCollection } from '../../../collection-manager';
import { useCompile } from '../../hooks';
import { EllipsisWithTooltip } from '../input';
interface CronSetProps extends SelectProps {
onChange: (v: string) => void;
}
const CronSetInternal = (props: CronSetProps) => {
const { onChange, value } = props;
const { getField } = useCollection();
const fieldSchema = useFieldSchema();
const uiSchemaOptions = getField(fieldSchema?.name)?.uiSchema.enum;
const compile = useCompile();
const [customizeOption, setCustomizeOption] = useState({
label: "{{t('Custom')}}",
value: '@daily',
});
const options = useMemo(() => {
return (props.options || [])
.concat((uiSchemaOptions as any[]) || [])
.concat([customizeOption])
.map((item) => {
return {
...item,
label: compile(item.label),
};
});
}, [props.options, customizeOption]);
useEffect(() => {
if (typeof value === 'undefined' || value === null) {
return;
}
const item = options.find((o) => o.value === value);
if (!item) {
setCustomizeOption({
...customizeOption,
value,
});
}
}, [options, value]);
const isCustomize = useMemo(() => {
return value === customizeOption.value;
}, [value, customizeOption]);
const onCronChange = (value, customize = false) => {
if (customize) {
setCustomizeOption({
...customizeOption,
value,
});
}
onChange(value);
};
return (
<fieldset>
<Select allowClear {...props} value={value} options={options} onChange={(value) => onCronChange(value)}></Select>
{isCustomize ? <Cron value={value} onChange={(v) => onCronChange(v, true)} allowedDropdowns={['period', 'week-days', 'months', 'month-days']} allowedPeriods={['year', 'month', 'week', 'day', ]} /> : null}
</fieldset>
);
};
const ReadPretty = (props: CronSetProps) => {
const { value } = props;
const compile = useCompile();
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const uiSchemaOptions = getField(fieldSchema?.name)?.uiSchema.enum;
const options = useMemo(() => {
return (props.options || []).concat((uiSchemaOptions as any[]) || []);
}, [props.options]);
const label = useMemo(() => {
return value && options?.find((o) => o.value === value)?.label;
}, [options, value]);
return <EllipsisWithTooltip ellipsis>{value && (label ? compile(label) : <Cron.ReadPretty {...props} />)}</EllipsisWithTooltip>;
};
export const CronSet = connect(CronSetInternal, mapReadPretty(ReadPretty));

View File

@ -1 +1,2 @@
export * from './Cron'; export * from './Cron';
export * from './CronSet';

View File

@ -1,7 +1,5 @@
import { DEFAULT_LOCALE_EN } from 'react-js-cron/dist/cjs/locale';
import zhCN from './zh-CN'; import zhCN from './zh-CN';
export default { export default {
'zh-CN': zhCN, 'zh-CN': zhCN,
'en-US': DEFAULT_LOCALE_EN,
}; };

View File

@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { useRecord } from '../../../record-provider'; import { useRecord } from '../../../record-provider';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplate';
import { uid } from '@formily/shared';
const FixedBlockContext = React.createContext({ const FixedBlockContext = React.createContext({
setFixedSchema: (schema: Schema) => {}, setFixedSchema: (schema: Schema) => {},
@ -40,6 +39,7 @@ export const useFixedSchema = () => {
}, },
[], [],
); );
return fieldSchema?.['x-decorator-props']?.fixedBlock
}; };
export const useFixedBlock = () => { export const useFixedBlock = () => {

View File

@ -12,5 +12,5 @@ export const getCronLocale = (lang: string) => {
locale = require(file).cron?.[lang]; locale = require(file).cron?.[lang];
} catch (error) {} } catch (error) {}
} }
return locale || require('react-js-cron/dist/cjs/locale').DEFAULT_LOCALE_EN; return locale;
}; };

View File

@ -14,7 +14,7 @@
"@nocobase/utils": "0.9.1-alpha.2", "@nocobase/utils": "0.9.1-alpha.2",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",
"react-js-cron": "^1.4.0" "react-js-cron": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/test": "0.9.1-alpha.2" "@nocobase/test": "0.9.1-alpha.2"

View File

@ -185,7 +185,7 @@ const RuleTypes = {
? ( ? (
<Cron <Cron
value={value} value={value}
setValue={onChange} onChange={onChange}
clearButton={false} clearButton={false}
/> />
) )

View File

@ -19,7 +19,7 @@
"cron-parser": "4.4.0", "cron-parser": "4.4.0",
"json-templates": "^4.2.0", "json-templates": "^4.2.0",
"moment": "^2.29.2", "moment": "^2.29.2",
"react-js-cron": "^1.4.0" "react-js-cron": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/test": "0.9.1-alpha.2", "@nocobase/test": "0.9.1-alpha.2",

View File

@ -21434,10 +21434,10 @@ react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-js-cron@^1.4.0: react-js-cron@^3.1.0:
version "1.4.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/react-js-cron/-/react-js-cron-1.4.0.tgz#58341434e3db00da11bc8009abb0ea6cd926cda7" resolved "https://registry.yarnpkg.com/react-js-cron/-/react-js-cron-3.1.0.tgz#cd11a43cdfbededee0aaa14e35c026adda0ee1c7"
integrity sha512-gbx1NXMDJgiKBnn1ljTS/jIXBw+jAEDMFOW/kWeGXuLwV+HEDIfj2oIsgStGGzCjRgcjgM3UTtHxaWrBdo0lFQ== integrity sha512-UzKDvkr6tt91sbMTYdX7sranHxjjLDHOP+aOZbS9QbjlnoBvVjO5JaDOKC4kQBbaZLha3gdbuEtRXriE+3wSxQ==
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4" version "3.0.4"