feat(db): add sql collection (#2419)
* feat(db): add sql collection * feat: frontend * perf: issue of select * fix: sql model * fix: sql collection schema * fix: implement sql collection * fix: dependency * fix: remove type declaration in actions * fix: backend test * chore: remove some ops of block using sql collection * chore: remove sql collections from Form and Kanban * feat: add execute button to sql input * feat(backend): support infer fields by parsing sql * feat(frontend): support infer interface by parsing sql * fix: fix update issues and improve * fix: update issue * chore: update yarn.lock * fix: fix T-1548 * fix: fix T-1544 * fix: fix T-1545 * fix: fix T-1549 * fix: test * fix: fix T-1556 * fix: remove map action diviver * chore: debug * chore: remove schema of sql collection * fix: sql collection schema * chore: remove debug log & fix T-1555 * fix: fix T-1679 * fix: sql update issue * fix: sql attribute issue * fix: bug of star attribute * fix: test * fix: test * fix: reset fields when updating sql collection * fix(collection-manager): redundant fields after set collection fields * fix: test * fix: destory with individuals hook * chore: save * chore: test * fix: fields sync issue * fix: remove underscored option of sql collection * chore: mutex in fields.afterDestroy * fix: test * chore: yarn.lock * chore: update collections.setFields * feat: improve sql input * fix: fix T-1742 & improve * chore: fix conflicts * fix: workspace * fix: build * fix: test * chore: add translations * fix: reviewed issues * chore: update yarn.lock --------- Co-authored-by: ChengLei Shao <chareice@live.com>
This commit is contained in:
parent
52198e57f9
commit
89635982b4
@ -23,6 +23,7 @@ import {
|
||||
SyncFieldsActionCom,
|
||||
ViewCollectionField,
|
||||
ViewFieldAction,
|
||||
SyncSQLFieldsAction,
|
||||
} from './Configuration';
|
||||
|
||||
import { CollectionCategroriesProvider } from './CollectionManagerProvider';
|
||||
@ -80,6 +81,7 @@ export const CollectionManagerPane = () => {
|
||||
EditCategoryAction,
|
||||
SyncFieldsAction,
|
||||
SyncFieldsActionCom,
|
||||
SyncSQLFieldsAction,
|
||||
}}
|
||||
/>
|
||||
// </Card>
|
||||
|
@ -98,7 +98,7 @@ const getSchema = (schema, category, compile): ISchema => {
|
||||
};
|
||||
|
||||
const getDefaultCollectionFields = (values) => {
|
||||
if (values?.template === 'view') {
|
||||
if (values?.template === 'view' || values?.template === 'sql') {
|
||||
return values.fields;
|
||||
}
|
||||
const defaults = values.fields ? [...values.fields] : [];
|
||||
|
@ -302,35 +302,37 @@ export const AddFieldAction = (props) => {
|
||||
};
|
||||
}, [getInterface, items, record]);
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
|
||||
{children || (
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Add field')}
|
||||
</Button>
|
||||
)}
|
||||
</Dropdown>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
useCancelAction,
|
||||
createOnly: true,
|
||||
isOverride: false,
|
||||
override: false,
|
||||
useCreateCollectionField,
|
||||
record,
|
||||
showReverseFieldConfig: true,
|
||||
targetScope,
|
||||
collections: currentCollections,
|
||||
isDialect,
|
||||
disabledJSONB: false,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
</ActionContextProvider>
|
||||
</RecordProvider>
|
||||
record.template !== 'sql' && (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
|
||||
{children || (
|
||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||
{t('Add field')}
|
||||
</Button>
|
||||
)}
|
||||
</Dropdown>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
useCancelAction,
|
||||
createOnly: true,
|
||||
isOverride: false,
|
||||
override: false,
|
||||
useCreateCollectionField,
|
||||
record,
|
||||
showReverseFieldConfig: true,
|
||||
targetScope,
|
||||
collections: currentCollections,
|
||||
isDialect,
|
||||
disabledJSONB: false,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
</ActionContextProvider>
|
||||
</RecordProvider>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ import { OverridingCollectionField } from './OverridingCollectionField';
|
||||
import { collection } from './schemas/collectionFields';
|
||||
import { SyncFieldsAction } from './SyncFieldsAction';
|
||||
import { ViewCollectionField } from './ViewInheritedField';
|
||||
import { SyncSQLFieldsAction } from './SyncSQLFieldsAction';
|
||||
import { Input } from '../../schema-component/antd/input';
|
||||
|
||||
const indentStyle = css`
|
||||
@ -419,6 +420,7 @@ export const CollectionFields = () => {
|
||||
>
|
||||
<Action {...deleteProps} />
|
||||
<SyncFieldsAction {...syncProps} />
|
||||
<SyncSQLFieldsAction refreshCMList={refreshAsync} />
|
||||
<AddCollectionField {...addProps} />
|
||||
</Space>
|
||||
<Table
|
||||
|
@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import omit from 'lodash/omit';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { useAPIClient, useRequest } from '../../api-client';
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
@ -100,11 +100,16 @@ export const useUpdateCollectionActionAndRefreshCM = (options) => {
|
||||
const ctx = useActionContext();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
const { [targetKey]: filterByTk, template } = useRecord();
|
||||
const api = useAPIClient();
|
||||
const collectionResource = template === 'sql' ? api.resource('sqlCollection') : resource;
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await resource.update({ filterByTk, values: omit(form.values, ['fields']) });
|
||||
await collectionResource.update({
|
||||
filterByTk,
|
||||
values: template === 'sql' ? form.values : omit(form.values, ['fields']),
|
||||
});
|
||||
ctx.setVisible(false);
|
||||
await form.reset();
|
||||
refresh();
|
||||
|
@ -0,0 +1,160 @@
|
||||
import { RecordProvider, useRecord } from '../../record-provider';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ActionContextProvider, FormProvider, SchemaComponent, useActionContext } from '../../schema-component';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useCancelAction } from '../action-hooks';
|
||||
import { FieldsConfigure, PreviewTable, SQLRequestProvider } from '../templates/components/sql-collection';
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormLayout } from '@formily/antd-v5';
|
||||
import { useCollectionManager } from '../hooks';
|
||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
import { useField, useForm } from '@formily/react';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '{{ t("Sync from database") }}',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'FormLayout',
|
||||
'x-decorator-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: {
|
||||
config: {
|
||||
type: 'void',
|
||||
'x-decorator': SQLRequestProvider,
|
||||
'x-decorator-props': {
|
||||
manual: false,
|
||||
},
|
||||
properties: {
|
||||
sql: {
|
||||
type: 'string',
|
||||
},
|
||||
sources: {
|
||||
type: 'array',
|
||||
title: '{{t("Source collections")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
title: '{{t("Fields")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': FieldsConfigure,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
title: '{{t("Preview")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': PreviewTable,
|
||||
},
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{ t("Cancel") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{ t("Submit") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useSyncFromDB }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useSyncFromDB = (refreshCMList?: any) => {
|
||||
const form = useForm();
|
||||
const ctx = useActionContext();
|
||||
const { refreshCM } = useCollectionManager();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { targetKey } = useResourceContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
const api = useAPIClient();
|
||||
const field = useField();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
field.data = field.data || {};
|
||||
field.data.loading = true;
|
||||
try {
|
||||
await api.resource('sqlCollection').setFields({
|
||||
filterByTk,
|
||||
values: {
|
||||
fields: form.values.fields,
|
||||
sources: form.values.sources,
|
||||
},
|
||||
});
|
||||
ctx.setVisible(false);
|
||||
await form.reset();
|
||||
field.data.loading = false;
|
||||
refresh();
|
||||
await refreshCM();
|
||||
await refreshCMList?.();
|
||||
} catch (err) {
|
||||
field.data.loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const SyncSQLFieldsAction: React.FC<{
|
||||
refreshCMList: any;
|
||||
}> = ({ refreshCMList }) => {
|
||||
const record = useRecord();
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
initialValues: record,
|
||||
}),
|
||||
[record],
|
||||
);
|
||||
|
||||
return (
|
||||
record.template === 'sql' && (
|
||||
<RecordProvider record={record}>
|
||||
<FormProvider form={form}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Button icon={<SyncOutlined />} onClick={(e) => setVisible(true)}>
|
||||
{t('Sync from database')}
|
||||
</Button>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ FormLayout }}
|
||||
scope={{
|
||||
useCancelAction,
|
||||
useSyncFromDB: () => useSyncFromDB(refreshCMList),
|
||||
}}
|
||||
/>
|
||||
</ActionContextProvider>
|
||||
</FormProvider>
|
||||
</RecordProvider>
|
||||
)
|
||||
);
|
||||
};
|
@ -15,6 +15,7 @@ export * from './ConfigurationTabs';
|
||||
export * from './AddCategoryAction';
|
||||
export * from './EditCategoryAction';
|
||||
export * from './SyncFieldsAction';
|
||||
export * from './SyncSQLFieldsAction';
|
||||
|
||||
registerValidateFormats({
|
||||
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
|
||||
|
@ -124,6 +124,14 @@ export const collectionFieldSchema: ISchema = {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
syncSQL: {
|
||||
type: 'void',
|
||||
title: '{{ t("Sync from database") }}',
|
||||
'x-component': 'SyncSQLFieldsAction',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{ t("Add new") }}',
|
||||
|
@ -0,0 +1,291 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAsyncData } from '../../../../async-data-provider';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Cascader, Input, Select, Spin, Table, Tag } from 'antd';
|
||||
import { observer, useField, useForm } from '@formily/react';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { getOptions } from '../../../Configuration/interfaces';
|
||||
import { useCompile } from '../../../../schema-component';
|
||||
import { useCollectionManager } from '../../../hooks';
|
||||
import dayjs from 'dayjs';
|
||||
import { FieldOptions } from '@nocobase/database';
|
||||
import { ResourceActionContext, useResourceContext } from '../../../ResourceActionProvider';
|
||||
import { useRecord } from '../../../../record-provider';
|
||||
import { last } from 'lodash';
|
||||
|
||||
const inferInterface = (field: string, value: any) => {
|
||||
if (field.toLowerCase().includes('id')) {
|
||||
return 'id';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
if (Number.isInteger(value)) {
|
||||
return 'integer';
|
||||
}
|
||||
return 'number';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return 'boolean';
|
||||
}
|
||||
if (dayjs(value).isValid()) {
|
||||
return 'datetime';
|
||||
}
|
||||
return 'input';
|
||||
};
|
||||
|
||||
const useSourceFieldsOptions = () => {
|
||||
const form = useForm();
|
||||
const { sources = [] } = form.values;
|
||||
const { t } = useTranslation();
|
||||
const { getCollection, getInheritCollections, getParentCollectionFields } = useCollectionManager();
|
||||
const data = [];
|
||||
sources.forEach((item: string) => {
|
||||
const collection = getCollection(item);
|
||||
const inherits = getInheritCollections(item);
|
||||
const result = inherits.map((v) => {
|
||||
const fields: FieldOptions[] = getParentCollectionFields(v, item);
|
||||
return {
|
||||
type: 'group',
|
||||
key: v,
|
||||
label: t(`Parent collection fields`) + t(`(${getCollection(v).title})`),
|
||||
children: fields
|
||||
.filter((v) => !['hasOne', 'hasMany', 'belongsToMany'].includes(v?.type))
|
||||
.map((k) => {
|
||||
return {
|
||||
value: k.name,
|
||||
label: t(k.uiSchema?.title),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
const children = (collection.fields as FieldOptions[])
|
||||
.filter((v) => !['hasOne', 'hasMany', 'belongsToMany'].includes(v?.type))
|
||||
?.map((v) => {
|
||||
return { value: v.name, label: t(v.uiSchema?.title) };
|
||||
});
|
||||
|
||||
data.push({
|
||||
value: item,
|
||||
label: t(collection.title),
|
||||
children: [...children, ...result],
|
||||
});
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const FieldsConfigure = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const { data: res, error, loading } = useAsyncData();
|
||||
const { data, fields: sourceFields } = res || {};
|
||||
const field: ArrayField = useField();
|
||||
const { data: curFields } = useContext(ResourceActionContext);
|
||||
const compile = useCompile();
|
||||
const { getInterface, getCollectionField } = useCollectionManager();
|
||||
const interfaceOptions = useMemo(
|
||||
() =>
|
||||
getOptions()
|
||||
.filter((v) => !['relation'].includes(v.key))
|
||||
.map((options, index) => ({
|
||||
...options,
|
||||
key: index,
|
||||
label: compile(options.label),
|
||||
options: options.children.map((option) => ({
|
||||
...option,
|
||||
label: compile(option.label),
|
||||
})),
|
||||
})),
|
||||
[compile],
|
||||
);
|
||||
const sourceFieldsOptions = useSourceFieldsOptions();
|
||||
|
||||
const refGetInterface = useRef(getInterface);
|
||||
useEffect(() => {
|
||||
const fieldsMp = new Map();
|
||||
if (!loading) {
|
||||
if (data && data.length) {
|
||||
Object.entries(data?.[0] || {}).forEach(([col, val]) => {
|
||||
const sourceField = sourceFields[col];
|
||||
const fieldInterface = inferInterface(col, val);
|
||||
const defaultConfig = refGetInterface.current(fieldInterface)?.default;
|
||||
const uiSchema = sourceField?.uiSchema || defaultConfig?.uiSchema || {};
|
||||
fieldsMp.set(col, {
|
||||
name: col,
|
||||
interface: sourceField?.interface || fieldInterface,
|
||||
type: sourceField?.type || defaultConfig?.type,
|
||||
source: sourceField?.source,
|
||||
uiSchema: {
|
||||
title: col,
|
||||
...uiSchema,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.entries(sourceFields || {}).forEach(([col, val]: [string, any]) =>
|
||||
fieldsMp.set(col, {
|
||||
name: col,
|
||||
...val,
|
||||
uiSchema: {
|
||||
title: col,
|
||||
...(val?.uiSchema || {}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.value?.length) {
|
||||
field.value.forEach((item) => {
|
||||
if (fieldsMp.has(item.name)) {
|
||||
fieldsMp.set(item.name, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// if (curFields?.data.length) {
|
||||
// curFields.data.forEach((field: any) => {
|
||||
// if (fieldsMp.has(field.name)) {
|
||||
// fieldsMp.set(field.name, field);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
const fields = Array.from(fieldsMp.values());
|
||||
if (!fields.length) {
|
||||
return;
|
||||
}
|
||||
setDataSource(fields);
|
||||
field.setValue(fields);
|
||||
}, [loading, data, field, sourceFields, curFields]);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
if (!data && !error) {
|
||||
return <Alert showIcon message={t('Please use a valid SELECT or WITH AS statement')} />;
|
||||
}
|
||||
const err = error as any;
|
||||
if (err) {
|
||||
const errMsg =
|
||||
err?.response?.data?.errors?.map?.((item: { message: string }) => item.message).join('\n') || err.message;
|
||||
return <Alert showIcon message={`${t('SQL error: ')}${errMsg}`} type="error" />;
|
||||
}
|
||||
|
||||
const handleFieldChange = (record: any, index: number) => {
|
||||
const fields = [...dataSource];
|
||||
fields.splice(index, 1, record);
|
||||
setDataSource(fields);
|
||||
field.setValue(
|
||||
fields.map((f) => ({
|
||||
...f,
|
||||
source: typeof f.source === 'string' ? f.source : f.source?.filter?.(Boolean)?.join('.') || null,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('Field name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: t('Field source'),
|
||||
dataIndex: 'source',
|
||||
key: 'source',
|
||||
width: 200,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const field = dataSource[index];
|
||||
return (
|
||||
<Cascader
|
||||
defaultValue={typeof text === 'string' ? text?.split('.') : text}
|
||||
allowClear
|
||||
options={compile(sourceFieldsOptions)}
|
||||
placeholder={t('Select field source')}
|
||||
onChange={(value: string[]) => {
|
||||
let sourceField = sourceFields[value?.[1]];
|
||||
if (!sourceField) {
|
||||
sourceField = getCollectionField(value?.join('.') || '');
|
||||
}
|
||||
handleFieldChange(
|
||||
{
|
||||
...field,
|
||||
source: value,
|
||||
interface: sourceField?.interface,
|
||||
type: sourceField?.type,
|
||||
uiSchema: sourceField?.uiSchema,
|
||||
},
|
||||
index,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('Field interface'),
|
||||
dataIndex: 'interface',
|
||||
key: 'interface',
|
||||
width: 150,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const field = dataSource[index];
|
||||
return field.source ? (
|
||||
<Tag>{compile(getInterface(text)?.title) || text}</Tag>
|
||||
) : (
|
||||
<Select
|
||||
defaultValue={field.interface || 'input'}
|
||||
style={{ width: '100%' }}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(value) => {
|
||||
const interfaceConfig = getInterface(value);
|
||||
handleFieldChange(
|
||||
{
|
||||
...field,
|
||||
interface: value || null,
|
||||
uiSchema: {
|
||||
...interfaceConfig?.default?.uiSchema,
|
||||
title: interfaceConfig?.default?.uiSchema?.title || field.uiSchema?.title,
|
||||
},
|
||||
type: interfaceConfig?.default?.type,
|
||||
},
|
||||
index,
|
||||
);
|
||||
}}
|
||||
allowClear={true}
|
||||
options={interfaceOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('Field display name'),
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
width: 180,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
const field = dataSource[index];
|
||||
return (
|
||||
<Input
|
||||
value={field.uiSchema?.title || text}
|
||||
defaultValue={field.uiSchema?.title !== undefined ? field.uiSchema.title : field?.name}
|
||||
onChange={(e) =>
|
||||
handleFieldChange({ ...field, uiSchema: { ...field?.uiSchema, title: e.target.value } }, index)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
scroll={{ y: 300 }}
|
||||
pagination={false}
|
||||
rowClassName="editable-row"
|
||||
rowKey="name"
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import { useAsyncData } from '../../../../async-data-provider';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { Schema, observer, useForm } from '@formily/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const PreviewTable = observer(() => {
|
||||
const { data: res, loading, error } = useAsyncData();
|
||||
const { data } = res || {};
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
|
||||
const fields = form.values.fields || [];
|
||||
const titleMp = fields.reduce((mp: { [name: string]: string }, field: any) => {
|
||||
mp[field.name] = field?.uiSchema?.title;
|
||||
return mp;
|
||||
}, {});
|
||||
|
||||
const columns = error
|
||||
? []
|
||||
: Object.keys(data?.[0] || {}).map((col) => {
|
||||
const title = titleMp[col];
|
||||
return {
|
||||
title: Schema.compile(title || col, { t }),
|
||||
dataIndex: col,
|
||||
key: col,
|
||||
};
|
||||
});
|
||||
|
||||
const dataSource = error
|
||||
? []
|
||||
: data?.map((record: any, index: number) => {
|
||||
const compiledRecord = Object.entries(record).reduce(
|
||||
(mp: { [key: string]: any }, [key, val]: [string, any]) => {
|
||||
if (typeof val !== 'string') {
|
||||
mp[key] = val;
|
||||
return mp;
|
||||
}
|
||||
const compiled = Schema.compile(val, { t });
|
||||
mp[key] = t(compiled);
|
||||
return mp;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return { ...compiledRecord, key: index };
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
scroll={{ x: columns.length * 150, y: 300 }}
|
||||
loading={loading}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,68 @@
|
||||
import { useField, useForm } from '@formily/react';
|
||||
import { useAsyncData } from '../../../../async-data-provider';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Input, SchemaComponent } from '../../../../schema-component';
|
||||
import { css } from '@emotion/css';
|
||||
import { Button } from 'antd';
|
||||
import { EditOutlined, RightSquareOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Field } from '@formily/core';
|
||||
|
||||
export const SQLInput = ({ disabled }) => {
|
||||
const { t } = useTranslation();
|
||||
const { run, loading, error } = useAsyncData();
|
||||
const field = useField<Field>();
|
||||
const execute = () => {
|
||||
if (!field.value) {
|
||||
return;
|
||||
}
|
||||
run(field.value);
|
||||
};
|
||||
const toggleEditing = () => {
|
||||
if (!disabled && !field.value) {
|
||||
return;
|
||||
}
|
||||
if (!disabled) {
|
||||
run(field.value);
|
||||
}
|
||||
field.setComponentProps({
|
||||
disabled: !disabled,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
field.setComponentProps({
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
}, [field, error]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
position: relative;
|
||||
.ant-input {
|
||||
width: 100%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Input.TextArea value={field.value} disabled={disabled} onChange={(e) => (field.value = e.target.value)} />
|
||||
<Button.Group>
|
||||
<Button onClick={toggleEditing} ghost size="small" type="primary" icon={<EditOutlined />}>
|
||||
{t(!disabled ? 'Confirm' : 'Edit')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => execute()}
|
||||
loading={loading}
|
||||
ghost
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<RightSquareOutlined />}
|
||||
>
|
||||
{t('Execute')}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { useAPIClient, useRequest } from '../../../../api-client';
|
||||
import { AsyncDataProvider } from '../../../../async-data-provider';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useRecord } from '../../../../record-provider';
|
||||
|
||||
export const SQLRequestProvider: React.FC<{
|
||||
manual?: boolean;
|
||||
}> = (props) => {
|
||||
const api = useAPIClient();
|
||||
const form = useForm();
|
||||
const record = useRecord();
|
||||
let { manual } = props;
|
||||
manual = manual === undefined ? true : manual;
|
||||
|
||||
const result = useRequest(
|
||||
(sql: string) =>
|
||||
api
|
||||
.resource('sqlCollection')
|
||||
.execute({
|
||||
values: {
|
||||
sql,
|
||||
},
|
||||
})
|
||||
.then((res) => res?.data?.data || { data: [], fields: [], sources: [] }),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
const { sources } = data;
|
||||
const formSources = form.values.sources || [];
|
||||
form.setValuesIn('sources', Array.from(new Set([...formSources, ...sources])));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { run } = result;
|
||||
const sql = form.values.sql || record.sql;
|
||||
const first = useRef(true);
|
||||
useEffect(() => {
|
||||
if (sql && first.current) {
|
||||
run(sql);
|
||||
}
|
||||
first.current = false;
|
||||
}, [manual, run, sql]);
|
||||
|
||||
return <AsyncDataProvider value={result}>{props.children}</AsyncDataProvider>;
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from './FieldsConfigure';
|
||||
export * from './PreviewTable';
|
||||
export * from './SQLRequestProvider';
|
||||
export * from './SQLInput';
|
@ -3,3 +3,4 @@ export * from './general';
|
||||
export * from './tree';
|
||||
export * from './expression';
|
||||
export * from './view';
|
||||
export * from './sql';
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { useAsyncData } from '../../async-data-provider';
|
||||
import { SQLInput, PreviewTable, FieldsConfigure, SQLRequestProvider } from './components/sql-collection';
|
||||
import { getConfigurableProperties } from './properties';
|
||||
import { ICollectionTemplate } from './types';
|
||||
import { i18n } from '../../i18n';
|
||||
|
||||
export const sql: ICollectionTemplate = {
|
||||
name: 'sql',
|
||||
title: '{{t("SQL collection")}}',
|
||||
order: 4,
|
||||
color: 'yellow',
|
||||
default: {
|
||||
fields: [],
|
||||
},
|
||||
configurableProperties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '{{ t("Collection display name") }}',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: '{{t("Collection name")}}',
|
||||
required: true,
|
||||
'x-disabled': '{{ !createOnly }}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'uid',
|
||||
description:
|
||||
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
|
||||
},
|
||||
config: {
|
||||
type: 'void',
|
||||
'x-decorator': SQLRequestProvider,
|
||||
properties: {
|
||||
sql: {
|
||||
type: 'string',
|
||||
title: '{{t("SQL")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': SQLInput,
|
||||
required: true,
|
||||
'x-validator': (value: string, rules, { form }) => {
|
||||
const field = form.query('sql').take() as Field;
|
||||
if (!field.componentProps.disabled) {
|
||||
return i18n.t('Please confirm the SQL statement first');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
sources: {
|
||||
type: 'array',
|
||||
title: '{{t("Source collections")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
title: '{{t("Fields")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': FieldsConfigure,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
title: '{{t("Preview")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': PreviewTable,
|
||||
},
|
||||
},
|
||||
},
|
||||
...getConfigurableProperties('category'),
|
||||
},
|
||||
};
|
@ -117,485 +117,498 @@ export default {
|
||||
'Tablet device': 'Tablet device',
|
||||
'Desktop device': 'Desktop device',
|
||||
'Large screen device': 'Large screen device',
|
||||
"Collapse": "Collapse",
|
||||
"Select data source": "Select data source",
|
||||
"Calendar": "Calendar",
|
||||
"Delete events": "Delete events",
|
||||
"This event": "This event",
|
||||
"This and following events": "This and following events",
|
||||
"All events": "All events",
|
||||
"Delete this event?": "Delete this event?",
|
||||
"Delete Event": "Delete Event",
|
||||
"Kanban": "Kanban",
|
||||
"Gantt":"Gantt",
|
||||
"Create gantt block":"Create gantt block",
|
||||
"Progress field":"Progress field",
|
||||
"Time scale":"Time scale",
|
||||
"Hour":"Hour",
|
||||
"Quarter of day":"Quarter of day",
|
||||
"Half of day":"Half of day",
|
||||
"Year":"Year",
|
||||
"QuarterYear":"QuarterYear",
|
||||
"Select grouping field": "Select grouping field",
|
||||
"Media": "Media",
|
||||
"Markdown": "Markdown",
|
||||
"Wysiwyg": "Wysiwyg",
|
||||
"Chart blocks": "Chart blocks",
|
||||
"Column chart": "Column chart",
|
||||
"Bar chart": "Bar chart",
|
||||
"Line chart": "Line chart",
|
||||
"Pie chart": "Pie chart",
|
||||
"Area chart": "Area chart",
|
||||
"Other chart": "Other chart",
|
||||
"Other blocks": "Other blocks",
|
||||
"In configuration": "In configuration",
|
||||
"Chart title": "Chart title",
|
||||
"Chart type": "Chart type",
|
||||
"Chart config": "Chart config",
|
||||
"Templates": "Templates",
|
||||
"Select template": "Select template",
|
||||
"Action logs": "Action logs",
|
||||
"Create template": "Create template",
|
||||
"Edit markdown": "Edit markdown",
|
||||
"Add block": "Add block",
|
||||
"Add new": "Add new",
|
||||
"Add record": "Add record",
|
||||
'Add child':'Add child',
|
||||
'Collapse all':'Collapse all',
|
||||
'Expand all':'Expand all',
|
||||
'Expand/Collapse':'Expand/Collapse',
|
||||
'Default collapse':'Default collapse',
|
||||
"Tree table":"Tree table",
|
||||
"Custom field display name": "Custom field display name",
|
||||
"Display fields": "Display collection fields",
|
||||
"Edit record": "Edit record",
|
||||
"Delete menu item": "Delete menu item",
|
||||
"Add page": "Add page",
|
||||
"Add group": "Add group",
|
||||
"Add link": "Add link",
|
||||
"Insert above": "Insert above",
|
||||
"Insert below": "Insert below",
|
||||
"Save": "Save",
|
||||
"Delete block": "Delete block",
|
||||
"Are you sure you want to delete it?": "Are you sure you want to delete it?",
|
||||
"This is a demo text, **supports Markdown syntax**.": "This is a demo text, **supports Markdown syntax**.",
|
||||
"Filter": "Filter",
|
||||
"Connect data blocks": "Connect data blocks",
|
||||
"Action type": "Action type",
|
||||
"Actions": "Actions",
|
||||
"Insert": "Insert",
|
||||
"Insert if not exists": "Insert if not exists",
|
||||
"Insert if not exists, or update": "Insert if not exists, or update",
|
||||
"Determine whether a record exists by the following fields": "Determine whether a record exists by the following fields",
|
||||
"Update": "Update",
|
||||
"View": "View",
|
||||
"View record": "View record",
|
||||
"Refresh": "Refresh",
|
||||
"Data changes": "Data changes",
|
||||
"Field name": "Field name",
|
||||
"Before change": "Before change",
|
||||
"After change": "After change",
|
||||
"Delete record": "Delete record",
|
||||
"Create collection": "Create collection",
|
||||
"Collection display name": "Collection display name",
|
||||
"Collection name": "Collection name",
|
||||
"Inherits": "Inherits",
|
||||
"Generate ID field automatically": "Generate ID field automatically",
|
||||
"Store the creation user of each record": "Store the creation user of each record",
|
||||
"Store the last update user of each record": "Store the last update user of each record",
|
||||
"Store the creation time of each record": "Store the creation time of each record",
|
||||
"Store the last update time of each record": "Store the last update time of each record",
|
||||
"More options": "More options",
|
||||
"Records can be sorted": "Records can be sorted",
|
||||
"Calendar collection": "Calendar collection",
|
||||
"General collection": "General collection",
|
||||
"Connect to database view":"Connect to database view",
|
||||
"Source collections":"Source collections",
|
||||
"Field source":"Field source",
|
||||
"Preview":"Preview",
|
||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
|
||||
"Edit": "Edit",
|
||||
"Edit collection": "Edit collection",
|
||||
"Configure fields": "Configure fields",
|
||||
"Configure columns": "Configure columns",
|
||||
"Edit field": "Edit field",
|
||||
"Override": "Override",
|
||||
"Override field": "Override field",
|
||||
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
||||
"Association fields filter": "Association fields filter",
|
||||
"PK & FK fields": "PK & FK fields",
|
||||
"Association fields": "Association fields",
|
||||
"Choices fields": "Choices fields",
|
||||
"System fields": "System fields",
|
||||
"General fields": "General fields",
|
||||
"Inherited fields": "Inherited fields",
|
||||
"Parent collection fields": "Parent collection fields",
|
||||
"Basic": "Basic",
|
||||
"Single line text": "Single line text",
|
||||
"Long text": "Long text",
|
||||
"Phone": "Phone",
|
||||
"Email": "Email",
|
||||
"Number": "Number",
|
||||
"Integer": "Integer",
|
||||
"Percent": "Percent",
|
||||
"Password": "Password",
|
||||
"Advanced type": "Advanced",
|
||||
"Formula": "Formula",
|
||||
"Formula description": "Compute a value in each record based on other fields in the same record.",
|
||||
"Choices": "Choices",
|
||||
"Checkbox": "Checkbox",
|
||||
"Single select": "Single select",
|
||||
"Multiple select": "Multiple select",
|
||||
"Radio group": "Radio group",
|
||||
"Checkbox group": "Checkbox group",
|
||||
"China region": "China region",
|
||||
"Date & Time": "Date & Time",
|
||||
"Datetime": "Datetime",
|
||||
"Relation": "Relation",
|
||||
"Link to": "Link to",
|
||||
"Link to description": "Used to create collection relationships quickly and compatible with most common scenarios. Suitable for non-developer use. When present as a field, it is a drop-down selection used to select records from the target collection. Once created, it will simultaneously generate the associated fields of the current collection in the target collection.",
|
||||
"Sub-table": "Sub-table",
|
||||
"Sub-details":"Sub-details",
|
||||
"Sub-form(Popover)":"Sub-form(Popover)",
|
||||
"System info": "System info",
|
||||
"Created at": "Created at",
|
||||
"Last updated at": "Last updated at",
|
||||
"Created by": "Created by",
|
||||
"Last updated by": "Last updated by",
|
||||
"Add field": "Add field",
|
||||
"Field display name": "Field display name",
|
||||
"Field type": "Field type",
|
||||
"Field interface": "Field interface",
|
||||
"Date format": "Date format",
|
||||
"Year/Month/Day": "Year/Month/Day",
|
||||
"Year-Month-Day": "Year-Month-Day",
|
||||
"Day/Month/Year": "Day/Month/Year",
|
||||
"Show time": "Show time",
|
||||
"Time format": "Time format",
|
||||
"12 hour": "12 hour",
|
||||
"24 hour": "24 hour",
|
||||
"Relationship type": "Relationship type",
|
||||
"Inverse relationship type": "Inverse relationship type",
|
||||
"Source collection": "Source collection",
|
||||
"Source key": "Source key",
|
||||
"Target collection": "Target collection",
|
||||
"Through collection": "Through collection",
|
||||
"Target key": "Target key",
|
||||
"Foreign key": "Foreign key",
|
||||
"One to one": "One to one",
|
||||
"One to many": "One to many",
|
||||
"Many to one": "Many to one",
|
||||
"Many to many": "Many to many",
|
||||
"Foreign key 1": "Foreign key 1",
|
||||
"Foreign key 2": "Foreign key 2",
|
||||
"One to one description": "Used to create one-to-one relationships. For example, a user has a profile.",
|
||||
"One to many description": "Used to create a one-to-many relationship. For example, a country will have many cities and a city can only be in one country. When present as a field, it is a sub-table that displays the records of the associated collection. When created, a Many-to-one field is automatically generated in the associated collection.",
|
||||
"Many to one description": "Used to create many-to-one relationships. For example, a city can belong to only one country and a country can have many cities. When present as a field, it is a drop-down selection used to select record from the associated collection. Once created, a One-to-many field is automatically generated in the associated collection.",
|
||||
"Many to many description": "Used to create many-to-many relationships. For example, a student will have many teachers and a teacher will have many students. When present as a field, it is a drop-down selection used to select records from the associated collection.",
|
||||
"Generated automatically if left blank": "Generated automatically if left blank",
|
||||
"Display association fields": "Display association fields",
|
||||
"Display field title": "Display field title",
|
||||
"Field component": "Field component",
|
||||
"Allow multiple": "Allow multiple",
|
||||
"Quick upload": "Quick upload",
|
||||
"Select file": "Select file",
|
||||
"Subtable": "Sub-table",
|
||||
"Sub-form": "Sub-form",
|
||||
"Field mode":"Field mode",
|
||||
"Allow add new data":"Allow add new data",
|
||||
"Record picker": "Record picker",
|
||||
"Toggles the subfield mode": "Toggles the subfield mode",
|
||||
"Selector mode": "Selector mode",
|
||||
"Subtable mode": "Sub-table mode",
|
||||
"Subform mode": "Sub-form mode",
|
||||
"Edit block title": "Edit block title",
|
||||
"Block title": "Block title",
|
||||
"Pattern": "Pattern",
|
||||
"Operator": "Operator",
|
||||
"Editable": "Editable",
|
||||
"Readonly": "Readonly",
|
||||
"Easy-reading": "Easy-reading",
|
||||
"Add filter": "Add filter",
|
||||
"Add filter group": "Add filter group",
|
||||
"Comparision": "Comparision",
|
||||
"is": "is",
|
||||
"is not": "is not",
|
||||
"contains": "contains",
|
||||
"does not contain": "does not contain",
|
||||
"starts with": "starts with",
|
||||
"not starts with": "not starts with",
|
||||
"ends with": "ends with",
|
||||
"not ends with": "not ends with",
|
||||
"is empty": "is empty",
|
||||
"is not empty": "is not empty",
|
||||
"Edit chart": "Edit chart",
|
||||
"Add text": "Add text",
|
||||
"Filterable fields": "Filterable fields",
|
||||
"Edit button": "Edit button",
|
||||
"Hide": "Hide",
|
||||
"Enable actions": "Enable actions",
|
||||
"Import": "Import",
|
||||
"Export": "Export",
|
||||
"Customize": "Customize",
|
||||
"Custom": "Custom",
|
||||
"Function": "Function",
|
||||
"Popup form": "Popup form",
|
||||
"Flexible popup": "Flexible popup",
|
||||
"Configure actions": "Configure actions",
|
||||
"Display order number": "Display order number",
|
||||
"Enable drag and drop sorting": "Enable drag and drop sorting",
|
||||
"Triggered when the row is clicked": "Triggered when the row is clicked",
|
||||
"Add tab": "Add tab",
|
||||
"Disable tabs": "Disable tabs",
|
||||
"Details": "Details",
|
||||
"Edit tab": "Edit tab",
|
||||
"Relationship blocks": "Relationship blocks",
|
||||
"Select record": "Select record",
|
||||
"Display name": "Display name",
|
||||
"Select icon": "Select icon",
|
||||
"Custom column name": "Custom column name",
|
||||
"Edit description": "Edit description",
|
||||
"Required": "Required",
|
||||
"Unique": "Unique",
|
||||
"Label field": "Label field",
|
||||
"Default is the ID field": "Default is the ID field",
|
||||
"Set default sorting rules": "Set default sorting rules",
|
||||
"Set validation rules": "Set validation rules",
|
||||
"Max length": "Max length",
|
||||
"Min length": "Min length",
|
||||
"Maximum": "Maximum",
|
||||
"Minimum": "Minimum",
|
||||
"Max length must greater than min length": "Max length must greater than min length",
|
||||
"Min length must less than max length": "Min length must less than max length",
|
||||
"Maximum must greater than minimum": "Maximum must greater than minimum",
|
||||
"Minimum must less than maximum": "Minimum must less than maximum",
|
||||
"Validation rule": "Validation rule",
|
||||
"Add validation rule": "Add validation rule",
|
||||
"Format": "Format",
|
||||
"Regular expression": "Pattern",
|
||||
"Error message": "Error message",
|
||||
"Length": "Length",
|
||||
"The field value cannot be greater than ": "The field value cannot be greater than ",
|
||||
"The field value cannot be less than ": "The field value cannot be less than ",
|
||||
"The field value is not an integer number": "The field value is not an integer number",
|
||||
"Set default value": "Set default value",
|
||||
"Default value": "Default value",
|
||||
"is before": "is before",
|
||||
"is after": "is after",
|
||||
"is on or after": "is on or after",
|
||||
"is on or before": "is on or before",
|
||||
"is between": "is between",
|
||||
"Upload": "Upload",
|
||||
"Select level": "Select level",
|
||||
"Province": "Province",
|
||||
"City": "City",
|
||||
"Area": "Area",
|
||||
"Street": "Street",
|
||||
"Village": "Village",
|
||||
"Must select to the last level": "Must select to the last level",
|
||||
"Move {{title}} to": "Move {{title}} to",
|
||||
"Target position": "Target position",
|
||||
"After": "After",
|
||||
"Before": "Before",
|
||||
"Add {{type}} before \"{{title}}\"": "Add {{type}} before \"{{title}}\"",
|
||||
"Add {{type}} after \"{{title}}\"": "Add {{type}} after \"{{title}}\"",
|
||||
"Add {{type}} in \"{{title}}\"": "Add {{type}} in \"{{title}}\"",
|
||||
"Original name": "Original name",
|
||||
"Custom name": "Custom name",
|
||||
"Custom Title": "Custom Title",
|
||||
"Options": "Options",
|
||||
"Option value": "Option value",
|
||||
"Option label": "Option label",
|
||||
"Color": "Color",
|
||||
"Add option": "Add option",
|
||||
"Related collection": "Related collection",
|
||||
"Allow linking to multiple records": "Allow linking to multiple records",
|
||||
"Allow uploading multiple files": "Allow uploading multiple files",
|
||||
"Configure calendar": "Configure calendar",
|
||||
"Title field": "Title field",
|
||||
"Custom title": "Custom title",
|
||||
"Daily": "Daily",
|
||||
"Weekly": "Weekly",
|
||||
"Monthly": "Monthly",
|
||||
"Yearly": "Yearly",
|
||||
"Repeats": "Repeats",
|
||||
"Show lunar": "Show lunar",
|
||||
"Start date field": "Start date field",
|
||||
"End date field": "End date field",
|
||||
"Navigate": "Navigate",
|
||||
"Title": "Title",
|
||||
"Description": "Description",
|
||||
"Select view": "Select view",
|
||||
"Reset": "Reset",
|
||||
"Importable fields": "Importable fields",
|
||||
"Exportable fields": "Exportable fields",
|
||||
"Saved successfully": "Saved successfully",
|
||||
"Nickname": "Nickname",
|
||||
"Sign in": "Sign in",
|
||||
"Sign in via account": "Sign in via account",
|
||||
"Sign in via phone": "Sign in via phone",
|
||||
"Create an account": "Create an account",
|
||||
"Sign up": "Sign up",
|
||||
"Confirm password": "Confirm password",
|
||||
"Log in with an existing account": "Log in with an existing account",
|
||||
"Signed up successfully. It will jump to the login page.": "Signed up successfully. It will jump to the login page.",
|
||||
"Password mismatch": "Password mismatch",
|
||||
"Users": "Users",
|
||||
"Verification code": "Verification code",
|
||||
"Send code": "Send code",
|
||||
"Retry after {{count}} seconds": "Retry after {{count}} seconds",
|
||||
"Roles": "Roles",
|
||||
"Add role": "Add role",
|
||||
"Role name": "Role name",
|
||||
"Configure": "Configure",
|
||||
"Configure permissions": "Configure permissions",
|
||||
"Edit role": "Edit role",
|
||||
"Action permissions": "Action permissions",
|
||||
"Menu permissions": "Menu permissions",
|
||||
"Menu item name": "Menu item name",
|
||||
"Allow access": "Allow access",
|
||||
"Action name": "Action name",
|
||||
"Allow action": "Allow action",
|
||||
"Action scope": "Action scope",
|
||||
"Operate on new data": "Operate on new data",
|
||||
"Operate on existing data": "Operate on existing data",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"Red": "Red",
|
||||
"Magenta": "Magenta",
|
||||
"Volcano": "Volcano",
|
||||
"Orange": "Orange",
|
||||
"Gold": "Gold",
|
||||
"Lime": "Lime",
|
||||
"Green": "Green",
|
||||
"Cyan": "Cyan",
|
||||
"Blue": "Blue",
|
||||
"Geek blue": "Geek blue",
|
||||
"Purple": "Purple",
|
||||
"Default": "Default",
|
||||
"Add card": "Add card",
|
||||
"edit title": "edit title",
|
||||
"Turn pages": "Turn pages",
|
||||
"Others": "Others",
|
||||
"Save as template": "Save as template",
|
||||
"Save as block template": "Save as block template",
|
||||
"Block templates": "Block templates",
|
||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||
"Template name": "Template name",
|
||||
"Block type": "Block type",
|
||||
"No blocks to connect": "No blocks to connect",
|
||||
"Action column": "Action column",
|
||||
"Records per page": "Records per page",
|
||||
"(Fields only)": "(Fields only)",
|
||||
"Button title": "Button title",
|
||||
"Button icon": "Button icon",
|
||||
"Submitted successfully": "Submitted successfully",
|
||||
"Operation succeeded": "Operation succeeded",
|
||||
"Operation failed": "Operation failed",
|
||||
"Open mode": "Open mode",
|
||||
"Popup size": "Popup size",
|
||||
"Small": "Small",
|
||||
"Middle": "Middle",
|
||||
"Large": "Large",
|
||||
"Menu item title": "Menu item title",
|
||||
"Menu item icon": "Menu item icon",
|
||||
"Target": "Target",
|
||||
"Position": "Position",
|
||||
"Insert before": "Insert before",
|
||||
"Insert after": "Insert after",
|
||||
"UI Editor": "UI Editor",
|
||||
"ASC": "ASC",
|
||||
"DESC": "DESC",
|
||||
"Add sort field": "Add sort field",
|
||||
"ID": "ID",
|
||||
"Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.",
|
||||
"Drawer": "Drawer",
|
||||
"Dialog": "Dialog",
|
||||
"Delete action": "Delete action",
|
||||
"Custom column title": "Custom column title",
|
||||
Collapse: 'Collapse',
|
||||
'Select data source': 'Select data source',
|
||||
Calendar: 'Calendar',
|
||||
'Delete events': 'Delete events',
|
||||
'This event': 'This event',
|
||||
'This and following events': 'This and following events',
|
||||
'All events': 'All events',
|
||||
'Delete this event?': 'Delete this event?',
|
||||
'Delete Event': 'Delete Event',
|
||||
Kanban: 'Kanban',
|
||||
Gantt: 'Gantt',
|
||||
'Create gantt block': 'Create gantt block',
|
||||
'Progress field': 'Progress field',
|
||||
'Time scale': 'Time scale',
|
||||
Hour: 'Hour',
|
||||
'Quarter of day': 'Quarter of day',
|
||||
'Half of day': 'Half of day',
|
||||
Year: 'Year',
|
||||
QuarterYear: 'QuarterYear',
|
||||
'Select grouping field': 'Select grouping field',
|
||||
Media: 'Media',
|
||||
Markdown: 'Markdown',
|
||||
Wysiwyg: 'Wysiwyg',
|
||||
'Chart blocks': 'Chart blocks',
|
||||
'Column chart': 'Column chart',
|
||||
'Bar chart': 'Bar chart',
|
||||
'Line chart': 'Line chart',
|
||||
'Pie chart': 'Pie chart',
|
||||
'Area chart': 'Area chart',
|
||||
'Other chart': 'Other chart',
|
||||
'Other blocks': 'Other blocks',
|
||||
'In configuration': 'In configuration',
|
||||
'Chart title': 'Chart title',
|
||||
'Chart type': 'Chart type',
|
||||
'Chart config': 'Chart config',
|
||||
Templates: 'Templates',
|
||||
'Select template': 'Select template',
|
||||
'Action logs': 'Action logs',
|
||||
'Create template': 'Create template',
|
||||
'Edit markdown': 'Edit markdown',
|
||||
'Add block': 'Add block',
|
||||
'Add new': 'Add new',
|
||||
'Add record': 'Add record',
|
||||
'Add child': 'Add child',
|
||||
'Collapse all': 'Collapse all',
|
||||
'Expand all': 'Expand all',
|
||||
'Expand/Collapse': 'Expand/Collapse',
|
||||
'Default collapse': 'Default collapse',
|
||||
'Tree table': 'Tree table',
|
||||
'Custom field display name': 'Custom field display name',
|
||||
'Display fields': 'Display collection fields',
|
||||
'Edit record': 'Edit record',
|
||||
'Delete menu item': 'Delete menu item',
|
||||
'Add page': 'Add page',
|
||||
'Add group': 'Add group',
|
||||
'Add link': 'Add link',
|
||||
'Insert above': 'Insert above',
|
||||
'Insert below': 'Insert below',
|
||||
Save: 'Save',
|
||||
'Delete block': 'Delete block',
|
||||
'Are you sure you want to delete it?': 'Are you sure you want to delete it?',
|
||||
'This is a demo text, **supports Markdown syntax**.': 'This is a demo text, **supports Markdown syntax**.',
|
||||
Filter: 'Filter',
|
||||
'Connect data blocks': 'Connect data blocks',
|
||||
'Action type': 'Action type',
|
||||
Actions: 'Actions',
|
||||
Insert: 'Insert',
|
||||
'Insert if not exists': 'Insert if not exists',
|
||||
'Insert if not exists, or update': 'Insert if not exists, or update',
|
||||
'Determine whether a record exists by the following fields':
|
||||
'Determine whether a record exists by the following fields',
|
||||
Update: 'Update',
|
||||
View: 'View',
|
||||
'View record': 'View record',
|
||||
Refresh: 'Refresh',
|
||||
'Data changes': 'Data changes',
|
||||
'Field name': 'Field name',
|
||||
'Before change': 'Before change',
|
||||
'After change': 'After change',
|
||||
'Delete record': 'Delete record',
|
||||
'Create collection': 'Create collection',
|
||||
'Collection display name': 'Collection display name',
|
||||
'Collection name': 'Collection name',
|
||||
Inherits: 'Inherits',
|
||||
'Generate ID field automatically': 'Generate ID field automatically',
|
||||
'Store the creation user of each record': 'Store the creation user of each record',
|
||||
'Store the last update user of each record': 'Store the last update user of each record',
|
||||
'Store the creation time of each record': 'Store the creation time of each record',
|
||||
'Store the last update time of each record': 'Store the last update time of each record',
|
||||
'More options': 'More options',
|
||||
'Records can be sorted': 'Records can be sorted',
|
||||
'Calendar collection': 'Calendar collection',
|
||||
'General collection': 'General collection',
|
||||
'Connect to database view': 'Connect to database view',
|
||||
'Source collections': 'Source collections',
|
||||
'Field source': 'Field source',
|
||||
Preview: 'Preview',
|
||||
'Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.':
|
||||
'Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.',
|
||||
Edit: 'Edit',
|
||||
'Edit collection': 'Edit collection',
|
||||
'Configure fields': 'Configure fields',
|
||||
'Configure columns': 'Configure columns',
|
||||
'Edit field': 'Edit field',
|
||||
Override: 'Override',
|
||||
'Override field': 'Override field',
|
||||
'Configure fields of {{title}}': 'Configure fields of {{title}}',
|
||||
'Association fields filter': 'Association fields filter',
|
||||
'PK & FK fields': 'PK & FK fields',
|
||||
'Association fields': 'Association fields',
|
||||
'Choices fields': 'Choices fields',
|
||||
'System fields': 'System fields',
|
||||
'General fields': 'General fields',
|
||||
'Inherited fields': 'Inherited fields',
|
||||
'Parent collection fields': 'Parent collection fields',
|
||||
Basic: 'Basic',
|
||||
'Single line text': 'Single line text',
|
||||
'Long text': 'Long text',
|
||||
Phone: 'Phone',
|
||||
Email: 'Email',
|
||||
Number: 'Number',
|
||||
Integer: 'Integer',
|
||||
Percent: 'Percent',
|
||||
Password: 'Password',
|
||||
'Advanced type': 'Advanced',
|
||||
Formula: 'Formula',
|
||||
'Formula description': 'Compute a value in each record based on other fields in the same record.',
|
||||
Choices: 'Choices',
|
||||
Checkbox: 'Checkbox',
|
||||
'Single select': 'Single select',
|
||||
'Multiple select': 'Multiple select',
|
||||
'Radio group': 'Radio group',
|
||||
'Checkbox group': 'Checkbox group',
|
||||
'China region': 'China region',
|
||||
'Date & Time': 'Date & Time',
|
||||
Datetime: 'Datetime',
|
||||
Relation: 'Relation',
|
||||
'Link to': 'Link to',
|
||||
'Link to description':
|
||||
'Used to create collection relationships quickly and compatible with most common scenarios. Suitable for non-developer use. When present as a field, it is a drop-down selection used to select records from the target collection. Once created, it will simultaneously generate the associated fields of the current collection in the target collection.',
|
||||
'Sub-table': 'Sub-table',
|
||||
'Sub-details': 'Sub-details',
|
||||
'Sub-form(Popover)': 'Sub-form(Popover)',
|
||||
'System info': 'System info',
|
||||
'Created at': 'Created at',
|
||||
'Last updated at': 'Last updated at',
|
||||
'Created by': 'Created by',
|
||||
'Last updated by': 'Last updated by',
|
||||
'Add field': 'Add field',
|
||||
'Field display name': 'Field display name',
|
||||
'Field type': 'Field type',
|
||||
'Field interface': 'Field interface',
|
||||
'Date format': 'Date format',
|
||||
'Year/Month/Day': 'Year/Month/Day',
|
||||
'Year-Month-Day': 'Year-Month-Day',
|
||||
'Day/Month/Year': 'Day/Month/Year',
|
||||
'Show time': 'Show time',
|
||||
'Time format': 'Time format',
|
||||
'12 hour': '12 hour',
|
||||
'24 hour': '24 hour',
|
||||
'Relationship type': 'Relationship type',
|
||||
'Inverse relationship type': 'Inverse relationship type',
|
||||
'Source collection': 'Source collection',
|
||||
'Source key': 'Source key',
|
||||
'Target collection': 'Target collection',
|
||||
'Through collection': 'Through collection',
|
||||
'Target key': 'Target key',
|
||||
'Foreign key': 'Foreign key',
|
||||
'One to one': 'One to one',
|
||||
'One to many': 'One to many',
|
||||
'Many to one': 'Many to one',
|
||||
'Many to many': 'Many to many',
|
||||
'Foreign key 1': 'Foreign key 1',
|
||||
'Foreign key 2': 'Foreign key 2',
|
||||
'One to one description': 'Used to create one-to-one relationships. For example, a user has a profile.',
|
||||
'One to many description':
|
||||
'Used to create a one-to-many relationship. For example, a country will have many cities and a city can only be in one country. When present as a field, it is a sub-table that displays the records of the associated collection. When created, a Many-to-one field is automatically generated in the associated collection.',
|
||||
'Many to one description':
|
||||
'Used to create many-to-one relationships. For example, a city can belong to only one country and a country can have many cities. When present as a field, it is a drop-down selection used to select record from the associated collection. Once created, a One-to-many field is automatically generated in the associated collection.',
|
||||
'Many to many description':
|
||||
'Used to create many-to-many relationships. For example, a student will have many teachers and a teacher will have many students. When present as a field, it is a drop-down selection used to select records from the associated collection.',
|
||||
'Generated automatically if left blank': 'Generated automatically if left blank',
|
||||
'Display association fields': 'Display association fields',
|
||||
'Display field title': 'Display field title',
|
||||
'Field component': 'Field component',
|
||||
'Allow multiple': 'Allow multiple',
|
||||
'Quick upload': 'Quick upload',
|
||||
'Select file': 'Select file',
|
||||
Subtable: 'Sub-table',
|
||||
'Sub-form': 'Sub-form',
|
||||
'Field mode': 'Field mode',
|
||||
'Allow add new data': 'Allow add new data',
|
||||
'Record picker': 'Record picker',
|
||||
'Toggles the subfield mode': 'Toggles the subfield mode',
|
||||
'Selector mode': 'Selector mode',
|
||||
'Subtable mode': 'Sub-table mode',
|
||||
'Subform mode': 'Sub-form mode',
|
||||
'Edit block title': 'Edit block title',
|
||||
'Block title': 'Block title',
|
||||
Pattern: 'Pattern',
|
||||
Operator: 'Operator',
|
||||
Editable: 'Editable',
|
||||
Readonly: 'Readonly',
|
||||
'Easy-reading': 'Easy-reading',
|
||||
'Add filter': 'Add filter',
|
||||
'Add filter group': 'Add filter group',
|
||||
Comparision: 'Comparision',
|
||||
is: 'is',
|
||||
'is not': 'is not',
|
||||
contains: 'contains',
|
||||
'does not contain': 'does not contain',
|
||||
'starts with': 'starts with',
|
||||
'not starts with': 'not starts with',
|
||||
'ends with': 'ends with',
|
||||
'not ends with': 'not ends with',
|
||||
'is empty': 'is empty',
|
||||
'is not empty': 'is not empty',
|
||||
'Edit chart': 'Edit chart',
|
||||
'Add text': 'Add text',
|
||||
'Filterable fields': 'Filterable fields',
|
||||
'Edit button': 'Edit button',
|
||||
Hide: 'Hide',
|
||||
'Enable actions': 'Enable actions',
|
||||
Import: 'Import',
|
||||
Export: 'Export',
|
||||
Customize: 'Customize',
|
||||
Custom: 'Custom',
|
||||
Function: 'Function',
|
||||
'Popup form': 'Popup form',
|
||||
'Flexible popup': 'Flexible popup',
|
||||
'Configure actions': 'Configure actions',
|
||||
'Display order number': 'Display order number',
|
||||
'Enable drag and drop sorting': 'Enable drag and drop sorting',
|
||||
'Triggered when the row is clicked': 'Triggered when the row is clicked',
|
||||
'Add tab': 'Add tab',
|
||||
'Disable tabs': 'Disable tabs',
|
||||
Details: 'Details',
|
||||
'Edit tab': 'Edit tab',
|
||||
'Relationship blocks': 'Relationship blocks',
|
||||
'Select record': 'Select record',
|
||||
'Display name': 'Display name',
|
||||
'Select icon': 'Select icon',
|
||||
'Custom column name': 'Custom column name',
|
||||
'Edit description': 'Edit description',
|
||||
Required: 'Required',
|
||||
Unique: 'Unique',
|
||||
'Label field': 'Label field',
|
||||
'Default is the ID field': 'Default is the ID field',
|
||||
'Set default sorting rules': 'Set default sorting rules',
|
||||
'Set validation rules': 'Set validation rules',
|
||||
'Max length': 'Max length',
|
||||
'Min length': 'Min length',
|
||||
Maximum: 'Maximum',
|
||||
Minimum: 'Minimum',
|
||||
'Max length must greater than min length': 'Max length must greater than min length',
|
||||
'Min length must less than max length': 'Min length must less than max length',
|
||||
'Maximum must greater than minimum': 'Maximum must greater than minimum',
|
||||
'Minimum must less than maximum': 'Minimum must less than maximum',
|
||||
'Validation rule': 'Validation rule',
|
||||
'Add validation rule': 'Add validation rule',
|
||||
Format: 'Format',
|
||||
'Regular expression': 'Pattern',
|
||||
'Error message': 'Error message',
|
||||
Length: 'Length',
|
||||
'The field value cannot be greater than ': 'The field value cannot be greater than ',
|
||||
'The field value cannot be less than ': 'The field value cannot be less than ',
|
||||
'The field value is not an integer number': 'The field value is not an integer number',
|
||||
'Set default value': 'Set default value',
|
||||
'Default value': 'Default value',
|
||||
'is before': 'is before',
|
||||
'is after': 'is after',
|
||||
'is on or after': 'is on or after',
|
||||
'is on or before': 'is on or before',
|
||||
'is between': 'is between',
|
||||
Upload: 'Upload',
|
||||
'Select level': 'Select level',
|
||||
Province: 'Province',
|
||||
City: 'City',
|
||||
Area: 'Area',
|
||||
Street: 'Street',
|
||||
Village: 'Village',
|
||||
'Must select to the last level': 'Must select to the last level',
|
||||
'Move {{title}} to': 'Move {{title}} to',
|
||||
'Target position': 'Target position',
|
||||
After: 'After',
|
||||
Before: 'Before',
|
||||
'Add {{type}} before "{{title}}"': 'Add {{type}} before "{{title}}"',
|
||||
'Add {{type}} after "{{title}}"': 'Add {{type}} after "{{title}}"',
|
||||
'Add {{type}} in "{{title}}"': 'Add {{type}} in "{{title}}"',
|
||||
'Original name': 'Original name',
|
||||
'Custom name': 'Custom name',
|
||||
'Custom Title': 'Custom Title',
|
||||
Options: 'Options',
|
||||
'Option value': 'Option value',
|
||||
'Option label': 'Option label',
|
||||
Color: 'Color',
|
||||
'Add option': 'Add option',
|
||||
'Related collection': 'Related collection',
|
||||
'Allow linking to multiple records': 'Allow linking to multiple records',
|
||||
'Allow uploading multiple files': 'Allow uploading multiple files',
|
||||
'Configure calendar': 'Configure calendar',
|
||||
'Title field': 'Title field',
|
||||
'Custom title': 'Custom title',
|
||||
Daily: 'Daily',
|
||||
Weekly: 'Weekly',
|
||||
Monthly: 'Monthly',
|
||||
Yearly: 'Yearly',
|
||||
Repeats: 'Repeats',
|
||||
'Show lunar': 'Show lunar',
|
||||
'Start date field': 'Start date field',
|
||||
'End date field': 'End date field',
|
||||
Navigate: 'Navigate',
|
||||
Title: 'Title',
|
||||
Description: 'Description',
|
||||
'Select view': 'Select view',
|
||||
Reset: 'Reset',
|
||||
'Importable fields': 'Importable fields',
|
||||
'Exportable fields': 'Exportable fields',
|
||||
'Saved successfully': 'Saved successfully',
|
||||
Nickname: 'Nickname',
|
||||
'Sign in': 'Sign in',
|
||||
'Sign in via account': 'Sign in via account',
|
||||
'Sign in via phone': 'Sign in via phone',
|
||||
'Create an account': 'Create an account',
|
||||
'Sign up': 'Sign up',
|
||||
'Confirm password': 'Confirm password',
|
||||
'Log in with an existing account': 'Log in with an existing account',
|
||||
'Signed up successfully. It will jump to the login page.': 'Signed up successfully. It will jump to the login page.',
|
||||
'Password mismatch': 'Password mismatch',
|
||||
Users: 'Users',
|
||||
'Verification code': 'Verification code',
|
||||
'Send code': 'Send code',
|
||||
'Retry after {{count}} seconds': 'Retry after {{count}} seconds',
|
||||
Roles: 'Roles',
|
||||
'Add role': 'Add role',
|
||||
'Role name': 'Role name',
|
||||
Configure: 'Configure',
|
||||
'Configure permissions': 'Configure permissions',
|
||||
'Edit role': 'Edit role',
|
||||
'Action permissions': 'Action permissions',
|
||||
'Menu permissions': 'Menu permissions',
|
||||
'Menu item name': 'Menu item name',
|
||||
'Allow access': 'Allow access',
|
||||
'Action name': 'Action name',
|
||||
'Allow action': 'Allow action',
|
||||
'Action scope': 'Action scope',
|
||||
'Operate on new data': 'Operate on new data',
|
||||
'Operate on existing data': 'Operate on existing data',
|
||||
Yes: 'Yes',
|
||||
No: 'No',
|
||||
Red: 'Red',
|
||||
Magenta: 'Magenta',
|
||||
Volcano: 'Volcano',
|
||||
Orange: 'Orange',
|
||||
Gold: 'Gold',
|
||||
Lime: 'Lime',
|
||||
Green: 'Green',
|
||||
Cyan: 'Cyan',
|
||||
Blue: 'Blue',
|
||||
'Geek blue': 'Geek blue',
|
||||
Purple: 'Purple',
|
||||
Default: 'Default',
|
||||
'Add card': 'Add card',
|
||||
'edit title': 'edit title',
|
||||
'Turn pages': 'Turn pages',
|
||||
Others: 'Others',
|
||||
'Save as template': 'Save as template',
|
||||
'Save as block template': 'Save as block template',
|
||||
'Block templates': 'Block templates',
|
||||
'Convert reference to duplicate': 'Convert reference to duplicate',
|
||||
'Template name': 'Template name',
|
||||
'Block type': 'Block type',
|
||||
'No blocks to connect': 'No blocks to connect',
|
||||
'Action column': 'Action column',
|
||||
'Records per page': 'Records per page',
|
||||
'(Fields only)': '(Fields only)',
|
||||
'Button title': 'Button title',
|
||||
'Button icon': 'Button icon',
|
||||
'Submitted successfully': 'Submitted successfully',
|
||||
'Operation succeeded': 'Operation succeeded',
|
||||
'Operation failed': 'Operation failed',
|
||||
'Open mode': 'Open mode',
|
||||
'Popup size': 'Popup size',
|
||||
Small: 'Small',
|
||||
Middle: 'Middle',
|
||||
Large: 'Large',
|
||||
'Menu item title': 'Menu item title',
|
||||
'Menu item icon': 'Menu item icon',
|
||||
Target: 'Target',
|
||||
Position: 'Position',
|
||||
'Insert before': 'Insert before',
|
||||
'Insert after': 'Insert after',
|
||||
'UI Editor': 'UI Editor',
|
||||
ASC: 'ASC',
|
||||
DESC: 'DESC',
|
||||
'Add sort field': 'Add sort field',
|
||||
ID: 'ID',
|
||||
'Identifier for program usage. Support letters, numbers and underscores, must start with an letter.':
|
||||
'Identifier for program usage. Support letters, numbers and underscores, must start with an letter.',
|
||||
Drawer: 'Drawer',
|
||||
Dialog: 'Dialog',
|
||||
'Delete action': 'Delete action',
|
||||
'Custom column title': 'Custom column title',
|
||||
'Column title': 'column title',
|
||||
"Original title: ": "Original title: ",
|
||||
"Delete table column": "Delete table column",
|
||||
"Skip required validation": "Skip required validation",
|
||||
"Form values": "Form values",
|
||||
"Fields values": "Fields values",
|
||||
'Original title: ': 'Original title: ',
|
||||
'Delete table column': 'Delete table column',
|
||||
'Skip required validation': 'Skip required validation',
|
||||
'Form values': 'Form values',
|
||||
'Fields values': 'Fields values',
|
||||
'The field has been deleted': 'The field has been deleted',
|
||||
"When submitting the following fields, the saved values are": "When submitting the following fields, the saved values are",
|
||||
"After successful submission": "After successful submission",
|
||||
"Then": "Then",
|
||||
"Stay on current page": "Stay on current page",
|
||||
"Redirect to": "Redirect to",
|
||||
"Save action": "Save action",
|
||||
"Exists": "Exists",
|
||||
"Add condition": "Add condition",
|
||||
"Add condition group": "Add condition group",
|
||||
"exists": "exists",
|
||||
"not exists": "not exists",
|
||||
"=": "=",
|
||||
"≠": "≠",
|
||||
">": ">",
|
||||
"≥": "≥",
|
||||
"<": "<",
|
||||
"≤": "≤",
|
||||
"Role UID": "Role UID",
|
||||
"Precision": "Precision",
|
||||
"Formula mode": "Formula mode",
|
||||
"Expression": "Expression",
|
||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.",
|
||||
"Formula error.": "Formula error.",
|
||||
"Rich Text": "Rich Text",
|
||||
"Junction collection": "Junction collection",
|
||||
"Leave it blank, unless you need a custom intermediate table": "Leave it blank, unless you need a custom intermediate table",
|
||||
"Fields": "Fields",
|
||||
"Edit field title": "Edit field title",
|
||||
"Field title": "Field title",
|
||||
"Original field title: ": "Original field title: ",
|
||||
"Edit tooltip": "Edit tooltip",
|
||||
"Delete field": "Delete field",
|
||||
"Select collection": "Select collection",
|
||||
"Blank block": "Blank block",
|
||||
"Duplicate template": "Duplicate template",
|
||||
"Reference template": "Reference template",
|
||||
"Create calendar block": "Create calendar block",
|
||||
"Create kanban block": "Create kanban block",
|
||||
"Grouping field": "Grouping field",
|
||||
"Single select and radio fields can be used as the grouping field": "Single select and radio fields can be used as the grouping field",
|
||||
"Tab name": "Tab name",
|
||||
"Current record blocks": "Current record blocks",
|
||||
"Popup message": "Popup message",
|
||||
"Delete role": "Delete role",
|
||||
"Role display name": "Role display name",
|
||||
"Default role": "Default role",
|
||||
"All collections use general action permissions by default; permission configured individually will override the default one.": "All collections use general action permissions by default; permission configured individually will override the default one.",
|
||||
"Allows configuration of the whole system, including UI, collections, permissions, etc.": "Allows configuration of the whole system, including UI, collections, permissions, etc.",
|
||||
"New menu items are allowed to be accessed by default.": "New menu items are allowed to be accessed by default.",
|
||||
"Global permissions": "Global permissions",
|
||||
"General permissions": "General permissions",
|
||||
"Global action permissions": "Global action permissions",
|
||||
"General action permissions": "General action permissions",
|
||||
"Plugin settings permissions": "Plugin settings permissions",
|
||||
"Allow to desgin pages": "Allow to desgin pages",
|
||||
"Allow to manage plugins": "Allow to manage plugins",
|
||||
"Allow to configure plugins": "Allow to configure plugins",
|
||||
"Allows to configure interface": "Allows to configure interface",
|
||||
"Allows to install, activate, disable plugins": "Allows to install, activate, disable plugins",
|
||||
"Allows to configure plugins": "Allows to configure plugins",
|
||||
"Action display name": "Action display name",
|
||||
"Allow": "Allow",
|
||||
"Data scope": "Data scope",
|
||||
"Action on new records": "Action on new records",
|
||||
"Action on existing records": "Action on existing records",
|
||||
"All records": "All records",
|
||||
"Own records": "Own records",
|
||||
"Permission policy": "Permission policy",
|
||||
"Individual": "Individual",
|
||||
"General": "General",
|
||||
"Accessible": "Accessible",
|
||||
"Configure permission": "Configure permission",
|
||||
"Action permission": "Action permission",
|
||||
"Field permission": "Field permission",
|
||||
"Scope name": "Scope name",
|
||||
"Unsaved changes": "Unsaved changes",
|
||||
'When submitting the following fields, the saved values are':
|
||||
'When submitting the following fields, the saved values are',
|
||||
'After successful submission': 'After successful submission',
|
||||
Then: 'Then',
|
||||
'Stay on current page': 'Stay on current page',
|
||||
'Redirect to': 'Redirect to',
|
||||
'Save action': 'Save action',
|
||||
Exists: 'Exists',
|
||||
'Add condition': 'Add condition',
|
||||
'Add condition group': 'Add condition group',
|
||||
exists: 'exists',
|
||||
'not exists': 'not exists',
|
||||
'=': '=',
|
||||
'≠': '≠',
|
||||
'>': '>',
|
||||
'≥': '≥',
|
||||
'<': '<',
|
||||
'≤': '≤',
|
||||
'Role UID': 'Role UID',
|
||||
Precision: 'Precision',
|
||||
'Formula mode': 'Formula mode',
|
||||
Expression: 'Expression',
|
||||
'Input +, -, *, /, ( ) to calculate, input @ to open field variables.':
|
||||
'Input +, -, *, /, ( ) to calculate, input @ to open field variables.',
|
||||
'Formula error.': 'Formula error.',
|
||||
'Rich Text': 'Rich Text',
|
||||
'Junction collection': 'Junction collection',
|
||||
'Leave it blank, unless you need a custom intermediate table':
|
||||
'Leave it blank, unless you need a custom intermediate table',
|
||||
Fields: 'Fields',
|
||||
'Edit field title': 'Edit field title',
|
||||
'Field title': 'Field title',
|
||||
'Original field title: ': 'Original field title: ',
|
||||
'Edit tooltip': 'Edit tooltip',
|
||||
'Delete field': 'Delete field',
|
||||
'Select collection': 'Select collection',
|
||||
'Blank block': 'Blank block',
|
||||
'Duplicate template': 'Duplicate template',
|
||||
'Reference template': 'Reference template',
|
||||
'Create calendar block': 'Create calendar block',
|
||||
'Create kanban block': 'Create kanban block',
|
||||
'Grouping field': 'Grouping field',
|
||||
'Single select and radio fields can be used as the grouping field':
|
||||
'Single select and radio fields can be used as the grouping field',
|
||||
'Tab name': 'Tab name',
|
||||
'Current record blocks': 'Current record blocks',
|
||||
'Popup message': 'Popup message',
|
||||
'Delete role': 'Delete role',
|
||||
'Role display name': 'Role display name',
|
||||
'Default role': 'Default role',
|
||||
'All collections use general action permissions by default; permission configured individually will override the default one.':
|
||||
'All collections use general action permissions by default; permission configured individually will override the default one.',
|
||||
'Allows configuration of the whole system, including UI, collections, permissions, etc.':
|
||||
'Allows configuration of the whole system, including UI, collections, permissions, etc.',
|
||||
'New menu items are allowed to be accessed by default.': 'New menu items are allowed to be accessed by default.',
|
||||
'Global permissions': 'Global permissions',
|
||||
'General permissions': 'General permissions',
|
||||
'Global action permissions': 'Global action permissions',
|
||||
'General action permissions': 'General action permissions',
|
||||
'Plugin settings permissions': 'Plugin settings permissions',
|
||||
'Allow to desgin pages': 'Allow to desgin pages',
|
||||
'Allow to manage plugins': 'Allow to manage plugins',
|
||||
'Allow to configure plugins': 'Allow to configure plugins',
|
||||
'Allows to configure interface': 'Allows to configure interface',
|
||||
'Allows to install, activate, disable plugins': 'Allows to install, activate, disable plugins',
|
||||
'Allows to configure plugins': 'Allows to configure plugins',
|
||||
'Action display name': 'Action display name',
|
||||
Allow: 'Allow',
|
||||
'Data scope': 'Data scope',
|
||||
'Action on new records': 'Action on new records',
|
||||
'Action on existing records': 'Action on existing records',
|
||||
'All records': 'All records',
|
||||
'Own records': 'Own records',
|
||||
'Permission policy': 'Permission policy',
|
||||
Individual: 'Individual',
|
||||
General: 'General',
|
||||
Accessible: 'Accessible',
|
||||
'Configure permission': 'Configure permission',
|
||||
'Action permission': 'Action permission',
|
||||
'Field permission': 'Field permission',
|
||||
'Scope name': 'Scope name',
|
||||
'Unsaved changes': 'Unsaved changes',
|
||||
"Are you sure you don't want to save?": "Are you sure you don't want to save?",
|
||||
"Dragging": "Dragging",
|
||||
"Popup": "Popup",
|
||||
@ -765,4 +778,7 @@ export default {
|
||||
"Select all":"Select all",
|
||||
"Restart": "Restart",
|
||||
"Restart application": "Restart application",
|
||||
Execute: 'Execute',
|
||||
'Please use a valid SELECT or WITH AS statement': 'Please use a valid SELECT or WITH AS statement',
|
||||
'Please confirm the SQL statement first': 'Please confirm the SQL statement first',
|
||||
};
|
||||
|
@ -222,6 +222,7 @@ export default {
|
||||
'Records can be sorted': '可以对行记录进行排序',
|
||||
'Calendar collection': '日历数据表',
|
||||
'General collection': '普通数据表',
|
||||
'SQL collection': 'SQL数据表',
|
||||
'Connect to database view': '连接数据库视图',
|
||||
'Source collections': '来源数据表',
|
||||
'Field source': '来源字段',
|
||||
@ -709,50 +710,52 @@ export default {
|
||||
'Plugin manager': '插件管理器',
|
||||
Local: '本地',
|
||||
'Built-in': '内置',
|
||||
'Marketplace': '插件市场',
|
||||
"Add plugin": "新增插件",
|
||||
"Upgrade": "可供更新",
|
||||
"Plugin dependencies check failed": "插件依赖检查失败",
|
||||
"Remove": "移除",
|
||||
"Docs": "文档",
|
||||
"More details": "更多详情",
|
||||
"Upload new version": "上传新版",
|
||||
"Official plugin": "官方插件",
|
||||
"Version": "版本",
|
||||
"Npm package": "Npm 包",
|
||||
"Upload plugin": "上传插件",
|
||||
"Npm package name": "Npm 包名",
|
||||
"Add type": "新增方式",
|
||||
"Plugin source": "插件来源",
|
||||
"Changelog": "更新日志",
|
||||
"Dependencies check": "依赖检查",
|
||||
"Update plugin": "更新插件",
|
||||
"Installing": "安装中",
|
||||
"The deletion was successful.": "删除成功",
|
||||
"Plugin Zip File": "插件压缩包",
|
||||
"Compressed file url": "压缩包地址",
|
||||
"Last updated": "最后更新",
|
||||
"PackageName": "包名",
|
||||
"DisplayName": "显示名称",
|
||||
"Readme": "说明文档",
|
||||
"Dependencies compatibility check": "依赖兼容性检查",
|
||||
"Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "插件兼容性检查失败,你应该修改依赖版本以满足版本要求。",
|
||||
"Version range": "版本范围",
|
||||
"Plugin's version": "插件的版本",
|
||||
"Result": "结果",
|
||||
"No CHANGELOG.md file": "没有 CHANGELOG.md 日志",
|
||||
"No README.md file": "没有 README.md 文件",
|
||||
"Homepage": "主页",
|
||||
'Drag and drop the file here or click to upload, file size should not exceed 30M': '将文件拖放到此处或单击上传,文件大小不应超过 30M',
|
||||
"Dependencies check failed, can't enable.": "依赖检查失败,无法启用。",
|
||||
"Plugin starting...": "插件启动中...",
|
||||
"Plugin stopping...": "插件停止中...",
|
||||
"Are you sure to delete this plugin?": "确定要删除此插件吗?",
|
||||
"re-download file": "重新下载文件",
|
||||
"Not enabled": "未启用",
|
||||
"Search plugin": "搜索插件",
|
||||
"Author": "作者",
|
||||
"Plugin loading failed. Please check the server logs.": "插件加载失败,请检查服务器日志。",
|
||||
Marketplace: '插件市场',
|
||||
'Add plugin': '新增插件',
|
||||
Upgrade: '可供更新',
|
||||
'Plugin dependencies check failed': '插件依赖检查失败',
|
||||
Remove: '移除',
|
||||
Docs: '文档',
|
||||
'More details': '更多详情',
|
||||
'Upload new version': '上传新版',
|
||||
'Official plugin': '官方插件',
|
||||
Version: '版本',
|
||||
'Npm package': 'Npm 包',
|
||||
'Upload plugin': '上传插件',
|
||||
'Npm package name': 'Npm 包名',
|
||||
'Add type': '新增方式',
|
||||
'Plugin source': '插件来源',
|
||||
Changelog: '更新日志',
|
||||
'Dependencies check': '依赖检查',
|
||||
'Update plugin': '更新插件',
|
||||
Installing: '安装中',
|
||||
'The deletion was successful.': '删除成功',
|
||||
'Plugin Zip File': '插件压缩包',
|
||||
'Compressed file url': '压缩包地址',
|
||||
'Last updated': '最后更新',
|
||||
PackageName: '包名',
|
||||
DisplayName: '显示名称',
|
||||
Readme: '说明文档',
|
||||
'Dependencies compatibility check': '依赖兼容性检查',
|
||||
'Plugin dependencies check failed, you should change the dependent version to meet the version requirements.':
|
||||
'插件兼容性检查失败,你应该修改依赖版本以满足版本要求。',
|
||||
'Version range': '版本范围',
|
||||
"Plugin's version": '插件的版本',
|
||||
Result: '结果',
|
||||
'No CHANGELOG.md file': '没有 CHANGELOG.md 日志',
|
||||
'No README.md file': '没有 README.md 文件',
|
||||
Homepage: '主页',
|
||||
'Drag and drop the file here or click to upload, file size should not exceed 30M':
|
||||
'将文件拖放到此处或单击上传,文件大小不应超过 30M',
|
||||
"Dependencies check failed, can't enable.": '依赖检查失败,无法启用。',
|
||||
'Plugin starting...': '插件启动中...',
|
||||
'Plugin stopping...': '插件停止中...',
|
||||
'Are you sure to delete this plugin?': '确定要删除此插件吗?',
|
||||
're-download file': '重新下载文件',
|
||||
'Not enabled': '未启用',
|
||||
'Search plugin': '搜索插件',
|
||||
Author: '作者',
|
||||
'Plugin loading failed. Please check the server logs.': '插件加载失败,请检查服务器日志。',
|
||||
'Coming soon...': '敬请期待...',
|
||||
'All plugin settings': '所有插件配置',
|
||||
Bookmark: '书签',
|
||||
@ -863,11 +866,13 @@ export default {
|
||||
'Date display format': '日期显示格式',
|
||||
'Assign data scope for the template': '为模板指定数据范围',
|
||||
'Table selected records': '表格中选中的记录',
|
||||
|
||||
Tag: '标签',
|
||||
'Tag color field': '标签颜色字段',
|
||||
'Sync successfully': '同步成功',
|
||||
'Sync from form fields': '同步表单字段',
|
||||
'Select all': '全选',
|
||||
'Determine whether a record exists by the following fields': '通过以下字段判断记录是否存在',
|
||||
Execute: '执行',
|
||||
'Please use a valid SELECT or WITH AS statement': '请使用有效的 SELECT 或 WITH AS 语句',
|
||||
'Please confirm the SQL statement first': '请先确认 SQL 语句',
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ export const CalendarActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -25,7 +25,7 @@ export const CalendarFormActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -38,7 +38,7 @@ export const CalendarFormActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -51,7 +51,7 @@ export const CalendarFormActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -146,7 +146,7 @@ export const CalendarFormActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -171,9 +171,9 @@ export const CalendarFormActionInitializers = {
|
||||
useProps: '{{ useCustomizeRequestActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -31,9 +31,9 @@ export const GridCardActionInitializers = {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' && collection.template !== 'file') || collection?.writableView;
|
||||
return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -55,6 +55,10 @@ export const GridCardActionInitializers = {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
@ -163,9 +167,9 @@ export const GridCardItemActionInitializers = {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -178,6 +182,10 @@ export const GridCardItemActionInitializers = {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -261,9 +269,9 @@ export const GridCardItemActionInitializers = {
|
||||
useProps: '{{ useCustomizeUpdateActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -287,9 +295,9 @@ export const GridCardItemActionInitializers = {
|
||||
useProps: '{{ useCustomizeRequestActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -33,7 +33,11 @@ export const ListActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'file';
|
||||
return (
|
||||
(collection.template !== 'view' || collection?.writableView) &&
|
||||
collection.template !== 'file' &&
|
||||
collection.template !== 'sql'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -55,6 +59,10 @@ export const ListActionInitializers = {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
@ -165,7 +173,7 @@ export const ListItemActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -178,6 +186,10 @@ export const ListItemActionInitializers = {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -263,7 +275,7 @@ export const ListItemActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -289,7 +301,7 @@ export const ListItemActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2,7 +2,7 @@ import { useCollection } from '../..';
|
||||
|
||||
const useVisibleCollection = () => {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
};
|
||||
// 表单的操作配置
|
||||
export const ReadPrettyFormActionInitializers = {
|
||||
|
@ -17,10 +17,11 @@ const recursiveParent = (schema: Schema) => {
|
||||
const useRelationFields = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const collection = useCollection();
|
||||
let fields = [];
|
||||
|
||||
if (fieldSchema['x-initializer']) {
|
||||
fields = useCollection().fields;
|
||||
fields = collection.fields;
|
||||
} else {
|
||||
const collection = recursiveParent(fieldSchema.parent);
|
||||
if (collection) {
|
||||
@ -184,7 +185,18 @@ export const RecordBlockInitializers = (props: any) => {
|
||||
const hasFormChildCollection = formChildrenCollections?.length > 0;
|
||||
const detailChildrenCollections = getChildrenCollections(collection.name, true);
|
||||
const hasDetailChildCollection = detailChildrenCollections?.length > 0;
|
||||
const modifyFlag = (collection as any).template !== 'view' || collection?.writableView;
|
||||
const modifyFlag = (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
const detailChildren = useDetailCollections({
|
||||
...props,
|
||||
childrenCollections: detailChildrenCollections,
|
||||
collection,
|
||||
});
|
||||
const formChildren = useFormCollections({
|
||||
...props,
|
||||
childrenCollections: formChildrenCollections,
|
||||
collection,
|
||||
});
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
@ -202,11 +214,7 @@ export const RecordBlockInitializers = (props: any) => {
|
||||
key: 'details',
|
||||
type: 'subMenu',
|
||||
title: '{{t("Details")}}',
|
||||
children: useDetailCollections({
|
||||
...props,
|
||||
childrenCollections: detailChildrenCollections,
|
||||
collection,
|
||||
}),
|
||||
children: detailChildren,
|
||||
}
|
||||
: {
|
||||
key: 'details',
|
||||
@ -220,11 +228,7 @@ export const RecordBlockInitializers = (props: any) => {
|
||||
key: 'form',
|
||||
type: 'subMenu',
|
||||
title: '{{t("Form")}}',
|
||||
children: useFormCollections({
|
||||
...props,
|
||||
childrenCollections: formChildrenCollections,
|
||||
collection,
|
||||
}),
|
||||
children: formChildren,
|
||||
}
|
||||
: modifyFlag && {
|
||||
key: 'form',
|
||||
|
@ -101,7 +101,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
visible: () => {
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
|
||||
@ -115,7 +115,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
visible: () => {
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
collection.tree &&
|
||||
@ -139,7 +139,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
},
|
||||
visible: () => {
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -225,7 +225,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -250,7 +250,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -34,7 +34,7 @@ export const TableActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' && collection.template !== 'file') || collection?.writableView;
|
||||
return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -47,7 +47,7 @@ export const TableActionInitializers = {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return !['view', 'sql'].includes(collection.template) || collection?.writableView;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -78,7 +78,7 @@ export const TableActionInitializers = {
|
||||
type: 'divider',
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return !['view', 'sql'].includes(collection.template) || collection?.writableView;
|
||||
},
|
||||
},
|
||||
// {
|
||||
@ -172,7 +172,7 @@ export const TableActionInitializers = {
|
||||
],
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' || collection?.writableView;
|
||||
return !['view', 'sql'].includes(collection.template) || collection?.writableView;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -18,7 +18,9 @@ export const DetailsBlockInitializer = (props) => {
|
||||
collection: item.name,
|
||||
rowKey: collection.filterTargetKey || 'id',
|
||||
actionInitializers:
|
||||
(collection.template !== 'view' || collection?.writableView) && 'DetailsActionInitializers',
|
||||
(collection.template !== 'view' || collection?.writableView) &&
|
||||
collection.template !== 'sql' &&
|
||||
'DetailsActionInitializers',
|
||||
});
|
||||
insert(schema);
|
||||
}}
|
||||
|
@ -862,7 +862,10 @@ export const useCollectionDataSourceItems = (componentName) => {
|
||||
const fields = getCollectionFields(item.name);
|
||||
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
|
||||
return false;
|
||||
} else if (['Kanban', 'FormItem'].includes(componentName) && item.template === 'view' && !item.writableView) {
|
||||
} else if (
|
||||
['Kanban', 'FormItem'].includes(componentName) &&
|
||||
((item.template === 'view' && !item.writableView) || item.template === 'sql')
|
||||
) {
|
||||
return false;
|
||||
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
||||
return false;
|
||||
|
@ -0,0 +1,63 @@
|
||||
import Database from '../../database';
|
||||
import { mockDatabase } from '../../mock-database';
|
||||
import { SQLModel } from '../../sql-collection/sql-model';
|
||||
|
||||
describe('infer fields', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = mockDatabase({ tablePrefix: '' });
|
||||
await db.clean({ drop: true });
|
||||
|
||||
db.collection({
|
||||
name: 'users',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'nickname', type: 'string', interface: 'input' },
|
||||
],
|
||||
});
|
||||
db.collection({
|
||||
name: 'roles',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'title', type: 'string', interface: 'input' },
|
||||
{ name: 'name', type: 'string', interface: 'uid' },
|
||||
],
|
||||
});
|
||||
db.collection({
|
||||
name: 'roles_users',
|
||||
schema: 'public',
|
||||
fields: [
|
||||
{ name: 'id', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'userId', type: 'bigInt', interface: 'id' },
|
||||
{ name: 'roleName', type: 'bigInt', interface: 'id' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should infer fields', async () => {
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
modelName: 'roles_users',
|
||||
tableName: 'roles_users',
|
||||
sequelize: db.sequelize,
|
||||
});
|
||||
model.database = db;
|
||||
model.sql = `select u.id as uid, u.nickname, r.title, r.name
|
||||
from users u left join roles_users ru on ru.user_id = u.id
|
||||
left join roles r on ru.role_name=r.name`;
|
||||
expect(model.inferFields()).toMatchObject({
|
||||
uid: { type: 'bigInt', source: 'users.id' },
|
||||
nickname: { type: 'string', source: 'users.nickname' },
|
||||
title: { type: 'string', source: 'roles.title' },
|
||||
name: { type: 'string', source: 'roles.name' },
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
import { SQLModel } from '../../sql-collection/sql-model';
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
describe('select query', () => {
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
modelName: 'users',
|
||||
tableName: 'users',
|
||||
sequelize: new Sequelize({
|
||||
dialect: 'postgres',
|
||||
}),
|
||||
});
|
||||
model.sql = 'SELECT * FROM "users"';
|
||||
model.collection = {
|
||||
fields: new Map(
|
||||
Object.entries({
|
||||
id: {},
|
||||
name: {},
|
||||
}),
|
||||
),
|
||||
} as any;
|
||||
const queryGenerator = model.queryInterface.queryGenerator as any;
|
||||
|
||||
test('plain sql', () => {
|
||||
const query = queryGenerator.selectQuery('users', {}, model);
|
||||
expect(query).toBe('SELECT * FROM "users";');
|
||||
});
|
||||
|
||||
test('attributes', () => {
|
||||
const query = queryGenerator.selectQuery('users', { attributes: ['id', 'name'] }, model);
|
||||
expect(query).toBe('SELECT "id", "name" FROM (SELECT * FROM "users") AS "users";');
|
||||
});
|
||||
|
||||
test('where', () => {
|
||||
const query = queryGenerator.selectQuery('users', { where: { id: 1 } }, model);
|
||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" WHERE "users"."id" = 1;');
|
||||
});
|
||||
|
||||
test('group', () => {
|
||||
const query = queryGenerator.selectQuery('users', { group: 'id' }, model);
|
||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" GROUP BY "id";');
|
||||
});
|
||||
|
||||
test('order', () => {
|
||||
const query = queryGenerator.selectQuery('users', { order: ['id'] }, model);
|
||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" ORDER BY "users"."id";');
|
||||
});
|
||||
|
||||
test('limit, offset', () => {
|
||||
const query = queryGenerator.selectQuery('users', { limit: 1, offset: 0 }, model);
|
||||
expect(query).toBe('SELECT * FROM (SELECT * FROM "users") AS "users" LIMIT 1 OFFSET 0;');
|
||||
});
|
||||
|
||||
test('complex sql', () => {
|
||||
const query = queryGenerator.selectQuery(
|
||||
'users',
|
||||
{
|
||||
attributes: ['id', 'name'],
|
||||
where: { id: 1 },
|
||||
group: 'id',
|
||||
order: ['id'],
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
model,
|
||||
);
|
||||
expect(query).toBe(
|
||||
'SELECT "id", "name" FROM (SELECT * FROM "users") AS "users" WHERE "users"."id" = 1 GROUP BY "id" ORDER BY "users"."id" LIMIT 1 OFFSET 0;',
|
||||
);
|
||||
});
|
||||
});
|
@ -10,4 +10,16 @@ describe('sql parser', () => {
|
||||
expect(firstColumn['expr']['table']).toEqual('users');
|
||||
expect(firstColumn['expr']['column']).toEqual('id');
|
||||
});
|
||||
|
||||
// it('should parse complex sql', function () {
|
||||
// const sql = `select u.id, u.nickname, r.title, r.name from users u left join roles_users ru on ru.user_id = u.id left join roles r on ru.role_name=r.name`;
|
||||
// const { ast } = sqlParser.parse(sql);
|
||||
// console.log(JSON.stringify(ast, null, 2));
|
||||
// });
|
||||
//
|
||||
// it('should parse with subquery', function () {
|
||||
// const sql = `with t as (select * from users), v as (select * from roles) select * from t, v where t.id = v.id;`;
|
||||
// const { ast } = sqlParser.parse(sql);
|
||||
// console.log(JSON.stringify(ast, null, 2));
|
||||
// });
|
||||
});
|
||||
|
@ -70,6 +70,7 @@ import {
|
||||
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
||||
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
||||
import { ViewCollection } from './view-collection';
|
||||
import { SqlCollection } from './sql-collection/sql-collection';
|
||||
|
||||
export type MergeOptions = merge.Options;
|
||||
|
||||
@ -358,6 +359,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
if (collection.options.schema) {
|
||||
collection.model._schema = collection.options.schema;
|
||||
}
|
||||
if (collection.options.sql) {
|
||||
collection.modelInit();
|
||||
}
|
||||
});
|
||||
|
||||
this.on('beforeDefineCollection', (options) => {
|
||||
@ -451,6 +455,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return ViewCollection;
|
||||
}
|
||||
|
||||
if (options.sql) {
|
||||
return SqlCollection;
|
||||
}
|
||||
|
||||
return Collection;
|
||||
})();
|
||||
|
||||
|
@ -41,4 +41,5 @@ export { snakeCase } from './utils';
|
||||
export * from './value-parsers';
|
||||
export * from './view-collection';
|
||||
export * from './view/view-inference';
|
||||
export * from './sql-collection';
|
||||
export * from './helpers';
|
||||
|
2
packages/core/database/src/sql-collection/index.ts
Normal file
2
packages/core/database/src/sql-collection/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './sql-model';
|
||||
export * from './sql-collection';
|
69
packages/core/database/src/sql-collection/query-generator.ts
Normal file
69
packages/core/database/src/sql-collection/query-generator.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { GroupOption, Order, ProjectionAlias, WhereOptions } from 'sequelize';
|
||||
import { SQLModel } from './sql-model';
|
||||
import { lodash } from '@nocobase/utils';
|
||||
import { Collection } from '../collection';
|
||||
|
||||
export function selectQuery(
|
||||
tableName: string,
|
||||
options: {
|
||||
attributes?: (string | ProjectionAlias)[];
|
||||
where?: WhereOptions;
|
||||
order?: Order;
|
||||
group?: GroupOption;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
model: SQLModel,
|
||||
) {
|
||||
options = options || {};
|
||||
if (lodash.isEmpty(options)) {
|
||||
return `${model.sql};`;
|
||||
}
|
||||
const queryItems = [];
|
||||
let attributes = options.attributes && options.attributes.slice();
|
||||
if (attributes) {
|
||||
const fields = Array.from((model.collection as Collection)?.fields.keys() || []);
|
||||
attributes = attributes.filter((attr: any) => attr === '*' || typeof attr !== 'string' || fields.includes(attr));
|
||||
}
|
||||
attributes = this.escapeAttributes(attributes, { model });
|
||||
attributes = attributes || ['*'];
|
||||
|
||||
// Add WHERE to sub or main query
|
||||
if (Object.prototype.hasOwnProperty.call(options, 'where')) {
|
||||
options.where = this.getWhereConditions(options.where, tableName, model, options);
|
||||
if (options.where) {
|
||||
queryItems.push(` WHERE ${options.where}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add GROUP BY to sub or main query
|
||||
if (options.group) {
|
||||
options.group = Array.isArray(options.group)
|
||||
? options.group.map((t) => this.aliasGrouping(t, model, tableName, options)).join(', ')
|
||||
: this.aliasGrouping(options.group, model, tableName, options);
|
||||
|
||||
if (options.group) {
|
||||
queryItems.push(` GROUP BY ${options.group}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add ORDER to sub or main query
|
||||
if (options.order) {
|
||||
const orders = this.getQueryOrders(options, model, false);
|
||||
if (orders.mainQueryOrder.length) {
|
||||
queryItems.push(` ORDER BY ${orders.mainQueryOrder.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add LIMIT, OFFSET to sub or main query
|
||||
const limitOrder = this.addLimitAndOffset(options, model);
|
||||
if (limitOrder) {
|
||||
queryItems.push(limitOrder);
|
||||
}
|
||||
|
||||
const query = `SELECT ${attributes.join(', ')} FROM (${model.sql}) ${this.getAliasToken()} ${this.quoteIdentifier(
|
||||
model.name,
|
||||
)}${queryItems.join('')}`;
|
||||
|
||||
return `${query};`;
|
||||
}
|
46
packages/core/database/src/sql-collection/sql-collection.ts
Normal file
46
packages/core/database/src/sql-collection/sql-collection.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Collection, CollectionContext, CollectionOptions } from '../collection';
|
||||
import { SQLModel } from './sql-model';
|
||||
|
||||
export class SqlCollection extends Collection {
|
||||
constructor(options: CollectionOptions, context: CollectionContext) {
|
||||
options.autoGenId = false;
|
||||
options.timestamps = false;
|
||||
options.underscored = false;
|
||||
|
||||
super(options, context);
|
||||
}
|
||||
|
||||
isSql() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public collectionSchema() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
modelInit() {
|
||||
const { autoGenId, sql } = this.options;
|
||||
const model = class extends SQLModel {};
|
||||
model.init(null, {
|
||||
...this.sequelizeModelOptions(),
|
||||
schema: undefined,
|
||||
});
|
||||
|
||||
if (!autoGenId) {
|
||||
model.removeAttribute('id');
|
||||
}
|
||||
|
||||
model.sql = sql;
|
||||
model.database = this.context.database;
|
||||
model.collection = this;
|
||||
|
||||
this.model = new Proxy(model, {
|
||||
get(target, prop) {
|
||||
if (prop === '_schema') {
|
||||
return undefined;
|
||||
}
|
||||
return Reflect.get(target, prop);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
157
packages/core/database/src/sql-collection/sql-model.ts
Normal file
157
packages/core/database/src/sql-collection/sql-model.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { Model } from '../model';
|
||||
import sqlParser from '../sql-parser';
|
||||
import { selectQuery } from './query-generator';
|
||||
|
||||
export class SQLModel extends Model {
|
||||
static sql: string;
|
||||
|
||||
static get queryInterface() {
|
||||
const queryInterface = this.sequelize.getQueryInterface();
|
||||
const queryGenerator = queryInterface.queryGenerator as any;
|
||||
const sqlGenerator = new Proxy(queryGenerator, {
|
||||
get(target, prop) {
|
||||
if (prop === 'selectQuery') {
|
||||
return selectQuery.bind(target);
|
||||
}
|
||||
return Reflect.get(target, prop);
|
||||
},
|
||||
});
|
||||
return new Proxy(queryInterface, {
|
||||
get(target, prop) {
|
||||
if (prop === 'queryGenerator') {
|
||||
return sqlGenerator;
|
||||
}
|
||||
return Reflect.get(target, prop);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async sync(): Promise<any> {}
|
||||
|
||||
private static parseTablesAndColumns(): {
|
||||
table: string;
|
||||
columns: string | { name: string; as: string }[];
|
||||
}[] {
|
||||
let { ast } = sqlParser.parse(this.sql);
|
||||
if (Array.isArray(ast)) {
|
||||
ast = ast[0];
|
||||
}
|
||||
if (ast.with) {
|
||||
const tables = new Set<string>();
|
||||
// parse sql includes with clause is not accurate
|
||||
// the parsed columns seems to be always '*'
|
||||
// it is supposed to be improved in the future
|
||||
ast.with.forEach((withItem: { tableList: string[] }) => {
|
||||
const tableList = withItem.tableList;
|
||||
tableList.forEach((table) => {
|
||||
const name = table.split('::')[2]; // "select::null::users"
|
||||
tables.add(name);
|
||||
});
|
||||
});
|
||||
return Array.from(tables).map((table) => ({ table, columns: '*' }));
|
||||
}
|
||||
if (ast.columns === '*') {
|
||||
return ast.from.map((fromItem: { table: string; as: string }) => ({
|
||||
table: fromItem.table,
|
||||
columns: '*',
|
||||
}));
|
||||
}
|
||||
const columns: string[] = ast.columns.reduce(
|
||||
(
|
||||
tableMp: { [table: string]: { name: string; as: string }[] },
|
||||
column: {
|
||||
as: string;
|
||||
expr: {
|
||||
table: string;
|
||||
column: string;
|
||||
type: string;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
if (column.expr.type !== 'column_ref') {
|
||||
return tableMp;
|
||||
}
|
||||
const table = column.expr.table;
|
||||
const columnAttr = { name: column.expr.column, as: column.as };
|
||||
if (!tableMp[table]) {
|
||||
tableMp[table] = [columnAttr];
|
||||
} else {
|
||||
tableMp[table].push(columnAttr);
|
||||
}
|
||||
return tableMp;
|
||||
},
|
||||
{},
|
||||
);
|
||||
ast.from.forEach((fromItem: { table: string; as: string }) => {
|
||||
if (columns[fromItem.as]) {
|
||||
columns[fromItem.table] = columns[fromItem.as];
|
||||
columns[fromItem.as] = undefined;
|
||||
}
|
||||
});
|
||||
return Object.entries(columns)
|
||||
.filter(([_, columns]) => columns)
|
||||
.map(([table, columns]) => ({ table, columns }));
|
||||
}
|
||||
|
||||
private static getTableNameWithSchema(table: string) {
|
||||
if (this.database.inDialect('postgres') && !table.includes('.')) {
|
||||
return `public.${table}`;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
static inferFields(): {
|
||||
[field: string]: {
|
||||
type: string;
|
||||
source: string;
|
||||
collection: string;
|
||||
interface: string;
|
||||
};
|
||||
} {
|
||||
const tables = this.parseTablesAndColumns();
|
||||
const fields = tables.reduce((fields, { table, columns }) => {
|
||||
const tableName = this.getTableNameWithSchema(table);
|
||||
const collection = this.database.tableNameCollectionMap.get(tableName);
|
||||
if (!collection) {
|
||||
return fields;
|
||||
}
|
||||
const attributes = collection.model.getAttributes();
|
||||
const sourceFields = {};
|
||||
if (columns === '*') {
|
||||
Object.values(attributes).forEach((attribute) => {
|
||||
const field = collection.getField((attribute as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[field.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
(columns as { name: string; as: string }[]).forEach((column) => {
|
||||
const modelField = Object.values(attributes).find((attribute) => attribute.field === column.name);
|
||||
if (!modelField) {
|
||||
return;
|
||||
}
|
||||
const field = collection.getField((modelField as any).fieldName);
|
||||
if (!field?.options.interface) {
|
||||
return;
|
||||
}
|
||||
sourceFields[column.as || column.name] = {
|
||||
collection: field.collection.name,
|
||||
type: field.type,
|
||||
source: `${field.collection.name}.${field.name}`,
|
||||
interface: field.options.interface,
|
||||
uiSchema: field.options.uiSchema,
|
||||
};
|
||||
});
|
||||
}
|
||||
return { ...fields, ...sourceFields };
|
||||
}, {});
|
||||
return fields;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ interface LoadOptions extends Transactionable {
|
||||
// TODO
|
||||
skipField?: boolean | Array<string>;
|
||||
skipExist?: boolean;
|
||||
resetFields?: boolean;
|
||||
}
|
||||
|
||||
export class CollectionModel extends MagicAttributeModel {
|
||||
@ -14,7 +15,7 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
}
|
||||
|
||||
async load(loadOptions: LoadOptions = {}) {
|
||||
const { skipExist, skipField, transaction } = loadOptions;
|
||||
const { skipExist, skipField, resetFields, transaction } = loadOptions;
|
||||
const name = this.get('name');
|
||||
|
||||
let collection: Collection;
|
||||
@ -23,7 +24,6 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
...this.get(),
|
||||
fields: [],
|
||||
};
|
||||
|
||||
if (this.db.hasCollection(name)) {
|
||||
collection = this.db.getCollection(name);
|
||||
|
||||
@ -31,6 +31,10 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
return collection;
|
||||
}
|
||||
|
||||
if (resetFields) {
|
||||
collection.resetFields();
|
||||
}
|
||||
|
||||
collection.updateOptions(collectionOptions);
|
||||
} else {
|
||||
collection = this.db.collection(collectionOptions);
|
||||
|
@ -0,0 +1,96 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { SQLModel, SqlCollection } from '@nocobase/database';
|
||||
import { CollectionModel } from '../models';
|
||||
|
||||
const updateCollection = async (ctx: Context, transaction: any) => {
|
||||
const { filterByTk, values } = ctx.action.params;
|
||||
const repo = ctx.db.getRepository('collections');
|
||||
const collection: CollectionModel = await repo.findOne({
|
||||
filter: {
|
||||
name: filterByTk,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
const existFields = await collection.getFields({ transaction });
|
||||
const deletedFields = existFields.filter((field: any) => !values.fields?.find((f: any) => f.name === field.name));
|
||||
for (const field of deletedFields) {
|
||||
await field.destroy({ transaction });
|
||||
}
|
||||
const upRes = await repo.update({
|
||||
filterByTk,
|
||||
values,
|
||||
updateAssociationValues: ['fields'],
|
||||
transaction,
|
||||
});
|
||||
|
||||
return { collection, upRes };
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'sqlCollection',
|
||||
actions: {
|
||||
execute: async (ctx: Context, next: Next) => {
|
||||
let {
|
||||
values: { sql },
|
||||
} = ctx.action.params;
|
||||
sql = sql.trim().split(';').shift();
|
||||
if (!sql) {
|
||||
ctx.throw(400, ctx.t('SQL is empty'));
|
||||
}
|
||||
if (!/^select/i.test(sql) && !/^with([\s\S]+)select([\s\S]+)/i.test(sql)) {
|
||||
ctx.throw(400, ctx.t('Only select query allowed'));
|
||||
}
|
||||
const tmpCollection = new SqlCollection({ name: 'tmp', sql }, { database: ctx.db });
|
||||
const model = tmpCollection.model as typeof SQLModel;
|
||||
// The result is for preview only, add limit clause to avoid too many results
|
||||
const data = await model.findAll({ attributes: ['*'], limit: 5, raw: true });
|
||||
let fields: {
|
||||
[field: string]: {
|
||||
type: string;
|
||||
source: string;
|
||||
collection: string;
|
||||
interface: string;
|
||||
};
|
||||
} = {};
|
||||
try {
|
||||
fields = model.inferFields();
|
||||
} catch (err) {
|
||||
ctx.logger.warn('resource: sql-collection, action: execute, error: ', err);
|
||||
fields = {};
|
||||
}
|
||||
const sources = Array.from(new Set(Object.values(fields).map((field) => field.collection)));
|
||||
ctx.body = { data, fields, sources };
|
||||
await next();
|
||||
},
|
||||
setFields: async (ctx: Context, next: Next) => {
|
||||
const transaction = await ctx.app.db.sequelize.transaction();
|
||||
try {
|
||||
const {
|
||||
upRes: [collection],
|
||||
} = await updateCollection(ctx, transaction);
|
||||
await collection.loadFields({
|
||||
transaction,
|
||||
});
|
||||
await transaction.commit();
|
||||
} catch (e) {
|
||||
await transaction.rollback();
|
||||
throw e;
|
||||
}
|
||||
await next();
|
||||
},
|
||||
update: async (ctx: Context, next: Next) => {
|
||||
const transaction = await ctx.app.db.sequelize.transaction();
|
||||
try {
|
||||
const { upRes } = await updateCollection(ctx, transaction);
|
||||
const [collection] = upRes;
|
||||
await (collection as CollectionModel).load({ transaction, resetFields: true });
|
||||
await transaction.commit();
|
||||
ctx.body = upRes;
|
||||
} catch (e) {
|
||||
await transaction.rollback();
|
||||
throw e;
|
||||
}
|
||||
await next();
|
||||
},
|
||||
},
|
||||
};
|
@ -17,6 +17,7 @@ import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollec
|
||||
import { CollectionModel, FieldModel } from './models';
|
||||
import collectionActions from './resourcers/collections';
|
||||
import viewResourcer from './resourcers/views';
|
||||
import sqlResourcer from './resourcers/sql';
|
||||
|
||||
export class CollectionManagerPlugin extends Plugin {
|
||||
public schema: string;
|
||||
@ -51,7 +52,7 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.collections`,
|
||||
actions: ['collections:*', 'collections.fields:*', 'dbViews:*', 'collectionCategories:*'],
|
||||
actions: ['collections:*', 'collections.fields:*', 'dbViews:*', 'collectionCategories:*', 'sqlCollection:*'],
|
||||
});
|
||||
|
||||
this.app.db.on('collections.beforeCreate', async (model) => {
|
||||
@ -277,6 +278,7 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
});
|
||||
|
||||
this.app.resource(viewResourcer);
|
||||
this.app.resource(sqlResourcer);
|
||||
this.app.actions(collectionActions);
|
||||
|
||||
const handleFieldSource = (fields) => {
|
||||
|
@ -14,6 +14,7 @@ describe('query', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
app = mockServer();
|
||||
app.db.options.underscored = true;
|
||||
app.db.collection({
|
||||
name: 'orders',
|
||||
fields: [
|
||||
|
@ -167,7 +167,7 @@ export const parseFieldAndAssociations = async (ctx: Context, next: Next) => {
|
||||
const { collection: collectionName, measures, dimensions, orders, filter } = ctx.action.params.values as QueryParams;
|
||||
const collection = ctx.db.getCollection(collectionName);
|
||||
const fields = collection.fields;
|
||||
const underscored = ctx.db.options.underscored;
|
||||
const underscored = collection.options.underscored;
|
||||
const models: {
|
||||
[target: string]: {
|
||||
type: string;
|
||||
|
@ -20,7 +20,11 @@ export const ImportInitializerProvider = (props: any) => {
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'file';
|
||||
return (
|
||||
(collection.template !== 'view' || collection?.writableView) &&
|
||||
collection.template !== 'file' &&
|
||||
collection.template !== 'sql'
|
||||
);
|
||||
},
|
||||
});
|
||||
return props.children;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useCollection } from '@nocobase/client';
|
||||
|
||||
// 表格操作配置
|
||||
export const MapActionInitializers = {
|
||||
title: "{{t('Configure actions')}}",
|
||||
@ -29,6 +31,10 @@ export const MapActionInitializers = {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
@ -42,6 +48,10 @@ export const MapActionInitializers = {
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'subMenu',
|
||||
@ -92,6 +102,10 @@ export const MapActionInitializers = {
|
||||
},
|
||||
},
|
||||
],
|
||||
visible: function useVisible() {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'sql';
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -23324,7 +23324,7 @@ vite@4.3.1:
|
||||
|
||||
vite@^4.4.9:
|
||||
version "4.4.9"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
|
||||
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
|
||||
dependencies:
|
||||
esbuild "^0.18.10"
|
||||
|
Loading…
Reference in New Issue
Block a user