mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 15:39:24 +08:00
Merge branch 'main' into feat/gantt-block
This commit is contained in:
commit
99b6b1c1be
@ -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:
|
82
.github/workflows/nocobase-test-lite.yml
vendored
Normal file
82
.github/workflows/nocobase-test-lite.yml
vendored
Normal 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
|
32
.github/workflows/uninstall-apps.yml
vendored
32
.github/workflows/uninstall-apps.yml
vendored
@ -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"
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ describe('create action', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
await app.db.clean({ drop: true });
|
||||
registerActions(app);
|
||||
|
||||
Post = app.collection({
|
||||
|
@ -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' }],
|
||||
|
@ -10,6 +10,7 @@ describe('set action', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
await app.db.clean({ drop: true });
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
|
@ -10,6 +10,7 @@ describe('toggle action', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
await app.db.clean({ drop: true });
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { DatePickerProvider } from '../schema-component';
|
||||
import { FormBlockProvider } from './FormBlockProvider';
|
||||
|
||||
export const FilterFormBlockProvider = (props) => {
|
||||
return (
|
||||
<DatePickerProvider value={{ utc: false }}>
|
||||
<FormBlockProvider {...props}></FormBlockProvider>
|
||||
</DatePickerProvider>
|
||||
);
|
||||
};
|
@ -94,7 +94,7 @@ export const useFormBlockProps = () => {
|
||||
const addChild = fieldSchema?.['x-component-props']?.addChild;
|
||||
useEffect(() => {
|
||||
if (addChild) {
|
||||
ctx.form.query('parent').take((field) => {
|
||||
ctx.form?.query('parent').take((field) => {
|
||||
field.disabled = true;
|
||||
field.value = new Proxy({ ...record }, {});
|
||||
});
|
||||
@ -102,7 +102,7 @@ export const useFormBlockProps = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
ctx.form.setInitialValues(ctx.service?.data?.data);
|
||||
ctx.form?.setInitialValues(ctx.service?.data?.data);
|
||||
}, []);
|
||||
return {
|
||||
form: ctx.form,
|
||||
|
@ -1,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';
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 },
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
};
|
@ -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_-]*$/,
|
||||
|
@ -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") }}',
|
||||
|
@ -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") }}')}`;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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'] || {};
|
||||
|
@ -15,6 +15,7 @@ export const checkbox: IField = {
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
availableTypes: ['boolean'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -17,6 +17,7 @@ export const checkboxGroup: IField = {
|
||||
'x-component': 'Checkbox.Group',
|
||||
},
|
||||
},
|
||||
availableTypes:['array'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -34,6 +34,7 @@ export const chinaRegion: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['belongsToMany'],
|
||||
initialize: (values: any) => {
|
||||
if (!values.through) {
|
||||
values.through = `t_${uid()}`;
|
||||
|
@ -20,6 +20,7 @@ export const createdAt: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes:['date'],
|
||||
properties: {
|
||||
...defaultProps,
|
||||
...dateTimeProps,
|
||||
|
@ -28,6 +28,7 @@ export const createdBy: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes:['belongsTo'],
|
||||
properties: {
|
||||
...defaultProps,
|
||||
},
|
||||
|
@ -20,6 +20,7 @@ export const datetime: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['date'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -18,6 +18,7 @@ export const email: IField = {
|
||||
'x-validator': 'email',
|
||||
},
|
||||
},
|
||||
availableTypes:['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -16,6 +16,7 @@ export const icon: IField = {
|
||||
'x-component': 'IconPicker',
|
||||
},
|
||||
},
|
||||
availableTypes:['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -21,6 +21,7 @@ export const id: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes:['bigInt','integer'],
|
||||
properties: {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
|
@ -18,6 +18,7 @@ export const input: IField = {
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
availableTypes:['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -29,6 +29,7 @@ export const integer: IField = {
|
||||
'x-validator': 'integer',
|
||||
},
|
||||
},
|
||||
availableTypes:['bigInt','integer'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -39,6 +39,7 @@ export const json: IField = {
|
||||
default: null
|
||||
},
|
||||
},
|
||||
availableTypes:['json','array'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -45,6 +45,7 @@ export const linkTo: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['belongsToMany'],
|
||||
schemaInitialize(schema: ISchema, { readPretty, block }) {
|
||||
if (block === 'Form') {
|
||||
if (schema['x-component'] === 'AssociationSelect') {
|
||||
|
@ -51,6 +51,7 @@ export const m2m: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['belongsToMany'],
|
||||
schemaInitialize(schema: ISchema, { readPretty, block }) {
|
||||
if (block === 'Form') {
|
||||
if (schema['x-component'] === 'AssociationSelect') {
|
||||
|
@ -50,6 +50,7 @@ export const m2o: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['belongsTo'],
|
||||
schemaInitialize(schema: ISchema, { block, readPretty }) {
|
||||
if (block === 'Form') {
|
||||
if (schema['x-component'] === 'AssociationSelect') {
|
||||
|
@ -17,6 +17,7 @@ export const markdown: IField = {
|
||||
'x-component': 'Markdown',
|
||||
},
|
||||
},
|
||||
availableTypes:['text'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -21,6 +21,7 @@ export const multipleSelect: IField = {
|
||||
enum: [],
|
||||
},
|
||||
},
|
||||
availableTypes:['array'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -22,6 +22,7 @@ export const number: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['double'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -50,6 +50,7 @@ export const o2m: IField = {
|
||||
},
|
||||
},
|
||||
},
|
||||
availableTypes:['hasMany'],
|
||||
schemaInitialize(schema: ISchema, { field, block, readPretty }) {
|
||||
if (block === 'Form') {
|
||||
if (schema['x-component'] === 'TableField') {
|
||||
|
@ -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)) {
|
||||
|
@ -18,6 +18,7 @@ export const password: IField = {
|
||||
'x-component': 'Password',
|
||||
},
|
||||
},
|
||||
availableTypes:['password'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -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: '/',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ export const phone: IField = {
|
||||
// 'x-validator': 'phone',
|
||||
},
|
||||
},
|
||||
availableTypes: ['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -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 },
|
||||
];
|
||||
|
@ -17,6 +17,7 @@ export const radioGroup: IField = {
|
||||
'x-component': 'Radio.Group',
|
||||
},
|
||||
},
|
||||
availableTypes: ['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -18,6 +18,7 @@ export const richText: IField = {
|
||||
'x-component': 'RichText',
|
||||
},
|
||||
},
|
||||
availableTypes: ['text'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -18,6 +18,7 @@ export const select: IField = {
|
||||
enum: [],
|
||||
},
|
||||
},
|
||||
availableTypes: ['string'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -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';
|
||||
|
@ -18,6 +18,7 @@ export const textarea: IField = {
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
},
|
||||
availableTypes: ['text'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -16,6 +16,7 @@ export const time: IField = {
|
||||
'x-component': 'TimePicker',
|
||||
},
|
||||
},
|
||||
availableTypes: ['time'],
|
||||
hasDefaultValue: true,
|
||||
properties: {
|
||||
...defaultProps,
|
||||
|
@ -20,6 +20,7 @@ export const updatedAt: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes: ['date'],
|
||||
properties: {
|
||||
...defaultProps,
|
||||
...dateTimeProps,
|
||||
|
@ -27,6 +27,7 @@ export const updatedBy: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes: ['belongsTo'],
|
||||
properties: {
|
||||
...defaultProps,
|
||||
},
|
||||
|
@ -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);
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export * from './calendar';
|
||||
export * from './general';
|
||||
export * from './tree';
|
||||
|
||||
export * from './view';
|
||||
|
@ -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
|
||||
|
106
packages/core/client/src/collection-manager/templates/view.tsx
Normal file
106
packages/core/client/src/collection-manager/templates/view.tsx
Normal 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'),
|
||||
},
|
||||
};
|
@ -57,7 +57,10 @@ export const FilterBlockRecord = ({
|
||||
const associatedFields = useAssociatedFields();
|
||||
const container = useRef(null);
|
||||
|
||||
const shouldApplyFilter = field.decoratorType !== 'FormBlockProvider' && field.decoratorProps.blockType !== 'filter';
|
||||
const shouldApplyFilter =
|
||||
field.decoratorType !== 'FilterFormBlockProvider' &&
|
||||
field.decoratorType !== 'FormBlockProvider' &&
|
||||
field.decoratorProps.blockType !== 'filter';
|
||||
|
||||
const addBlockToDataBlocks = () => {
|
||||
recordDataBlocks({
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} more items",
|
||||
"Total {{count}} items": "Total {{count}} items",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Tomorrow": "Tomorrow",
|
||||
"Month": "Month",
|
||||
"Week": "Week",
|
||||
"This week": "This week",
|
||||
"This month": "This month",
|
||||
"This year": "This year",
|
||||
"Next year": "Next year",
|
||||
"Last week": "Last week",
|
||||
"Next week": "Next week",
|
||||
"Last month": "Last month",
|
||||
"Next month": "Next month",
|
||||
"Last quarter": "Last quarter",
|
||||
"This quarter": "This quarter",
|
||||
"Next quarter": "Next quarter",
|
||||
"Last year": "Last year",
|
||||
"Last 7 days": "Last 7 days",
|
||||
"Last 30 days": "Last 30 days",
|
||||
"Last 90 days": "Last 90 days",
|
||||
"Next 7 days": "Next 7 days",
|
||||
"Next 30 days": "Next 30 days",
|
||||
"Next 90 days": "Next 90 days",
|
||||
"Work week": "Work week",
|
||||
"Day": "Day",
|
||||
"Agenda": "Agenda",
|
||||
@ -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",
|
||||
|
@ -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": "が現在ログインしているユーザー",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} больше элементов",
|
||||
"Total {{count}} items": "Всего {{count}} элементов",
|
||||
"Today": "Сегодня",
|
||||
"Yesterday": "Вчера",
|
||||
"Tomorrow": "Завтра",
|
||||
"Month": "Месяц",
|
||||
"Week": "Неделя",
|
||||
"This week": "Эта неделя",
|
||||
"Next week": "Следующая неделя",
|
||||
"This month": "Этот месяц",
|
||||
"Next month": "Следующий месяц",
|
||||
"Last quarter": "Прошлый квартал",
|
||||
"This quarter": "Этот квартал",
|
||||
"Next quarter": "Следующий квартал",
|
||||
"This year": "Этот год",
|
||||
"Next year": "Следующий год",
|
||||
"Last week": "Прошлая неделя",
|
||||
"Last month": "Прошлый месяц",
|
||||
"Last year": "Прошлый год",
|
||||
"Last 7 days": "Последние 7 дней",
|
||||
"Last 30 days": "Последние 30 дней",
|
||||
"Last 90 days": "Последние 90 дней",
|
||||
"Next 7 days": "Следующие 7 дней",
|
||||
"Next 30 days": "Следующие 30 дней",
|
||||
"Next 90 days": "Следующие 90 дней",
|
||||
"Work week": "Рабочая неделя",
|
||||
"Day": "День",
|
||||
"Agenda": "Расписание",
|
||||
@ -185,7 +205,6 @@ export default {
|
||||
"Comparision": "Сравнение",
|
||||
"is": "соответствует",
|
||||
"is not": "не соответствует",
|
||||
"is variable": "это переменная",
|
||||
"contains": "содержит",
|
||||
"does not contain": "не содержит",
|
||||
"starts with": "начинается с",
|
||||
@ -227,6 +246,7 @@ export default {
|
||||
"is after": "находится после",
|
||||
"is on or after": "находится на или после",
|
||||
"is on or before": "находится на или до",
|
||||
"is between": "находится в диапазоне",
|
||||
"Upload": "Закачать",
|
||||
"Select level": "Выберите уровень",
|
||||
"Province": "Область",
|
||||
@ -353,7 +373,6 @@ export default {
|
||||
"Add condition group": "Добавить группу правил",
|
||||
"exists": "существуют",
|
||||
"not exists": "не существуют",
|
||||
"is current logged-in user": "текущий пользователь",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "{{count}} öğe daha",
|
||||
"Total {{count}} items": "Toplam {{count}} adet öğe",
|
||||
"Today": "Bugün",
|
||||
"Yesterday": "Dün",
|
||||
"Tomorrow": "Yarın",
|
||||
"Month": "Ay",
|
||||
"Week": "Hafta",
|
||||
"This week": "Bu Hafta",
|
||||
"Next week": "Gelecek Hafta",
|
||||
"This month": "Bu Ay",
|
||||
"Next month": "Gelecek Ay",
|
||||
"Last quarter": "Geçen Çeyrek",
|
||||
"This quarter": "Bu Çeyrek",
|
||||
"Next quarter": "Gelecek Çeyrek",
|
||||
"This year": "Bu Yıl",
|
||||
"Next year": "Gelecek Yıl",
|
||||
"Last week": "Geçen Hafta",
|
||||
"Last month": "Geçen Ay",
|
||||
"Last year": "Geçen Yıl",
|
||||
"Last 7 days": "Son 7 Gün",
|
||||
"Last 30 days": "Son 30 Gün",
|
||||
"Last 90 days": "Son 90 Gün",
|
||||
"Next 7 days": "Sonraki 7 Gün",
|
||||
"Next 30 days": "Sonraki 30 Gün",
|
||||
"Next 90 days": "Sonraki 90 Gün",
|
||||
"Work week": "Çalışma Haftası",
|
||||
"Day": "Gün",
|
||||
"Agenda": "Ajanda",
|
||||
@ -184,7 +204,6 @@ export default {
|
||||
"Comparision": "Karşılaştırma",
|
||||
"is": "eşittir",
|
||||
"is not": "eşit değildir",
|
||||
"is variable": "is variable",
|
||||
"contains": "içerir",
|
||||
"does not contain": "içermez",
|
||||
"starts with": "ile başlar",
|
||||
@ -226,6 +245,7 @@ export default {
|
||||
"is after": "sonra",
|
||||
"is on or after": "açık veya sonra",
|
||||
"is on or before": "açık veya önce",
|
||||
"is between": "aralık",
|
||||
"Upload": "Yükle",
|
||||
"Select level": "Seviye seç",
|
||||
"Province": "Bölge",
|
||||
@ -352,7 +372,6 @@ export default {
|
||||
"Add condition group": "Koşul grubu ekle",
|
||||
"exists": "var olanlar",
|
||||
"not exists": "var olmayanlar",
|
||||
"is current logged-in user": "mevcut oturum açmış kullanıcı",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
|
@ -6,8 +6,28 @@ export default {
|
||||
"{{count}} more items": "还有 {{count}} 项",
|
||||
"Total {{count}} items": "总共 {{count}} 条",
|
||||
"Today": "今天",
|
||||
"Yesterday": "昨天",
|
||||
"Tomorrow": "明天",
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
"This week": "本周",
|
||||
"Next week": "下周",
|
||||
"This month": "本月",
|
||||
"Next month": "下月",
|
||||
"Last quarter": "上季度",
|
||||
"This quarter": "本季度",
|
||||
"Next quarter": "下季度",
|
||||
"This year": "今年",
|
||||
"Next year": "明年",
|
||||
"Last week": "上周",
|
||||
"Last month": "上月",
|
||||
"Last year": "去年",
|
||||
"Last 7 days": "最近 7 天",
|
||||
"Last 30 days": "最近 30 天",
|
||||
"Last 90 days": "最近 90 天",
|
||||
"Next 7 days": "未来 7 天",
|
||||
"Next 30 days": "未来 30 天",
|
||||
"Next 90 days": "未来 90 天",
|
||||
"Work week": "工作日",
|
||||
"Day": "天",
|
||||
"Agenda": "列表",
|
||||
@ -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': '假',
|
||||
|
@ -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 { 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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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 {
|
||||
|
@ -4,19 +4,21 @@ import { Field } from '@formily/core';
|
||||
import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext, useFilterByTk } from '../../../block-provider';
|
||||
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
|
||||
import {
|
||||
useCollectionManager,
|
||||
useCollection,
|
||||
useSortFields,
|
||||
useCollectionFilterOptions,
|
||||
useCollectionManager,
|
||||
useSortFields
|
||||
} from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useDesignable, useCompile, useFieldComponentOptions, useFieldTitle } from '../../hooks';
|
||||
import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
import { defaultFieldNames } from '../select';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import useServiceOptions from './useServiceOptions';
|
||||
|
||||
@ -102,7 +104,7 @@ AssociationSelect.Designer = () => {
|
||||
const compile = useCompile();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
||||
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||
const originalTitle = collectionField?.uiSchema?.title;
|
||||
@ -423,7 +425,7 @@ AssociationSelect.Designer = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form && !isSubFormAssocitionField && fieldComponentOptions && (
|
||||
{form && !isSubFormAssociationField && fieldComponentOptions && (
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Field component')}
|
||||
options={fieldComponentOptions}
|
||||
@ -483,12 +485,15 @@ AssociationSelect.Designer = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(field.componentProps, 'service.params.filter', filter);
|
||||
fieldSchema['x-component-props'] = field.componentProps;
|
||||
dn.emit('patch', {
|
||||
@ -692,13 +697,9 @@ AssociationSelect.FilterDesigner = () => {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const tk = useFilterByTk();
|
||||
const {} = useCollection();
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const { dn, refresh } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||
const originalTitle = collectionField?.uiSchema?.title;
|
||||
@ -1012,12 +1013,15 @@ AssociationSelect.FilterDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(field.componentProps, 'service.params.filter', filter);
|
||||
fieldSchema['x-component-props'] = field.componentProps;
|
||||
dn.emit('patch', {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FixedBlockDesignerItem, useCompile, useDesignable } from '../..';
|
||||
import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..';
|
||||
import { useCalendarBlockContext } from '../../../block-provider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const CalendarDesigner = () => {
|
||||
const field = useField();
|
||||
@ -115,7 +116,9 @@ export const CalendarDesigner = () => {
|
||||
default: defaultFilter,
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
@ -127,6 +130,7 @@ export const CalendarDesigner = () => {
|
||||
}
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -5,23 +5,68 @@ import type {
|
||||
RangePickerProps as AntdRangePickerProps
|
||||
} from 'antd/lib/date-picker';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import { mapDateFormat } from './util';
|
||||
import { getDateRanges, mapDatePicker, mapRangePicker } from './util';
|
||||
|
||||
interface IDatePickerProps {
|
||||
utc?: boolean;
|
||||
}
|
||||
|
||||
type ComposedDatePicker = React.FC<AntdDatePickerProps> & {
|
||||
RangePicker?: React.FC<AntdRangePickerProps>;
|
||||
};
|
||||
|
||||
export const DatePicker: ComposedDatePicker = connect(
|
||||
const DatePickerContext = React.createContext<IDatePickerProps>({ utc: true });
|
||||
|
||||
export const useDatePickerContext = () => React.useContext(DatePickerContext);
|
||||
export const DatePickerProvider = DatePickerContext.Provider;
|
||||
|
||||
const _DatePicker: ComposedDatePicker = connect(
|
||||
AntdDatePicker,
|
||||
mapProps(mapDateFormat()),
|
||||
mapProps(mapDatePicker()),
|
||||
mapReadPretty(ReadPretty.DatePicker),
|
||||
);
|
||||
|
||||
DatePicker.RangePicker = connect(
|
||||
const _RangePicker = connect(
|
||||
AntdDatePicker.RangePicker,
|
||||
mapProps(mapDateFormat()),
|
||||
mapProps(mapRangePicker()),
|
||||
mapReadPretty(ReadPretty.DateRangePicker),
|
||||
);
|
||||
|
||||
export const DatePicker = (props) => {
|
||||
const { utc = true } = useDatePickerContext();
|
||||
props = { utc, ...props };
|
||||
return <_DatePicker {...props} />;
|
||||
};
|
||||
|
||||
DatePicker.RangePicker = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { utc = true } = useDatePickerContext();
|
||||
const rangesValue = getDateRanges();
|
||||
const ranges = {
|
||||
[t('Today')]: rangesValue.today,
|
||||
[t('Last week')]: rangesValue.lastWeek,
|
||||
[t('This week')]: rangesValue.thisWeek,
|
||||
[t('Next week')]: rangesValue.nextWeek,
|
||||
[t('Last month')]: rangesValue.lastMonth,
|
||||
[t('This month')]: rangesValue.thisMonth,
|
||||
[t('Next month')]: rangesValue.nextMonth,
|
||||
[t('Last quarter')]: rangesValue.lastQuarter,
|
||||
[t('This quarter')]: rangesValue.thisQuarter,
|
||||
[t('Next quarter')]: rangesValue.nextQuarter,
|
||||
[t('Last year')]: rangesValue.lastYear,
|
||||
[t('This year')]: rangesValue.thisYear,
|
||||
[t('Next year')]: rangesValue.nextYear,
|
||||
[t('Last 7 days')]: rangesValue.last7Days,
|
||||
[t('Next 7 days')]: rangesValue.next7Days,
|
||||
[t('Last 30 days')]: rangesValue.last30Days,
|
||||
[t('Next 30 days')]: rangesValue.next30Days,
|
||||
[t('Last 90 days')]: rangesValue.last90Days,
|
||||
[t('Next 90 days')]: rangesValue.next90Days,
|
||||
};
|
||||
props = { utc, ranges, ...props };
|
||||
return <_RangePicker {...props} />;
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
|
@ -0,0 +1,120 @@
|
||||
import moment from 'moment';
|
||||
import { getDateRanges } from '../util';
|
||||
|
||||
describe('getDateRanges', () => {
|
||||
const dateRanges = getDateRanges();
|
||||
|
||||
it('today', () => {
|
||||
const [start, end] = dateRanges.today();
|
||||
expect(start.toISOString()).toBe(moment().startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('day').toISOString());
|
||||
});
|
||||
|
||||
it('lastWeek', () => {
|
||||
const [start, end] = dateRanges.lastWeek();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'week').startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'week').endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('thisWeek', () => {
|
||||
const [start, end] = dateRanges.thisWeek();
|
||||
expect(start.toISOString()).toBe(moment().startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('nextWeek', () => {
|
||||
const [start, end] = dateRanges.nextWeek();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'week').startOf('isoWeek').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'week').endOf('isoWeek').toISOString());
|
||||
});
|
||||
|
||||
it('lastMonth', () => {
|
||||
const [start, end] = dateRanges.lastMonth();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'month').startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'month').endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('thisMonth', () => {
|
||||
const [start, end] = dateRanges.thisMonth();
|
||||
expect(start.toISOString()).toBe(moment().startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('nextMonth', () => {
|
||||
const [start, end] = dateRanges.nextMonth();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'month').startOf('month').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'month').endOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('lastQuarter', () => {
|
||||
const [start, end] = dateRanges.lastQuarter();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'quarter').startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'quarter').endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('thisQuarter', () => {
|
||||
const [start, end] = dateRanges.thisQuarter();
|
||||
expect(start.toISOString()).toBe(moment().startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('nextQuarter', () => {
|
||||
const [start, end] = dateRanges.nextQuarter();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'quarter').startOf('quarter').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'quarter').endOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('lastYear', () => {
|
||||
const [start, end] = dateRanges.lastYear();
|
||||
expect(start.toISOString()).toBe(moment().add(-1, 'year').startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(-1, 'year').endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('thisYear', () => {
|
||||
const [start, end] = dateRanges.thisYear();
|
||||
expect(start.toISOString()).toBe(moment().startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('nextYear', () => {
|
||||
const [start, end] = dateRanges.nextYear();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'year').startOf('year').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(1, 'year').endOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('last7Days', () => {
|
||||
const [start, end] = dateRanges.last7Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-6, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next7Days', () => {
|
||||
const [start, end] = dateRanges.next7Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(7, 'days').endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('last30Days', () => {
|
||||
const [start, end] = dateRanges.last30Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-29, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next30Days', () => {
|
||||
const [start, end] = dateRanges.next30Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(30, 'days').endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('last90Days', () => {
|
||||
const [start, end] = dateRanges.last90Days();
|
||||
expect(start.toISOString()).toBe(moment().add(-89, 'days').startOf('days').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
|
||||
});
|
||||
|
||||
it('next90Days', () => {
|
||||
const [start, end] = dateRanges.next90Days();
|
||||
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
|
||||
expect(end.toISOString()).toBe(moment().add(90, 'days').endOf('days').toISOString());
|
||||
});
|
||||
});
|
@ -0,0 +1,220 @@
|
||||
import moment from 'moment';
|
||||
import { mapDatePicker } from '../util';
|
||||
|
||||
describe('mapDatePicker', () => {
|
||||
it('showTime is true and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T22:22:22.000Z',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 22:22:22');
|
||||
});
|
||||
|
||||
it('showTime is true and gmt is false', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T22:22:22.000Z',
|
||||
showTime: true,
|
||||
gmt: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe(
|
||||
moment('2022-02-22T22:22:22.000Z').format('YYYY-MM-DD HH:mm:ss'),
|
||||
);
|
||||
});
|
||||
|
||||
it('showTime is false and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-02-22T00:00:00.000Z',
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||
});
|
||||
|
||||
it('showTime is false and gmt is false', () => {
|
||||
const props = {
|
||||
value: '2022-02-22',
|
||||
showTime: false,
|
||||
gmt: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is true and gmt is true', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22 22:22:22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is true and gmt is false', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-22 22:22:22');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is false and gmt is true', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when showTime is false and gmt is false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-22');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'year',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-01-01T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'year',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is month and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'month',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is month and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'month',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is quarter and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'quarter',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is quarter and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'quarter',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-01 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is week and gmt is true', () => {
|
||||
const props = {
|
||||
picker: 'week',
|
||||
gmt: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment.utc('2022-02-21T00:00:00.000Z');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is week and gmt is false', () => {
|
||||
const props = {
|
||||
picker: 'week',
|
||||
gmt: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
const m = moment('2022-02-21 00:00:00');
|
||||
result.onChange(m);
|
||||
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when utc is false', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment('2022-02-22 22:22:22'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22');
|
||||
});
|
||||
|
||||
it('should call onChange with correct value when picker is year and utc is false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
result.onChange(moment('2022-01-01 23:00:00'));
|
||||
expect(props.onChange).toHaveBeenCalledWith('2022-01-01');
|
||||
});
|
||||
|
||||
it('utc is false and gmt is true', () => {
|
||||
const props = {
|
||||
value: '2022-01-01 23:00:00',
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
};
|
||||
const result = mapDatePicker()(props);
|
||||
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-01-01 23:00:00');
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import moment from 'moment';
|
||||
import { mapRangePicker } from '../util';
|
||||
|
||||
describe('mapRangePicker', () => {
|
||||
it('should work with showTime=false, gmt=true, utc=true', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T23:59:59.999Z']);
|
||||
});
|
||||
|
||||
it('should work with showTime=true, gmt=true, utc=true', () => {
|
||||
const props = {
|
||||
showTime: true,
|
||||
gmt: true,
|
||||
utc: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T00:00:00.000Z']);
|
||||
});
|
||||
|
||||
it('should work with showTime=false, gmt=true, utc=false', () => {
|
||||
const props = {
|
||||
showTime: false,
|
||||
gmt: true,
|
||||
utc: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { onChange } = mapRangePicker()(props);
|
||||
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
|
||||
onChange(value);
|
||||
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01', '2023-01-02']);
|
||||
});
|
||||
});
|
@ -44,33 +44,93 @@ describe('str2moment', () => {
|
||||
|
||||
describe('moment2str', () => {
|
||||
test('gmt date', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true, gmt: true });
|
||||
expect(str).toBe('2022-06-21T10:10:00.000Z');
|
||||
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||
});
|
||||
|
||||
test('gmt date only', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const str = moment2str(m);
|
||||
expect(str).toBe('2022-06-21T00:00:00.000Z');
|
||||
test('showTime is true, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true, gmt: false });
|
||||
expect(str).toBe(m.toISOString());
|
||||
});
|
||||
|
||||
test('gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { gmt: true });
|
||||
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||
});
|
||||
|
||||
test('gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { gmt: false });
|
||||
expect(str).toBe(moment('2023-06-21 10:10:00').toISOString());
|
||||
});
|
||||
|
||||
test('with time', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { showTime: true });
|
||||
expect(str).toBe(m.toISOString());
|
||||
});
|
||||
|
||||
test('picker is year, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year', gmt: false });
|
||||
expect(str).toBe(moment('2023-01-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is year, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year', gmt: true });
|
||||
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is year', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'year' });
|
||||
expect(str).toBe('2022-01-01T00:00:00.000Z');
|
||||
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is quarter, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'quarter', gmt: false });
|
||||
expect(str).toBe(moment('2023-04-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is quarter, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'quarter', gmt: true });
|
||||
expect(str).toBe('2023-04-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is month, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month', gmt: false });
|
||||
expect(str).toBe(moment('2023-06-01 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is month, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month', gmt: true });
|
||||
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is month', () => {
|
||||
const m = moment('2022-06-21 10:10:00');
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'month' });
|
||||
expect(str).toBe('2022-06-01T00:00:00.000Z');
|
||||
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('picker is week, gmt is false', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'week', gmt: false });
|
||||
expect(str).toBe(moment('2023-06-19 00:00:00').toISOString());
|
||||
});
|
||||
|
||||
test('picker is week, gmt is true', () => {
|
||||
const m = moment('2023-06-21 10:10:00');
|
||||
const str = moment2str(m, { picker: 'week', gmt: true });
|
||||
expect(str).toBe('2023-06-19T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('value is null', async () => {
|
||||
|
@ -2,7 +2,7 @@
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
@ -13,28 +13,52 @@ const schema = {
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-reactions': {
|
||||
target: 'read',
|
||||
'x-component-props': {
|
||||
gmt: true,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
read: {
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: true,
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ DatePicker, FormItem }}>
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* title: DatePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: false,
|
||||
utc: false,
|
||||
},
|
||||
'x-reactions': {
|
||||
target: '*(read1,read2)',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateFormat: 'YYYY/MM/DD',
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
gmt: false,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* title: DatePicker.RangePicker
|
||||
*/
|
||||
import { FormItem } from '@formily/antd';
|
||||
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'boolean',
|
||||
title: `Editable`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
'x-component-props': {
|
||||
utc: false,
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'read1',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'read2',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
read1: {
|
||||
type: 'boolean',
|
||||
title: `Read pretty`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker.RangePicker',
|
||||
},
|
||||
read2: {
|
||||
type: 'string',
|
||||
title: `Value`,
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
@ -17,10 +17,22 @@ group:
|
||||
|
||||
<code src="./demos/demo3.tsx" />
|
||||
|
||||
### RangePicker
|
||||
### DatePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo5.tsx" />
|
||||
|
||||
### RangePicker (GMT)
|
||||
|
||||
<code src="./demos/demo2.tsx" />
|
||||
|
||||
### RangePicker (non-GMT)
|
||||
|
||||
<code src="./demos/demo6.tsx" />
|
||||
|
||||
### RangePicker (non-UTC)
|
||||
|
||||
<code src="./demos/demo7.tsx" />
|
||||
|
||||
## API
|
||||
|
||||
基于 antd 的 [DatePicker](https://ant.design/components/date-picker/#API),新增了以下扩展属性,用于支持 NocoBase 的日期字段设置。
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
|
||||
import moment from 'moment';
|
||||
|
||||
const toStringByPicker = (value, picker) => {
|
||||
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
|
||||
if (!moment.isMoment(value)) return value;
|
||||
if (timezone === 'local') {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
return moment(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
|
||||
}
|
||||
|
||||
if (picker === 'year') {
|
||||
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||
}
|
||||
@ -9,52 +15,63 @@ const toStringByPicker = (value, picker) => {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'quarter') {
|
||||
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||
}
|
||||
if (picker === 'week') {
|
||||
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
}
|
||||
return value.format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
};
|
||||
|
||||
const toGmtByPicker = (value: moment.Moment | moment.Moment[], picker?: any) => {
|
||||
if (!value) {
|
||||
const toGmtByPicker = (value: moment.Moment, picker?: any) => {
|
||||
if (!value || !moment.isMoment(value)) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => toStringByPicker(val, picker));
|
||||
}
|
||||
if (moment.isMoment(value)) {
|
||||
return toStringByPicker(value, picker);
|
||||
return toStringByPicker(value, picker, 'gmt');
|
||||
};
|
||||
|
||||
const toLocalByPicker = (value: moment.Moment, picker?: any) => {
|
||||
if (!value || !moment.isMoment(value)) {
|
||||
return value;
|
||||
}
|
||||
return toStringByPicker(value, picker, 'local');
|
||||
};
|
||||
|
||||
export interface Moment2strOptions {
|
||||
showTime?: boolean;
|
||||
gmt?: boolean;
|
||||
utc?: boolean;
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
}
|
||||
|
||||
export const moment2str = (value?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker } = options;
|
||||
export const moment2str = (value?: moment.Moment, options: Moment2strOptions = {}) => {
|
||||
const { showTime, gmt, picker, utc = true } = options;
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (!utc) {
|
||||
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
return value.format(format);
|
||||
}
|
||||
if (showTime) {
|
||||
return gmt ? toGmt(value) : toLocal(value);
|
||||
}
|
||||
if (typeof gmt === 'boolean') {
|
||||
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
|
||||
}
|
||||
return toGmtByPicker(value, picker);
|
||||
};
|
||||
|
||||
export const mapDateFormat = function () {
|
||||
return (props: any, field) => {
|
||||
export const mapDatePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: moment.Moment | moment.Moment[]) => {
|
||||
onChange: (value: moment.Moment) => {
|
||||
if (onChange) {
|
||||
onChange(moment2str(value, props));
|
||||
}
|
||||
@ -62,3 +79,77 @@ export const mapDateFormat = function () {
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRangePicker = function () {
|
||||
return (props: any) => {
|
||||
const format = getDefaultFormat(props) as any;
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
format: format,
|
||||
value: str2moment(props.value, props),
|
||||
onChange: (value: moment.Moment[]) => {
|
||||
if (onChange) {
|
||||
onChange(
|
||||
value
|
||||
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
};
|
||||
|
||||
function getRangeStart(value: moment.Moment, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.startOf('day');
|
||||
}
|
||||
|
||||
function getRangeEnd(value: moment.Moment, options: Moment2strOptions) {
|
||||
const { showTime } = options;
|
||||
if (showTime) {
|
||||
return value;
|
||||
}
|
||||
return value.endOf('day');
|
||||
}
|
||||
|
||||
const getStart = (offset: any, unit: moment.unitOfTime.StartOf) => {
|
||||
return moment()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.startOf(unit);
|
||||
};
|
||||
|
||||
const getEnd = (offset: any, unit: moment.unitOfTime.StartOf) => {
|
||||
return moment()
|
||||
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||
.endOf(unit);
|
||||
};
|
||||
|
||||
export const getDateRanges = () => {
|
||||
return {
|
||||
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
|
||||
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
|
||||
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
|
||||
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
|
||||
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
|
||||
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
|
||||
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
|
||||
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
|
||||
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
|
||||
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
|
||||
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
|
||||
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
|
||||
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
|
||||
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
|
||||
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
|
||||
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
|
||||
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
|
||||
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
|
||||
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
|
||||
};
|
||||
};
|
||||
|
@ -1,56 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { createForm, onFieldValueChange } from '@formily/core';
|
||||
import { connect, FieldContext, FormContext } from '@formily/react';
|
||||
import { FieldContext, FormContext } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { Cascader } from 'antd';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { useCompile, useComponent } from '../../hooks';
|
||||
import { useComponent } from '../../hooks';
|
||||
import { FilterContext } from './context';
|
||||
import { useFilterOptions } from './useFilterActionProps';
|
||||
|
||||
const VariableCascader = connect((props) => {
|
||||
const fields = useFilterOptions('users');
|
||||
const compile = useCompile();
|
||||
const { value, onChange } = props;
|
||||
return (
|
||||
<Cascader
|
||||
className={css`
|
||||
width: 160px;
|
||||
`}
|
||||
value={value ? value.split('.') : []}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
children: 'children',
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange(value ? value.join('.') : null);
|
||||
}}
|
||||
options={compile([
|
||||
{
|
||||
title: '{{t("Current user")}}',
|
||||
name: 'currentUser',
|
||||
children: fields
|
||||
.filter((field) => {
|
||||
if (!field.target) {
|
||||
return true;
|
||||
}
|
||||
return field.type === 'belongsTo';
|
||||
})
|
||||
.map((field) => {
|
||||
if (field.children) {
|
||||
field.children = field.children.filter((child) => {
|
||||
return !child.target;
|
||||
});
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
},
|
||||
])}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const isDateComponent = {
|
||||
'DatePicker.RangePicker': true,
|
||||
DatePicker: true,
|
||||
};
|
||||
|
||||
export const DynamicComponent = (props) => {
|
||||
const { dynamicComponent, disabled } = useContext(FilterContext);
|
||||
@ -85,9 +44,6 @@ export const DynamicComponent = (props) => {
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
components={{
|
||||
VariableCascader,
|
||||
}}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { DatePickerProvider } from '../date-picker';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||
import { FilterAction } from './FilterAction';
|
||||
@ -26,12 +27,20 @@ export const Filter: any = observer((props: any) => {
|
||||
});
|
||||
return (
|
||||
<div className={className}>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ export interface FilterContextProps {
|
||||
fieldSchema?: Schema;
|
||||
dynamicComponent?: any;
|
||||
options?: any[];
|
||||
disabled?: boolean
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RemoveConditionContext = createContext(null);
|
||||
|
@ -62,7 +62,7 @@ export const useValues = () => {
|
||||
const s2 = cloneDeep(operator?.schema);
|
||||
field.data.schema = merge(s1, s2);
|
||||
field.data.dataIndex = dataIndex;
|
||||
field.data.value = operator.noValue ? operator.default || true : null;
|
||||
field.data.value = operator?.noValue ? operator.default || true : null;
|
||||
data2value();
|
||||
},
|
||||
setOperator(operatorValue) {
|
||||
|
@ -2,24 +2,19 @@ import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext } from '../../../block-provider';
|
||||
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useActionContext } from '../action';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const FormDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
const ctx = useFormBlockContext();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const { visible } = useActionContext();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
|
||||
return (
|
||||
@ -108,12 +103,15 @@ export const DetailsDesigner = () => {
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {},
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user