Merge branch 'main' into feat/gantt-block

This commit is contained in:
katherinehhh 2023-04-03 11:12:00 +08:00
commit 99b6b1c1be
213 changed files with 19130 additions and 1065 deletions

View File

@ -1,4 +1,4 @@
name: NocoBase Test
name: NocoBase Test Full
on:
push:
@ -6,16 +6,24 @@ on:
- main
- develop
paths:
- 'packages/**'
- 'packages/core/acl/**'
- 'packages/core/actions/**'
- 'packages/core/database/**'
- 'packages/core/server/**'
- 'packages/plugins/**/src/server/**'
pull_request:
paths:
- 'packages/**'
- 'packages/core/acl/**'
- 'packages/core/actions/**'
- 'packages/core/database/**'
- 'packages/core/server/**'
- 'packages/plugins/**/src/server/**'
jobs:
build-test:
strategy:
matrix:
node_version: ['16', '18']
node_version: ['18']
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
steps:

View File

@ -0,0 +1,82 @@
name: NocoBase Test Lite
on:
push:
branches:
- main
- develop
paths:
- 'packages/**'
- '!packages/core/acl/**'
- '!packages/core/actions/**'
- '!packages/core/database/**'
- '!packages/core/server/**'
- '!packages/plugins/**/src/server/**'
pull_request:
paths:
- 'packages/**'
- '!packages/core/acl/**'
- '!packages/core/actions/**'
- '!packages/core/database/**'
- '!packages/core/server/**'
- '!packages/plugins/**/src/server/**'
jobs:
build-test:
strategy:
matrix:
node_version: ['18']
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
- run: yarn install
- run: yarn build
postgres-test:
strategy:
matrix:
node_version: ['18']
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres:10
# Provide the password for postgres
env:
POSTGRES_USER: nocobase
POSTGRES_PASSWORD: password
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
- run: yarn install
# - run: yarn build
- name: Test with postgres
run: yarn nocobase install -f && yarn test
env:
DB_DIALECT: postgres
DB_HOST: postgres
DB_PORT: 5432
DB_USER: nocobase
DB_PASSWORD: password
DB_DATABASE: nocobase
DB_UNDERSCORED: true
DB_SCHEMA: nocobase
COLLECTION_MANAGER_SCHEMA: user_schema

View File

@ -1,32 +0,0 @@
name: Uninstall apps
on:
pull_request:
types:
- closed
branches:
- '**'
jobs:
down:
runs-on: ubuntu-latest
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
nocobase/nocobase
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Down ${{ steps.meta.outputs.tags }}
env:
IMAGE_TAG: ${{ steps.meta.outputs.tags }}
run: |
echo $IMAGE_TAG
export APP_NAME=$(echo $IMAGE_TAG | cut -d ":" -f 2)
echo $APP_NAME
curl --retry 2 --location --request DELETE "${{secrets.NOCOBASE_DEPLOY_HOST}}$APP_NAME"

View File

@ -25,13 +25,15 @@ services:
networks:
- nocobase
postgres:
image: postgres:10
image: postgres:latest
restart: always
networks:
- nocobase
command: postgres -c wal_level=logical
ports:
- "${DB_POSTGRES_PORT}:5432"
volumes:
- ./storage/db/postgres/backups:/backups
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_DATABASE}

View File

