Merge branch 'main' into feat/gantt-block

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

View File

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

View File

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

View File

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

View File

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

View File

@ -176,16 +176,23 @@ app.resourcer.registerActionHandlers({
## Video ## Video
### Static data ### 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 ### 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 ### More charts
Theoretically supports all charts on [https://g2plot.antv.vision/en/examples](https://g2plot.antv.vision/en/examples) 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 ## JS Expressions
@ -197,5 +204,7 @@ Syntax
} }
``` ```
https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4 <video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4" type="video/mp4">
</video>

View File

@ -176,16 +176,22 @@ app.resourcer.registerActionHandlers({
## Video ## Video
### Static data ### 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 ### 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 ### More charts
Theoretically supports all charts on [https://g2plot.antv.vision/en/examples](https://g2plot.antv.vision/en/examples) 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 ## JS Expressions
@ -197,5 +203,7 @@ Syntax
} }
``` ```
https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4 <video width="100%" height="440" controls>
<source src="https://user-images.githubusercontent.com/1267426/198877361-808a51cc-6c91-429f-8cfc-8ad7f747645a.mp4" type="video/mp4">
</video>

View File

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

View File

@ -25,7 +25,7 @@ export default defineConfig({
resolveNocobasePackagesAlias(memo); resolveNocobasePackagesAlias(memo);
// 在引入 mermaid 之后,运行 yarn dev 的时候会报错,添加下面的代码可以解决。 // 在引入 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; return memo;
}, },
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -292,9 +292,27 @@ export const useSourceIdFromParentRecord = () => {
export const useParamsFromRecord = () => { export const useParamsFromRecord = () => {
const filterByTk = useFilterByTk(); 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, filterByTk: filterByTk,
}; };
if (!filterByTk) {
obj['filter'] = filter;
}
return obj;
}; };
export const RecordLink = (props) => { export const RecordLink = (props) => {

View File

@ -3,6 +3,7 @@ import { SchemaComponentOptions } from '../schema-component/core/SchemaComponent
import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider'; import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider';
import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider'; import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider';
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider'; import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider'; import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
import * as bp from './hooks'; import * as bp from './hooks';
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider'; import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
@ -22,6 +23,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
TableBlockProvider, TableBlockProvider,
TableSelectorProvider, TableSelectorProvider,
FormBlockProvider, FormBlockProvider,
FilterFormBlockProvider,
FormFieldProvider, FormFieldProvider,
DetailsBlockProvider, DetailsBlockProvider,
KanbanBlockProvider, KanbanBlockProvider,

View File

@ -0,0 +1,11 @@
import React from 'react';
import { DatePickerProvider } from '../schema-component';
import { FormBlockProvider } from './FormBlockProvider';
export const FilterFormBlockProvider = (props) => {
return (
<DatePickerProvider value={{ utc: false }}>
<FormBlockProvider {...props}></FormBlockProvider>
</DatePickerProvider>
);
};

View File

@ -94,7 +94,7 @@ export const useFormBlockProps = () => {
const addChild = fieldSchema?.['x-component-props']?.addChild; const addChild = fieldSchema?.['x-component-props']?.addChild;
useEffect(() => { useEffect(() => {
if (addChild) { if (addChild) {
ctx.form.query('parent').take((field) => { ctx.form?.query('parent').take((field) => {
field.disabled = true; field.disabled = true;
field.value = new Proxy({ ...record }, {}); field.value = new Proxy({ ...record }, {});
}); });
@ -102,7 +102,7 @@ export const useFormBlockProps = () => {
}); });
useEffect(() => { useEffect(() => {
ctx.form.setInitialValues(ctx.service?.data?.data); ctx.form?.setInitialValues(ctx.service?.data?.data);
}, []); }, []);
return { return {
form: ctx.form, form: ctx.form,

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,24 @@ const getSchema = (schema: IField, record: any, compile) => {
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema); properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem'; 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 = { const initialValue: any = {
name: `f_${uid()}`, name: `f_${uid()}`,
@ -194,6 +212,7 @@ export const AddFieldAction = (props) => {
return optionArr; return optionArr;
}; };
return ( return (
record.template !== 'view' && (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}> <ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown <Dropdown
@ -261,5 +280,6 @@ export const AddFieldAction = (props) => {
/> />
</ActionContext.Provider> </ActionContext.Provider>
</RecordProvider> </RecordProvider>
)
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ export const collectionFieldSchema: ISchema = {
params: { params: {
paginate: false, paginate: false,
filter: { filter: {
'interface.$not': null, $or: [{ 'interface.$not': null }, { 'options.source.$notEmpty': true }],
}, },
sort: ['sort'], sort: ['sort'],
// appends: ['uiSchema'], // 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: { create: {
type: 'void', type: 'void',
title: '{{ t("Add new") }}', title: '{{ t("Add new") }}',

View File

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

View File

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

View File

@ -62,7 +62,7 @@ export const useCollectionManager = () => {
return getParents(name); return getParents(name);
}; };
const getChildrenCollections = (name) => { const getChildrenCollections = (name, isSupportView = false) => {
const children = []; const children = [];
const getChildren = (name) => { const getChildren = (name) => {
const inheritCollections = collections.filter((v) => { const inheritCollections = collections.filter((v) => {
@ -73,6 +73,16 @@ export const useCollectionManager = () => {
children.push(v); children.push(v);
return getChildren(collectionKey); 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 uniqBy(children, 'key');
}; };
return getChildren(name); return getChildren(name);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,6 +52,7 @@ export const datetime = [
{ label: "{{ t('is after') }}", value: '$dateAfter' }, { label: "{{ t('is after') }}", value: '$dateAfter' },
{ label: "{{ t('is on or after') }}", value: '$dateNotBefore' }, { label: "{{ t('is on or after') }}", value: '$dateNotBefore' },
{ label: "{{ t('is on or before') }}", value: '$dateNotAfter' }, { 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 empty') }}", value: '$empty', noValue: true },
{ label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true }, { label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true },
]; ];
@ -70,30 +71,6 @@ export const number = [
export const id = [ export const id = [
{ label: '{{t("is")}}', value: '$eq', selected: true }, { label: '{{t("is")}}', value: '$eq', selected: true },
{ label: '{{t("is not")}}', value: '$ne' }, { 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("exists")}}', value: '$exists', noValue: true },
{ label: '{{t("not exists")}}', value: '$notExists', noValue: true }, { label: '{{t("not exists")}}', value: '$notExists', noValue: true },
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,10 @@ export const FilterBlockRecord = ({
const associatedFields = useAssociatedFields(); const associatedFields = useAssociatedFields();
const container = useRef(null); 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 = () => { const addBlockToDataBlocks = () => {
recordDataBlocks({ recordDataBlocks({

View File

@ -6,8 +6,28 @@ export default {
"{{count}} more items": "{{count}} more items", "{{count}} more items": "{{count}} more items",
"Total {{count}} items": "Total {{count}} items", "Total {{count}} items": "Total {{count}} items",
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday",
"Tomorrow": "Tomorrow",
"Month": "Month", "Month": "Month",
"Week": "Week", "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", "Work week": "Work week",
"Day": "Day", "Day": "Day",
"Agenda": "Agenda", "Agenda": "Agenda",
@ -52,6 +72,7 @@ export default {
"Value":"Value", "Value":"Value",
"Disabled":"Disabled", "Disabled":"Disabled",
"Enabled":"Enabled", "Enabled":"Enabled",
"Empty":"Empty",
"Linkage rule":"Linkage rule", "Linkage rule":"Linkage rule",
"Linkage rules":"Linkage rules", "Linkage rules":"Linkage rules",
"Condition":"Condition", "Condition":"Condition",
@ -168,6 +189,10 @@ export default {
"Collection template": "Collection template", "Collection template": "Collection template",
"Calendar collection": "Calendar collection", "Calendar collection": "Calendar collection",
"General collection": "General 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.", "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", "Storage type": "Storage type",
"Edit": "Edit", "Edit": "Edit",
@ -269,7 +294,6 @@ export default {
"Comparision": "Comparision", "Comparision": "Comparision",
"is": "is", "is": "is",
"is not": "is not", "is not": "is not",
"is variable": "is variable",
"contains": "contains", "contains": "contains",
"does not contain": "does not contain", "does not contain": "does not contain",
"starts with": "starts with", "starts with": "starts with",
@ -334,6 +358,7 @@ export default {
"is after": "is after", "is after": "is after",
"is on or after": "is on or after", "is on or after": "is on or after",
"is on or before": "is on or before", "is on or before": "is on or before",
"is between": "is between",
"Upload": "Upload", "Upload": "Upload",
"Select level": "Select level", "Select level": "Select level",
"Province": "Province", "Province": "Province",
@ -478,8 +503,6 @@ export default {
"Add condition group": "Add condition group", "Add condition group": "Add condition group",
"exists": "exists", "exists": "exists",
"not exists": "not 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 user": "Current user",
"Current record": "Current record", "Current record": "Current record",
"Current time": "Current time", "Current time": "Current time",
"System variables": "System variables",
"Date variables": "Date variables",
"Popup close method": "Popup close method", "Popup close method": "Popup close method",
"Automatic close": "Automatic close", "Automatic close": "Automatic close",
"Manually close": "Manually close", "Manually close": "Manually close",

View File

@ -6,8 +6,28 @@ export default {
"{{count}} more items": "{{count}} 件以上", "{{count}} more items": "{{count}} 件以上",
"Total {{count}} items": "合計 {{count}} 件", "Total {{count}} items": "合計 {{count}} 件",
"Today": "今日", "Today": "今日",
"Yesterday": "昨日",
"Tomorrow": "明日",
"Month": "月", "Month": "月",
"Week": "週", "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": "稼働日", "Work week": "稼働日",
"Day": "日", "Day": "日",
"Agenda": "アジェンダ", "Agenda": "アジェンダ",
@ -58,6 +78,7 @@ export default {
"Value":"フィールド値", "Value":"フィールド値",
"Disabled":"無効化", "Disabled":"無効化",
"Enabled":"有効化", "Enabled":"有効化",
"Empty":"くうきち",
"Linkage rule":"連動規則", "Linkage rule":"連動規則",
"Linkage rules":"連動規則", "Linkage rules":"連動規則",
"Condition":"条件#ジョウケン#", "Condition":"条件#ジョウケン#",
@ -159,6 +180,10 @@ export default {
"Collection template":"データテーブルテンプレート", "Collection template":"データテーブルテンプレート",
"Calendar collection":"カレンダデータテーブル", "Calendar collection":"カレンダデータテーブル",
"General 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.": "ランダムに生成され、変更可能です。 アルファベット、数字、アンダースコアをサポートし、アルファベットから始まる必要があります。", "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "ランダムに生成され、変更可能です。 アルファベット、数字、アンダースコアをサポートし、アルファベットから始まる必要があります。",
"Storage type": "ストレージタイプ", "Storage type": "ストレージタイプ",
"Edit": "編集", "Edit": "編集",
@ -234,7 +259,6 @@ export default {
"Comparision": "比較", "Comparision": "比較",
"is": "が同じである", "is": "が同じである",
"is not": "が同じではない", "is not": "が同じではない",
"is variable": "が変数である",
"contains": "を含む", "contains": "を含む",
"does not contain": "を含まない", "does not contain": "を含まない",
"starts with": "で始まる", "starts with": "で始まる",
@ -276,6 +300,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": "範囲",
"Upload": "アップロード", "Upload": "アップロード",
"Select level": "レベルを選択", "Select level": "レベルを選択",
"Province": "州", "Province": "州",
@ -402,7 +427,6 @@ export default {
"Add condition group": "条件グループの追加", "Add condition group": "条件グループの追加",
"exists": "が存在する", "exists": "が存在する",
"not exists": "が存在しない", "not exists": "が存在しない",
"is current logged-in user": "が現在ログインしているユーザー",
"=": "=", "=": "=",
"≠": "≠", "≠": "≠",
">": ">", ">": ">",

View File

@ -6,8 +6,28 @@ export default {
"{{count}} more items": "{{count}} больше элементов", "{{count}} more items": "{{count}} больше элементов",
"Total {{count}} items": "Всего {{count}} элементов", "Total {{count}} items": "Всего {{count}} элементов",
"Today": "Сегодня", "Today": "Сегодня",
"Yesterday": "Вчера",
"Tomorrow": "Завтра",
"Month": "Месяц", "Month": "Месяц",
"Week": "Неделя", "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": "Рабочая неделя", "Work week": "Рабочая неделя",
"Day": "День", "Day": "День",
"Agenda": "Расписание", "Agenda": "Расписание",
@ -185,7 +205,6 @@ export default {
"Comparision": "Сравнение", "Comparision": "Сравнение",
"is": "соответствует", "is": "соответствует",
"is not": "не соответствует", "is not": "не соответствует",
"is variable": "это переменная",
"contains": "содержит", "contains": "содержит",
"does not contain": "не содержит", "does not contain": "не содержит",
"starts with": "начинается с", "starts with": "начинается с",
@ -227,6 +246,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": "находится в диапазоне",
"Upload": "Закачать", "Upload": "Закачать",
"Select level": "Выберите уровень", "Select level": "Выберите уровень",
"Province": "Область", "Province": "Область",
@ -353,7 +373,6 @@ export default {
"Add condition group": "Добавить группу правил", "Add condition group": "Добавить группу правил",
"exists": "существуют", "exists": "существуют",
"not exists": "не существуют", "not exists": "не существуют",
"is current logged-in user": "текущий пользователь",
"=": "=", "=": "=",
"≠": "≠", "≠": "≠",
">": ">", ">": ">",

View File

@ -6,8 +6,28 @@ export default {
"{{count}} more items": "{{count}} öğe daha", "{{count}} more items": "{{count}} öğe daha",
"Total {{count}} items": "Toplam {{count}} adet öğe", "Total {{count}} items": "Toplam {{count}} adet öğe",
"Today": "Bugün", "Today": "Bugün",
"Yesterday": "Dün",
"Tomorrow": "Yarın",
"Month": "Ay", "Month": "Ay",
"Week": "Hafta", "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ı", "Work week": "Çalışma Haftası",
"Day": "Gün", "Day": "Gün",
"Agenda": "Ajanda", "Agenda": "Ajanda",
@ -184,7 +204,6 @@ export default {
"Comparision": "Karşılaştırma", "Comparision": "Karşılaştırma",
"is": "eşittir", "is": "eşittir",
"is not": "eşit değildir", "is not": "eşit değildir",
"is variable": "is variable",
"contains": "içerir", "contains": "içerir",
"does not contain": "içermez", "does not contain": "içermez",
"starts with": "ile başlar", "starts with": "ile başlar",
@ -226,6 +245,7 @@ export default {
"is after": "sonra", "is after": "sonra",
"is on or after": "açık veya sonra", "is on or after": "açık veya sonra",
"is on or before": "açık veya önce", "is on or before": "açık veya önce",
"is between": "aralık",
"Upload": "Yükle", "Upload": "Yükle",
"Select level": "Seviye seç", "Select level": "Seviye seç",
"Province": "Bölge", "Province": "Bölge",
@ -352,7 +372,6 @@ export default {
"Add condition group": "Koşul grubu ekle", "Add condition group": "Koşul grubu ekle",
"exists": "var olanlar", "exists": "var olanlar",
"not exists": "var olmayanlar", "not exists": "var olmayanlar",
"is current logged-in user": "mevcut oturum açmış kullanıcı",
"=": "=", "=": "=",
"≠": "≠", "≠": "≠",
">": ">", ">": ">",

View File

@ -6,8 +6,28 @@ export default {
"{{count}} more items": "还有 {{count}} 项", "{{count}} more items": "还有 {{count}} 项",
"Total {{count}} items": "总共 {{count}} 条", "Total {{count}} items": "总共 {{count}} 条",
"Today": "今天", "Today": "今天",
"Yesterday": "昨天",
"Tomorrow": "明天",
"Month": "月", "Month": "月",
"Week": "周", "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": "工作日", "Work week": "工作日",
"Day": "天", "Day": "天",
"Agenda": "列表", "Agenda": "列表",
@ -50,6 +70,7 @@ export default {
"Value":"字段值", "Value":"字段值",
"Disabled":"禁用", "Disabled":"禁用",
"Enabled":"启用", "Enabled":"启用",
"Empty":"赋空值",
"Linkage rule":"联动规则", "Linkage rule":"联动规则",
"Linkage rules":"联动规则", "Linkage rules":"联动规则",
"Condition":"条件", "Condition":"条件",
@ -176,6 +197,10 @@ export default {
"Collection template": "数据表模板", "Collection template": "数据表模板",
"Calendar collection": "日历数据表", "Calendar collection": "日历数据表",
"General 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.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。", "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Storage type": "存储类型", "Storage type": "存储类型",
"Types will be used in database": "数据库使用的类型", "Types will be used in database": "数据库使用的类型",
@ -285,7 +310,6 @@ export default {
"Comparision": "值比较", "Comparision": "值比较",
"is": "等于", "is": "等于",
"is not": "不等于", "is not": "不等于",
"is variable": "为动态变量",
"contains": "包含", "contains": "包含",
"does not contain": "不包含", "does not contain": "不包含",
"starts with": "开头是", "starts with": "开头是",
@ -352,6 +376,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": "介于",
"Upload": "上传", "Upload": "上传",
@ -512,8 +537,6 @@ export default {
'Add condition group': '添加条件分组', 'Add condition group': '添加条件分组',
'exists': '存在', 'exists': '存在',
'not exists': '不存在', 'not exists': '不存在',
'is current logged-in user': '为当前登录用户',
'is not current logged-in user': '不为当前登录用户',
'=': '=', '=': '=',
'≠': '≠', '≠': '≠',
'>': '>', '>': '>',
@ -612,6 +635,7 @@ export default {
'Current user': '当前用户', 'Current user': '当前用户',
'Current record': '当前记录', 'Current record': '当前记录',
'Current time': '当前时间', 'Current time': '当前时间',
'Now': '现在',
'Popup close method': '弹窗关闭方式', 'Popup close method': '弹窗关闭方式',
'Automatic close': '自动关闭', 'Automatic close': '自动关闭',
'Manually close': '手动关闭', 'Manually close': '手动关闭',
@ -695,6 +719,8 @@ export default {
'Column width': '列宽', 'Column width': '列宽',
'Sortable': '可排序的', 'Sortable': '可排序的',
'Constant': '常量', 'Constant': '常量',
'System variables': '系统变量',
'Date variables': '日期变量',
'Use variable': '使用变量', 'Use variable': '使用变量',
'True': '真', 'True': '真',
'False': '假', 'False': '假',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,19 +4,21 @@ import { Field } from '@formily/core';
import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFormBlockContext, useFilterByTk } from '../../../block-provider'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { import {
useCollectionManager,
useCollection, useCollection,
useSortFields,
useCollectionFilterOptions, useCollectionFilterOptions,
useCollectionManager,
useSortFields
} from '../../../collection-manager'; } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; 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 { RemoteSelect, RemoteSelectProps } from '../remote-select';
import { defaultFieldNames } from '../select'; import { defaultFieldNames } from '../select';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import useServiceOptions from './useServiceOptions'; import useServiceOptions from './useServiceOptions';
@ -102,7 +104,7 @@ AssociationSelect.Designer = () => {
const compile = useCompile(); const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const fieldComponentOptions = useFieldComponentOptions(); 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 interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
const originalTitle = collectionField?.uiSchema?.title; const originalTitle = collectionField?.uiSchema?.title;
@ -423,7 +425,7 @@ AssociationSelect.Designer = () => {
}} }}
/> />
)} )}
{form && !isSubFormAssocitionField && fieldComponentOptions && ( {form && !isSubFormAssociationField && fieldComponentOptions && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Field component')} title={t('Field component')}
options={fieldComponentOptions} options={fieldComponentOptions}
@ -483,12 +485,15 @@ AssociationSelect.Designer = () => {
// title: '数据范围', // title: '数据范围',
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': {}, 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
}, },
}, },
} as ISchema } as ISchema
} }
onSubmit={({ filter }) => { onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter); _.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps; fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', { dn.emit('patch', {
@ -692,13 +697,9 @@ AssociationSelect.FilterDesigner = () => {
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
const tk = useFilterByTk(); const { dn, refresh } = useDesignable();
const {} = useCollection();
const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile(); const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); 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 interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
const originalTitle = collectionField?.uiSchema?.title; const originalTitle = collectionField?.uiSchema?.title;
@ -1012,12 +1013,15 @@ AssociationSelect.FilterDesigner = () => {
// title: '数据范围', // title: '数据范围',
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': {}, 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
}, },
}, },
} as ISchema } as ISchema
} }
onSubmit={({ filter }) => { onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter); _.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps; fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', { dn.emit('patch', {

View File

@ -1,12 +1,13 @@
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FixedBlockDesignerItem, useCompile, useDesignable } from '../..'; import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..';
import { useCalendarBlockContext } from '../../../block-provider'; import { useCalendarBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks'; import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const CalendarDesigner = () => { export const CalendarDesigner = () => {
const field = useField(); const field = useField();
@ -115,7 +116,9 @@ export const CalendarDesigner = () => {
default: defaultFilter, default: defaultFilter,
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': {}, 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
}, },
}, },
} as ISchema } as ISchema
@ -127,6 +130,7 @@ export const CalendarDesigner = () => {
} }
} }
onSubmit={({ filter }) => { onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {}; const params = field.decoratorProps.params || {};
params.filter = filter; params.filter = filter;
field.decoratorProps.params = params; field.decoratorProps.params = params;

View File

@ -5,23 +5,68 @@ import type {
RangePickerProps as AntdRangePickerProps RangePickerProps as AntdRangePickerProps
} from 'antd/lib/date-picker'; } from 'antd/lib/date-picker';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { mapDateFormat } from './util'; import { getDateRanges, mapDatePicker, mapRangePicker } from './util';
interface IDatePickerProps {
utc?: boolean;
}
type ComposedDatePicker = React.FC<AntdDatePickerProps> & { type ComposedDatePicker = React.FC<AntdDatePickerProps> & {
RangePicker?: React.FC<AntdRangePickerProps>; 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, AntdDatePicker,
mapProps(mapDateFormat()), mapProps(mapDatePicker()),
mapReadPretty(ReadPretty.DatePicker), mapReadPretty(ReadPretty.DatePicker),
); );
DatePicker.RangePicker = connect( const _RangePicker = connect(
AntdDatePicker.RangePicker, AntdDatePicker.RangePicker,
mapProps(mapDateFormat()), mapProps(mapRangePicker()),
mapReadPretty(ReadPretty.DateRangePicker), 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; export default DatePicker;

View File

@ -0,0 +1,120 @@
import moment from 'moment';
import { getDateRanges } from '../util';
describe('getDateRanges', () => {
const dateRanges = getDateRanges();
it('today', () => {
const [start, end] = dateRanges.today();
expect(start.toISOString()).toBe(moment().startOf('day').toISOString());
expect(end.toISOString()).toBe(moment().endOf('day').toISOString());
});
it('lastWeek', () => {
const [start, end] = dateRanges.lastWeek();
expect(start.toISOString()).toBe(moment().add(-1, 'week').startOf('isoWeek').toISOString());
expect(end.toISOString()).toBe(moment().add(-1, 'week').endOf('isoWeek').toISOString());
});
it('thisWeek', () => {
const [start, end] = dateRanges.thisWeek();
expect(start.toISOString()).toBe(moment().startOf('isoWeek').toISOString());
expect(end.toISOString()).toBe(moment().endOf('isoWeek').toISOString());
});
it('nextWeek', () => {
const [start, end] = dateRanges.nextWeek();
expect(start.toISOString()).toBe(moment().add(1, 'week').startOf('isoWeek').toISOString());
expect(end.toISOString()).toBe(moment().add(1, 'week').endOf('isoWeek').toISOString());
});
it('lastMonth', () => {
const [start, end] = dateRanges.lastMonth();
expect(start.toISOString()).toBe(moment().add(-1, 'month').startOf('month').toISOString());
expect(end.toISOString()).toBe(moment().add(-1, 'month').endOf('month').toISOString());
});
it('thisMonth', () => {
const [start, end] = dateRanges.thisMonth();
expect(start.toISOString()).toBe(moment().startOf('month').toISOString());
expect(end.toISOString()).toBe(moment().endOf('month').toISOString());
});
it('nextMonth', () => {
const [start, end] = dateRanges.nextMonth();
expect(start.toISOString()).toBe(moment().add(1, 'month').startOf('month').toISOString());
expect(end.toISOString()).toBe(moment().add(1, 'month').endOf('month').toISOString());
});
it('lastQuarter', () => {
const [start, end] = dateRanges.lastQuarter();
expect(start.toISOString()).toBe(moment().add(-1, 'quarter').startOf('quarter').toISOString());
expect(end.toISOString()).toBe(moment().add(-1, 'quarter').endOf('quarter').toISOString());
});
it('thisQuarter', () => {
const [start, end] = dateRanges.thisQuarter();
expect(start.toISOString()).toBe(moment().startOf('quarter').toISOString());
expect(end.toISOString()).toBe(moment().endOf('quarter').toISOString());
});
it('nextQuarter', () => {
const [start, end] = dateRanges.nextQuarter();
expect(start.toISOString()).toBe(moment().add(1, 'quarter').startOf('quarter').toISOString());
expect(end.toISOString()).toBe(moment().add(1, 'quarter').endOf('quarter').toISOString());
});
it('lastYear', () => {
const [start, end] = dateRanges.lastYear();
expect(start.toISOString()).toBe(moment().add(-1, 'year').startOf('year').toISOString());
expect(end.toISOString()).toBe(moment().add(-1, 'year').endOf('year').toISOString());
});
it('thisYear', () => {
const [start, end] = dateRanges.thisYear();
expect(start.toISOString()).toBe(moment().startOf('year').toISOString());
expect(end.toISOString()).toBe(moment().endOf('year').toISOString());
});
it('nextYear', () => {
const [start, end] = dateRanges.nextYear();
expect(start.toISOString()).toBe(moment().add(1, 'year').startOf('year').toISOString());
expect(end.toISOString()).toBe(moment().add(1, 'year').endOf('year').toISOString());
});
it('last7Days', () => {
const [start, end] = dateRanges.last7Days();
expect(start.toISOString()).toBe(moment().add(-6, 'days').startOf('days').toISOString());
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
});
it('next7Days', () => {
const [start, end] = dateRanges.next7Days();
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
expect(end.toISOString()).toBe(moment().add(7, 'days').endOf('days').toISOString());
});
it('last30Days', () => {
const [start, end] = dateRanges.last30Days();
expect(start.toISOString()).toBe(moment().add(-29, 'days').startOf('days').toISOString());
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
});
it('next30Days', () => {
const [start, end] = dateRanges.next30Days();
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
expect(end.toISOString()).toBe(moment().add(30, 'days').endOf('days').toISOString());
});
it('last90Days', () => {
const [start, end] = dateRanges.last90Days();
expect(start.toISOString()).toBe(moment().add(-89, 'days').startOf('days').toISOString());
expect(end.toISOString()).toBe(moment().endOf('days').toISOString());
});
it('next90Days', () => {
const [start, end] = dateRanges.next90Days();
expect(start.toISOString()).toBe(moment().add(1, 'day').startOf('day').toISOString());
expect(end.toISOString()).toBe(moment().add(90, 'days').endOf('days').toISOString());
});
});

View File

@ -0,0 +1,220 @@
import moment from 'moment';
import { mapDatePicker } from '../util';
describe('mapDatePicker', () => {
it('showTime is true and gmt is true', () => {
const props = {
value: '2022-02-22T22:22:22.000Z',
showTime: true,
gmt: true,
};
const result = mapDatePicker()(props);
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 22:22:22');
});
it('showTime is true and gmt is false', () => {
const props = {
value: '2022-02-22T22:22:22.000Z',
showTime: true,
gmt: false,
};
const result = mapDatePicker()(props);
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe(
moment('2022-02-22T22:22:22.000Z').format('YYYY-MM-DD HH:mm:ss'),
);
});
it('showTime is false and gmt is true', () => {
const props = {
value: '2022-02-22T00:00:00.000Z',
showTime: false,
gmt: true,
};
const result = mapDatePicker()(props);
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
});
it('showTime is false and gmt is false', () => {
const props = {
value: '2022-02-22',
showTime: false,
gmt: false,
};
const result = mapDatePicker()(props);
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
});
it('should call onChange with correct value when showTime is true and gmt is true', () => {
const props = {
showTime: true,
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment.utc('2022-02-22 22:22:22'));
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z');
});
it('should call onChange with correct value when showTime is true and gmt is false', () => {
const props = {
showTime: true,
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-22 22:22:22');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
});
it('should call onChange with correct value when showTime is false and gmt is true', () => {
const props = {
showTime: false,
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment.utc('2022-02-22'));
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z');
});
it('should call onChange with correct value when showTime is false and gmt is false', () => {
const props = {
showTime: false,
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-22');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
});
it('should call onChange with correct value when picker is year and gmt is true', () => {
const props = {
picker: 'year',
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment.utc('2022-01-01T00:00:00.000Z'));
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
});
it('should call onChange with correct value when picker is year and gmt is false', () => {
const props = {
picker: 'year',
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-01 00:00:00');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString());
});
it('should call onChange with correct value when picker is month and gmt is true', () => {
const props = {
picker: 'month',
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z');
});
it('should call onChange with correct value when picker is month and gmt is false', () => {
const props = {
picker: 'month',
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-01 00:00:00');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString());
});
it('should call onChange with correct value when picker is quarter and gmt is true', () => {
const props = {
picker: 'quarter',
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment.utc('2022-02-22T00:00:00.000Z'));
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
});
it('should call onChange with correct value when picker is quarter and gmt is false', () => {
const props = {
picker: 'quarter',
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-01 00:00:00');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString());
});
it('should call onChange with correct value when picker is week and gmt is true', () => {
const props = {
picker: 'week',
gmt: true,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment.utc('2022-02-21T00:00:00.000Z');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
});
it('should call onChange with correct value when picker is week and gmt is false', () => {
const props = {
picker: 'week',
gmt: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
const m = moment('2022-02-21 00:00:00');
result.onChange(m);
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
});
it('should call onChange with correct value when utc is false', () => {
const props = {
showTime: true,
gmt: true,
utc: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment('2022-02-22 22:22:22'));
expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22');
});
it('should call onChange with correct value when picker is year and utc is false', () => {
const props = {
showTime: false,
gmt: true,
utc: false,
onChange: jest.fn(),
};
const result = mapDatePicker()(props);
result.onChange(moment('2022-01-01 23:00:00'));
expect(props.onChange).toHaveBeenCalledWith('2022-01-01');
});
it('utc is false and gmt is true', () => {
const props = {
value: '2022-01-01 23:00:00',
showTime: true,
gmt: true,
utc: false,
};
const result = mapDatePicker()(props);
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-01-01 23:00:00');
});
});

View File

@ -0,0 +1,43 @@
import moment from 'moment';
import { mapRangePicker } from '../util';
describe('mapRangePicker', () => {
it('should work with showTime=false, gmt=true, utc=true', () => {
const props = {
showTime: false,
gmt: true,
utc: true,
onChange: jest.fn(),
};
const { onChange } = mapRangePicker()(props);
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
onChange(value);
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T23:59:59.999Z']);
});
it('should work with showTime=true, gmt=true, utc=true', () => {
const props = {
showTime: true,
gmt: true,
utc: true,
onChange: jest.fn(),
};
const { onChange } = mapRangePicker()(props);
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
onChange(value);
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T00:00:00.000Z']);
});
it('should work with showTime=false, gmt=true, utc=false', () => {
const props = {
showTime: false,
gmt: true,
utc: false,
onChange: jest.fn(),
};
const { onChange } = mapRangePicker()(props);
const value = [moment.utc('2023-01-01T00:00:00.000Z'), moment.utc('2023-01-02T00:00:00.000Z')];
onChange(value);
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01', '2023-01-02']);
});
});

View File

@ -44,33 +44,93 @@ describe('str2moment', () => {
describe('moment2str', () => { describe('moment2str', () => {
test('gmt date', () => { 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 }); 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', () => { test('showTime is true, gmt is false', () => {
const m = moment('2022-06-21 10:10:00'); const m = moment('2023-06-21 10:10:00');
const str = moment2str(m); const str = moment2str(m, { showTime: true, gmt: false });
expect(str).toBe('2022-06-21T00:00:00.000Z'); 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', () => { 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 }); const str = moment2str(m, { showTime: true });
expect(str).toBe(m.toISOString()); 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', () => { 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' }); 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', () => { 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' }); 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 () => { test('value is null', async () => {

View File

@ -2,7 +2,7 @@
* title: DatePicker.RangePicker * title: DatePicker.RangePicker
*/ */
import { FormItem } from '@formily/antd'; import { FormItem } from '@formily/antd';
import { DatePicker, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react'; import React from 'react';
const schema = { const schema = {
@ -13,28 +13,52 @@ const schema = {
title: `Editable`, title: `Editable`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker', 'x-component': 'DatePicker.RangePicker',
'x-reactions': { 'x-component-props': {
target: 'read', gmt: true,
},
'x-reactions': [
{
target: 'read1',
fulfill: { fulfill: {
state: { state: {
value: '{{$self.value}}', value: '{{$self.value}}',
}, },
}, },
}, },
{
target: 'read2',
fulfill: {
state: {
value: '{{$self.value && $self.value.join(" ~ ")}}',
}, },
read: { },
},
],
},
read1: {
type: 'boolean', type: 'boolean',
title: `Read pretty`, title: `Read pretty`,
'x-read-pretty': true, 'x-read-pretty': true,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker', '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 () => { export default () => {
return ( return (
<SchemaComponentProvider components={{ DatePicker, FormItem }}> <SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} />
</SchemaComponentProvider> </SchemaComponentProvider>
); );

View File

@ -0,0 +1,58 @@
/**
* title: DatePicker
*/
import { FormItem } from '@formily/antd';
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema = {
type: 'object',
properties: {
input: {
type: 'boolean',
title: `Editable`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
dateFormat: 'YYYY/MM/DD',
showTime: false,
utc: false,
},
'x-reactions': {
target: '*(read1,read2)',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read1: {
type: 'boolean',
title: `Read pretty`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
dateFormat: 'YYYY/MM/DD',
showTime: true,
},
},
read2: {
type: 'string',
title: `Value`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,62 @@
/**
* title: DatePicker.RangePicker
*/
import { FormItem } from '@formily/antd';
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema = {
type: 'object',
properties: {
input: {
type: 'boolean',
title: `Editable`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker',
'x-component-props': {
gmt: false,
},
'x-reactions': [
{
target: 'read1',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
{
target: 'read2',
fulfill: {
state: {
value: '{{$self.value && $self.value.join(" ~ ")}}',
},
},
},
],
},
read1: {
type: 'boolean',
title: `Read pretty`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker',
},
read2: {
type: 'string',
title: `Value`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,62 @@
/**
* title: DatePicker.RangePicker
*/
import { FormItem } from '@formily/antd';
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema = {
type: 'object',
properties: {
input: {
type: 'boolean',
title: `Editable`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker',
'x-component-props': {
utc: false,
},
'x-reactions': [
{
target: 'read1',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
{
target: 'read2',
fulfill: {
state: {
value: '{{$self.value && $self.value.join(" ~ ")}}',
},
},
},
],
},
read1: {
type: 'boolean',
title: `Read pretty`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'DatePicker.RangePicker',
},
read2: {
type: 'string',
title: `Value`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -17,10 +17,22 @@ group:
<code src="./demos/demo3.tsx" /> <code src="./demos/demo3.tsx" />
### RangePicker ### DatePicker (non-UTC)
<code src="./demos/demo5.tsx" />
### RangePicker (GMT)
<code src="./demos/demo2.tsx" /> <code src="./demos/demo2.tsx" />
### RangePicker (non-GMT)
<code src="./demos/demo6.tsx" />
### RangePicker (non-UTC)
<code src="./demos/demo7.tsx" />
## API ## API
基于 antd 的 [DatePicker](https://ant.design/components/date-picker/#API),新增了以下扩展属性,用于支持 NocoBase 的日期字段设置。 基于 antd 的 [DatePicker](https://ant.design/components/date-picker/#API),新增了以下扩展属性,用于支持 NocoBase 的日期字段设置。

View File

@ -1,7 +1,13 @@
import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client'; import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
import moment from 'moment'; 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') { if (picker === 'year') {
return value.format('YYYY') + '-01-01T00:00:00.000Z'; 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'; return value.format('YYYY-MM') + '-01T00:00:00.000Z';
} }
if (picker === 'quarter') { 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') { 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) => { const toGmtByPicker = (value: moment.Moment, picker?: any) => {
if (!value) { if (!value || !moment.isMoment(value)) {
return value; return value;
} }
if (Array.isArray(value)) { return toStringByPicker(value, picker, 'gmt');
return value.map((val) => toStringByPicker(val, picker)); };
}
if (moment.isMoment(value)) { const toLocalByPicker = (value: moment.Moment, picker?: any) => {
return toStringByPicker(value, picker); if (!value || !moment.isMoment(value)) {
return value;
} }
return toStringByPicker(value, picker, 'local');
}; };
export interface Moment2strOptions { export interface Moment2strOptions {
showTime?: boolean; showTime?: boolean;
gmt?: boolean; gmt?: boolean;
utc?: boolean;
picker?: 'year' | 'month' | 'week' | 'quarter'; picker?: 'year' | 'month' | 'week' | 'quarter';
} }
export const moment2str = (value?: moment.Moment | moment.Moment[], options: Moment2strOptions = {}) => { export const moment2str = (value?: moment.Moment, options: Moment2strOptions = {}) => {
const { showTime, gmt, picker } = options; const { showTime, gmt, picker, utc = true } = options;
if (!value) { if (!value) {
return value; return value;
} }
if (!utc) {
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
return value.format(format);
}
if (showTime) { if (showTime) {
return gmt ? toGmt(value) : toLocal(value); return gmt ? toGmt(value) : toLocal(value);
} }
if (typeof gmt === 'boolean') {
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
}
return toGmtByPicker(value, picker); return toGmtByPicker(value, picker);
}; };
export const mapDateFormat = function () { export const mapDatePicker = function () {
return (props: any, field) => { return (props: any) => {
const format = getDefaultFormat(props) as any; const format = getDefaultFormat(props) as any;
const onChange = props.onChange; const onChange = props.onChange;
return { return {
...props, ...props,
format: format, format: format,
value: str2moment(props.value, props), value: str2moment(props.value, props),
onChange: (value: moment.Moment | moment.Moment[]) => { onChange: (value: moment.Moment) => {
if (onChange) { if (onChange) {
onChange(moment2str(value, props)); onChange(moment2str(value, props));
} }
@ -62,3 +79,77 @@ export const mapDateFormat = function () {
}; };
}; };
}; };
export const mapRangePicker = function () {
return (props: any) => {
const format = getDefaultFormat(props) as any;
const onChange = props.onChange;
return {
...props,
format: format,
value: str2moment(props.value, props),
onChange: (value: moment.Moment[]) => {
if (onChange) {
onChange(
value
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
: [],
);
}
},
} as any;
};
};
function getRangeStart(value: moment.Moment, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.startOf('day');
}
function getRangeEnd(value: moment.Moment, options: Moment2strOptions) {
const { showTime } = options;
if (showTime) {
return value;
}
return value.endOf('day');
}
const getStart = (offset: any, unit: moment.unitOfTime.StartOf) => {
return moment()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.startOf(unit);
};
const getEnd = (offset: any, unit: moment.unitOfTime.StartOf) => {
return moment()
.add(offset, unit === 'isoWeek' ? 'week' : unit)
.endOf(unit);
};
export const getDateRanges = () => {
return {
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
};
};

View File

@ -1,56 +1,15 @@
import { css } from '@emotion/css';
import { createForm, onFieldValueChange } from '@formily/core'; import { createForm, onFieldValueChange } from '@formily/core';
import { connect, FieldContext, FormContext } from '@formily/react'; import { FieldContext, FormContext } from '@formily/react';
import { merge } from '@formily/shared'; import { merge } from '@formily/shared';
import { Cascader } from 'antd';
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import { SchemaComponent } from '../../core'; import { SchemaComponent } from '../../core';
import { useCompile, useComponent } from '../../hooks'; import { useComponent } from '../../hooks';
import { FilterContext } from './context'; import { FilterContext } from './context';
import { useFilterOptions } from './useFilterActionProps';
const VariableCascader = connect((props) => { const isDateComponent = {
const fields = useFilterOptions('users'); 'DatePicker.RangePicker': true,
const compile = useCompile(); DatePicker: true,
const { value, onChange } = props; };
return (
<Cascader
className={css`
width: 160px;
`}
value={value ? value.split('.') : []}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
onChange={(value) => {
onChange(value ? value.join('.') : null);
}}
options={compile([
{
title: '{{t("Current user")}}',
name: 'currentUser',
children: fields
.filter((field) => {
if (!field.target) {
return true;
}
return field.type === 'belongsTo';
})
.map((field) => {
if (field.children) {
field.children = field.children.filter((child) => {
return !child.target;
});
}
return field;
}),
},
])}
/>
);
});
export const DynamicComponent = (props) => { export const DynamicComponent = (props) => {
const { dynamicComponent, disabled } = useContext(FilterContext); const { dynamicComponent, disabled } = useContext(FilterContext);
@ -85,9 +44,6 @@ export const DynamicComponent = (props) => {
'x-validator': undefined, 'x-validator': undefined,
'x-decorator': undefined, 'x-decorator': undefined,
}} }}
components={{
VariableCascader,
}}
/> />
</FieldContext.Provider> </FieldContext.Provider>
); );

View File

@ -3,6 +3,7 @@ import { observer, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useRequest } from '../../../api-client'; import { useRequest } from '../../../api-client';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { DatePickerProvider } from '../date-picker';
import { FilterContext } from './context'; import { FilterContext } from './context';
import { FilterActionDesigner } from './Filter.Action.Designer'; import { FilterActionDesigner } from './Filter.Action.Designer';
import { FilterAction } from './FilterAction'; import { FilterAction } from './FilterAction';
@ -26,12 +27,20 @@ export const Filter: any = observer((props: any) => {
}); });
return ( return (
<div className={className}> <div className={className}>
<DatePickerProvider value={{ utc: false }}>
<FilterContext.Provider <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} /> <FilterGroup {...props} bordered={false} />
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */} {/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
</FilterContext.Provider> </FilterContext.Provider>
</DatePickerProvider>
</div> </div>
); );
}); });

View File

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

View File

@ -62,7 +62,7 @@ export const useValues = () => {
const s2 = cloneDeep(operator?.schema); const s2 = cloneDeep(operator?.schema);
field.data.schema = merge(s1, s2); field.data.schema = merge(s1, s2);
field.data.dataIndex = dataIndex; field.data.dataIndex = dataIndex;
field.data.value = operator.noValue ? operator.default || true : null; field.data.value = operator?.noValue ? operator.default || true : null;
data2value(); data2value();
}, },
setOperator(operatorValue) { setOperator(operatorValue) {

View File

@ -2,24 +2,19 @@ import { ArrayItems } from '@formily/antd';
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider';
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider'; import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
import { useCollection } from '../../../collection-manager'; import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks'; import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { useActionContext } from '../action'; import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const FormDesigner = () => { export const FormDesigner = () => {
const { name, title } = useCollection(); const { name, title } = useCollection();
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const ctx = useFormBlockContext();
const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { t } = useTranslation();
const { visible } = useActionContext();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return ( return (
@ -108,12 +103,15 @@ export const DetailsDesigner = () => {
// title: '数据范围', // title: '数据范围',
enum: dataSource, enum: dataSource,
'x-component': 'Filter', 'x-component': 'Filter',
'x-component-props': {}, 'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
}, },
}, },
} as ISchema } as ISchema
} }
onSubmit={({ filter }) => { onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {}; const params = field.decoratorProps.params || {};
params.filter = filter; params.filter = filter;
field.decoratorProps.params = params; field.decoratorProps.params = params;

View File

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

View File

@ -1,47 +1,13 @@
import { last, get } from 'lodash'; import { last, cloneDeep } from 'lodash';
import * as functions from '@formulajs/formulajs';
import { conditionAnalyse } from '../../common/utils/uitls'; import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { ActionType } from '../../../schema-settings/LinkageRules/type';
import evaluators from '@nocobase/evaluators/client';
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);
}
export const linkageMergeAction = ({ operator, value }, field, condition, values) => { export const linkageMergeAction = ({ operator, value }, field, condition, values) => {
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false]; const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display]; const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern]; const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern];
const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value]; const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value];
const { evaluate } = evaluators.get('formula.js');
switch (operator) { switch (operator) {
case ActionType.Required: case ActionType.Required:
@ -91,17 +57,23 @@ export const linkageMergeAction = ({ operator, value }, field, condition, values
case ActionType.Value: case ActionType.Value:
if (conditionAnalyse(condition, values)) { if (conditionAnalyse(condition, values)) {
if (value?.mode === 'express') { 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); valueResult.push(result);
} else { } catch (error) {
}
} else if (value?.mode === 'constant') {
valueResult.push(value?.value || value); valueResult.push(value?.value || value);
} else {
valueResult.push(null);
} }
} }
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
value: valueResult, value: valueResult,
}; };
setTimeout(() => (field.value = last(valueResult) === undefined ? field.value : last(valueResult))); field.value = last(valueResult) === undefined ? field.value : last(valueResult);
break; break;
default: default:
return null; return null;

View File

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

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