feat(parse-variables): support to parse variables in filter params (#1558)
* fix: add field linkage on setting default datetime * fix: fix dateonly timezone problem * fix: improve test * docs(DatePicker): add demos * fix(DatePicker): should return the beginning of a second * feat(DatePicker): support non-UTC * refactor: rename * fix(RangePicker): get correct end date * test(mapDatePicker): add test * test(mapRangePicker): add test * feat(Filter): use non-UTC to filter * feat(FilterBlock): use non-UTC to filter * feat: add '$dateBetween' operator in datetime * feat: use RangePicker on toggled to 'dateBetween' operator * feat: set ranges for RangePicker * feat: backend support to parse 'dateBetween' operator * fix: fix build error * fix: adaptive content width * feat: support to use var on data scope * feat: add parse-variables plugin * feat: support to parse variables * feat: support only to set system variables * test: rename * feat: cover all * fix: fix build error * feat(RangePicker): extend more shortcut keys * feat(parse-variables): support more date var * feat: support user variables * feat: disable unmatched options * fix: use component name to filter option * fix: fix build error * feat: remove some operator of id * chore: remove useless operators * fix: built in plugin * refactor: move to core from plugin * refactor: remove code of plugin * refactor: remove useless code * fix: should after acl * Update server.ts * fix: compatible with old version * feat: test cases * refactor: rename to 'is between' * refactor: parse filter * fix: improve code * feat: test cases * fix: fix error * fix: improve parse date * fix: date variables * fix: day range * fix: test error * fix: typo * fix: test error * feat: $user variable * fix: toDate * fix: fix the value range of shortcuts * feat: add quarter and test * feat: support to use user's association fields to filter * refactor: use maxDepth * refactor: remove useless code * fix: make AssociationSelect.Designer to support variables * fix: getField * fix: parse utc * fix: remove only * fix: filter by ctx.db.getFieldByPath * fix: avoid error * fix: add translation * fix(RangePicker): can be set to empty * feat(utils): add hasEmptyValue * fix: should not save empty * fix: last few days should include today * fix: limit user variable type to display * fix: parse filter error * fix: empty * test: [skip ci] * fix: remove ';' * feat: improve code --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
c2885ee1d6
commit
098140d511
@ -3,13 +3,14 @@ import { SchemaComponentOptions } from '../schema-component/core/SchemaComponent
|
||||
import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider';
|
||||
import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider';
|
||||
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
|
||||
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
|
||||
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
|
||||
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';
|
||||
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
|
||||
|
||||
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
return (
|
||||
@ -20,6 +21,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
TableBlockProvider,
|
||||
TableSelectorProvider,
|
||||
FormBlockProvider,
|
||||
FilterFormBlockProvider,
|
||||
FormFieldProvider,
|
||||
DetailsBlockProvider,
|
||||
KanbanBlockProvider,
|
||||
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { DatePickerProvider } from '../schema-component';
|
||||
import { FormBlockProvider } from './FormBlockProvider';
|
||||
|
||||
export const FilterFormBlockProvider = (props) => {
|
||||
return (
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
<FormBlockProvider {...props}></FormBlockProvider>
|
||||
</DatePickerProvider>
|
||||
);
|
||||
};
|
@ -94,7 +94,7 @@ export const useFormBlockProps = () => {
|
||||
const addChild = fieldSchema?.['x-component-props']?.addChild;
|
||||
useEffect(() => {
|
||||
if (addChild) {
|
||||
ctx.form.query('parent').take((field) => {
|
||||
ctx.form?.query('parent').take((field) => {
|
||||
field.disabled = true;
|
||||
field.value = new Proxy({ ...record }, {});
|
||||
});
|
||||
@ -102,7 +102,7 @@ export const useFormBlockProps = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
ctx.form.setInitialValues(ctx.service?.data?.data);
|
||||
ctx.form?.setInitialValues(ctx.service?.data?.data);
|
||||
}, []);
|
||||
return {
|
||||
form: ctx.form,
|
||||
|
@ -1,10 +1,12 @@
|
||||
export * from './BlockProvider';
|
||||
export * from './BlockSchemaComponentProvider';
|
||||
export * from './CalendarBlockProvider';
|
||||
export * from './FilterFormBlockProvider';
|
||||
export * from './FormBlockProvider';
|
||||
export * from './FormFieldProvider';
|
||||
export * from './KanbanBlockProvider';
|
||||
export * from './SharedFilterProvider';
|
||||
export * from './TableBlockProvider';
|
||||
export * from './TableFieldProvider';
|
||||
export * from './TableSelectorProvider';
|
||||
export * from './FormFieldProvider';
|
||||
export * from './SharedFilterProvider';
|
||||
|
||||
|
@ -28,6 +28,24 @@ const getSchema = (schema: IField, record: any, compile) => {
|
||||
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
|
||||
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
|
||||
properties['defaultValue']['x-decorator'] = 'FormItem';
|
||||
properties['defaultValue']['x-reactions'] = {
|
||||
dependencies: [
|
||||
'uiSchema.x-component-props.gmt',
|
||||
'uiSchema.x-component-props.showTime',
|
||||
'uiSchema.x-component-props.dateFormat',
|
||||
'uiSchema.x-component-props.timeFormat',
|
||||
],
|
||||
fulfill: {
|
||||
state: {
|
||||
componentProps: {
|
||||
gmt: '{{$deps[0]}}',
|
||||
showTime: '{{$deps[1]}}',
|
||||
dateFormat: '{{$deps[2]}}',
|
||||
timeFormat: '{{$deps[3]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const initialValue: any = {
|
||||
name: `f_${uid()}`,
|
||||
|
@ -25,6 +25,24 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
|
||||
properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
|
||||
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
|
||||
properties['defaultValue']['x-decorator'] = 'FormItem';
|
||||
properties['defaultValue']['x-reactions'] = {
|
||||
dependencies: [
|
||||
'uiSchema.x-component-props.gmt',
|
||||
'uiSchema.x-component-props.showTime',
|
||||
'uiSchema.x-component-props.dateFormat',
|
||||
'uiSchema.x-component-props.timeFormat',
|
||||
],
|
||||
fulfill: {
|
||||
state: {
|
||||
componentProps: {
|
||||
gmt: '{{$deps[0]}}',
|
||||
showTime: '{{$deps[1]}}',
|
||||
dateFormat: '{{$deps[2]}}',
|
||||
timeFormat: '{{$deps[3]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@ -115,7 +133,7 @@ export const EditCollectionField = (props) => {
|
||||
};
|
||||
|
||||
export const EditFieldAction = (props) => {
|
||||
const { scope, getContainer, item: record,children } = props;
|
||||
const { scope, getContainer, item: record, children } = props;
|
||||
const { getInterface } = useCollectionManager();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [schema, setSchema] = useState({});
|
||||
@ -155,7 +173,7 @@ export const EditFieldAction = (props) => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{children||t('Edit')}
|
||||
{children || t('Edit')}
|
||||
</a>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
|
@ -52,6 +52,7 @@ export const datetime = [
|
||||
{ label: "{{ t('is after') }}", value: '$dateAfter' },
|
||||
{ label: "{{ t('is on or after') }}", value: '$dateNotBefore' },
|
||||
{ label: "{{ t('is on or before') }}", value: '$dateNotAfter' },
|
||||
{ label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } },
|
||||
{ label: "{{ t('is empty') }}", value: '$empty', noValue: true },
|
||||
{ label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true },
|
||||
];
|
||||
@ -70,30 +71,6 @@ export const number = [
|
||||
export const id = [
|
||||
{ label: '{{t("is")}}', value: '$eq', selected: true },
|
||||
{ label: '{{t("is not")}}', value: '$ne' },
|
||||
{
|
||||
label: '{{t("is variable")}}',
|
||||
value: '$isVar',
|
||||
schema: {
|
||||
'x-component': 'VariableCascader',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '{{t("is current logged-in user")}}',
|
||||
value: '$isCurrentUser',
|
||||
noValue: true,
|
||||
visible(field) {
|
||||
return field.collectionName === 'users';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '{{t("is not current logged-in user")}}',
|
||||
value: '$isNotCurrentUser',
|
||||
noValue: true,
|
||||
visible(field) {
|
||||
return field.collectionName === 'users';
|
||||
},
|
||||
},
|
||||
{ label: '{{t("exists")}}', value: '$exists', noValue: true },
|
||||
{ label: '{{t("not exists")}}', value: '$notExists', noValue: true },
|
||||
];
|
||||
|
@ -57,7 +57,10 @@ export const FilterBlockRecord = ({
|
||||
const associatedFields = useAssociatedFields();
|
||||
const container = useRef(null);
|
||||
|
||||
const shouldApplyFilter = field.decoratorType !== 'FormBlockProvider' && field.decoratorProps.blockType !== 'filter';
|
||||
const shouldApplyFilter =
|
||||
field.decoratorType !== 'FilterFormBlockProvider' &&
|
||||
field.decoratorType !== 'FormBlockProvider' &&
|
||||
field.decoratorProps.blockType !== 'filter';
|
||||
|
||||
const addBlockToDataBlocks = () => {
|
||||
recordDataBlocks({
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} more items",
|
||||
"Total {{count}} items": "Total {{count}} items",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Tomorrow": "Tomorrow",
|
||||
"Month": "Month",
|
||||
"Week": "Week",
|
||||
"This week": "This week",
|
||||
"This month": "This month",
|
||||
"This year": "This year",
|
||||
"Next year": "Next year",
|
||||
"Last week": "Last week",
|
||||
"Next week": "Next week",
|
||||
"Last month": "Last month",
|
||||
"Next month": "Next month",
|
||||
"Last quarter": "Last quarter",
|
||||
"This quarter": "This quarter",
|
||||
"Next quarter": "Next quarter",
|
||||
"Last year": "Last year",
|
||||
"Last 7 days": "Last 7 days",
|
||||
"Last 30 days": "Last 30 days",
|
||||
"Last 90 days": "Last 90 days",
|
||||
"Next 7 days": "Next 7 days",
|
||||
"Next 30 days": "Next 30 days",
|
||||
"Next 90 days": "Next 90 days",
|
||||
"Work week": "Work week",
|
||||
"Day": "Day",
|
||||
"Agenda": "Agenda",
|
||||
@ -261,7 +281,6 @@ export default {
|
||||
"Comparision": "Comparision",
|
||||
"is": "is",
|
||||
"is not": "is not",
|
||||
"is variable": "is variable",
|
||||
"contains": "contains",
|
||||
"does not contain": "does not contain",
|
||||
"starts with": "starts with",
|
||||
@ -326,6 +345,7 @@ export default {
|
||||
"is after": "is after",
|
||||
"is on or after": "is on or after",
|
||||
"is on or before": "is on or before",
|
||||
"is between": "is between",
|
||||
"Upload": "Upload",
|
||||
"Select level": "Select level",
|
||||
"Province": "Province",
|
||||
@ -470,8 +490,6 @@ export default {
|
||||
"Add condition group": "Add condition group",
|
||||
"exists": "exists",
|
||||
"not exists": "not exists",
|
||||
"is current logged-in user": "is current logged-in user",
|
||||
"is not current logged-in user": "is not current logged-in user",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
@ -565,6 +583,8 @@ export default {
|
||||
"Current user": "Current user",
|
||||
"Current record": "Current record",
|
||||
"Current time": "Current time",
|
||||
"System variables": "System variables",
|
||||
"Date variables": "Date variables",
|
||||
"Popup close method": "Popup close method",
|
||||
"Automatic close": "Automatic close",
|
||||
"Manually close": "Manually close",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} 件以上",
|
||||
"Total {{count}} items": "合計 {{count}} 件",
|
||||
"Today": "今日",
|
||||
"Yesterday": "昨日",
|
||||
"Tomorrow": "明日",
|
||||
"Month": "月",
|
||||
"Week": "週",
|
||||
"This week": "今週",
|
||||
"Next week": "来週",
|
||||
"This month": "今月",
|
||||
"Next month": "来月",
|
||||
"Last quarter": "前四半期",
|
||||
"This quarter": "今四半期",
|
||||
"Next quarter": "来四半期",
|
||||
"This year": "今年",
|
||||
"Next year": "来年",
|
||||
"Last week": "先週",
|
||||
"Last month": "先月",
|
||||
"Last year": "去年",
|
||||
"Last 7 days": "過去 7 日間",
|
||||
"Last 30 days": "過去 30 日間",
|
||||
"Last 90 days": "過去 90 日間",
|
||||
"Next 7 days": "次の 7 日間",
|
||||
"Next 30 days": "次の 30 日間",
|
||||
"Next 90 days": "次の 90 日間",
|
||||
"Work week": "稼働日",
|
||||
"Day": "日",
|
||||
"Agenda": "アジェンダ",
|
||||
@ -226,7 +246,6 @@ export default {
|
||||
"Comparision": "比較",
|
||||
"is": "が同じである",
|
||||
"is not": "が同じではない",
|
||||
"is variable": "が変数である",
|
||||
"contains": "を含む",
|
||||
"does not contain": "を含まない",
|
||||
"starts with": "で始まる",
|
||||
@ -268,6 +287,7 @@ export default {
|
||||
"is after": "より後",
|
||||
"is on or after": "以降",
|
||||
"is on or before": "以前",
|
||||
"is between": "範囲",
|
||||
"Upload": "アップロード",
|
||||
"Select level": "レベルを選択",
|
||||
"Province": "州",
|
||||
@ -394,7 +414,6 @@ export default {
|
||||
"Add condition group": "条件グループの追加",
|
||||
"exists": "が存在する",
|
||||
"not exists": "が存在しない",
|
||||
"is current logged-in user": "が現在ログインしているユーザー",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} больше элементов",
|
||||
"Total {{count}} items": "Всего {{count}} элементов",
|
||||
"Today": "Сегодня",
|
||||
"Yesterday": "Вчера",
|
||||
"Tomorrow": "Завтра",
|
||||
"Month": "Месяц",
|
||||
"Week": "Неделя",
|
||||
"This week": "Эта неделя",
|
||||
"Next week": "Следующая неделя",
|
||||
"This month": "Этот месяц",
|
||||
"Next month": "Следующий месяц",
|
||||
"Last quarter": "Прошлый квартал",
|
||||
"This quarter": "Этот квартал",
|
||||
"Next quarter": "Следующий квартал",
|
||||
"This year": "Этот год",
|
||||
"Next year": "Следующий год",
|
||||
"Last week": "Прошлая неделя",
|
||||
"Last month": "Прошлый месяц",
|
||||
"Last year": "Прошлый год",
|
||||
"Last 7 days": "Последние 7 дней",
|
||||
"Last 30 days": "Последние 30 дней",
|
||||
"Last 90 days": "Последние 90 дней",
|
||||
"Next 7 days": "Следующие 7 дней",
|
||||
"Next 30 days": "Следующие 30 дней",
|
||||
"Next 90 days": "Следующие 90 дней",
|
||||
"Work week": "Рабочая неделя",
|
||||
"Day": "День",
|
||||
"Agenda": "Расписание",
|
||||
@ -185,7 +205,6 @@ export default {
|
||||
"Comparision": "Сравнение",
|
||||
"is": "соответствует",
|
||||
"is not": "не соответствует",
|
||||
"is variable": "это переменная",
|
||||
"contains": "содержит",
|
||||
"does not contain": "не содержит",
|
||||
"starts with": "начинается с",
|
||||
@ -227,6 +246,7 @@ export default {
|
||||
"is after": "находится после",
|
||||
"is on or after": "находится на или после",
|
||||
"is on or before": "находится на или до",
|
||||
"is between": "находится в диапазоне",
|
||||
"Upload": "Закачать",
|
||||
"Select level": "Выберите уровень",
|
||||
"Province": "Область",
|
||||
@ -353,7 +373,6 @@ export default {
|
||||
"Add condition group": "Добавить группу правил",
|
||||
"exists": "существуют",
|
||||
"not exists": "не существуют",
|
||||
"is current logged-in user": "текущий пользователь",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} öğe daha",
|
||||
"Total {{count}} items": "Toplam {{count}} adet öğe",
|
||||
"Today": "Bugün",
|
||||
"Yesterday": "Dün",
|
||||
"Tomorrow": "Yarın",
|
||||
"Month": "Ay",
|
||||
"Week": "Hafta",
|
||||
"This week": "Bu Hafta",
|
||||
"Next week": "Gelecek Hafta",
|
||||
"This month": "Bu Ay",
|
||||
"Next month": "Gelecek Ay",
|
||||
"Last quarter": "Geçen Çeyrek",
|
||||
"This quarter": "Bu Çeyrek",
|
||||
"Next quarter": "Gelecek Çeyrek",
|
||||
"This year": "Bu Yıl",
|
||||
"Next year": "Gelecek Yıl",
|
||||
"Last week": "Geçen Hafta",
|
||||
"Last month": "Geçen Ay",
|
||||
"Last year": "Geçen Yıl",
|
||||
"Last 7 days": "Son 7 Gün",
|
||||
"Last 30 days": "Son 30 Gün",
|
||||
"Last 90 days": "Son 90 Gün",
|
||||
"Next 7 days": "Sonraki 7 Gün",
|
||||
"Next 30 days": "Sonraki 30 Gün",
|
||||
"Next 90 days": "Sonraki 90 Gün",
|
||||
"Work week": "Çalışma Haftası",
|
||||
"Day": "Gün",
|
||||
"Agenda": "Ajanda",
|
||||
@ -184,7 +204,6 @@ export default {
|
||||
"Comparision": "Karşılaştırma",
|
||||
"is": "eşittir",
|
||||
"is not": "eşit değildir",
|
||||
"is variable": "is variable",
|
||||
"contains": "içerir",
|
||||
"does not contain": "içermez",
|
||||
"starts with": "ile başlar",
|
||||
@ -226,6 +245,7 @@ export default {
|
||||
"is after": "sonra",
|
||||
"is on or after": "açık veya sonra",
|
||||
"is on or before": "açık veya önce",
|
||||
"is between": "aralık",
|
||||
"Upload": "Yükle",
|
||||
"Select level": "Seviye seç",
|
||||
"Province": "Bölge",
|
||||
@ -352,7 +372,6 @@ export default {
|
||||
"Add condition group": "Koşul grubu ekle",
|
||||
"exists": "var olanlar",
|
||||
"not exists": "var olmayanlar",
|
||||
"is current logged-in user": "mevcut oturum açmış kullanıcı",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "还有 {{count}} 项",
|
||||
"Total {{count}} items": "总共 {{count}} 条",
|
||||
"Today": "今天",
|
||||
"Yesterday": "昨天",
|
||||
"Tomorrow": "明天",
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
"This week": "本周",
|
||||
"Next week": "下周",
|
||||
"This month": "本月",
|
||||
"Next month": "下月",
|
||||
"Last quarter": "上季度",
|
||||
"This quarter": "本季度",
|
||||
"Next quarter": "下季度",
|
||||
"This year": "今年",
|
||||
"Next year": "明年",
|
||||
"Last week": "上周",
|
||||
"Last month": "上月",
|
||||
"Last year": "去年",
|
||||
"Last 7 days": "最近 7 天",
|
||||
"Last 30 days": "最近 30 天",
|
||||
"Last 90 days": "最近 90 天",
|
||||
"Next 7 days": "未来 7 天",
|
||||
"Next 30 days": "未来 30 天",
|
||||
"Next 90 days": "未来 90 天",
|
||||
"Work week": "工作日",
|
||||
"Day": "天",
|
||||
"Agenda": "列表",
|
||||
@ -277,7 +297,6 @@ export default {
|
||||
"Comparision": "值比较",
|
||||
"is": "等于",
|
||||
"is not": "不等于",
|
||||
"is variable": "为动态变量",
|
||||
"contains": "包含",
|
||||
"does not contain": "不包含",
|
||||
"starts with": "开头是",
|
||||
@ -344,6 +363,7 @@ export default {
|
||||
"is after": "晚于",
|
||||
"is on or after": "不早于",
|
||||
"is on or before": "不晚于",
|
||||
"is between": "介于",
|
||||
|
||||
"Upload": "上传",
|
||||
|
||||
@ -504,8 +524,6 @@ export default {
|
||||
'Add condition group': '添加条件分组',
|
||||
'exists': '存在',
|
||||
'not exists': '不存在',
|
||||
'is current logged-in user': '为当前登录用户',
|
||||
'is not current logged-in user': '不为当前登录用户',
|
||||
'=': '=',
|
||||
'≠': '≠',
|
||||
'>': '>',
|
||||
@ -604,6 +622,7 @@ export default {
|
||||
'Current user': '当前用户',
|
||||
'Current record': '当前记录',
|
||||
'Current time': '当前时间',
|
||||
'Now': '现在',
|
||||
'Popup close method': '弹窗关闭方式',
|
||||
'Automatic close': '自动关闭',
|
||||
'Manually close': '手动关闭',
|
||||
@ -687,6 +706,8 @@ export default {
|
||||
'Column width': '列宽',
|
||||
'Sortable': '可排序的',
|
||||
'Constant': '常量',
|
||||
'System variables': '系统变量',
|
||||
'Date variables': '日期变量',
|
||||
'Use variable': '使用变量',
|
||||
'True': '真',
|
||||
'False': '假',
|
||||
|
@ -4,19 +4,21 @@ import { Field } from '@formily/core';
|
||||
import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext, useFilterByTk } from '../../../block-provider';
|
||||
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
|
||||
import {
|
||||
useCollectionManager,
|
||||
useCollection,
|
||||
useSortFields,
|
||||
useCollectionFilterOptions,
|
||||
useCollectionManager,
|
||||
useSortFields
|
||||
} from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useDesignable, useCompile, useFieldComponentOptions, useFieldTitle } from '../../hooks';
|
||||
import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
import { defaultFieldNames } from '../select';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import useServiceOptions from './useServiceOptions';
|
||||
|
||||
@ -102,7 +104,7 @@ AssociationSelect.Designer = () => {
|
||||
const compile = useCompile();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
||||
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||
const originalTitle = collectionField?.uiSchema?.title;
|
||||
@ -423,7 +425,7 @@ AssociationSelect.Designer = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form && !isSubFormAssocitionField && fieldComponentOptions && (
|
||||
{form && !isSubFormAssociationField && fieldComponentOptions && (
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Field component')}
|
||||
options={fieldComponentOptions}
|
||||
@ -483,12 +485,15 @@ AssociationSelect.Designer = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(field.componentProps, 'service.params.filter', filter);
|
||||
fieldSchema['x-component-props'] = field.componentProps;
|
||||
dn.emit('patch', {
|
||||
@ -692,13 +697,9 @@ AssociationSelect.FilterDesigner = () => {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const tk = useFilterByTk();
|
||||
const {} = useCollection();
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const { dn, refresh } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||
const originalTitle = collectionField?.uiSchema?.title;
|
||||
@ -1012,12 +1013,15 @@ AssociationSelect.FilterDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(field.componentProps, 'service.params.filter', filter);
|
||||
fieldSchema['x-component-props'] = field.componentProps;
|
||||
dn.emit('patch', {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FixedBlockDesignerItem, useCompile, useDesignable } from '../..';
|
||||
import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..';
|
||||
import { useCalendarBlockContext } from '../../../block-provider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const CalendarDesigner = () => {
|
||||
const field = useField();
|
||||
@ -115,7 +116,9 @@ export const CalendarDesigner = () => {
|
||||
default: defaultFilter,
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
@ -127,6 +130,7 @@ export const CalendarDesigner = () => {
|
||||
}
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -5,23 +5,68 @@ import type {
|
||||
RangePickerProps as AntdRangePickerProps
|
||||
} from 'antd/lib/date-picker';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import { mapDateFormat } from './util';
|
||||
import { getDateRanges, mapDatePicker, mapRangePicker } from './util';
|
||||
|
||||
interface IDatePickerProps {
|
||||
utc?: boolean;
|
||||
}
|
||||
|
||||
type ComposedDatePicker = React.FC<AntdDatePickerProps> & {
|
||||
RangePicker?: React.FC<AntdRangePickerProps>;
|
||||
};
|
||||
|
||||
export const DatePicker: ComposedDatePicker = connect(
|
||||
const DatePickerContext = React.createContext<IDatePickerProps>({ utc: true });
|
||||
|
||||
export const useDatePickerContext = () => React.useContext(DatePickerContext);
|
||||
export const DatePickerProvider = DatePickerContext.Provider;
|
||||
|
||||
const _DatePicker: ComposedDatePicker = connect(
|
||||
AntdDatePicker,
|
||||
mapProps(mapDateFormat()),
|
||||
mapProps(mapDatePicker()),
|
||||
mapReadPretty(ReadPretty.DatePicker),
|
||||
);
|
||||
|
||||
DatePicker.RangePicker = connect(
|
||||
const _RangePicker = connect(
|
||||
AntdDatePicker.RangePicker,
|
||||
mapProps(mapDateFormat()),
|
||||
mapProps(mapRangePicker()),
|
||||
mapReadPretty(ReadPretty.DateRangePicker),
|
||||
);
|
||||
|
||||
export const DatePicker = (props) => {
|
||||
const { utc = true } = useDatePickerContext();
|
||||
props = { utc, ...props };
|
||||
return <_DatePicker {...props} />;
|
||||
};
|
||||
|
||||
DatePicker.RangePicker = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { utc = true } = useDatePickerContext();
|
||||
const rangesValue = getDateRanges();
|
||||
const ranges = {
|
||||
[t('Today')]: rangesValue.today,
|
||||
[t('Last week')]: rangesValue.lastWeek,
|
||||
[t('This week')]: rangesValue.thisWeek,
|
||||
[t('Next week')]: rangesValue.nextWeek,
|
||||
[t('Last month')]: rangesValue.lastMonth,
|
||||
[t('This month')]: rangesValue.thisMonth,
|
||||
[t('Next month')]: rangesValue.nextMonth,
|
||||
[t('Last quarter')]: rangesValue.lastQuarter,
|
||||
[t('This quarter')]: rangesValue.thisQuarter,
|
||||
[t('Next quarter')]: rangesValue.nextQuarter,
|
||||
[t('Last year')]: rangesValue.lastYear,
|
||||
[t('This year')]: rangesValue.thisYear,
|
||||
[t('Next year')]: rangesValue.nextYear,
|
||||
[t('Last 7 days')]: rangesValue.last7Days,
|
||||
[t('Next 7 days')]: rangesValue.next7Days,
|
||||
[t('Last 30 days')]: rangesValue.last30Days,
|
||||
[t('Next 30 days')]: rangesValue.next30Days,
|
||||
[t('Last 90 days')]: rangesValue.last90Days,
|
||||
[t('Next 90 days')]: rangesValue.next90Days,
|
||||
};
|
||||
props = { utc, ranges, ...props };
|
||||
return <_RangePicker {...props} />;
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
|
@ -0,0 +1,120 @@
|
||||
import moment from 'moment';
|
||||
import { getDateRanges } from '../util';
|
||||
|
||||
describe('getDateRanges', () => {
|
||||
const dateRanges = getDateRanges();
|
||||
|
||||
it('today', () => {
|
||||
const [start, end] = dateRanges.today();
|
||||
expect(start.toISOString()).toBe(moment().startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('day').toISOString());
|
||||
});
|
||||
|
||||
it('lastWeek', () => {
|
||||
const [start, end] = dateRanges.lastWeek();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'week').startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'week').endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('thisWeek', () => {
|
||||
const [start, end] = dateRanges.thisWeek();
|
||||
expect(start.toISOString()).toBe(moment().startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('nextWeek', () => {
|
||||
const [start, end] = dateRanges.nextWeek();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'week').startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'week').endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('lastMonth', () => {
|
||||
const [start, end] = dateRanges.lastMonth();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'month').startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'month').endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('thisMonth', () => {
|
||||
const [start, end] = dateRanges.thisMonth();
|
||||
expect(start.toISOString()).toBe(moment().startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('nextMonth', () => {
|
||||
const [start, end] = dateRanges.nextMonth();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'month').startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'month').endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('lastQuarter', () => {
|
||||
const [start, end] = dateRanges.lastQuarter();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'quarter').startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'quarter').endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('thisQuarter', () => {
|
||||
const [start, end] = dateRanges.thisQuarter();
|
||||
expect(start.toISOString()).toBe(moment().startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('nextQuarter', () => {
|
||||
const [start, end] = dateRanges.nextQuarter();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'quarter').startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'quarter').endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('lastYear', () => {
|
||||
const [start, end] = dateRanges.lastYear();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'year').startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'year').endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('thisYear', () => {
|
||||
const [start, end] = dateRanges.thisYear();
|
||||
expect(start.toISOString()).toBe(moment().startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('nextYear', () => {
|
||||
const [start, end] = dateRanges.nextYear();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'year').startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'year').endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('last7Days', () => {
|
||||
const [start, end] = dateRanges.last7Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-6, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next7Days', () => {
|
||||
const [start, end] = dateRanges.next7Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(7, 'days').endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('last30Days', () => {
|
||||
const [start, end] = dateRanges.last30Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-29, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next30Days', () => {
|
||||
const [start, end] = dateRanges.next30Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(30, 'days').endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('last90Days', () => {
|
||||
const [start, end] = dateRanges.last90Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-89, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next90Days', () => {
|
||||
const [start, end] = dateRanges.next90Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(90, 'days').endOf('days').toISOString());
|
||||
});
|
||||
});
|
@ -0,0 +1,220 @@
|
||||
import moment from 'moment';
|
||||
import { mapDatePicker } from '../util';
|
||||
|
||||
describe('mapDatePicker', () => {
|
||||
it('showTime is true and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T22:22:22.000Z',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 22:22:22');
|
||||
});
|
||||
|
||||
it('showTime is true and gmt is false', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T22:22:22.000Z',
|
||||
showTime: true,
|
||||
gmt: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe(
|
||||
moment('2022-02-22T22:22:22.000Z').format('YYYY-MM-DD HH:mm:ss'),
|
||||
);
|
||||
});
|
||||
|
||||
it('showTime is false and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T00:00:00.000Z',
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||
});
|
||||
|
||||
it('showTime is false and gmt is false', () => {
|
||||
const props = {
|
||||
value: '2022-02-22',
|
||||
showTime: false,
|
||||
gmt: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is true and gmt is true', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22 22:22:22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is true and gmt is false', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-22 22:22:22');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is false and gmt is true', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is false and gmt is false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-22');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'year',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-01-01T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'year',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is month and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'month',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is month and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'month',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is quarter and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'quarter',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is quarter and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'quarter',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is week and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'week',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment.utc('2022-02-21T00:00:00.000Z');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is week and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'week',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-21 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when utc is false', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment('2022-02-22 22:22:22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and utc is false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment('2022-01-01 23:00:00'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01');
|
||||
});
|
||||
|
||||
it('utc is false and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-01-01 23:00:00',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-01-01 23:00:00');
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import moment from 'moment';
|
||||
import { mapRangePicker } from '../util';
|
||||
|
||||
describe('mapRangePicker', () => {
|
||||
it('should work with showTime=false, gmt=true, utc=true', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T23:59:59.999Z']);
|
||||
});
|
||||
|
||||
it('should work with showTime=true, gmt=true, utc=true', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T00:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should work with showTime=false, gmt=true, utc=false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01', '2023-01-02']);
|
||||
});
|
||||
});
|
@ -44,33 +44,93 @@ describe('str2moment', () => {
|
||||
|
||||
describe('moment2str', () => {
|
||||
test('gmt date', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true, gmt: true });
|
||||
expect(str).toBe('2022-06-21T10:10:00.000Z');
|
||||
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||
});
|
||||
|
||||
test('gmt date only', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const str = moment2str(m);
|
||||
expect(str).toBe('2022-06-21T00:00:00.000Z');
|
||||
test('showTime is true, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true, gmt: false });
|
||||
expect(str).toBe(m.toISOString());
|
||||
});
|
||||
|
||||
test('gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { gmt: true });
|
||||
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||
});
|
||||
|
||||
test('gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { gmt: false });
|
||||
expect(str).toBe(moment('2023-06-21 10:10:00').toISOString());
|
||||
});
|
||||
|
||||
test('with time', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true });
|
||||
expect(str).toBe(m.toISOString());
|
||||
});
|
||||
|
||||
test('picker is year, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year', gmt: false });
|
||||
expect(str).toBe(moment('2023-01-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is year, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year', gmt: true });
|
||||
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is year', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year' });
|
||||
expect(str).toBe('2022-01-01T00:00:00.000Z');
|
||||
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is quarter, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'quarter', gmt: false });
|
||||
expect(str).toBe(moment('2023-04-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is quarter, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'quarter', gmt: true });
|
||||
expect(str).toBe('2023-04-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is month, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month', gmt: false });
|
||||
expect(str).toBe(moment('2023-06-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is month, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month', gmt: true });
|
||||
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is month', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month' });
|
||||
expect(str).toBe('2022-06-01T00:00:00.000Z');
|
||||
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is week, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'week', gmt: false });
|
||||
expect(str).toBe(moment('2023-06-19 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is week, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'week', gmt: true });
|
||||
expect(str).toBe('2023-06-19T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('value is null', async () => {
|
||||
|
@ -2,7 +2,7 @@
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
@ -13,28 +13,52 @@ const schema = {
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-reactions': {
|
||||
target: 'read',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
'x-component-props': {
|
||||
gmt: true,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read: {
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: true,
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ DatePicker, FormItem }}>
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* title: DatePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, 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': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: false,
|
||||
utc: false,
|
||||
},
|
||||
'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': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, 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': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: false,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, 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': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
utc: false,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -17,10 +17,22 @@ group:
|
||||
|
||||
<code src="./demos/demo3.tsx" />
|
||||
|
||||
### RangePicker
|
||||
### DatePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo5.tsx" />
|
||||
|
||||
### RangePicker (GMT)
|
||||
|
||||
<code src="./demos/demo2.tsx" />
|
||||
|
||||
### RangePicker (non-GMT)
|
||||
|
||||
<code src="./demos/demo6.tsx" />
|
||||
|
||||
### RangePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo7.tsx" />
|
||||
|
||||
## API
|
||||
|
||||
基于 antd 的 [DatePicker](https://ant.design/components/date-picker/#API),新增了以下扩展属性,用于支持 NocoBase 的日期字段设置。
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
|
||||
import moment from 'moment';
|
||||
|
||||
const toStringByPicker = (value, picker) => {
|
||||
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
|
||||
if (!moment.isMoment(value)) return value;
|
||||
if (timezone === 'local') {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
return moment(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
|
||||
}
|
||||
|
||||
if (picker === 'year') {
|
||||
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||
}
|
||||
@ -9,52 +15,63 @@ const toStringByPicker = (value, picker) => {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'quarter') {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'week') {
|
||||
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
}
|
||||
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
};
|
||||
|
||||
const toGmtByPicker = (value: moment.Moment | moment.Moment[], picker?: any) => {
|
||||
if (!value) {
|
||||
const toGmtByPicker = (value: moment.Moment, picker?: any) => {
|
||||
if (!value || !moment.isMoment(value)) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => toStringByPicker(val, picker));
|
||||
}
|
||||
if (moment.isMoment(value)) {
|
||||
return toStringByPicker(value, picker);
|
||||
return toStringByPicker(value, picker, 'gmt');
|
||||
};
|
||||
|
||||
const toLocalByPicker = (value: moment.Moment, picker?: any) => {
|
||||
if (!value || !moment.isMoment(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?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker } = options;
|
||||
export const moment2str = (value?: moment.Moment, 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 mapDateFormat = function () {
|
||||
return (props: any, field) => {
|
||||
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: moment.Moment | moment.Moment[]) => {
|
||||
onChange: (value: moment.Moment) => {
|
||||
if (onChange) {
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
@ -62,3 +79,77 @@ export const mapDateFormat = function () {
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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: moment.Moment[]) => {
|
||||
if (onChange) {
|
||||
onChange(
|
||||
value
|
||||
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
};
|
||||
|
||||
function getRangeStart(value: moment.Moment, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.startOf('day');
|
||||
}
|
||||
|
||||
function getRangeEnd(value: moment.Moment, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.endOf('day');
|
||||
}
|
||||
|
||||
const getStart = (offset: any, unit: moment.unitOfTime.StartOf) => {
|
||||
return moment()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.startOf(unit);
|
||||
};
|
||||
|
||||
const getEnd = (offset: any, unit: moment.unitOfTime.StartOf) => {
|
||||
return moment()
|
||||
.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')],
|
||||
};
|
||||
};
|
||||
|
@ -1,56 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { createForm, onFieldValueChange } from '@formily/core';
|
||||
import { connect, FieldContext, FormContext } from '@formily/react';
|
||||
import { FieldContext, FormContext } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { Cascader } from 'antd';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { useCompile, useComponent } from '../../hooks';
|
||||
import { useComponent } from '../../hooks';
|
||||
import { FilterContext } from './context';
|
||||
import { useFilterOptions } from './useFilterActionProps';
|
||||
|
||||
const VariableCascader = connect((props) => {
|
||||
const fields = useFilterOptions('users');
|
||||
const compile = useCompile();
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<Cascader
|
||||
className={css`
|
||||
width: 160px;
|
||||
`}
|
||||
value={value ? value.split('.') : []}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
children: 'children',
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange(value ? value.join('.') : null);
|
||||
}}
|
||||
options={compile([
|
||||
{
|
||||
title: '{{t("Current user")}}',
|
||||
name: 'currentUser',
|
||||
children: fields
|
||||
.filter((field) => {
|
||||
if (!field.target) {
|
||||
return true;
|
||||
}
|
||||
return field.type === 'belongsTo';
|
||||
})
|
||||
.map((field) => {
|
||||
if (field.children) {
|
||||
field.children = field.children.filter((child) => {
|
||||
return !child.target;
|
||||
});
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
},
|
||||
])}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const isDateComponent = {
|
||||
'DatePicker.RangePicker': true,
|
||||
DatePicker: true,
|
||||
};
|
||||
|
||||
export const DynamicComponent = (props) => {
|
||||
const { dynamicComponent, disabled } = useContext(FilterContext);
|
||||
@ -85,9 +44,6 @@ export const DynamicComponent = (props) => {
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
components={{
|
||||
VariableCascader,
|
||||
}}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { DatePickerProvider } from '../date-picker';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||
import { FilterAction } from './FilterAction';
|
||||
@ -26,12 +27,20 @@ export const Filter: any = observer((props: any) => {
|
||||
});
|
||||
return (
|
||||
<div className={className}>
|
||||
<FilterContext.Provider
|
||||
value={{ field, fieldSchema, dynamicComponent, options: options || field.dataSource || [], disabled: props.disabled }}
|
||||
>
|
||||
<FilterGroup {...props} bordered={false} />
|
||||
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
|
||||
</FilterContext.Provider>
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
<FilterContext.Provider
|
||||
value={{
|
||||
field,
|
||||
fieldSchema,
|
||||
dynamicComponent,
|
||||
options: options || field.dataSource || [],
|
||||
disabled: props.disabled,
|
||||
}}
|
||||
>
|
||||
<FilterGroup {...props} bordered={false} />
|
||||
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
|
||||
</FilterContext.Provider>
|
||||
</DatePickerProvider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ export interface FilterContextProps {
|
||||
fieldSchema?: Schema;
|
||||
dynamicComponent?: any;
|
||||
options?: any[];
|
||||
disabled?: boolean
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RemoveConditionContext = createContext(null);
|
||||
|
@ -62,7 +62,7 @@ export const useValues = () => {
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = merge(s1, s2);
|
||||
field.data.dataIndex = dataIndex;
|
||||
field.data.value = operator.noValue ? operator.default || true : null;
|
||||
field.data.value = operator?.noValue ? operator.default || true : null;
|
||||
data2value();
|
||||
},
|
||||
setOperator(operatorValue) {
|
||||
|
@ -2,24 +2,19 @@ import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext } from '../../../block-provider';
|
||||
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useActionContext } from '../action';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const FormDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
const ctx = useFormBlockContext();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const { visible } = useActionContext();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
|
||||
return (
|
||||
@ -108,12 +103,15 @@ export const DetailsDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -7,7 +7,9 @@ import { useCollectionFilterOptions } from '../../../collection-manager/action-h
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FixedBlockDesignerItem } from '../page';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const KanbanDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
@ -35,12 +37,15 @@ export const KanbanDesigner = () => {
|
||||
default: defaultFilter,
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -31,7 +31,7 @@ Radio.Group = connect(
|
||||
if (!isValid(props.value)) {
|
||||
return <div></div>;
|
||||
}
|
||||
const { options = [], value } = props;
|
||||
const { value } = props;
|
||||
const field = useField<any>();
|
||||
const dataSource = field.dataSource || [];
|
||||
return (
|
||||
|
@ -0,0 +1,197 @@
|
||||
import React from 'react';
|
||||
import { useCompile } from '../..';
|
||||
import { useValues } from '../filter/useValues';
|
||||
import { Variable } from '../variable';
|
||||
import { useUserVariable } from './hooks/useUserVariable';
|
||||
|
||||
const useVariableTypes = () => {
|
||||
const { operator, schema } = useValues();
|
||||
const operatorValue = operator?.value || '';
|
||||
const userVariable = useUserVariable({ schema, operator });
|
||||
|
||||
if (!operator || !schema) return [];
|
||||
|
||||
const systemOptions = [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("Now")}}`,
|
||||
disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
|
||||
},
|
||||
];
|
||||
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema['x-component']);
|
||||
const dateOptions = [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("Now")}}`,
|
||||
disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
|
||||
},
|
||||
{
|
||||
key: 'yesterday',
|
||||
value: 'yesterday',
|
||||
label: `{{t("Yesterday")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'today',
|
||||
value: 'today',
|
||||
label: `{{t("Today")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'tomorrow',
|
||||
value: 'tomorrow',
|
||||
label: `{{t("Tomorrow")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'lastIsoWeek',
|
||||
value: 'lastIsoWeek',
|
||||
label: `{{t("Last week")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'thisIsoWeek',
|
||||
value: 'thisIsoWeek',
|
||||
label: `{{t("This week")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'nextIsoWeek',
|
||||
value: 'nextIsoWeek',
|
||||
label: `{{t("Next week")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'lastMonth',
|
||||
value: 'lastMonth',
|
||||
label: `{{t("Last month")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'thisMonth',
|
||||
value: 'thisMonth',
|
||||
label: `{{t("This month")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'nextMonth',
|
||||
value: 'nextMonth',
|
||||
label: `{{t("Next month")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'lastQuarter',
|
||||
value: 'lastQuarter',
|
||||
label: `{{t("Last quarter")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'thisQuarter',
|
||||
value: 'thisQuarter',
|
||||
label: `{{t("This quarter")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'nextQuarter',
|
||||
value: 'nextQuarter',
|
||||
label: `{{t("Next quarter")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'lastYear',
|
||||
value: 'lastYear',
|
||||
label: `{{t("Last year")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'thisYear',
|
||||
value: 'thisYear',
|
||||
label: `{{t("This year")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'nextYear',
|
||||
value: 'nextYear',
|
||||
label: `{{t("Next year")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'last7Days',
|
||||
value: 'last7Days',
|
||||
label: `{{t("Last 7 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'next7Days',
|
||||
value: 'next7Days',
|
||||
label: `{{t("Next 7 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'last30Days',
|
||||
value: 'last30Days',
|
||||
label: `{{t("Last 30 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'next30Days',
|
||||
value: 'next30Days',
|
||||
label: `{{t("Next 30 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'last90Days',
|
||||
value: 'last90Days',
|
||||
label: `{{t("Last 90 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
{
|
||||
key: 'next90Days',
|
||||
value: 'next90Days',
|
||||
label: `{{t("Next 90 days")}}`,
|
||||
disabled,
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
userVariable,
|
||||
// {
|
||||
// title: `{{t("System variables")}}`,
|
||||
// value: '$system',
|
||||
// disabled: systemOptions.every((option) => option.disabled),
|
||||
// options: systemOptions,
|
||||
// },
|
||||
{
|
||||
title: `{{t("Date variables")}}`,
|
||||
value: '$date',
|
||||
disabled: dateOptions.every((option) => option.disabled),
|
||||
options: dateOptions,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const useVariableOptions = () => {
|
||||
const compile = useCompile();
|
||||
const options = useVariableTypes().map((item) => {
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(item.options),
|
||||
disabled: item.disabled,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
};
|
||||
|
||||
export function FilterDynamicComponent(props) {
|
||||
const { value, onChange, renderSchemaComponent } = props;
|
||||
const scope = useVariableOptions();
|
||||
return (
|
||||
<Variable.Input value={value} onChange={onChange} scope={scope}>
|
||||
{renderSchemaComponent()}
|
||||
</Variable.Input>
|
||||
);
|
||||
}
|
@ -10,7 +10,9 @@ import { FilterBlockType } from '../../../filter-provider/utils';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FixedBlockDesignerItem } from '../page';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
|
||||
export const TableBlockDesigner = () => {
|
||||
const { name, title, sortable } = useCollection();
|
||||
@ -94,12 +96,15 @@ export const TableBlockDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -9,6 +9,8 @@ import { useCollectionFilterOptions, useSortFields } from '../../../collection-m
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
|
||||
export const TableSelectorDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
@ -49,12 +51,15 @@ export const TableSelectorDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { useFilterOptions } from '../../filter';
|
||||
|
||||
interface GetOptionsParams {
|
||||
schema: any;
|
||||
operator: string;
|
||||
maxDepth: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const useOptions = (collectionName: string, { schema, operator, maxDepth, count = 1 }: GetOptionsParams) => {
|
||||
if (count > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = useFilterOptions(collectionName).map((option) => {
|
||||
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
||||
disabled: schema?.['x-component'] !== option.schema['x-component'],
|
||||
};
|
||||
}
|
||||
|
||||
const children =
|
||||
useOptions(option.target, {
|
||||
schema,
|
||||
operator,
|
||||
maxDepth,
|
||||
count: count + 1,
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
children,
|
||||
disabled: children.every((child) => child.disabled),
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useUserVariable = ({ schema, operator }) => {
|
||||
const options = useOptions('users', { schema, operator, maxDepth: 3 }) || [];
|
||||
|
||||
return {
|
||||
title: `{{t("Current user")}}`,
|
||||
value: '$user',
|
||||
disabled: options.every((option) => option.disabled),
|
||||
options: options,
|
||||
};
|
||||
};
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Cascader, Input as AntInput, Button, Tag, InputNumber, Select, DatePicker } from 'antd';
|
||||
import { CloseCircleFilled } from '@ant-design/icons';
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Button, Cascader, DatePicker, Input as AntInput, InputNumber, Select, Tag } from 'antd';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCompile } from '../..';
|
||||
|
||||
@ -136,7 +136,7 @@ export function Input(props) {
|
||||
|
||||
const variableText = variable
|
||||
?.reduce((opts, key, i) => {
|
||||
const option = (i ? (opts[i - 1] as VariableOptions).children : options)?.find((item) => item.value === key);
|
||||
const option = (i ? (opts[i - 1] as VariableOptions)?.children : options)?.find((item) => item.value === key);
|
||||
return option ? opts.concat(option) : opts;
|
||||
}, [] as VariableOptions[])
|
||||
.map((item) => item.label)
|
||||
|
@ -931,7 +931,7 @@ export const createFilterFormBlockSchema = (options) => {
|
||||
const resourceName = resource || association || collection;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator': 'FilterFormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
...others,
|
||||
action,
|
||||
|
@ -1,56 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { createForm, onFieldValueChange } from '@formily/core';
|
||||
import { connect, FieldContext, FormContext } from '@formily/react';
|
||||
import { FieldContext, FormContext } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { Cascader } from 'antd';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { SchemaComponent } from '../../schema-component/core';
|
||||
import { useCompile, useComponent } from '../../schema-component/hooks';
|
||||
import { useComponent } from '../../schema-component/hooks';
|
||||
import { FilterContext } from './context';
|
||||
import { useFilterOptions } from '../../schema-component/antd/filter/useFilterActionProps';
|
||||
|
||||
const VariableCascader = connect((props) => {
|
||||
const fields = useFilterOptions('users');
|
||||
const compile = useCompile();
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<Cascader
|
||||
className={css`
|
||||
width: 160px;
|
||||
`}
|
||||
value={value ? value.split('.') : []}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
children: 'children',
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange(value ? value.join('.') : null);
|
||||
}}
|
||||
options={compile([
|
||||
{
|
||||
title: '{{t("Current user")}}',
|
||||
name: 'currentUser',
|
||||
children: fields
|
||||
.filter((field) => {
|
||||
if (!field.target) {
|
||||
return true;
|
||||
}
|
||||
return field.type === 'belongsTo';
|
||||
})
|
||||
.map((field) => {
|
||||
if (field.children) {
|
||||
field.children = field.children.filter((child) => {
|
||||
return !child.target;
|
||||
});
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
},
|
||||
])}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const DynamicComponent = (props) => {
|
||||
const { dynamicComponent, disabled } = useContext(FilterContext) || {};
|
||||
@ -86,9 +40,6 @@ export const DynamicComponent = (props) => {
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
components={{
|
||||
VariableCascader,
|
||||
}}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
|
@ -775,7 +775,7 @@ SchemaSettings.ModalItem = (props) => {
|
||||
onSubmit,
|
||||
asyncGetInitialValues,
|
||||
initialValues,
|
||||
width,
|
||||
width = 'fit-content',
|
||||
...others
|
||||
} = props;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
@ -792,7 +792,7 @@ SchemaSettings.ModalItem = (props) => {
|
||||
return (
|
||||
<CollectionManagerContext.Provider value={cm}>
|
||||
<SchemaComponentOptions scope={options.scope} components={options.components}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
|
||||
<SchemaComponent components={components} scope={scope} schema={schema} />
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
|
75
packages/core/database/src/__tests__/fields/date.test.ts
Normal file
75
packages/core/database/src/__tests__/fields/date.test.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { mockDatabase } from '../';
|
||||
import { Database } from '../../database';
|
||||
import { Repository } from '../../repository';
|
||||
|
||||
describe('date-field', () => {
|
||||
let db: Database;
|
||||
let repository: Repository;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ name: 'date1', type: 'date' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
repository = db.getRepository('tests');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
const createExpectToBe = async (key, actual, expected) => {
|
||||
const instance = await repository.create({
|
||||
values: {
|
||||
[key]: actual,
|
||||
},
|
||||
});
|
||||
return expect(instance.get(key).toISOString()).toEqual(expected);
|
||||
};
|
||||
|
||||
test('create', async () => {
|
||||
// sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE
|
||||
await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z');
|
||||
await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z');
|
||||
});
|
||||
|
||||
// dateXX 相关 Operator 都是去 time 比较的
|
||||
describe('dateOn', () => {
|
||||
test('dateOn operator', async () => {
|
||||
console.log('timezone', db.options.timezone);
|
||||
// 默认的情况,时区为 db.options.timezone
|
||||
await repository.find({
|
||||
filter: {
|
||||
date1: {
|
||||
// 由 db.options.timezone 来处理日期转换,假设是 +08:00 的时区
|
||||
// 2023-03-24表示的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00
|
||||
$dateOn: '2023-03-24',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await repository.find({
|
||||
filter: {
|
||||
date1: {
|
||||
// +06:00 时区 2023-03-24 的范围:2023-03-23T18:00:00 ~ 2023-03-24T18:00:00
|
||||
$dateOn: '2023-03-24+06:00',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await repository.find({
|
||||
filter: {
|
||||
date1: {
|
||||
// 2023-03-23T20:00:00+08:00 在 +08:00 时区的时间是:2023-03-24 04:00:00
|
||||
// 也就是 +08:00 时区 2023-03-24 这一天的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00
|
||||
$dateOn: '2023-03-23T20:00:00+08:00',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -47,14 +47,14 @@ describe('filter', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const response = await UserCollection.repository.find({
|
||||
const count = await PostCollection.repository.count({
|
||||
filter: {
|
||||
'posts.createdAt': {
|
||||
'user.createdAt': {
|
||||
$dateOn: user.get('createdAt'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
@ -1,24 +1,26 @@
|
||||
import { mockDatabase } from '../index';
|
||||
import { Collection } from '../../collection';
|
||||
import Database from '../../database';
|
||||
import { Repository } from '../../repository';
|
||||
import { mockDatabase } from '../index';
|
||||
|
||||
describe('date operator test', () => {
|
||||
let db: Database;
|
||||
|
||||
let User: Collection;
|
||||
let repository: Repository;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
db = mockDatabase({
|
||||
timezone: '+00:00',
|
||||
});
|
||||
await db.clean({ drop: true });
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
name: 'birthday',
|
||||
name: 'date1',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
@ -27,139 +29,283 @@ describe('date operator test', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync({ force: true, alter: { drop: false } });
|
||||
repository = Test.repository;
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
test('$dateOn', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-02 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateOn': '2023',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(2);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateOn': '2023+08:00',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateOn': '1990-01-01' },
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(user1.get('id'));
|
||||
expect(count).toBe(4);
|
||||
});
|
||||
|
||||
test('$dateNotOn', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-02 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotOn': '2023',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(2);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotOn': '2023+08:00',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateNotOn': '1990-01-01' },
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(u0.get('id'));
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
test('$dateBefore', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-05-01 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-30T15:59:59.999Z',
|
||||
name: 'u4',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateBefore': '2023',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(3);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateBefore': '2023+08:00',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateBefore': '1990-04-01' },
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(user1.get('id'));
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test('$dateNotBefore', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-05-01 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-30T15:59:59.999Z',
|
||||
name: 'u4',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotBefore': '2023',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(2);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotBefore': '2023+08:00',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateNotBefore': '1990-04-01' },
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(u0.get('id'));
|
||||
expect(count).toBe(4);
|
||||
});
|
||||
|
||||
test('$dateAfter', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-05-01 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-30T15:59:59.999Z',
|
||||
name: 'u4',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateAfter': '2022',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(2);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateAfter': '2022+08:00',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateAfter': '1990-04-01' },
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(u0.get('id'));
|
||||
expect(count).toBe(4);
|
||||
});
|
||||
|
||||
test('$dateNotAfter', async () => {
|
||||
const u0 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-05-01 12:03:02',
|
||||
name: 'u0',
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.001Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-30T15:59:59.999Z',
|
||||
name: 'u4',
|
||||
},
|
||||
],
|
||||
});
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotAfter': '2022',
|
||||
},
|
||||
});
|
||||
|
||||
const user1 = await User.repository.create({
|
||||
values: {
|
||||
birthday: '1990-01-01 12:03:02',
|
||||
name: 'user1',
|
||||
expect(count).toBe(3);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateNotAfter': '2022+08:00',
|
||||
},
|
||||
});
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
const user = await User.repository.findOne({
|
||||
filter: { 'birthday.$dateNotAfter': '1990-04-01' },
|
||||
test('$dateBetween', async () => {
|
||||
await repository.create({
|
||||
values: [
|
||||
{
|
||||
date1: '2023-01-01T00:00:00.000Z',
|
||||
name: 'u0',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-05T16:00:00.000Z',
|
||||
name: 'u1',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.000Z',
|
||||
name: 'u2',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-31T16:00:00.001Z',
|
||||
name: 'u3',
|
||||
},
|
||||
{
|
||||
date1: '2022-12-30T15:59:59.999Z',
|
||||
name: 'u4',
|
||||
},
|
||||
{
|
||||
date1: '2023-01-04T16:00:00.000Z',
|
||||
name: 'u1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(user.get('id')).toEqual(user1.get('id'));
|
||||
let count: number;
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateBetween': '[2023-01-01,2023-01-05]',
|
||||
},
|
||||
});
|
||||
expect(count).toBe(3);
|
||||
count = await repository.count({
|
||||
filter: {
|
||||
'date1.$dateBetween': '[2023-01-01,2023-01-05]+08:00',
|
||||
},
|
||||
});
|
||||
expect(count).toBe(4);
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,10 @@ export class DateField extends Field {
|
||||
return DataTypes.DATE(3);
|
||||
}
|
||||
|
||||
get timezone() {
|
||||
return this.isGMT() ? '+00:00' : null;
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.options?.uiSchema?.['x-component-props'] || {};
|
||||
}
|
||||
@ -17,7 +21,7 @@ export class DateField extends Field {
|
||||
|
||||
isGMT() {
|
||||
const props = this.getProps();
|
||||
return props.gmt || !props.showTime;
|
||||
return props.gmt;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,43 +1,125 @@
|
||||
import moment, { MomentInput } from 'moment';
|
||||
import { parseDate } from '@nocobase/utils';
|
||||
import { Op } from 'sequelize';
|
||||
function stringToDate(value: string): Date {
|
||||
return moment(value).toDate();
|
||||
|
||||
function isDate(input) {
|
||||
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
|
||||
}
|
||||
|
||||
function getNextDay(value: MomentInput): Date {
|
||||
return moment(value).add(1, 'd').toDate();
|
||||
}
|
||||
const toDate = (date) => {
|
||||
if (isDate(date)) {
|
||||
return date;
|
||||
}
|
||||
return new Date(date);
|
||||
};
|
||||
|
||||
export default {
|
||||
$dateOn(value, ctx) {
|
||||
// const field = ctx.db.getFieldByPath(ctx.fieldPath);
|
||||
// console.log(field.isDateOnly());
|
||||
return {
|
||||
[Op.and]: [{ [Op.gte]: stringToDate(value) }, { [Op.lt]: getNextDay(value) }],
|
||||
};
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.eq]: toDate(r),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }],
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateNotOn(value) {
|
||||
return {
|
||||
[Op.or]: [{ [Op.lt]: stringToDate(value) }, { [Op.gte]: getNextDay(value) }],
|
||||
};
|
||||
$dateNotOn(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.ne]: toDate(r),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.or]: [{ [Op.lt]: toDate(r[0]) }, { [Op.gte]: toDate(r[1]) }],
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateBefore(value) {
|
||||
return { [Op.lt]: stringToDate(value) };
|
||||
$dateBefore(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.lt]: toDate(r),
|
||||
};
|
||||
} else if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.lt]: toDate(r[0]),
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateNotBefore(value) {
|
||||
return {
|
||||
[Op.gte]: stringToDate(value),
|
||||
};
|
||||
$dateNotBefore(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.gte]: toDate(r),
|
||||
};
|
||||
} else if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.gte]: toDate(r[0]),
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateAfter(value) {
|
||||
return { [Op.gte]: getNextDay(value) };
|
||||
$dateAfter(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.gt]: toDate(r),
|
||||
};
|
||||
} else if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.gte]: toDate(r[1]),
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateNotAfter(value) {
|
||||
return { [Op.lt]: getNextDay(value) };
|
||||
$dateNotAfter(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.lte]: toDate(r),
|
||||
};
|
||||
} else if (Array.isArray(r)) {
|
||||
return {
|
||||
[Op.lt]: toDate(r[1]),
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
|
||||
$dateBetween(value, ctx) {
|
||||
const r = parseDate(value, {
|
||||
timezone: ctx.db.options.timezone,
|
||||
});
|
||||
if (r) {
|
||||
return {
|
||||
[Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }],
|
||||
};
|
||||
}
|
||||
throw new Error(`Invalid Date ${JSON.stringify(value)}`);
|
||||
},
|
||||
} as Record<string, any>;
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@nocobase/database": "0.9.1-alpha.2",
|
||||
"@nocobase/logger": "0.9.1-alpha.2",
|
||||
"@nocobase/resourcer": "0.9.1-alpha.2",
|
||||
"@nocobase/utils": "0.9.1-alpha.2",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^9.2.0",
|
||||
"find-package-json": "^1.2.0",
|
||||
@ -22,7 +23,8 @@
|
||||
"koa-static": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.7",
|
||||
"xpipe": "^1.0.5"
|
||||
"xpipe": "^1.0.5",
|
||||
"moment": "^2.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "^7.3.9"
|
||||
|
@ -4,6 +4,7 @@ import Resourcer from '@nocobase/resourcer';
|
||||
import i18next from 'i18next';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import Application, { ApplicationOptions } from './application';
|
||||
import { parseVariables } from './middlewares';
|
||||
import { dataWrapping } from './middlewares/data-wrapping';
|
||||
import { db2resource } from './middlewares/db2resource';
|
||||
import { i18n } from './middlewares/i18n';
|
||||
@ -70,6 +71,8 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
||||
app.use(dataWrapping(), { tag: 'dataWrapping', after: 'i18n' });
|
||||
}
|
||||
|
||||
app.resourcer.use(parseVariables, { tag: 'parseVariables', after: 'acl' });
|
||||
|
||||
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
||||
app.use(app.resourcer.restApiMiddleware(), { tag: 'restApi', after: 'db2resource' });
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './data-wrapping';
|
||||
export * from './db2resource';
|
||||
|
||||
export { parseVariables } from './parse-variables';
|
||||
|
55
packages/core/server/src/middlewares/parse-variables.ts
Normal file
55
packages/core/server/src/middlewares/parse-variables.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { getDateVars, parseFilter } from '@nocobase/utils';
|
||||
|
||||
function getUser(ctx) {
|
||||
return async ({ fields }) => {
|
||||
const userFields = fields.filter((f) => f && ctx.db.getFieldByPath('users.' + f));
|
||||
ctx.logger?.info('filter-parse: ', { userFields });
|
||||
if (!ctx.state.currentUser) {
|
||||
return;
|
||||
}
|
||||
if (!userFields.length) {
|
||||
return;
|
||||
}
|
||||
const user = await ctx.db.getRepository('users').findOne({
|
||||
filterByTk: ctx.state.currentUser.id,
|
||||
fields: userFields,
|
||||
});
|
||||
ctx.logger?.info('filter-parse: ', {
|
||||
$user: user?.toJSON(),
|
||||
});
|
||||
return user;
|
||||
};
|
||||
}
|
||||
|
||||
function isNumeric(str: any) {
|
||||
if (typeof str === 'number') return true;
|
||||
if (typeof str != 'string') return false;
|
||||
return !isNaN(str as any) && !isNaN(parseFloat(str));
|
||||
}
|
||||
|
||||
export const parseVariables = async (ctx, next) => {
|
||||
const filter = ctx.action.params.filter;
|
||||
if (!filter) {
|
||||
return next();
|
||||
}
|
||||
ctx.action.params.filter = await parseFilter(filter, {
|
||||
timezone: ctx.get('x-timezone'),
|
||||
now: new Date().toISOString(),
|
||||
getField: (path) => {
|
||||
const fieldPath = path
|
||||
.split('.')
|
||||
.filter((p) => !p.startsWith('$') && !isNumeric(p))
|
||||
.join('.');
|
||||
const { resourceName } = ctx.action;
|
||||
return ctx.db.getFieldByPath(`${resourceName}.${fieldPath}`);
|
||||
},
|
||||
vars: {
|
||||
$system: {
|
||||
now: new Date().toISOString(),
|
||||
},
|
||||
$date: getDateVars(),
|
||||
$user: getUser(ctx),
|
||||
},
|
||||
});
|
||||
await next();
|
||||
};
|
73
packages/core/utils/src/__tests__/common.test.ts
Normal file
73
packages/core/utils/src/__tests__/common.test.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { hasEmptyValue } from '../client';
|
||||
|
||||
describe('hasEmptyValue', () => {
|
||||
it('should return false when there is no empty value', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: 'hello',
|
||||
c: [1, 2, 3],
|
||||
d: {
|
||||
e: 'world',
|
||||
f: [4, 5, 6],
|
||||
g: {
|
||||
h: 'foo',
|
||||
i: 'bar',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hasEmptyValue(obj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when there is an empty value in an object', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: '',
|
||||
c: [1, 2, 3],
|
||||
d: {
|
||||
e: 'world',
|
||||
f: [4, 5, 6],
|
||||
g: {
|
||||
h: 'foo',
|
||||
i: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hasEmptyValue(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when there is an empty value in an array', () => {
|
||||
const arr = [1, '', 3, [4, 5, 6], { a: 'foo', b: null }];
|
||||
expect(hasEmptyValue(arr)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when there is an empty value in an array and an object', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: '',
|
||||
c: [1, 2, 3],
|
||||
d: {
|
||||
e: 'world',
|
||||
f: [4, 5, 6],
|
||||
g: {
|
||||
h: 'foo',
|
||||
i: null,
|
||||
},
|
||||
},
|
||||
h: [1, '', 3, [4, 5, 6], { a: 'foo', b: null }],
|
||||
};
|
||||
expect(hasEmptyValue(obj)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when the input is an empty object', () => {
|
||||
expect(hasEmptyValue({})).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when the input is an empty array', () => {
|
||||
expect(hasEmptyValue([])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when the input is an object with an empty value', () => {
|
||||
const obj = { $and: [{ f_rto697s6udb: { $dateOn: null } }] };
|
||||
expect(hasEmptyValue(obj)).toBe(true);
|
||||
});
|
||||
});
|
21
packages/core/utils/src/__tests__/forEach.test.ts
Normal file
21
packages/core/utils/src/__tests__/forEach.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { forEach } from '../forEach';
|
||||
|
||||
describe('forEach', () => {
|
||||
test('array', () => {
|
||||
const arr = [1, 2, 3];
|
||||
const result = [];
|
||||
forEach(arr, (value, index) => {
|
||||
result.push(value);
|
||||
});
|
||||
expect(result).toEqual(arr);
|
||||
});
|
||||
|
||||
test('object', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
const result = [];
|
||||
forEach(obj, (value, key) => {
|
||||
result.push(value);
|
||||
});
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
193
packages/core/utils/src/__tests__/parse-date.test.ts
Normal file
193
packages/core/utils/src/__tests__/parse-date.test.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import moment from 'moment';
|
||||
import { parseDate } from '../parse-date';
|
||||
|
||||
describe('parse date', () => {
|
||||
const expectDate = (date: any, options?: any) => {
|
||||
const r = parseDate(date, options);
|
||||
console.log(date, r);
|
||||
return expect(r);
|
||||
};
|
||||
|
||||
it('should parse empty', async () => {
|
||||
expectDate(null).toBeUndefined();
|
||||
expectDate('').toBeUndefined();
|
||||
});
|
||||
|
||||
it('should parse year', async () => {
|
||||
expectDate('2023').toEqual(['2023-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z']);
|
||||
expectDate('2023+08:00').toEqual(['2022-12-31T16:00:00.000Z', '2023-12-31T16:00:00.000Z']);
|
||||
expectDate('2023', { timezone: '+08:00' }).toEqual(['2022-12-31T16:00:00.000Z', '2023-12-31T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse quarter', async () => {
|
||||
expectDate('2023Q1').toEqual(['2023-01-01T00:00:00.000Z', '2023-04-01T00:00:00.000Z']);
|
||||
expectDate('2023Q1+08:00').toEqual(['2022-12-31T16:00:00.000Z', '2023-03-31T16:00:00.000Z']);
|
||||
expectDate('2023Q1', { timezone: '+08:00' }).toEqual(['2022-12-31T16:00:00.000Z', '2023-03-31T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse iso week', async () => {
|
||||
expectDate('2023W01').toEqual(['2023-01-02T00:00:00.000Z', '2023-01-09T00:00:00.000Z']);
|
||||
expectDate('2023W01+08:00').toEqual(['2023-01-01T16:00:00.000Z', '2023-01-08T16:00:00.000Z']);
|
||||
expectDate('2023W01', { timezone: '+08:00' }).toEqual(['2023-01-01T16:00:00.000Z', '2023-01-08T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse week', async () => {
|
||||
expectDate('2023w01').toEqual(['2023-01-01T00:00:00.000Z', '2023-01-08T00:00:00.000Z']);
|
||||
expectDate('2023w01+08:00').toEqual(['2022-12-31T16:00:00.000Z', '2023-01-07T16:00:00.000Z']);
|
||||
expectDate('2023w01', { timezone: '+08:00' }).toEqual(['2022-12-31T16:00:00.000Z', '2023-01-07T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse month', () => {
|
||||
expectDate('2023-03').toEqual(['2023-03-01T00:00:00.000Z', '2023-04-01T00:00:00.000Z']);
|
||||
expectDate('2023-03+08:00').toEqual(['2023-02-28T16:00:00.000Z', '2023-03-31T16:00:00.000Z']);
|
||||
expectDate('2023-03', { timezone: '+08:00' }).toEqual(['2023-02-28T16:00:00.000Z', '2023-03-31T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse day', () => {
|
||||
expectDate('2023-01-12').toEqual(['2023-01-12T00:00:00.000Z', '2023-01-13T00:00:00.000Z']);
|
||||
expectDate('2023-01-12+08:00').toEqual(['2023-01-11T16:00:00.000Z', '2023-01-12T16:00:00.000Z']);
|
||||
expectDate('2023-01-12', { timezone: '+08:00' }).toEqual(['2023-01-11T16:00:00.000Z', '2023-01-12T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse hour', () => {
|
||||
expectDate('2023-01-12T12').toEqual(['2023-01-12T12:00:00.000Z', '2023-01-12T13:00:00.000Z']);
|
||||
expectDate('2023-01-12T12+08:00').toEqual(['2023-01-12T04:00:00.000Z', '2023-01-12T05:00:00.000Z']);
|
||||
expectDate('2023-01-12T12', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:00:00.000Z',
|
||||
'2023-01-12T05:00:00.000Z',
|
||||
]);
|
||||
expectDate('2023-01-12 12').toEqual(['2023-01-12T12:00:00.000Z', '2023-01-12T13:00:00.000Z']);
|
||||
expectDate('2023-01-12 12+08:00').toEqual(['2023-01-12T04:00:00.000Z', '2023-01-12T05:00:00.000Z']);
|
||||
expectDate('2023-01-12 12', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:00:00.000Z',
|
||||
'2023-01-12T05:00:00.000Z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse minute', () => {
|
||||
expectDate('2023-01-12T12:23').toEqual(['2023-01-12T12:23:00.000Z', '2023-01-12T12:24:00.000Z']);
|
||||
expectDate('2023-01-12T12:23+08:00').toEqual(['2023-01-12T04:23:00.000Z', '2023-01-12T04:24:00.000Z']);
|
||||
expectDate('2023-01-12T12:23', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:00.000Z',
|
||||
'2023-01-12T04:24:00.000Z',
|
||||
]);
|
||||
expectDate('2023-01-12 12:23').toEqual(['2023-01-12T12:23:00.000Z', '2023-01-12T12:24:00.000Z']);
|
||||
expectDate('2023-01-12 12:23+08:00').toEqual(['2023-01-12T04:23:00.000Z', '2023-01-12T04:24:00.000Z']);
|
||||
expectDate('2023-01-12 12:23', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:00.000Z',
|
||||
'2023-01-12T04:24:00.000Z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse second', () => {
|
||||
expectDate('2023-01-12T12:23:59').toEqual(['2023-01-12T12:23:59.000Z', '2023-01-12T12:24:00.000Z']);
|
||||
expectDate('2023-01-12T12:23:59+08:00').toEqual(['2023-01-12T04:23:59.000Z', '2023-01-12T04:24:00.000Z']);
|
||||
expectDate('2023-01-12T12:23:59', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:59.000Z',
|
||||
'2023-01-12T04:24:00.000Z',
|
||||
]);
|
||||
expectDate('2023-01-12 12:23:59').toEqual(['2023-01-12T12:23:59.000Z', '2023-01-12T12:24:00.000Z']);
|
||||
expectDate('2023-01-12 12:23:59+08:00').toEqual(['2023-01-12T04:23:59.000Z', '2023-01-12T04:24:00.000Z']);
|
||||
expectDate('2023-01-12 12:23:59', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:59.000Z',
|
||||
'2023-01-12T04:24:00.000Z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse millisecond', () => {
|
||||
expectDate('2023-01-12T12:23:59.326').toEqual(['2023-01-12T12:23:59.326Z', '2023-01-12T12:23:59.327Z']);
|
||||
expectDate('2023-01-12T12:23:59.326+08:00').toEqual(['2023-01-12T04:23:59.326Z', '2023-01-12T04:23:59.327Z']);
|
||||
expectDate('2023-01-12T12:23:59.326', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:59.326Z',
|
||||
'2023-01-12T04:23:59.327Z',
|
||||
]);
|
||||
expectDate('2023-01-12 12:23:59.326').toEqual(['2023-01-12T12:23:59.326Z', '2023-01-12T12:23:59.327Z']);
|
||||
expectDate('2023-01-12 12:23:59.326+08:00').toEqual(['2023-01-12T04:23:59.326Z', '2023-01-12T04:23:59.327Z']);
|
||||
expectDate('2023-01-12 12:23:59.326', { timezone: '+08:00' }).toEqual([
|
||||
'2023-01-12T04:23:59.326Z',
|
||||
'2023-01-12T04:23:59.327Z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse utc', () => {
|
||||
expectDate(new Date('2023-01-12T12:23:59.326Z')).toEqual('2023-01-12T12:23:59.326Z');
|
||||
expectDate(moment('2023-01-12T12:23:59.326Z')).toEqual('2023-01-12T12:23:59.326Z');
|
||||
expectDate('2023-01-12T12:23:59.326Z').toEqual('2023-01-12T12:23:59.326Z');
|
||||
expectDate('2023-01-12T12:23:59.326Z+08:00').toEqual('2023-01-12T12:23:59.326Z');
|
||||
expectDate('2023-01-12T12:23:59.326Z', { timezone: '+08:00' }).toEqual('2023-01-12T12:23:59.326Z');
|
||||
});
|
||||
|
||||
describe('parse date between', () => {
|
||||
it('should parse year', async () => {
|
||||
expectDate('[2023,2024]').toEqual(['2023-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z']);
|
||||
expectDate('(2023,2024]').toEqual(['2024-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z']);
|
||||
expectDate('[2023,2024)').toEqual(['2023-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z']);
|
||||
expectDate('(2023,2026)').toEqual(['2024-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z']);
|
||||
expectDate(['2023', '2024']).toEqual(['2023-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '[]']).toEqual(['2023-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '(]']).toEqual(['2024-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '[)']).toEqual(['2023-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z']);
|
||||
expectDate(['2023', '2026', '()']).toEqual(['2024-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z']);
|
||||
expectDate('[2023,2024]+08:00').toEqual(['2022-12-31T16:00:00.000Z', '2024-12-31T16:00:00.000Z']);
|
||||
expectDate('(2023,2024]+08:00').toEqual(['2023-12-31T16:00:00.000Z', '2024-12-31T16:00:00.000Z']);
|
||||
expectDate('[2023,2024)+08:00').toEqual(['2022-12-31T16:00:00.000Z', '2023-12-31T16:00:00.000Z']);
|
||||
expectDate('(2023,2026)+08:00').toEqual(['2023-12-31T16:00:00.000Z', '2025-12-31T16:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '[]', '+08:00']).toEqual(['2022-12-31T16:00:00.000Z', '2024-12-31T16:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '(]', '+08:00']).toEqual(['2023-12-31T16:00:00.000Z', '2024-12-31T16:00:00.000Z']);
|
||||
expectDate(['2023', '2024', '[)', '+08:00']).toEqual(['2022-12-31T16:00:00.000Z', '2023-12-31T16:00:00.000Z']);
|
||||
expectDate(['2023', '2026', '()', '+08:00']).toEqual(['2023-12-31T16:00:00.000Z', '2025-12-31T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse day', async () => {
|
||||
expectDate('[2023-01-12,2023-09-12]').toEqual(['2023-01-12T00:00:00.000Z', '2023-09-13T00:00:00.000Z']);
|
||||
expectDate('[2023-01-12,2023-09-12)').toEqual(['2023-01-12T00:00:00.000Z', '2023-09-12T00:00:00.000Z']);
|
||||
expectDate('(2023-01-12,2023-09-12]').toEqual(['2023-01-13T00:00:00.000Z', '2023-09-13T00:00:00.000Z']);
|
||||
expectDate('(2023-01-12,2023-09-12)').toEqual(['2023-01-13T00:00:00.000Z', '2023-09-12T00:00:00.000Z']);
|
||||
expectDate('[2023-01-12,2023-09-12]+08:00').toEqual(['2023-01-11T16:00:00.000Z', '2023-09-12T16:00:00.000Z']);
|
||||
expectDate('[2023-01-12,2023-09-12)+08:00').toEqual(['2023-01-11T16:00:00.000Z', '2023-09-11T16:00:00.000Z']);
|
||||
expectDate('(2023-01-12,2023-09-12]+08:00').toEqual(['2023-01-12T16:00:00.000Z', '2023-09-12T16:00:00.000Z']);
|
||||
expectDate('(2023-01-12,2023-09-12)+08:00').toEqual(['2023-01-12T16:00:00.000Z', '2023-09-11T16:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should parse utc', async () => {
|
||||
expectDate('[2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z]').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'[]',
|
||||
]);
|
||||
expectDate('[2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z)').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
]);
|
||||
expectDate('(2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z)').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'()'
|
||||
]);
|
||||
expectDate('(2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z]').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'(]'
|
||||
]);
|
||||
expectDate('[2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z]+08:00').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'[]',
|
||||
]);
|
||||
expectDate('[2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z)+08:00').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
]);
|
||||
expectDate('(2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z)+08:00').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'()'
|
||||
]);
|
||||
expectDate('(2023-01-12T12:23:59.326Z,2023-01-12T12:24:59.326Z]+08:00').toEqual([
|
||||
'2023-01-12T12:23:59.326Z',
|
||||
'2023-01-12T12:24:59.326Z',
|
||||
'(]'
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
311
packages/core/utils/src/__tests__/parse-filter.test.ts
Normal file
311
packages/core/utils/src/__tests__/parse-filter.test.ts
Normal file
@ -0,0 +1,311 @@
|
||||
import { getDateVars, getDayRange, parseFilter, utc2unit, Utc2unitOptions } from '../parse-filter';
|
||||
|
||||
describe('utc to unit', () => {
|
||||
const expectUtc2unit = (options: Utc2unitOptions) => {
|
||||
const r = utc2unit(options);
|
||||
console.log(options, r);
|
||||
return expect(r);
|
||||
};
|
||||
it('should be year', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'year',
|
||||
}).toBe('2023+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'year',
|
||||
}).toBe('2023+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T15:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'year',
|
||||
}).toBe('2022+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
unit: 'year',
|
||||
}).toBe('2023+00:00');
|
||||
});
|
||||
it('should be month', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'month',
|
||||
}).toBe('2023-01+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'month',
|
||||
}).toBe('2023-01+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
unit: 'month',
|
||||
}).toBe('2023-01+00:00');
|
||||
});
|
||||
it('should be quarter', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'quarter',
|
||||
}).toBe('2023Q1+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'quarter',
|
||||
}).toBe('2023Q1+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T15:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'quarter',
|
||||
}).toBe('2022Q4+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
unit: 'quarter',
|
||||
}).toBe('2023Q1+00:00');
|
||||
});
|
||||
it('should be week', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-08T00:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'week',
|
||||
}).toBe('2023w02+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'week',
|
||||
}).toBe('2023w01+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-01T00:00:00.000Z',
|
||||
unit: 'week',
|
||||
}).toBe('2023w01+00:00');
|
||||
});
|
||||
it('should be iso week', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-08T00:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'isoWeek',
|
||||
}).toBe('2023W01+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'isoWeek',
|
||||
}).toBe('2022W52+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-01T00:00:00.000Z',
|
||||
unit: 'isoWeek',
|
||||
}).toBe('2022W52+00:00');
|
||||
});
|
||||
it('should be day', async () => {
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
timezone: '+00:00',
|
||||
unit: 'day',
|
||||
}).toBe('2023-01-05+00:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
unit: 'day',
|
||||
}).toBe('2023-01-06+08:00');
|
||||
expectUtc2unit({
|
||||
now: '2023-01-05T16:00:00.000Z',
|
||||
unit: 'day',
|
||||
}).toBe('2023-01-05+00:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDayRange', () => {
|
||||
const expectDayRange = (options) => {
|
||||
const r = getDayRange(options);
|
||||
console.log(r, options);
|
||||
return expect(r);
|
||||
};
|
||||
test('next7days', () => {
|
||||
expectDayRange({
|
||||
now: '2023-03-28T16:00:00.000Z',
|
||||
offset: 7,
|
||||
timezone: '+00:00',
|
||||
}).toEqual(['2023-03-29', '2023-04-05', '[)', '+00:00']);
|
||||
expectDayRange({
|
||||
now: '2023-03-28T16:00:00.000Z',
|
||||
offset: 7,
|
||||
timezone: '+08:00',
|
||||
}).toEqual(['2023-03-30', '2023-04-06', '[)', '+08:00']);
|
||||
});
|
||||
test('last7days', () => {
|
||||
expectDayRange({
|
||||
now: '2023-03-28T16:00:00.000Z',
|
||||
offset: -7,
|
||||
timezone: '+00:00',
|
||||
}).toEqual(['2023-03-22', '2023-03-29', '[)', '+00:00']);
|
||||
expectDayRange({
|
||||
now: '2023-03-28T16:00:00.000Z',
|
||||
offset: -7,
|
||||
timezone: '+08:00',
|
||||
}).toEqual(['2023-03-23', '2023-03-30', '[)', '+08:00']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFilter', () => {
|
||||
const expectParseFilter = (filter, options) => {
|
||||
return {
|
||||
async toEqual(expected) {
|
||||
const r = await parseFilter(filter, options);
|
||||
console.log(filter, r);
|
||||
return expect(r).toEqual(expected);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test('timezone', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'a.$dateOn': '2023',
|
||||
'b.$dateOn': '2023+08:00',
|
||||
},
|
||||
{
|
||||
timezone: '+00:00',
|
||||
},
|
||||
).toEqual({ a: { $dateOn: '2023+00:00' }, b: { $dateOn: '2023+08:00' } });
|
||||
});
|
||||
|
||||
test('timezone', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'a.$dateOn': '2023',
|
||||
'b.$dateOn': '2023+08:00',
|
||||
'c.$dateOn': '2023+08:00',
|
||||
},
|
||||
{
|
||||
timezone: '+00:00',
|
||||
getField(path) {
|
||||
if (path === 'a.$dateOn') {
|
||||
return {
|
||||
timezone: '+06:00',
|
||||
};
|
||||
}
|
||||
if (path === 'c.$dateOn') {
|
||||
return {
|
||||
timezone: '+06:00',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
).toEqual({ a: { $dateOn: '2023+06:00' }, b: { $dateOn: '2023+08:00' }, c: { $dateOn: '2023+08:00' } });
|
||||
});
|
||||
|
||||
test('vars', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'a.$dateOn': '{{$date.today}}',
|
||||
'b.$eq': '{{$custom.foo}}',
|
||||
'b.$ne': '{{$foo.bar}}',
|
||||
},
|
||||
{
|
||||
timezone: '+08:00',
|
||||
vars: {
|
||||
$custom: {
|
||||
foo: () => 'bar',
|
||||
},
|
||||
$date: {
|
||||
today: () => '2023-01-01',
|
||||
},
|
||||
},
|
||||
},
|
||||
).toEqual({ a: { $dateOn: '2023-01-01+08:00' }, b: { $eq: 'bar', $ne: null } });
|
||||
});
|
||||
|
||||
test('$date.today', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'a.$dateOn': '{{$date.today}}',
|
||||
},
|
||||
{
|
||||
now: '2022-12-31T16:00:00.000Z',
|
||||
timezone: '+08:00',
|
||||
vars: {
|
||||
$date: getDateVars(),
|
||||
},
|
||||
},
|
||||
).toEqual({ a: { $dateOn: '2023-01-01+08:00' } });
|
||||
});
|
||||
|
||||
test('$user', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'user.id.$eq': '{{$user.id}}',
|
||||
'team.id.$eq': '{{$user.team.id}}',
|
||||
'team.name.$eq': '{{$user.team.name}}',
|
||||
},
|
||||
{
|
||||
vars: {
|
||||
$user: async (fields) => {
|
||||
return {
|
||||
id: 1,
|
||||
team: {
|
||||
id: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
).toEqual({ user: { id: { $eq: 1 } }, team: { id: { $eq: 2 }, name: { $eq: null } } });
|
||||
});
|
||||
|
||||
test('$user', async () => {
|
||||
await expectParseFilter(
|
||||
{
|
||||
'user.id.$eq': '{{$user.id}}',
|
||||
'team.id.$eq': '{{$user.team.id}}',
|
||||
'team.name.$eq': '{{$user.team.name}}',
|
||||
},
|
||||
{
|
||||
vars: {
|
||||
$user: async (fields) => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
},
|
||||
).toEqual({ user: { id: { $eq: null } }, team: { id: { $eq: null }, name: { $eq: null } } });
|
||||
});
|
||||
|
||||
test('$user', async () => {
|
||||
const date = new Date();
|
||||
await expectParseFilter(
|
||||
{
|
||||
'createdAt.$eq': '{{$user.team.createdAt}}',
|
||||
},
|
||||
{
|
||||
vars: {
|
||||
$user: async (fields) => {
|
||||
return {
|
||||
team: {
|
||||
createdAt: date,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
).toEqual({ createdAt: { $eq: date } });
|
||||
});
|
||||
|
||||
test('$dateOn', async () => {
|
||||
const date = new Date();
|
||||
await expectParseFilter(
|
||||
{
|
||||
'createdAt.$dateOn': '{{$user.team.createdAt}}',
|
||||
},
|
||||
{
|
||||
vars: {
|
||||
$user: async (fields) => {
|
||||
return {
|
||||
team: {
|
||||
createdAt: date,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
).toEqual({ createdAt: { $dateOn: date.toISOString() } });
|
||||
});
|
||||
});
|
@ -1,9 +1,10 @@
|
||||
export * from './collections-graph';
|
||||
export * from './common';
|
||||
export * from './date';
|
||||
export * from './merge';
|
||||
export * from './number';
|
||||
export * from './parse-filter';
|
||||
export * from './registry';
|
||||
// export * from './toposort';
|
||||
export * from './uid';
|
||||
export * from './common';
|
||||
|
||||
|
@ -1,3 +1,11 @@
|
||||
export const isString = (value: any): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isArray = (value: any): value is Array<any> => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
export const isEmpty = (value: unknown) => {
|
||||
if (isPlainObject(value)) {
|
||||
return Object.keys(value).length === 0;
|
||||
@ -16,3 +24,20 @@ export const isPlainObject = (value) => {
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === null || prototype === Object.prototype;
|
||||
};
|
||||
|
||||
export const hasEmptyValue = (objOrArr: object | any[]) => {
|
||||
let result = true;
|
||||
for (const key in objOrArr) {
|
||||
result = false;
|
||||
if (isArray(objOrArr[key]) && objOrArr[key].length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!objOrArr[key]) {
|
||||
return true;
|
||||
}
|
||||
if (isPlainObject(objOrArr[key]) || isArray(objOrArr[key])) {
|
||||
return hasEmptyValue(objOrArr[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ export interface Str2momentOptions {
|
||||
gmt?: boolean;
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
utcOffset?: any;
|
||||
utc?: boolean;
|
||||
}
|
||||
|
||||
export const getDefaultFormat = (props: any) => {
|
||||
@ -28,27 +29,22 @@ export const getDefaultFormat = (props: any) => {
|
||||
return props['showTime'] ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
};
|
||||
|
||||
export const toGmt = (value: moment.Moment | moment.Moment[]) => {
|
||||
if (!value) {
|
||||
export const toGmt = (value: moment.Moment) => {
|
||||
if (!value || !moment.isMoment(value)) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => `${val.format('YYYY-MM-DD')}T${val.format('HH:mm:ss.SSS')}Z`);
|
||||
}
|
||||
if (moment.isMoment(value)) {
|
||||
return `${value.format('YYYY-MM-DD')}T${value.format('HH:mm:ss.SSS')}Z`;
|
||||
}
|
||||
return `${value.format('YYYY-MM-DD')}T${value.format('HH:mm:ss.SSS')}Z`;
|
||||
};
|
||||
|
||||
export const toLocal = (value: moment.Moment | moment.Moment[]) => {
|
||||
export const toLocal = (value: moment.Moment) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => val.toISOString());
|
||||
return value.map((val) => val.startOf('second').toISOString());
|
||||
}
|
||||
if (moment.isMoment(value)) {
|
||||
return value.toISOString();
|
||||
return value.startOf('second').toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
@ -57,10 +53,15 @@ const toMoment = (val: any, options?: Str2momentOptions) => {
|
||||
return;
|
||||
}
|
||||
const offset = options.utcOffset || -1 * new Date().getTimezoneOffset();
|
||||
const { gmt, picker, utc = true } = options;
|
||||
|
||||
if (!utc) {
|
||||
return moment(val);
|
||||
}
|
||||
|
||||
if (moment.isMoment(val)) {
|
||||
return val.utcOffset(offset);
|
||||
}
|
||||
const { gmt, picker } = options;
|
||||
if (gmt || picker) {
|
||||
return moment(val).utcOffset(0);
|
||||
}
|
||||
@ -111,7 +112,7 @@ export interface Moment2strOptions {
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
}
|
||||
|
||||
export const moment2str = (value?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => {
|
||||
export const moment2str = (value?: moment.Moment, options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker } = options;
|
||||
if (!value) {
|
||||
return value;
|
||||
@ -121,20 +122,3 @@ export const moment2str = (value?: moment.Moment | moment.Moment[], options: Mom
|
||||
}
|
||||
return toGmtByPicker(value, picker);
|
||||
};
|
||||
|
||||
export const mapDateFormat = 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: moment.Moment | moment.Moment[]) => {
|
||||
if (onChange) {
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
9
packages/core/utils/src/forEach.ts
Normal file
9
packages/core/utils/src/forEach.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const forEach = (obj: any, callback: (value: any, key: string | number) => void) => {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach(callback);
|
||||
} else {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
callback(obj[key], key);
|
||||
});
|
||||
}
|
||||
};
|
@ -1,12 +1,16 @@
|
||||
export * from './assign';
|
||||
export * from './collections-graph';
|
||||
export * from './common';
|
||||
export * from './date';
|
||||
export * from './forEach';
|
||||
export * from './merge';
|
||||
export * from './mixin';
|
||||
export * from './mixin/AsyncEmitter';
|
||||
export * from './number';
|
||||
export * from './parse-date';
|
||||
export * from './parse-filter';
|
||||
export * from './registry';
|
||||
export * from './requireModule';
|
||||
export * from './toposort';
|
||||
export * from './uid';
|
||||
export * from './assign';
|
||||
export * from './collections-graph';
|
||||
export * from './common';
|
||||
|
||||
|
213
packages/core/utils/src/parse-date.ts
Normal file
213
packages/core/utils/src/parse-date.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function parseUTC(value) {
|
||||
if (moment.isDate(value) || moment.isMoment(value)) {
|
||||
return {
|
||||
unit: 'utc',
|
||||
start: value.toISOString(),
|
||||
};
|
||||
}
|
||||
if (value.endsWith('Z')) {
|
||||
return {
|
||||
unit: 'utc',
|
||||
start: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseYear(value) {
|
||||
if (/^\d\d\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'year',
|
||||
start: `${value}-01-01 00:00:00`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseQuarter(value) {
|
||||
if (/^\d\d\d\d\Q\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'quarter',
|
||||
start: moment(value, 'YYYY[Q]Q').format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseWeek(value) {
|
||||
if (/^\d\d\d\d[W]\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'isoWeek',
|
||||
start: moment(value, 'GGGG[W]W').format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
if (/^\d\d\d\d[w]\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'week',
|
||||
start: moment(value, 'gggg[w]w').format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseMonth(value) {
|
||||
if (/^\d\d\d\d\-\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'month',
|
||||
start: `${value}-01 00:00:00`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseDay(value) {
|
||||
if (/^\d\d\d\d\-\d\d\-\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'day',
|
||||
start: `${value} 00:00:00`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseHour(value) {
|
||||
if (/^\d\d\d\d\-\d\d\-\d\d(\T|\s)\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'hour',
|
||||
start: `${value}:00:00`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseMinute(value) {
|
||||
if (/^\d\d\d\d\-\d\d\-\d\d(\T|\s)\d\d\:\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'minute',
|
||||
start: `${value}:00`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseSecond(value) {
|
||||
if (/^\d\d\d\d\-\d\d\-\d\d(\T|\s)\d\d\:\d\d\:\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'second',
|
||||
start: `${value}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseMillisecond(value) {
|
||||
if (/^\d\d\d\d\-\d\d\-\d\d(\T|\s)\d\d\:\d\d\:\d\d\.\d\d\d$/.test(value)) {
|
||||
return {
|
||||
unit: 'millisecond',
|
||||
start: `${value}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const parsers = [
|
||||
parseUTC,
|
||||
parseYear,
|
||||
parseQuarter,
|
||||
parseWeek,
|
||||
parseMonth,
|
||||
parseDay,
|
||||
parseHour,
|
||||
parseMinute,
|
||||
parseSecond,
|
||||
parseMillisecond,
|
||||
];
|
||||
|
||||
type ParseDateResult = {
|
||||
unit: any;
|
||||
start: string;
|
||||
timezone?: string;
|
||||
};
|
||||
|
||||
function toISOString(m: moment.Moment) {
|
||||
return m.toISOString();
|
||||
}
|
||||
|
||||
function dateRange(r: ParseDateResult) {
|
||||
if (!r.timezone) {
|
||||
r.timezone = '+00:00';
|
||||
}
|
||||
let m: moment.Moment;
|
||||
if (r.unit === 'utc') {
|
||||
return moment(r?.start).toISOString();
|
||||
} else {
|
||||
m = moment(`${r?.start}${r?.timezone}`);
|
||||
}
|
||||
m = m.utcOffset(r.timezone);
|
||||
return [m.startOf(r.unit), m.clone().add(1, r.unit).startOf(r.unit)].map(toISOString);
|
||||
}
|
||||
|
||||
export function parseDate(value: any, options = {} as { timezone?: string }) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return parseDateBetween(value, options);
|
||||
}
|
||||
let timezone = options.timezone || '+00:00';
|
||||
const input = value;
|
||||
if (typeof value === 'string') {
|
||||
const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value);
|
||||
if (match) {
|
||||
value = match[1];
|
||||
timezone = match[2];
|
||||
}
|
||||
if (/^(\(|\[)/.test(value)) {
|
||||
return parseDateBetween(input, options);
|
||||
}
|
||||
}
|
||||
for (const parse of parsers) {
|
||||
const r = parse(value);
|
||||
if (r) {
|
||||
r['input'] = input;
|
||||
if (!r['timezone']) {
|
||||
r['timezone'] = timezone;
|
||||
}
|
||||
return dateRange(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseDateBetween(value: any, options = {} as { timezone?: string }) {
|
||||
if (Array.isArray(value) && value.length > 1) {
|
||||
const [startValue, endValue, op = '[]', timezone] = value;
|
||||
const r0 = parseDate(startValue, { timezone });
|
||||
const r1 = parseDate(endValue, { timezone });
|
||||
let start;
|
||||
let startOp;
|
||||
let end;
|
||||
let endOp;
|
||||
if (typeof r0 === 'string') {
|
||||
start = r0;
|
||||
startOp = op[0];
|
||||
} else {
|
||||
start = op.startsWith('(') ? r0[1] : r0[0];
|
||||
startOp = '[';
|
||||
}
|
||||
if (typeof r1 === 'string') {
|
||||
end = r1;
|
||||
endOp = op[1];
|
||||
} else {
|
||||
end = op.endsWith(')') ? r1[0] : r1[1];
|
||||
endOp = ')';
|
||||
}
|
||||
const newOp = startOp + endOp;
|
||||
return newOp === '[)' ? [start, end] : [start, end, newOp];
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value);
|
||||
let timezone = options.timezone || '+00:00';
|
||||
if (match) {
|
||||
value = match[1];
|
||||
timezone = match[2];
|
||||
}
|
||||
const m = /^(\(|\[)(.+)\,(.+)(\)|\])$/.exec(value);
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
return parseDateBetween([m[2], m[3], `${m[1]}${m[4]}`, timezone]);
|
||||
}
|
295
packages/core/utils/src/parse-filter.ts
Normal file
295
packages/core/utils/src/parse-filter.ts
Normal file
@ -0,0 +1,295 @@
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import moment from 'moment';
|
||||
|
||||
const re = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
||||
|
||||
function isBuffer(obj) {
|
||||
return obj && obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj);
|
||||
}
|
||||
|
||||
function keyIdentity(key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
function flatten(target, opts?: any) {
|
||||
opts = opts || {};
|
||||
|
||||
const delimiter = opts.delimiter || '.';
|
||||
const maxDepth = opts.maxDepth;
|
||||
const transformKey = opts.transformKey || keyIdentity;
|
||||
const transformValue = opts.transformValue || keyIdentity;
|
||||
const output = {};
|
||||
|
||||
function step(object, prev?: any, currentDepth?: any) {
|
||||
currentDepth = currentDepth || 1;
|
||||
Object.keys(object).forEach(function (key) {
|
||||
const value = object[key];
|
||||
const isarray = opts.safe && Array.isArray(value);
|
||||
const type = Object.prototype.toString.call(value);
|
||||
const isbuffer = isBuffer(value);
|
||||
const isobject = type === '[object Object]' || type === '[object Array]';
|
||||
|
||||
const newKey = prev ? prev + delimiter + transformKey(key) : transformKey(key);
|
||||
|
||||
if (opts.breakOn({ key })) {
|
||||
output[newKey] = transformValue(value, newKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isarray &&
|
||||
!isbuffer &&
|
||||
isobject &&
|
||||
Object.keys(value).length &&
|
||||
(!opts.maxDepth || currentDepth < maxDepth)
|
||||
) {
|
||||
return step(value, newKey, currentDepth + 1);
|
||||
}
|
||||
|
||||
output[newKey] = transformValue(value, newKey);
|
||||
});
|
||||
}
|
||||
|
||||
step(target);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function unflatten(obj, opts: any = {}) {
|
||||
const parsed = {};
|
||||
const transformValue = opts.transformValue || keyIdentity;
|
||||
Object.keys(obj).forEach((key) => {
|
||||
set(parsed, key, transformValue(obj[key], key));
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const parsePath = (path: string) => {
|
||||
let operator = path.split('.').pop() || '';
|
||||
if (!operator.startsWith('$')) {
|
||||
operator = '';
|
||||
}
|
||||
return { operator };
|
||||
};
|
||||
|
||||
const isDateOperator = (op) => {
|
||||
return [
|
||||
'$dateOn',
|
||||
'$dateNotOn',
|
||||
'$dateBefore',
|
||||
'$dateAfter',
|
||||
'$dateNotBefore',
|
||||
'$dateNotAfter',
|
||||
'$dateBetween',
|
||||
].includes(op);
|
||||
};
|
||||
|
||||
function isDate(input) {
|
||||
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
|
||||
}
|
||||
|
||||
const dateValueWrapper = (value: any, timezone?: string) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 2) {
|
||||
value.push('[]', timezone);
|
||||
} else if (value.length === 3) {
|
||||
value.push(timezone);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (!timezone || /(\+|\-)\d\d\:\d\d$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return value + timezone;
|
||||
}
|
||||
|
||||
if (isDate(value)) {
|
||||
return value.toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
export type ParseFilterOptions = {
|
||||
vars?: Record<string, any>;
|
||||
now?: any;
|
||||
timezone?: string;
|
||||
getField?: any;
|
||||
};
|
||||
|
||||
export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) => {
|
||||
const userFieldsSet = new Set();
|
||||
const vars = opts.vars || {};
|
||||
const timezone = opts.timezone;
|
||||
const now = opts.now;
|
||||
const getField = opts.getField;
|
||||
|
||||
const flat = flatten(filter, {
|
||||
breakOn({ key }) {
|
||||
return key.startsWith('$') && key !== '$and' && key !== '$or';
|
||||
},
|
||||
transformValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
// parse user fields parameter
|
||||
const match = re.exec(value);
|
||||
if (match) {
|
||||
const key = match[1].trim();
|
||||
if (key.startsWith('$user')) {
|
||||
userFieldsSet.add(key.substring(6));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
if (userFieldsSet.size > 0) {
|
||||
const $user = await vars.$user({ fields: [...userFieldsSet.values()] });
|
||||
Object.assign(vars, { $user });
|
||||
}
|
||||
|
||||
return unflatten(flat, {
|
||||
transformValue(value, path) {
|
||||
const { operator } = parsePath(path);
|
||||
// parse string variables
|
||||
if (typeof value === 'string') {
|
||||
const match = re.exec(value);
|
||||
if (match) {
|
||||
const key = match[1].trim();
|
||||
const val = get(vars, key, null);
|
||||
const field = getField?.(path);
|
||||
value = typeof val === 'function' ? val?.({ field, operator, timezone, now }) : val;
|
||||
}
|
||||
}
|
||||
if (isDateOperator(operator)) {
|
||||
const field = getField?.(path);
|
||||
return dateValueWrapper(value, field?.timezone || timezone);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type GetDayRangeOptions = {
|
||||
now?: any;
|
||||
timezone?: string;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export function getDayRange(options: GetDayRangeOptions) {
|
||||
const { now, timezone = '+00:00', offset } = options;
|
||||
let m = toMoment(now).utcOffset(timezone);
|
||||
if (offset > 0) {
|
||||
return [
|
||||
// 第二天开始计算
|
||||
m.add(1, 'day').startOf('day').format('YYYY-MM-DD'),
|
||||
// 第九天开始前结束
|
||||
m.clone().add(offset, 'day').startOf('day').format('YYYY-MM-DD'),
|
||||
'[)',
|
||||
timezone,
|
||||
];
|
||||
}
|
||||
return [
|
||||
// 今天开始前
|
||||
m
|
||||
.clone()
|
||||
.subtract(-1 * offset - 1, 'day')
|
||||
.startOf('day')
|
||||
.format('YYYY-MM-DD'),
|
||||
// 明天开始前
|
||||
m.clone().add(1, 'day').startOf('day').format('YYYY-MM-DD'),
|
||||
'[)',
|
||||
timezone,
|
||||
];
|
||||
}
|
||||
|
||||
function toMoment(value) {
|
||||
if (!value) {
|
||||
return moment();
|
||||
}
|
||||
if (moment.isMoment(value)) {
|
||||
return value;
|
||||
}
|
||||
return moment(value);
|
||||
}
|
||||
|
||||
export type Utc2unitOptions = {
|
||||
now?: any;
|
||||
unit: any;
|
||||
timezone?: string;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export function utc2unit(options: Utc2unitOptions) {
|
||||
const { now, unit, timezone = '+00:00', offset } = options;
|
||||
let m = toMoment(now);
|
||||
m.utcOffset(timezone);
|
||||
m.startOf(unit);
|
||||
if (offset > 0) {
|
||||
m.add(offset, unit === 'isoWeek' ? 'week' : unit);
|
||||
} else if (offset < 0) {
|
||||
m.subtract(-1 * offset, unit === 'isoWeek' ? 'week' : unit);
|
||||
}
|
||||
const fn = {
|
||||
year: () => m.format('YYYY'),
|
||||
quarter: () => m.format('YYYY[Q]Q'),
|
||||
month: () => m.format('YYYY-MM'),
|
||||
week: () => m.format('gggg[w]ww'),
|
||||
isoWeek: () => m.format('GGGG[W]WW'),
|
||||
day: () => m.format('YYYY-MM-DD'),
|
||||
};
|
||||
const r = fn[unit]?.();
|
||||
return timezone ? r + timezone : r;
|
||||
}
|
||||
|
||||
const toUnit = (unit, offset?: number) => {
|
||||
return ({ now, timezone, field }) => {
|
||||
if (field?.timezone) {
|
||||
timezone = field?.timezone;
|
||||
}
|
||||
return utc2unit({ now, timezone, unit, offset });
|
||||
};
|
||||
};
|
||||
|
||||
const toDays = (offset: number) => {
|
||||
return ({ now, timezone, field }) => {
|
||||
if (field?.timezone) {
|
||||
timezone = field?.timezone;
|
||||
}
|
||||
return getDayRange({ now, timezone, offset });
|
||||
};
|
||||
};
|
||||
|
||||
export function getDateVars() {
|
||||
return {
|
||||
today: toUnit('day'),
|
||||
yesterday: toUnit('day', -1),
|
||||
tomorrow: toUnit('day', 1),
|
||||
thisWeek: toUnit('week'),
|
||||
lastWeek: toUnit('week', -1),
|
||||
nextWeek: toUnit('week', 1),
|
||||
thisIsoWeek: toUnit('isoWeek'),
|
||||
lastIsoWeek: toUnit('isoWeek', -1),
|
||||
nextIsoWeek: toUnit('isoWeek', 1),
|
||||
thisMonth: toUnit('month'),
|
||||
lastMonth: toUnit('month', -1),
|
||||
nextMonth: toUnit('month', 1),
|
||||
thisQuarter: toUnit('quarter'),
|
||||
lastQuarter: toUnit('quarter', -1),
|
||||
nextQuarter: toUnit('quarter', 1),
|
||||
thisYear: toUnit('year'),
|
||||
lastYear: toUnit('year', -1),
|
||||
nextYear: toUnit('year', 1),
|
||||
last7Days: toDays(-7),
|
||||
next7Days: toDays(7),
|
||||
last30Days: toDays(-30),
|
||||
next30Days: toDays(30),
|
||||
last90Days: toDays(-90),
|
||||
next90Days: toDays(90),
|
||||
};
|
||||
}
|
@ -5,23 +5,20 @@ import {
|
||||
useCollection,
|
||||
useCollectionFilterOptions,
|
||||
useDesignable,
|
||||
useSortFields,
|
||||
useTableBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AuditLogsDesigner = () => {
|
||||
const { name, title, sortable } = useCollection();
|
||||
const { name, title } = useCollection();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const dataSource = useCollectionFilterOptions(name);
|
||||
const sortFields = useSortFields(name);
|
||||
const { service } = useTableBlockContext();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<SchemaSettings.ModalItem
|
||||
|
@ -12,6 +12,21 @@
|
||||
"Today": "Today",
|
||||
"Month": "Month",
|
||||
"Week": "Week",
|
||||
"This week": "This week",
|
||||
"Next week": "Next week",
|
||||
"This month": "This month",
|
||||
"Next month": "Next month",
|
||||
"This year": "This year",
|
||||
"Next year": "Next year",
|
||||
"Last week": "Last week",
|
||||
"Last month": "Last month",
|
||||
"Last year": "Last year",
|
||||
"Last 7 days": "last 7 days",
|
||||
"Last 30 days": "last 30 days",
|
||||
"Last 90 days": "last 90 days",
|
||||
"Next 7 days": "next 7 days",
|
||||
"Next 30 days": "next 30 days",
|
||||
"Next 90 days": "next 90 days",
|
||||
"Work week": "Work week",
|
||||
"Day": "Day",
|
||||
"Agenda": "Agenda",
|
||||
@ -233,7 +248,6 @@
|
||||
"Comparision": "Comparision",
|
||||
"is": "is",
|
||||
"is not": "is not",
|
||||
"is variable": "is variable",
|
||||
"contains": "contains",
|
||||
"does not contain": "does not contain",
|
||||
"starts with": "starts with",
|
||||
@ -297,6 +311,7 @@
|
||||
"is after": "is after",
|
||||
"is on or after": "is on or after",
|
||||
"is on or before": "is on or before",
|
||||
"is between": "is between",
|
||||
"Upload": "Upload",
|
||||
"Select level": "Select level",
|
||||
"Province": "Province",
|
||||
@ -436,8 +451,6 @@
|
||||
"Add condition group": "Add condition group",
|
||||
"exists": "exists",
|
||||
"not exists": "not exists",
|
||||
"is current logged-in user": "is current logged-in user",
|
||||
"is not current logged-in user": "is not current logged-in user",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
@ -531,6 +544,8 @@
|
||||
"Current user": "Current user",
|
||||
"Current record": "Current record",
|
||||
"Current time": "Current time",
|
||||
"System variables": "System variables",
|
||||
"Date variables": "Date variables",
|
||||
"Popup close method": "Popup close method",
|
||||
"Automatic close": "Automatic close",
|
||||
"Manually close": "Manually close",
|
||||
|
@ -12,6 +12,21 @@
|
||||
"Today": "今天",
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
"This week": "本周",
|
||||
"Next week": "下周",
|
||||
"This month": "本月",
|
||||
"Next month": "下月",
|
||||
"This year": "今年",
|
||||
"Next year": "明年",
|
||||
"Last week": "上周",
|
||||
"Last month": "上月",
|
||||
"Last year": "去年",
|
||||
"Last 7 days": "过去 7 天",
|
||||
"Last 30 days": "过去 30 天",
|
||||
"Last 90 days": "过去 90 天",
|
||||
"Next 7 days": "未来 7 天",
|
||||
"Next 30 days": "未来 30 天",
|
||||
"Next 90 days": "未来 90 天",
|
||||
"Work week": "工作日",
|
||||
"Day": "天",
|
||||
"Agenda": "列表",
|
||||
@ -233,7 +248,6 @@
|
||||
"Comparision": "值比较",
|
||||
"is": "等于",
|
||||
"is not": "不等于",
|
||||
"is variable": "为动态变量",
|
||||
"contains": "包含",
|
||||
"does not contain": "不包含",
|
||||
"starts with": "开头是",
|
||||
@ -297,6 +311,7 @@
|
||||
"is after": "晚于",
|
||||
"is on or after": "不早于",
|
||||
"is on or before": "不晚于",
|
||||
"is between": "介于",
|
||||
"Upload": "上传",
|
||||
"Select level": "选择层级",
|
||||
"Province": "省",
|
||||
@ -436,8 +451,6 @@
|
||||
"Add condition group": "添加条件分组",
|
||||
"exists": "存在",
|
||||
"not exists": "不存在",
|
||||
"is current logged-in user": "为当前登录用户",
|
||||
"is not current logged-in user": "不为当前登录用户",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
@ -531,6 +544,8 @@
|
||||
"Current user": "当前用户",
|
||||
"Current record": "当前记录",
|
||||
"Current time": "当前时间",
|
||||
"System variables": "系统变量",
|
||||
"Date Variables": "日期变量",
|
||||
"Popup close method": "弹窗关闭方式",
|
||||
"Automatic close": "自动关闭",
|
||||
"Manually close": "手动关闭",
|
||||
|
@ -0,0 +1,58 @@
|
||||
import Migration from '../../migrations/20230330214649-filter-form-block';
|
||||
|
||||
import { Database } from '@nocobase/database';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import PluginUiSchema, { UiSchemaRepository } from '../..';
|
||||
|
||||
describe('migration-20230330214649-filter-form-block', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
let uiSchemaRepository: UiSchemaRepository;
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
await db.clean({ drop: true });
|
||||
|
||||
app.plugin(PluginUiSchema, { name: 'ui-schema-storage' });
|
||||
|
||||
await app.loadAndInstall();
|
||||
uiSchemaRepository = db.getCollection('uiSchemas').repository as UiSchemaRepository;
|
||||
});
|
||||
|
||||
test('update x-decorator', async () => {
|
||||
await uiSchemaRepository.create({
|
||||
values: {
|
||||
'x-uid': '78bijc1kw1q',
|
||||
name: 'xbixv9hl42i',
|
||||
schema: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': { resource: 'tt_org', collection: 'tt_org' },
|
||||
'x-designer': 'FormV2.FilterDesigner',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-filter-operators': {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const migration = new Migration({ db: app.db } as any);
|
||||
migration.context.app = {
|
||||
version: {
|
||||
satisfies: async (v) => true,
|
||||
},
|
||||
};
|
||||
await migration.up();
|
||||
const instance = await uiSchemaRepository.findById('78bijc1kw1q');
|
||||
expect(instance.schema['x-decorator']).toBe('FilterFormBlockProvider');
|
||||
});
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { Migration } from '@nocobase/server';
|
||||
|
||||
export default class extends Migration {
|
||||
async up() {
|
||||
const result = await this.app.version.satisfies('<0.9.1-alpha.3');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const r = this.db.getRepository('uiSchemas');
|
||||
const items = await r.find({
|
||||
filter: {
|
||||
'schema.x-designer': 'FormV2.FilterDesigner',
|
||||
},
|
||||
});
|
||||
await this.db.sequelize.transaction(async (transaction) => {
|
||||
for (const item of items) {
|
||||
const schema = item.schema;
|
||||
const decorator = schema['x-decorator'];
|
||||
schema['x-decorator'] = 'FilterFormBlockProvider';
|
||||
item.set('schema', schema);
|
||||
console.log(item['x-uid'], decorator, schema['x-decorator']);
|
||||
await item.save({ transaction });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { MagicAttributeModel } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { resolve } from 'path';
|
||||
import path, { resolve } from 'path';
|
||||
import { uiSchemaActions } from './actions/ui-schema-action';
|
||||
import { UiSchemaModel } from './model';
|
||||
import UiSchemaRepository from './repository';
|
||||
@ -89,6 +89,14 @@ export class UiSchemaStoragePlugin extends Plugin {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.db.addMigrations({
|
||||
namespace: 'collection-manager',
|
||||
directory: path.resolve(__dirname, './migrations'),
|
||||
context: {
|
||||
plugin: this,
|
||||
},
|
||||
});
|
||||
|
||||
await this.importCollections(resolve(__dirname, 'collections'));
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import parse from 'json-templates';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { Collection, Op } from '@nocobase/database';
|
||||
import { HandlerType } from '@nocobase/resourcer';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
import parse from 'json-templates';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { namespace } from './';
|
||||
import * as actions from './actions/users';
|
||||
|
@ -60,6 +60,7 @@ export default {
|
||||
'Arithmetic calculation': '算术运算',
|
||||
'String operation': '字符串',
|
||||
'System variables': '系统变量',
|
||||
'Date variables': '日期变量',
|
||||
'Current time': '当前时间',
|
||||
|
||||
'Executed at': '执行于',
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { useCollectionManager, useCompile } from "@nocobase/client";
|
||||
|
||||
import { instructions, useAvailableUpstreams, useNodeContext } from "./nodes";
|
||||
import { useFlowContext } from "./FlowContext";
|
||||
import { triggers } from "./triggers";
|
||||
import { NAMESPACE } from "./locale";
|
||||
import { useCollectionManager, useCompile } from '@nocobase/client';
|
||||
import { useFlowContext } from './FlowContext';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { instructions, useAvailableUpstreams, useNodeContext } from './nodes';
|
||||
import { triggers } from './triggers';
|
||||
|
||||
export type VariableOption = {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
children?: VariableOption[]
|
||||
children?: VariableOption[];
|
||||
};
|
||||
|
||||
const VariableTypes = [
|
||||
@ -51,18 +50,18 @@ const VariableTypes = [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("Current time", { ns: "${NAMESPACE}" })}}`,
|
||||
}
|
||||
]
|
||||
}
|
||||
label: `{{t("Now")}}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const TypeSets = {
|
||||
boolean: new Set(['boolean']),
|
||||
number: new Set(['integer', 'bigInt', 'float', 'double', 'real', 'decimal']),
|
||||
string: new Set(['string', 'text', 'password']),
|
||||
date: new Set(['date', 'time'])
|
||||
}
|
||||
date: new Set(['date', 'time']),
|
||||
};
|
||||
|
||||
function matchFieldType(field, type): Boolean {
|
||||
if (typeof type === 'string') {
|
||||
@ -70,17 +69,17 @@ function matchFieldType(field, type): Boolean {
|
||||
}
|
||||
|
||||
if (typeof type === 'object' && type.type === 'reference') {
|
||||
return (field.collectionName === type.options?.collection && field.name === 'id')
|
||||
|| (field.type === 'belongsTo' && field.target === type.options?.collection);
|
||||
return (
|
||||
(field.collectionName === type.options?.collection && field.name === 'id') ||
|
||||
(field.type === 'belongsTo' && field.target === type.options?.collection)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function filterTypedFields(fields, types) {
|
||||
return types
|
||||
? fields.filter(field => types.some(type => matchFieldType(field, type)))
|
||||
: fields;
|
||||
return types ? fields.filter((field) => types.some((type) => matchFieldType(field, type))) : fields;
|
||||
}
|
||||
|
||||
export function useWorkflowVariableOptions() {
|
||||
@ -92,7 +91,7 @@ export function useWorkflowVariableOptions() {
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(options),
|
||||
disabled: options && !options.length
|
||||
disabled: options && !options.length,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
@ -102,26 +101,28 @@ export function useCollectionFieldOptions(props) {
|
||||
const { fields, collection, types } = props;
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const result = filterTypedFields((fields ?? getCollectionFields(collection)), types)
|
||||
.map(field => ({
|
||||
label: compile(field.uiSchema?.title || field.name),
|
||||
key: field.name,
|
||||
value: field.name,
|
||||
children: ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
|
||||
? getCollectionFields(field.target)?.filter(subField => subField.interface && (!subField.target || subField.type === 'belongsTo'))
|
||||
.map(subField => subField.type === 'belongsTo'
|
||||
const result = filterTypedFields(fields ?? getCollectionFields(collection), types).map((field) => ({
|
||||
label: compile(field.uiSchema?.title || field.name),
|
||||
key: field.name,
|
||||
value: field.name,
|
||||
children: ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
|
||||
? getCollectionFields(field.target)
|
||||
?.filter((subField) => subField.interface && (!subField.target || subField.type === 'belongsTo'))
|
||||
.map((subField) =>
|
||||
subField.type === 'belongsTo'
|
||||
? {
|
||||
label: `${compile(subField.uiSchema?.title || subField.name)} ID`,
|
||||
key: subField.foreignKey,
|
||||
value: subField.foreignKey,
|
||||
}
|
||||
label: `${compile(subField.uiSchema?.title || subField.name)} ID`,
|
||||
key: subField.foreignKey,
|
||||
value: subField.foreignKey,
|
||||
}
|
||||
: {
|
||||
label: compile(subField.uiSchema?.title || subField.name),
|
||||
key: subField.name,
|
||||
value: subField.name,
|
||||
})
|
||||
: null
|
||||
}));
|
||||
label: compile(subField.uiSchema?.title || subField.name),
|
||||
key: subField.name,
|
||||
value: subField.name,
|
||||
},
|
||||
)
|
||||
: null,
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ export class ShopPlugin extends Plugin {
|
||||
},
|
||||
list: {
|
||||
filter: {
|
||||
// TODO: 该操作符已废弃,该处代码需要重构
|
||||
// 由 users 插件扩展的过滤器运算符
|
||||
$isCurrentUser: true,
|
||||
status: {
|
||||
|
Loading…
Reference in New Issue
Block a user