@ -176,16 +176,23 @@ app.resourcer.registerActionHandlers({
## Video
### Static data
https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4" type="video/mp4">
</video>
### Dynamic data
https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4" type="video/mp4">
</video>
### More charts
Theoretically supports all charts on [https://g2plot.antv.vision/en/examples](https://g2plot.antv.vision/en/examples)
https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4" type="video/mp4">
</video>
## JS Expressions
@ -197,5 +204,7 @@ Syntax
}
```
https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4" type="video/mp4">
</video>

View File

@ -176,16 +176,22 @@ app.resourcer.registerActionHandlers({
## Video
### Static data
https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4" type="video/mp4">
</video>
### Dynamic data
https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4" type="video/mp4">
</video>
### More charts
Theoretically supports all charts on [https://g2plot.antv.vision/en/examples](https://g2plot.antv.vision/en/examples)
https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4" type="video/mp4">
</video>
## JS Expressions
@ -197,5 +203,7 @@ Syntax
}
```
https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4" type="video/mp4">
</video>

View File

@ -177,18 +177,24 @@ app.resourcer.registerActionHandlers({
### 静态数据
https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877269-1c56562b-167a-4808-ada3-578f0872bce1.mp4" type="video/mp4">
</video>
### 动态数据
https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877336-6bd85f0b-17c5-40a5-9442-8045717cc7b0.mp4" type="video/mp4">
</video>
### 更多图表
理论上支持 https://g2plot.antv.vision/en/examples 上的所有图表
https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877347-7fc2544c-b938-4e34-8a83-721b3f62525e.mp4" type="video/mp4">
</video>
## JS 表达式
@ -200,5 +206,7 @@ Syntax
}
```
https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4
<video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4" type="video/mp4">
</video>

View File

@ -25,7 +25,7 @@ export default defineConfig({
resolveNocobasePackagesAlias(memo);
// 在引入 mermaid 之后,运行 yarn dev 的时候会报错,添加下面的代码可以解决。
memo.module.rule('js-in-node_modules').test(/.*mermaid.*\.js$/).include.clear();
memo.module.rule('js-in-node_modules').test(/(htmlparser2|(.*mermaid.*\.js$))/).include.clear();
return memo;
},
});

View File

@ -9,6 +9,7 @@ describe('create action', () => {
beforeEach(async () => {
app = mockServer();
await app.db.clean({ drop: true });
registerActions(app);
Post = app.collection({

View File

@ -13,6 +13,8 @@ describe('remove action', () => {
app = mockServer();
registerActions(app);
await app.db.clean({ drop: true });
PostTag = app.collection({
name: 'posts_tags',
fields: [{ type: 'string', name: 'tagged_at' }],

View File

@ -10,6 +10,7 @@ describe('set action', () => {
beforeEach(async () => {
app = mockServer();
await app.db.clean({ drop: true });
registerActions(app);
PostTag = app.collection({

View File

@ -10,6 +10,7 @@ describe('toggle action', () => {
beforeEach(async () => {
app = mockServer();
await app.db.clean({ drop: true });
registerActions(app);
PostTag = app.collection({

View File

@ -31,7 +31,6 @@
"mathjs": "^10.6.0",
"react-beautiful-dnd": "^13.1.0",
"react-big-calendar": "^0.38.7",
"react-contenteditable": "^3.3.6",
"react-drag-listview": "^0.1.9",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^3.4.7",
@ -42,6 +41,7 @@
"react-quill": "^1.3.5",
"react-router-dom": "^5.2.0",
"react-to-print": "^2.14.7",
"sanitize-html": "2.10.0",
"solarlunar-es": "^1.0.9",
"use-deep-compare-effect": "^1.8.1"
},

View File

@ -292,9 +292,27 @@ export const useSourceIdFromParentRecord = () => {
export const useParamsFromRecord = () => {
const filterByTk = useFilterByTk();
return {
const record = useRecord();
const { fields } = useCollection();
const filterFields = fields
.filter((v) => {
return ['boolean', 'date', 'integer', 'radio', 'sort', 'string', 'time', 'uid', 'uuid'].includes(v.type);
})
.map((v) => v.name);
const filter = Object.keys(record)
.filter((key) => filterFields.includes(key))
.reduce((result, key) => {
result[key] = record[key];
return result;
}, {});
const obj = {
filterByTk: filterByTk,
};
if (!filterByTk) {
obj['filter'] = filter;
}
return obj;
};
export const RecordLink = (props) => {

View File

@ -3,6 +3,7 @@ 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 * as bp from './hooks';
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
@ -22,6 +23,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
TableBlockProvider,
TableSelectorProvider,
FormBlockProvider,
FilterFormBlockProvider,
FormFieldProvider,
DetailsBlockProvider,
KanbanBlockProvider,

View File

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

View File

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

View File

@ -1,8 +1,10 @@
export * from './BlockProvider';
export * from './BlockSchemaComponentProvider';
export * from './CalendarBlockProvider';
export * from './FilterFormBlockProvider';
export * from './FormBlockProvider';
export * from './KanbanBlockProvider';
export * from './SharedFilterProvider';
export * from './TableBlockProvider';
export * from './TableFieldProvider';
export * from './TableSelectorProvider';

View File

@ -13,7 +13,7 @@ const InternalField: React.FC = (props) => {
const fieldSchema = useFieldSchema();
const { name, interface: interfaceType, uiSchema, defaultValue } = useCollectionField();
const collectionField = useCollectionField();
const component = useComponent(uiSchema?.['x-component']);
const component = useComponent(uiSchema?.['x-component'] || 'Input');
const compile = useCompile();
const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
@ -73,7 +73,6 @@ const InternalField: React.FC = (props) => {
if (!uiSchema) {
return null;
}
return React.createElement(component, props, props.children);
};
@ -107,7 +106,6 @@ export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
const field = fieldSchema?.['x-component-props']?.['field'];
const { snapshot } = useActionContext();
return (
<CollectionFieldProvider
name={fieldSchema.name}

View File

@ -26,6 +26,8 @@ import {
ConfigurationTabs,
EditCategory,
EditCategoryAction,
SyncFieldsAction,
SyncFieldsActionCom
} from './Configuration';
import { CollectionCategroriesProvider } from './CollectionManagerProvider';
@ -81,6 +83,8 @@ export const CollectionManagerPane = () => {
ViewFieldAction,
EditCategory,
EditCategoryAction,
SyncFieldsAction,
SyncFieldsActionCom
}}
/>
// </Card>

View File

@ -28,8 +28,9 @@ const getSchema = (schema, category, compile): ISchema => {
properties['defaultValue']['x-decorator'] = 'FormItem';
}
const initialValue: any = {
name: `t_${uid()}`,
name: schema.name !== 'view' ? `t_${uid()}` : null,
template: schema.name,
view: schema.name === 'view',
category,
...cloneDeep(schema.default),
};
@ -201,7 +202,7 @@ const useCreateCollection = (schema?: any) => {
if (schema?.events?.beforeSubmit) {
schema.events.beforeSubmit(values);
}
const fields = useDefaultCollectionFields(values);
const fields = values?.template !== 'view' ? useDefaultCollectionFields(values) : values.fields;
if (values.autoCreateReverseField) {
} else {
delete values.reverseField;
@ -235,8 +236,15 @@ export const AddCollectionAction = (props) => {
const [schema, setSchema] = useState({});
const compile = useCompile();
const { t } = useTranslation();
const items = templateOptions().map((option) => {
return { label: compile(option.title), key: option.name };
const collectionTemplates = templateOptions();
const items = [];
collectionTemplates.forEach((item) => {
if (item.divider) {
items.push({
type: 'divider',
});
}
items.push({ label: compile(item.title), key: item.name });
});
const {
state: { category },

View File

@ -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()}`,
@ -194,6 +212,7 @@ export const AddFieldAction = (props) => {
return optionArr;
};
return (
record.template !== 'view' && (
<RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown
@ -261,5 +280,6 @@ export const AddFieldAction = (props) => {
/>
</ActionContext.Provider>
</RecordProvider>
)
);
};

View File

@ -14,6 +14,7 @@ import { AddSubFieldAction } from './AddSubFieldAction';
import { FieldSummary } from './components/FieldSummary';
import { EditSubFieldAction } from './EditSubFieldAction';
import { collectionSchema } from './schemas/collections';
import { useAPIClient } from '../../api-client';
const useAsyncDataSource = (service: any) => {
return (field: any, options?: any) => {
@ -77,20 +78,29 @@ export const ConfigurationTable = () => {
const {
data: { database },
} = useCurrentAppInfo();
const data = useContext(CollectionCategroriesContext);
const api = useAPIClient();
const resource = api.resource('dbViews');
const collectonsRef: any = useRef();
collectonsRef.current = collections;
const compile = useCompile();
const loadCollections = async (field, options) => {
const { targetScope } = options;
return collectonsRef.current
?.filter((item) => !(item.autoCreate && item.isThrough))
.filter((item) =>
targetScope
? targetScope['template']?.includes(item.template) || targetScope[field.props.name]?.includes(item.name)
: true,
)
.map((item: any) => ({
const isFieldInherits = field.props?.name === 'inherits';
const filteredItems = collectonsRef.current.filter((item) => {
const isAutoCreateAndThrough = item.autoCreate && item.isThrough;
if (isAutoCreateAndThrough) {
return false;
}
if (isFieldInherits && item.template === 'view') {
return false;
}
const templateIncluded = !targetScope?.template || targetScope.template.includes(item.template);
const nameIncluded = !targetScope?.[field.props?.name] || targetScope[field.props.name].includes(item.name);
return templateIncluded && nameIncluded;
});
return filteredItems.map((item) => ({
label: compile(item.title),
value: item.name,
}));
@ -101,6 +111,18 @@ export const ConfigurationTable = () => {
value: item.id,
}));
};
const loadDBViews = async () => {
return resource.list().then(({ data }) => {
return data?.data?.map((item: any) => {
const schema = item.schema;
return {
label: schema ? `${schema}.${compile(item.name)}` : item.name,
value: schema?`${schema}_${item.name}`:item.name
};
});
});
};
const ctx = useContext(SchemaComponentContext);
return (
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
@ -119,11 +141,13 @@ export const ConfigurationTable = () => {
useAsyncDataSource,
loadCollections,
loadCategories,
loadDBViews,
useCurrentFields,
useNewId,
useCancelAction,
interfaces,
enableInherits: database?.dialect === 'postgres',
isPG:database?.dialect === 'postgres',
}}
/>
</SchemaComponentContext.Provider>

View File

@ -19,12 +19,32 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
return;
}
const properties = cloneDeep(schema.properties) as any;
if (properties?.name) {
properties.name['x-disabled'] = true;
}
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
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 +135,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({});
@ -138,7 +158,7 @@ export const EditFieldAction = (props) => {
const defaultValues: any = cloneDeep(data?.data) || {};
if (!defaultValues?.reverseField) {
defaultValues.autoCreateReverseField = false;
defaultValues.reverseField = interfaceConf.default?.reverseField;
defaultValues.reverseField = interfaceConf?.default?.reverseField;
set(defaultValues.reverseField, 'name', `f_${uid()}`);
set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
}
@ -155,7 +175,7 @@ export const EditFieldAction = (props) => {
setVisible(true);
}}
>
{children||t('Edit')}
{children || t('Edit')}
</a>
<SchemaComponent
schema={schema}

View File

@ -0,0 +1,194 @@
import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd';
import { useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { Button } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useCancelAction } from '../action-hooks';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components';
import { useAPIClient } from '../../api-client';
import { PreviewFields } from '../templates/components/PreviewFields';
import { PreviewTable } from '../templates/components/PreviewTable';
const getSchema = (schema: IField, record: any, compile) => {
if (!schema) {
return;
}
const properties = cloneDeep(schema.properties) as any;
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
}
const initialValue: any = {
name: `f_${uid()}`,
...cloneDeep(schema.default),
interface: schema.name,
};
if (initialValue.reverseField) {
initialValue.reverseField.name = `f_${uid()}`;
}
// initialValue.uiSchema.title = schema.title;
return {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
getContainer: '{{ getContainer }}',
},
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
return useRequest(
() =>
Promise.resolve({
data: initialValue,
}),
options,
);
},
},
title: `${compile('{{ t("Sync from database") }}')}`,
properties: {
schema: {
type: 'string',
'x-hidden': true,
default: record?.schema,
},
viewName: {
type: 'string',
'x-hidden': true,
default: record?.viewName,
},
fields: {
type: 'array',
'x-component': PreviewFields,
'x-component-props': {
...record,
},
default: record.fields,
},
preview: {
type: 'object',
'x-component': PreviewTable,
'x-component-props': {
...record,
},
'x-reactions': {
dependencies: ['fields'],
fulfill: {
schema: {
'x-component-props': '{{{...record,...$form.values}}}', //任意层次属性都支持表达式
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useSyncFromDatabase }}',
},
},
},
},
},
},
},
};
};
const useSyncFromDatabase = () => {
const form = useForm();
const { refreshCM } = useCollectionManager();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
const { targetKey } = useResourceContext();
const { [targetKey]: filterByTk } = useRecord();
const api = useAPIClient();
return {
async run() {
await form.submit();
await api.resource(`collections`).setFields({
filterByTk,
values: form.values,
});
ctx.setVisible(false);
await form.reset();
refresh();
await refreshCM();
},
};
};
export const SyncFieldsAction = (props) => {
const record = useRecord();
return <SyncFieldsActionCom item={record} {...props} />;
};
export const SyncFieldsActionCom = (props) => {
const { scope, getContainer, item: record, children } = props;
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
const compile = useCompile();
const { t } = useTranslation();
return (
record.template === 'view' && (
<RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}>
{children || (
<Button
icon={<PlusOutlined />}
onClick={(e) => {
const schema = getSchema({}, record, compile);
if (schema) {
setSchema(schema);
setVisible(true);
}
}}
>
{t('Sync from database')}
</Button>
)}
<SchemaComponent
schema={schema}
components={{ ...components, ArrayTable }}
scope={{
getContainer,
useCancelAction,
createOnly: true,
isOverride: false,
useSyncFromDatabase,
record,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
)
);
};

View File

@ -13,7 +13,8 @@ export * from './AddCollectionAction';
export * from './EditCollectionAction';
export * from './ConfigurationTabs';
export * from './AddCategoryAction';
export * from './EditCategoryAction'
export * from './EditCategoryAction';
export * from './SyncFieldsAction';
registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,

View File

@ -73,7 +73,7 @@ export const collectionFieldSchema: ISchema = {
params: {
paginate: false,
filter: {
'interface.$not': null,
$or: [{ 'interface.$not': null }, { 'options.source.$notEmpty': true }],
},
sort: ['sort'],
// appends: ['uiSchema'],
@ -106,6 +106,14 @@ export const collectionFieldSchema: ISchema = {
},
},
},
syncfromDatabase: {
type: 'void',
title: '{{ t("Sync from database") }}',
'x-component': 'SyncFieldsAction',
'x-component-props': {
type: 'primary',
},
},
create: {
type: 'void',
title: '{{ t("Add new") }}',

View File

@ -258,7 +258,8 @@ export const collectionTableSchema: ISchema = {
},
'x-reactions': (field) => {
const i = field.path.segments[1];
const table = field.form.getValuesIn(`table.${i}`);
const key = field.path.segments[0];
const table = field.form.getValuesIn(`${key}.${i}`);
if (table) {
field.title = `${compile(table.title)} - ${compile('{{ t("Configure fields") }}')}`;
}

View File

@ -126,6 +126,7 @@ export const useCollectionFilterOptions = (collectionName: string) => {
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
interface: field.interface,
};
if (field.target && depth > 2) {
return;

View File

@ -62,7 +62,7 @@ export const useCollectionManager = () => {
return getParents(name);
};
const getChildrenCollections = (name) => {
const getChildrenCollections = (name, isSupportView = false) => {
const children = [];
const getChildren = (name) => {
const inheritCollections = collections.filter((v) => {
@ -73,6 +73,16 @@ export const useCollectionManager = () => {
children.push(v);
return getChildren(collectionKey);
});
if (isSupportView) {
const sourceCollections = collections.filter((v) => {
return v.sources?.length === 1 && v?.sources[0] === name;
});
sourceCollections.forEach((v) => {
const collectionKey = v.name;
children.push(v);
return getChildren(collectionKey);
});
}
return uniqBy(children, 'key');
};
return getChildren(name);

View File

@ -22,6 +22,7 @@ export const attachment: IField = {
},
},
},
availableTypes:['belongsToMany'],
schemaInitialize(schema: ISchema, { block }) {
if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {};

View File

@ -15,6 +15,7 @@ export const checkbox: IField = {
'x-component': 'Checkbox',
},
},
availableTypes: ['boolean'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -17,6 +17,7 @@ export const checkboxGroup: IField = {
'x-component': 'Checkbox.Group',
},
},
availableTypes:['array'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -34,6 +34,7 @@ export const chinaRegion: IField = {
},
},
},
availableTypes:['belongsToMany'],
initialize: (values: any) => {
if (!values.through) {
values.through = `t_${uid()}`;

View File

@ -20,6 +20,7 @@ export const createdAt: IField = {
'x-read-pretty': true,
},
},
availableTypes:['date'],
properties: {
...defaultProps,
...dateTimeProps,

View File

@ -28,6 +28,7 @@ export const createdBy: IField = {
'x-read-pretty': true,
},
},
availableTypes:['belongsTo'],
properties: {
...defaultProps,
},

View File

@ -20,6 +20,7 @@ export const datetime: IField = {
},
},
},
availableTypes:['date'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -18,6 +18,7 @@ export const email: IField = {
'x-validator': 'email',
},
},
availableTypes:['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -16,6 +16,7 @@ export const icon: IField = {
'x-component': 'IconPicker',
},
},
availableTypes:['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -21,6 +21,7 @@ export const id: IField = {
'x-read-pretty': true,
},
},
availableTypes:['bigInt','integer'],
properties: {
'uiSchema.title': {
type: 'string',

View File

@ -18,6 +18,7 @@ export const input: IField = {
'x-component': 'Input',
},
},
availableTypes:['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -29,6 +29,7 @@ export const integer: IField = {
'x-validator': 'integer',
},
},
availableTypes:['bigInt','integer'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -39,6 +39,7 @@ export const json: IField = {
default: null
},
},
availableTypes:['json','array'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -45,6 +45,7 @@ export const linkTo: IField = {
},
},
},
availableTypes:['belongsToMany'],
schemaInitialize(schema: ISchema, { readPretty, block }) {
if (block === 'Form') {
if (schema['x-component'] === 'AssociationSelect') {

View File

@ -51,6 +51,7 @@ export const m2m: IField = {
},
},
},
availableTypes:['belongsToMany'],
schemaInitialize(schema: ISchema, { readPretty, block }) {
if (block === 'Form') {
if (schema['x-component'] === 'AssociationSelect') {

View File

@ -50,6 +50,7 @@ export const m2o: IField = {
},
},
},
availableTypes:['belongsTo'],
schemaInitialize(schema: ISchema, { block, readPretty }) {
if (block === 'Form') {
if (schema['x-component'] === 'AssociationSelect') {

View File

@ -17,6 +17,7 @@ export const markdown: IField = {
'x-component': 'Markdown',
},
},
availableTypes:['text'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -21,6 +21,7 @@ export const multipleSelect: IField = {
enum: [],
},
},
availableTypes:['array'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -22,6 +22,7 @@ export const number: IField = {
},
},
},
availableTypes:['double'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -50,6 +50,7 @@ export const o2m: IField = {
},
},
},
availableTypes:['hasMany'],
schemaInitialize(schema: ISchema, { field, block, readPretty }) {
if (block === 'Form') {
if (schema['x-component'] === 'TableField') {

View File

@ -117,6 +117,7 @@ export const o2o: IField = {
},
},
},
availableTypes:['hasOne'],
schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
internalSchameInitialize(schema, { field, block, readPretty, action });
if (['Table', 'Kanban'].includes(block)) {

View File

@ -18,6 +18,7 @@ export const password: IField = {
'x-component': 'Password',
},
},
availableTypes:['password'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -13,7 +13,7 @@ registerValidateRules({
return {
type: 'error',
message: `${i18n.t('The field value cannot be greater than ')}${maxValue * 100}%`,
}
};
}
}
@ -22,7 +22,7 @@ registerValidateRules({
return {
type: 'error',
message: `${i18n.t('The field value cannot be less than ')}${minValue * 100}%`,
}
};
}
}
@ -36,12 +36,12 @@ registerValidateRules({
return {
type: 'error',
message: `${i18n.t('The field value is not an integer number')}`,
}
};
}
return true;
}
})
},
});
// registerValidateFormats({
// percentInteger: /^(\d+)(.\d{0,2})?$/,
@ -68,6 +68,7 @@ export const percent: IField = {
},
},
},
availableTypes: ['float'],
hasDefaultValue: true,
properties: {
...defaultProps,
@ -104,7 +105,9 @@ export const percent: IField = {
'x-reactions': `{{(field) => {
const targetValue = field.query('.minimum').value();
field.selfErrors =
!!targetValue && !!field.value && targetValue > field.value ? '${i18n.t('Maximum must greater than minimum')}' : ''
!!targetValue && !!field.value && targetValue > field.value ? '${i18n.t(
'Maximum must greater than minimum',
)}' : ''
}}}`,
},
minValue: {
@ -119,7 +122,9 @@ export const percent: IField = {
dependencies: ['.maximum'],
fulfill: {
state: {
selfErrors: `{{!!$deps[0] && !!$self.value && $deps[0] < $self.value ? '${i18n.t('Minimum must less than maximum')}' : ''}}`,
selfErrors: `{{!!$deps[0] && !!$self.value && $deps[0] < $self.value ? '${i18n.t(
'Minimum must less than maximum',
)}' : ''}}`,
},
},
},
@ -132,10 +137,12 @@ export const percent: IField = {
'x-component-props': {
allowClear: true,
},
enum: [{
enum: [
{
label: '{{ t("Integer") }}',
value: 'Integer',
}]
},
],
},
pattern: {
type: 'string',
@ -145,8 +152,8 @@ export const percent: IField = {
'x-component-props': {
prefix: '/',
suffix: '/',
}
},
},
};
}
},
};

View File

@ -21,6 +21,7 @@ export const phone: IField = {
// 'x-validator': 'phone',
},
},
availableTypes: ['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

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

View File

@ -17,6 +17,7 @@ export const radioGroup: IField = {
'x-component': 'Radio.Group',
},
},
availableTypes: ['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -18,6 +18,7 @@ export const richText: IField = {
'x-component': 'RichText',
},
},
availableTypes: ['text'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -18,6 +18,7 @@ export const select: IField = {
enum: [],
},
},
availableTypes: ['string'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -20,6 +20,7 @@ export const subTable: IField = {
'x-component-props': {},
},
},
availableTypes: ['hasMany'],
schemaInitialize(schema: ISchema, { field, readPretty }) {
const association = `${field.collectionName}.${field.name}`;
schema['type'] = 'void';

View File

@ -18,6 +18,7 @@ export const textarea: IField = {
'x-component': 'Input.TextArea',
},
},
availableTypes: ['text'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -16,6 +16,7 @@ export const time: IField = {
'x-component': 'TimePicker',
},
},
availableTypes: ['time'],
hasDefaultValue: true,
properties: {
...defaultProps,

View File

@ -20,6 +20,7 @@ export const updatedAt: IField = {
'x-read-pretty': true,
},
},
availableTypes: ['date'],
properties: {
...defaultProps,
...dateTimeProps,

View File

@ -27,6 +27,7 @@ export const updatedBy: IField = {
'x-read-pretty': true,
},
},
availableTypes: ['belongsTo'],
properties: {
...defaultProps,
},

View File

@ -0,0 +1,216 @@
import { Cascader } from '@formily/antd';
import { useField, useForm } from '@formily/react';
import { Input, Select, Spin, Table, Tag } from 'antd';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ResourceActionContext, useCompile } from '../../../';
import { useAPIClient } from '../../../api-client';
import { getOptions } from '../../Configuration/interfaces';
import { useCollectionManager } from '../../hooks/useCollectionManager';
const getInterfaceOptions = (data, type) => {
const interfaceOptions = [];
data.forEach((item) => {
const options = item.children.filter((h) => h?.availableTypes?.includes(type));
interfaceOptions.push({
label: item.label,
key: item.key,
children: options,
});
});
return interfaceOptions.filter((v) => v.children.length > 0);
};
const PreviewCom = (props) => {
const { name, sources, viewName, schema } = props;
const { data: fields } = useContext(ResourceActionContext);
const api = useAPIClient();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [sourceFields, setSourceFields] = useState([]);
const field: any = useField();
const form = useForm();
const { getCollection } = useCollectionManager();
const compile = useCompile();
const initOptions = getOptions().filter((v) => !['relation', 'systemInfo'].includes(v.key));
useEffect(() => {
const data = [];
sources.forEach((item) => {
const collection = getCollection(item);
const children = collection.fields?.map((v) => {
return { value: v.name, label: v.uiSchema?.title };
});
data.push({
value: item,
label: collection.title,
children,
});
});
setSourceFields(data);
}, [sources, name]);
useEffect(() => {
if (name) {
setLoading(true);
api
.resource(`dbViews`)
.get({ filterByTk: viewName, schema })
.then(({ data }) => {
if (data) {
setLoading(false);
setDataSource([]);
const fieldsData = Object.values(data?.data?.fields)?.map((v: any) => {
if (v.source) {
return v;
} else {
return fields?.data.find((h) => h.name === v.name) || v;
}
});
field.value = fieldsData;
setDataSource(fieldsData);
form.setValuesIn('sources', data.data?.sources);
}
});
}
}, [name]);
const handleFieldChange = (record, index) => {
dataSource.splice(index, 1, record);
setDataSource(dataSource);
field.value = dataSource.map((v) => {
return {
...v,
source: typeof v.source === 'string' ? v.source : v.source?.join('.'),
};
});
};
const columns = [
{
title: t('Field name'),
dataIndex: 'name',
key: 'name',
width: 130,
},
{
title: t('Field source'),
dataIndex: 'source',
key: 'source',
width: 200,
render: (text, record, index) => {
return (
<Cascader
defaultValue={typeof text === 'string' ? text?.split('.') : text}
allowClear
style={{ width: '100%' }}
options={compile(sourceFields)}
onChange={(value, selectedOptions) => {
handleFieldChange({ ...record, source: value }, index);
}}
placeholder={t('Select field source')}
/>
);
},
},
{
title: t('Field type'),
dataIndex: 'type',
width: 140,
key: 'type',
render: (text, _, index) => {
const item = dataSource[index];
return item?.source || !item?.possibleTypes ? (
<Tag>{text}</Tag>
) : (
<Select
defaultValue={text}
style={{ width: '100%' }}
options={
item?.possibleTypes.map((v) => {
return { label: v, value: v };
}) || []
}
onChange={(value) => handleFieldChange({ ...item, type: value }, index)}
/>
);
},
},
{
title: t('Field interface'),
dataIndex: 'interface',
key: 'interface',
width: 150,
render: (text, _, index) => {
const item = dataSource[index];
const data = getInterfaceOptions(initOptions, item.type);
return item.source ? (
text
) : (
<Select
defaultValue={text}
style={{ width: '100%' }}
onChange={(value) => handleFieldChange({ ...item, interface: value }, index)}
>
{data.map((group) => (
<Select.OptGroup key={group.key} label={compile(group.label)}>
{group.children.map((item) => (
<Select.Option key={item.value} value={item.value}>
{compile(item.label)}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
);
},
},
{
title: t('Field display name'),
dataIndex: 'title',
key: 'title',
width: 180,
render: (text, record, index) => {
const item = dataSource[index];
return item.source ? (
record?.uiSchema?.title
) : (
<Input
defaultValue={record?.uiSchema?.title}
onChange={(e) => handleFieldChange({ ...item, uiSchema: { title: e.target.value } }, index)}
/>
);
},
},
];
return (
<Spin spinning={loading}>
{dataSource.length > 0 && (
<>
<div className="ant-formily-item-label">
<div className="ant-formily-item-label-content">
<span>
<label>{t('Fields')}</label>
</span>
</div>
<span className="ant-formily-item-colon">:</span>
</div>
<Table
bordered
size={'middle'}
columns={columns}
dataSource={dataSource}
scroll={{ y: 300 }}
pagination={false}
rowClassName="editable-row"
key={name}
/>
</>
)}
</Spin>
);
};
function areEqual(prevProps, nextProps) {
return nextProps.name === prevProps.name && nextProps.sources === prevProps.sources;
}
export const PreviewFields = React.memo(PreviewCom, areEqual);

View File

@ -0,0 +1,105 @@
import { RecursionField, useForm } from '@formily/react';
import { Spin, Table } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { EllipsisWithTooltip, useCompile } from '../../../';
import { useAPIClient } from '../../../api-client';
import { useCollectionManager } from '../../hooks/useCollectionManager';
export const PreviewTable = (props) => {
const { name, viewName, schema, fields } = props;
const [previewColumns, setPreviewColumns] = useState([]);
const [previewData, setPreviewData] = useState([]);
const compile = useCompile();
const [loading, setLoading] = useState(false);
const { getCollection, getCollectionField, getInterface } = useCollectionManager();
const api = useAPIClient();
const { t } = useTranslation();
const form = useForm();
useEffect(() => {
if (name) {
getPreviewData();
}
}, [name]);
useEffect(() => {
const pColumns = formatPreviewColumns(fields);
setPreviewColumns(pColumns);
}, [form.values.fields]);
const getPreviewData = () => {
setLoading(true);
api
.resource(`dbViews`)
.query({ filterByTk: viewName, schema })
.then(({ data }) => {
if (data) {
setLoading(false);
setPreviewData(data?.data || []);
}
});
};
const formatPreviewColumns = (data) => {
return data
.filter((k) => k.source || k.interface)
?.map((item) => {
const fieldSource = typeof item?.source === 'string' ? item?.source?.split('.') : item?.source;
const sourceField = getCollection(fieldSource?.[0])?.fields.find((v) => v.name === fieldSource?.[1])?.uiSchema
?.title;
const target = sourceField || item?.uiSchema?.title || item.name;
const schema: any = item.source
? getCollectionField(typeof item.source === 'string' ? item.source : item.source.join('.'))?.uiSchema
: getInterface(item.interface)?.default?.uiSchema;
return {
title: compile(target),
dataIndex: item.name,
key: item.name,
width: 200,
render: (v, record, index) => {
const content = record[item.name];
const objSchema: any = {
type: 'object',
properties: {
[item.name]: { ...schema, default: content, 'x-read-pretty': true, title: null },
},
};
return (
<EllipsisWithTooltip ellipsis={true}>
<RecursionField schema={objSchema} name={index} onlyRenderProperties />
</EllipsisWithTooltip>
);
},
};
});
};
return (
<Spin spinning={loading}>
<div
style={{
marginBottom: 22,
}}
>
{previewColumns?.length > 0 && [
<div className="ant-formily-item-label" style={{ marginTop: 24 }}>
<div className="ant-formily-item-label-content">
<span>
<label>{t('Preview')}</label>
</span>
</div>
<span className="ant-formily-item-colon">:</span>
</div>,
<Table
size={'middle'}
pagination={false}
bordered
columns={previewColumns}
dataSource={previewData}
scroll={{ x: 1000, y: 300 }}
key={name}
/>,
]}
</div>
</Spin>
);
};

View File

@ -1,4 +1,4 @@
export * from './calendar';
export * from './general';
export * from './tree';
export * from './view';

View File

@ -14,6 +14,8 @@ export interface ICollectionTemplate {
configurableProperties?: Record<string, ISchema>;
/** 当前模板可用的字段类型 */
availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude;
/** 是否分割线 */
divider?: boolean;
}
interface AvailableFieldInterfacesInclude {
@ -24,7 +26,6 @@ interface AvailableFieldInterfacesExclude {
exclude?: any[];
}
interface CollectionOptions {
/**
* id

View File

@ -0,0 +1,106 @@
import { getConfigurableProperties } from './properties';
import { ICollectionTemplate } from './types';
import { PreviewFields } from './components/PreviewFields';
import { PreviewTable } from './components/PreviewTable';
export const view: ICollectionTemplate = {
name: 'view',
title: '{{t("Connect to database view")}}',
order: 4,
color: 'yellow',
default: {
fields: [],
},
divider: true,
configurableProperties: {
title: {
type: 'string',
title: '{{ t("Collection display name") }}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
title: '{{t("Connect to database view")}}',
type: 'single',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': ['{{useAsyncDataSource(loadDBViews)}}'],
'x-disabled': '{{ !createOnly }}',
},
schema: {
type: 'string',
'x-hidden': true,
'x-reactions': {
dependencies: ['name'],
when: "{{isPG}}",
fulfill: {
state: {
value: "{{$deps[0].split('_')?.[0]}}",
},
},
otherwise: {
state: {
value: null,
},
},
},
},
viewName: {
type: 'string',
'x-hidden': true,
'x-reactions': {
dependencies: ['name'],
when: "{{isPG}}",
fulfill: {
state: {
value: '{{$deps[0].match(/^([^_]+)_(.*)$/)?.[2]}}',
},
},
otherwise: {
state: {
value: '{{$deps[0]}}',
},
},
},
},
sources: {
type: 'array',
title: '{{ t("Source collections") }}',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-disabled': true,
},
fields: {
type: 'array',
'x-component': PreviewFields,
'x-reactions': {
dependencies: ['name'],
fulfill: {
schema: {
'x-component-props': '{{$form.values}}', //任意层次属性都支持表达式
},
},
},
},
preview: {
type: 'object',
'x-component': PreviewTable,
'x-reactions': {
dependencies: ['name','fields'],
fulfill: {
schema: {
'x-component-props': '{{$form.values}}', //任意层次属性都支持表达式
},
},
},
},
...getConfigurableProperties('category'),
},
};

View File

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

View File

@ -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",
@ -52,6 +72,7 @@ export default {
"Value":"Value",
"Disabled":"Disabled",
"Enabled":"Enabled",
"Empty":"Empty",
"Linkage rule":"Linkage rule",
"Linkage rules":"Linkage rules",
"Condition":"Condition",
@ -168,6 +189,10 @@ export default {
"Collection template": "Collection template",
"Calendar collection": "Calendar collection",
"General collection": "General collection",
"Connect to database view":"Connect to database view",
"Source collections":"Source collections",
"Field source":"Field source",
"Preview":"Preview",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
"Storage type": "Storage type",
"Edit": "Edit",
@ -269,7 +294,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",
@ -334,6 +358,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",
@ -478,8 +503,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",
"=": "=",
"≠": "≠",
">": ">",
@ -573,6 +596,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",

View File

@ -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": "アジェンダ",
@ -58,6 +78,7 @@ export default {
"Value":"フィールド値",
"Disabled":"無効化",
"Enabled":"有効化",
"Empty":"くうきち",
"Linkage rule":"連動規則",
"Linkage rules":"連動規則",
"Condition":"条件#ジョウケン#",
@ -159,6 +180,10 @@ export default {
"Collection template":"データテーブルテンプレート",
"Calendar collection":"カレンダデータテーブル",
"General collection":"一般データテーブル",
"Connect to database view":"ビューに接続",
"Source collections":"ソースデータセット",
"Field source":"ソースフィールド",
"Preview":"プレビュー",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "ランダムに生成され、変更可能です。 アルファベット、数字、アンダースコアをサポートし、アルファベットから始まる必要があります。",
"Storage type": "ストレージタイプ",
"Edit": "編集",
@ -234,7 +259,6 @@ export default {
"Comparision": "比較",
"is": "が同じである",
"is not": "が同じではない",
"is variable": "が変数である",
"contains": "を含む",
"does not contain": "を含まない",
"starts with": "で始まる",
@ -276,6 +300,7 @@ export default {
"is after": "より後",
"is on or after": "以降",
"is on or before": "以前",
"is between": "範囲",
"Upload": "アップロード",
"Select level": "レベルを選択",
"Province": "州",
@ -402,7 +427,6 @@ export default {
"Add condition group": "条件グループの追加",
"exists": "が存在する",
"not exists": "が存在しない",
"is current logged-in user": "が現在ログインしているユーザー",
"=": "=",
"≠": "≠",
">": ">",

View File

@ -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": "текущий пользователь",
"=": "=",
"≠": "≠",
">": ">",

View File

@ -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ı",
"=": "=",
"≠": "≠",
">": ">",

View File

@ -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": "列表",
@ -50,6 +70,7 @@ export default {
"Value":"字段值",
"Disabled":"禁用",
"Enabled":"启用",
"Empty":"赋空值",
"Linkage rule":"联动规则",
"Linkage rules":"联动规则",
"Condition":"条件",
@ -176,6 +197,10 @@ export default {
"Collection template": "数据表模板",
"Calendar collection": "日历数据表",
"General collection": "普通数据表",
"Connect to database view":"连接数据库视图",
"Source collections":"来源数据表",
"Field source":"来源字段",
"Preview":"预览",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Storage type": "存储类型",
"Types will be used in database": "数据库使用的类型",
@ -285,7 +310,6 @@ export default {
"Comparision": "值比较",
"is": "等于",
"is not": "不等于",
"is variable": "为动态变量",
"contains": "包含",
"does not contain": "不包含",
"starts with": "开头是",
@ -352,6 +376,7 @@ export default {
"is after": "晚于",
"is on or after": "不早于",
"is on or before": "不晚于",
"is between": "介于",
"Upload": "上传",
@ -512,8 +537,6 @@ export default {
'Add condition group': '添加条件分组',
'exists': '存在',
'not exists': '不存在',
'is current logged-in user': '为当前登录用户',
'is not current logged-in user': '不为当前登录用户',
'=': '=',
'≠': '≠',
'>': '>',
@ -612,6 +635,7 @@ export default {
'Current user': '当前用户',
'Current record': '当前记录',
'Current time': '当前时间',
'Now': '现在',
'Popup close method': '弹窗关闭方式',
'Automatic close': '自动关闭',
'Manually close': '手动关闭',
@ -695,6 +719,8 @@ export default {
'Column width': '列宽',
'Sortable': '可排序的',
'Constant': '常量',
'System variables': '系统变量',
'Date variables': '日期变量',
'Use variable': '使用变量',
'True': '真',
'False': '假',

View File

@ -2,13 +2,14 @@ import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Modal, Popover } from 'antd';
import classnames from 'classnames';
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useActionContext } from '../..';
import { useDesignable } from '../../';
import { Icon } from '../../../icon';
import { useRecord } from '../../../record-provider';
import { SortableItem } from '../../common';
import { useCompile, useComponent, useDesigner } from '../../hooks';
import { useProps } from '../../hooks/useProps';
import { useRecord } from '../../../record-provider';
import ActionContainer from './Action.Container';
import { ActionDesigner } from './Action.Designer';
import { ActionDrawer } from './Action.Drawer';
@ -18,7 +19,6 @@ import { ActionPage } from './Action.Page';
import { ActionContext } from './context';
import { useA } from './hooks';
import { ComposedAction } from './types';
import { useDesignable } from '../../';
import { linkageAction } from './utils';
export const actionDesignerCss = css`
@ -96,11 +96,13 @@ export const Action: ComposedAction = observer((props: any) => {
const disabled = form.disabled || field.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable, } = useDesignable();
const tarComponent=useComponent(component)||component;
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
useEffect(() => {
field.linkageProperty = {};
linkageRules.map((v) => {
linkageRules
.filter((k) => !k.disabled)
.map((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values);
});

View File

@ -13,6 +13,7 @@ export const ActionBar = observer((props: any) => {
const { designable } = useDesignable();
if (layout === 'one-column') {
return (
<DndContext>
<div style={{ display: 'flex', ...style }} {...others}>
{props.children && (
<div style={{ marginRight: 8 }}>
@ -25,6 +26,7 @@ export const ActionBar = observer((props: any) => {
)}
{render()}
</div>
</DndContext>
);
}
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;

View File

@ -1,4 +1,5 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -9,7 +10,6 @@ import {
} from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks';
import _ from 'lodash';
export const AssociationFilterItemDesigner = (props) => {
const fieldSchema = useFieldSchema();
@ -30,9 +30,10 @@ export const AssociationFilterItemDesigner = (props) => {
const targetFields = collectionField?.target ? getCollectionFields(collectionField?.target) : [];
const options = targetFields
.filter(
(field) => field?.interface && ['id', 'input', 'phone', 'email', 'integer', 'number'].includes(field?.interface),
)
// .filter(
// (field) => field?.interface && ['id', 'input', 'phone', 'email', 'integer', 'number'].includes(field?.interface),
// )
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,

View File

@ -6,7 +6,9 @@ import cls from 'classnames';
import React, { ChangeEvent, MouseEvent, useMemo, useState } from 'react';
import { SortableItem } from '../../common';
import { useCompile, useDesigner, useProps } from '../../hooks';
import { getLabelFormatValue, useLabelUiSchema } from '../record-picker';
import { AssociationFilter } from './AssociationFilter';
import { EllipsisWithTooltip } from '../input';
const { Panel } = Collapse;
@ -78,6 +80,7 @@ export const AssociationFilterItem = (props) => {
};
const title = fieldSchema.title ?? collectionField.uiSchema?.title;
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.title || 'label');
return (
<SortableItem
@ -216,12 +219,23 @@ export const AssociationFilterItem = (props) => {
<Tree
style={{ padding: '16px 0' }}
onExpand={onExpand}
rootClassName={css`
.ant-tree-node-content-wrapper {
overflow-x: hidden;
}
`}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={list}
onSelect={onSelect}
fieldNames={fieldNames}
titleRender={(node) => compile(node[labelKey])}
titleRender={(node) => {
return (
<EllipsisWithTooltip ellipsis>
{getLabelFormatValue(labelUiSchema, compile(node[labelKey]))}
</EllipsisWithTooltip>
);
}}
selectedKeys={selectedKeys}
blockNode
/>

View File

@ -26,6 +26,8 @@ export const AssociationFilter = (props) => {
'nb-block-item',
props.className,
css`
height: 100%;
overflow-y: auto;
position: relative;
&:hover {
> .general-schema-designer {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 的日期字段设置。

View File

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

View File

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

View File

@ -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}>
<DatePickerProvider value={{ utc: false }}>
<FilterContext.Provider
value={{ field, fieldSchema, dynamicComponent, options: options || field.dataSource || [], disabled: props.disabled }}
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>
);
});

View File

@ -7,7 +7,7 @@ export interface FilterContextProps {
fieldSchema?: Schema;
dynamicComponent?: any;
options?: any[];
disabled?: boolean
disabled?: boolean;
}
export const RemoveConditionContext = createContext(null);

View File

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

View File

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

View File

@ -69,7 +69,8 @@ const WithForm = (props) => {
const { form } = props;
const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext();
const linkageRules = getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'] || [];
const linkageRules =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
@ -100,7 +101,9 @@ const WithForm = (props) => {
};
});
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => {
field.linkageProperty = {};
field.linkageProperty = {
display: field.linkageProperty?.display,
};
});
}
});
@ -111,9 +114,11 @@ const WithForm = (props) => {
};
}, []);
useEffect(() => {
if (linkageRules.length > 0) {
const id = uid();
form.addEffects(id, () => {
const linkagefields = [];
const formGraph = form.getFormGraph();
form.addEffects(id, () => {
return linkageRules.map((v, index) => {
return v.actions?.map((h) => {
if (h.targetFields) {
@ -135,7 +140,10 @@ const WithForm = (props) => {
});
return () => {
form.removeEffects(id);
form.clearFormGraph();
form.setFormGraph(formGraph);
};
}
}, [linkageRules]);
return fieldSchema['x-decorator'] === 'Form' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
};

View File

@ -1,47 +1,13 @@
import { last, get } from 'lodash';
import * as functions from '@formulajs/formulajs';
import { last, cloneDeep } from 'lodash';
import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
function now() {
return new Date();
}
const fnNames = Object.keys(functions).filter((key) => key !== 'default');
const fns = fnNames.map((key) => functions[key]);
function formula(expression: string, scope = {}) {
try {
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
const result = fn(...fns, ...Object.values(scope));
if (typeof result === 'number') {
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return functions.ROUND(result, 9);
}
if (typeof result === 'function') {
return result();
}
return result;
} catch (error) {
return undefined;
}
}
export function evaluate(expression: string, scope = {}) {
const mergeScope = { ...scope, now };
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
const item: any = get(scope, v);
const key = v.replace(/\.(\d+)/g, '["$1"]');
return ` ${typeof item === 'function' ? item() : key} `;
});
return formula(exp, mergeScope);
}
import evaluators from '@nocobase/evaluators/client';
export const linkageMergeAction = ({ operator, value }, field, condition, values) => {
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern];
const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value];
const { evaluate } = evaluators.get('formula.js');
switch (operator) {
case ActionType.Required:
@ -91,17 +57,23 @@ export const linkageMergeAction = ({ operator, value }, field, condition, values
case ActionType.Value:
if (conditionAnalyse(condition, values)) {
if (value?.mode === 'express') {
const result = evaluate(value.result || value.value, values);
const scope = cloneDeep(values);
try {
const result = evaluate(value.result || value.value, { ...scope, now: () => new Date().toString() });
valueResult.push(result);
} else {
} catch (error) {
}
} else if (value?.mode === 'constant') {
valueResult.push(value?.value || value);
} else {
valueResult.push(null);
}
}
field.linkageProperty = {
...field.linkageProperty,
value: valueResult,
};
setTimeout(() => (field.value = last(valueResult) === undefined ? field.value : last(valueResult)));
field.value = last(valueResult) === undefined ? field.value : last(valueResult);
break;
default:
return null;

View File

@ -126,7 +126,7 @@ const ColDivider = (props) => {
className={cls(
'nb-col-divider',
css`
width: 24px;
width: var(--nb-spacing);
`,
)}
style={{ ...droppableStyle }}
@ -152,7 +152,7 @@ const ColDivider = (props) => {
background: rgba(241, 139, 98, 0.06) !important;
}
}
width: 24px;
width: var(--nb-spacing);
height: 100%;
position: absolute;
cursor: col-resize;
@ -219,10 +219,10 @@ const RowDivider = (props) => {
className={cls(
'nb-row-divider',
css`
height: 24px;
height: var(--nb-spacing);
width: 100%;
position: absolute;
margin-top: -24px;
margin-top: calc(-1 * var(--nb-spacing));
`,
)}
style={{
@ -370,7 +370,7 @@ Grid.Row = observer(() => {
className={cls(
'nb-grid-row',
css`
margin: 0 -24px;
margin: 0 calc(-1 * var(--nb-spacing));
display: flex;
position: relative;
/* z-index: 0; */
@ -419,7 +419,7 @@ Grid.Col = observer((props: any) => {
let width = '';
if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - 24px - 24px / ${cols.length})`;
width = `calc(${w}% - var(--nb-spacing) * 2 / ${cols.length})`;
}
const { setNodeRef } = useDroppable({
id: field.address.toString(),

Some files were not shown because too many files have changed in this diff Show More