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:
YANG QIA 2023-09-25 15:03:23 +08:00 committed by GitHub
parent 52198e57f9
commit 89635982b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1934 additions and 605 deletions

View File

@ -23,6 +23,7 @@ import {
SyncFieldsActionCom, SyncFieldsActionCom,
ViewCollectionField, ViewCollectionField,
ViewFieldAction, ViewFieldAction,
SyncSQLFieldsAction,
} from './Configuration'; } from './Configuration';
import { CollectionCategroriesProvider } from './CollectionManagerProvider'; import { CollectionCategroriesProvider } from './CollectionManagerProvider';
@ -80,6 +81,7 @@ export const CollectionManagerPane = () => {
EditCategoryAction, EditCategoryAction,
SyncFieldsAction, SyncFieldsAction,
SyncFieldsActionCom, SyncFieldsActionCom,
SyncSQLFieldsAction,
}} }}
/> />
// </Card> // </Card>

View File

@ -98,7 +98,7 @@ const getSchema = (schema, category, compile): ISchema => {
}; };
const getDefaultCollectionFields = (values) => { const getDefaultCollectionFields = (values) => {
if (values?.template === 'view') { if (values?.template === 'view' || values?.template === 'sql') {
return values.fields; return values.fields;
} }
const defaults = values.fields ? [...values.fields] : []; const defaults = values.fields ? [...values.fields] : [];

View File

@ -302,6 +302,7 @@ export const AddFieldAction = (props) => {
}; };
}, [getInterface, items, record]); }, [getInterface, items, record]);
return ( return (
record.template !== 'sql' && (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}> <Dropdown getPopupContainer={getContainer} trigger={trigger} align={align} menu={menu}>
@ -332,5 +333,6 @@ export const AddFieldAction = (props) => {
/> />
</ActionContextProvider> </ActionContextProvider>
</RecordProvider> </RecordProvider>
)
); );
}; };

View File

@ -25,6 +25,7 @@ import { OverridingCollectionField } from './OverridingCollectionField';
import { collection } from './schemas/collectionFields'; import { collection } from './schemas/collectionFields';
import { SyncFieldsAction } from './SyncFieldsAction'; import { SyncFieldsAction } from './SyncFieldsAction';
import { ViewCollectionField } from './ViewInheritedField'; import { ViewCollectionField } from './ViewInheritedField';
import { SyncSQLFieldsAction } from './SyncSQLFieldsAction';
import { Input } from '../../schema-component/antd/input'; import { Input } from '../../schema-component/antd/input';
const indentStyle = css` const indentStyle = css`
@ -419,6 +420,7 @@ export const CollectionFields = () => {
> >
<Action {...deleteProps} /> <Action {...deleteProps} />
<SyncFieldsAction {...syncProps} /> <SyncFieldsAction {...syncProps} />
<SyncSQLFieldsAction refreshCMList={refreshAsync} />
<AddCollectionField {...addProps} /> <AddCollectionField {...addProps} />
</Space> </Space>
<Table <Table

View File

@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
@ -100,11 +100,16 @@ export const useUpdateCollectionActionAndRefreshCM = (options) => {
const ctx = useActionContext(); const ctx = useActionContext();
const { refresh } = useResourceActionContext(); const { refresh } = useResourceActionContext();
const { resource, targetKey } = useResourceContext(); 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 { return {
async run() { async run() {
await form.submit(); 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); ctx.setVisible(false);
await form.reset(); await form.reset();
refresh(); refresh();

View File

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

View File

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

View File

@ -124,6 +124,14 @@ export const collectionFieldSchema: ISchema = {
type: 'primary', type: 'primary',
}, },
}, },
syncSQL: {
type: 'void',
title: '{{ t("Sync from database") }}',
'x-component': 'SyncSQLFieldsAction',
'x-component-props': {
type: 'primary',
},
},
create: { create: {
type: 'void', type: 'void',
title: '{{ t("Add new") }}', title: '{{ t("Add new") }}',

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export * from './FieldsConfigure';
export * from './PreviewTable';
export * from './SQLRequestProvider';
export * from './SQLInput';

View File

@ -3,3 +3,4 @@ export * from './general';
export * from './tree'; export * from './tree';
export * from './expression'; export * from './expression';
export * from './view'; export * from './view';
export * from './sql';

View File

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

View File

@ -117,485 +117,498 @@ export default {
'Tablet device': 'Tablet device', 'Tablet device': 'Tablet device',
'Desktop device': 'Desktop device', 'Desktop device': 'Desktop device',
'Large screen device': 'Large screen device', 'Large screen device': 'Large screen device',
"Collapse": "Collapse", Collapse: 'Collapse',
"Select data source": "Select data source", 'Select data source': 'Select data source',
"Calendar": "Calendar", Calendar: 'Calendar',
"Delete events": "Delete events", 'Delete events': 'Delete events',
"This event": "This event", 'This event': 'This event',
"This and following events": "This and following events", 'This and following events': 'This and following events',
"All events": "All events", 'All events': 'All events',
"Delete this event?": "Delete this event?", 'Delete this event?': 'Delete this event?',
"Delete Event": "Delete Event", 'Delete Event': 'Delete Event',
"Kanban": "Kanban", Kanban: 'Kanban',
"Gantt":"Gantt", Gantt: 'Gantt',
"Create gantt block":"Create gantt block", 'Create gantt block': 'Create gantt block',
"Progress field":"Progress field", 'Progress field': 'Progress field',
"Time scale":"Time scale", 'Time scale': 'Time scale',
"Hour":"Hour", Hour: 'Hour',
"Quarter of day":"Quarter of day", 'Quarter of day': 'Quarter of day',
"Half of day":"Half of day", 'Half of day': 'Half of day',
"Year":"Year", Year: 'Year',
"QuarterYear":"QuarterYear", QuarterYear: 'QuarterYear',
"Select grouping field": "Select grouping field", 'Select grouping field': 'Select grouping field',
"Media": "Media", Media: 'Media',
"Markdown": "Markdown", Markdown: 'Markdown',
"Wysiwyg": "Wysiwyg", Wysiwyg: 'Wysiwyg',
"Chart blocks": "Chart blocks", 'Chart blocks': 'Chart blocks',
"Column chart": "Column chart", 'Column chart': 'Column chart',
"Bar chart": "Bar chart", 'Bar chart': 'Bar chart',
"Line chart": "Line chart", 'Line chart': 'Line chart',
"Pie chart": "Pie chart", 'Pie chart': 'Pie chart',
"Area chart": "Area chart", 'Area chart': 'Area chart',
"Other chart": "Other chart", 'Other chart': 'Other chart',
"Other blocks": "Other blocks", 'Other blocks': 'Other blocks',
"In configuration": "In configuration", 'In configuration': 'In configuration',
"Chart title": "Chart title", 'Chart title': 'Chart title',
"Chart type": "Chart type", 'Chart type': 'Chart type',
"Chart config": "Chart config", 'Chart config': 'Chart config',
"Templates": "Templates", Templates: 'Templates',
"Select template": "Select template", 'Select template': 'Select template',
"Action logs": "Action logs", 'Action logs': 'Action logs',
"Create template": "Create template", 'Create template': 'Create template',
"Edit markdown": "Edit markdown", 'Edit markdown': 'Edit markdown',
"Add block": "Add block", 'Add block': 'Add block',
"Add new": "Add new", 'Add new': 'Add new',
"Add record": "Add record", 'Add record': 'Add record',
'Add child':'Add child', 'Add child': 'Add child',
'Collapse all':'Collapse all', 'Collapse all': 'Collapse all',
'Expand all':'Expand all', 'Expand all': 'Expand all',
'Expand/Collapse':'Expand/Collapse', 'Expand/Collapse': 'Expand/Collapse',
'Default collapse':'Default collapse', 'Default collapse': 'Default collapse',
"Tree table":"Tree table", 'Tree table': 'Tree table',
"Custom field display name": "Custom field display name", 'Custom field display name': 'Custom field display name',
"Display fields": "Display collection fields", 'Display fields': 'Display collection fields',
"Edit record": "Edit record", 'Edit record': 'Edit record',
"Delete menu item": "Delete menu item", 'Delete menu item': 'Delete menu item',
"Add page": "Add page", 'Add page': 'Add page',
"Add group": "Add group", 'Add group': 'Add group',
"Add link": "Add link", 'Add link': 'Add link',
"Insert above": "Insert above", 'Insert above': 'Insert above',
"Insert below": "Insert below", 'Insert below': 'Insert below',
"Save": "Save", Save: 'Save',
"Delete block": "Delete block", 'Delete block': 'Delete block',
"Are you sure you want to delete it?": "Are you sure you want to delete it?", '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**.", 'This is a demo text, **supports Markdown syntax**.': 'This is a demo text, **supports Markdown syntax**.',
"Filter": "Filter", Filter: 'Filter',
"Connect data blocks": "Connect data blocks", 'Connect data blocks': 'Connect data blocks',
"Action type": "Action type", 'Action type': 'Action type',
"Actions": "Actions", Actions: 'Actions',
"Insert": "Insert", Insert: 'Insert',
"Insert if not exists": "Insert if not exists", 'Insert if not exists': 'Insert if not exists',
"Insert if not exists, or update": "Insert if not exists, or update", '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", 'Determine whether a record exists by the following fields':
"Update": "Update", 'Determine whether a record exists by the following fields',
"View": "View", Update: 'Update',
"View record": "View record", View: 'View',
"Refresh": "Refresh", 'View record': 'View record',
"Data changes": "Data changes", Refresh: 'Refresh',
"Field name": "Field name", 'Data changes': 'Data changes',
"Before change": "Before change", 'Field name': 'Field name',
"After change": "After change", 'Before change': 'Before change',
"Delete record": "Delete record", 'After change': 'After change',
"Create collection": "Create collection", 'Delete record': 'Delete record',
"Collection display name": "Collection display name", 'Create collection': 'Create collection',
"Collection name": "Collection name", 'Collection display name': 'Collection display name',
"Inherits": "Inherits", 'Collection name': 'Collection name',
"Generate ID field automatically": "Generate ID field automatically", Inherits: 'Inherits',
"Store the creation user of each record": "Store the creation user of each record", 'Generate ID field automatically': 'Generate ID field automatically',
"Store the last update user of each record": "Store the last update user of each record", 'Store the creation user of each record': 'Store the creation user of each record',
"Store the creation time of each record": "Store the creation time of each record", 'Store the last update user of each record': 'Store the last update user of each record',
"Store the last update time of each record": "Store the last update time of each record", 'Store the creation time of each record': 'Store the creation time of each record',
"More options": "More options", 'Store the last update time of each record': 'Store the last update time of each record',
"Records can be sorted": "Records can be sorted", 'More options': 'More options',
"Calendar collection": "Calendar collection", 'Records can be sorted': 'Records can be sorted',
"General collection": "General collection", 'Calendar collection': 'Calendar collection',
"Connect to database view":"Connect to database view", 'General collection': 'General collection',
"Source collections":"Source collections", 'Connect to database view': 'Connect to database view',
"Field source":"Field source", 'Source collections': 'Source collections',
"Preview":"Preview", 'Field source': 'Field source',
"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.", Preview: 'Preview',
"Edit": "Edit", 'Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.':
"Edit collection": "Edit collection", 'Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.',
"Configure fields": "Configure fields", Edit: 'Edit',
"Configure columns": "Configure columns", 'Edit collection': 'Edit collection',
"Edit field": "Edit field", 'Configure fields': 'Configure fields',
"Override": "Override", 'Configure columns': 'Configure columns',
"Override field": "Override field", 'Edit field': 'Edit field',
"Configure fields of {{title}}": "Configure fields of {{title}}", Override: 'Override',
"Association fields filter": "Association fields filter", 'Override field': 'Override field',
"PK & FK fields": "PK & FK fields", 'Configure fields of {{title}}': 'Configure fields of {{title}}',
"Association fields": "Association fields", 'Association fields filter': 'Association fields filter',
"Choices fields": "Choices fields", 'PK & FK fields': 'PK & FK fields',
"System fields": "System fields", 'Association fields': 'Association fields',
"General fields": "General fields", 'Choices fields': 'Choices fields',
"Inherited fields": "Inherited fields", 'System fields': 'System fields',
"Parent collection fields": "Parent collection fields", 'General fields': 'General fields',
"Basic": "Basic", 'Inherited fields': 'Inherited fields',
"Single line text": "Single line text", 'Parent collection fields': 'Parent collection fields',
"Long text": "Long text", Basic: 'Basic',
"Phone": "Phone", 'Single line text': 'Single line text',
"Email": "Email", 'Long text': 'Long text',
"Number": "Number", Phone: 'Phone',
"Integer": "Integer", Email: 'Email',
"Percent": "Percent", Number: 'Number',
"Password": "Password", Integer: 'Integer',
"Advanced type": "Advanced", Percent: 'Percent',
"Formula": "Formula", Password: 'Password',
"Formula description": "Compute a value in each record based on other fields in the same record.", 'Advanced type': 'Advanced',
"Choices": "Choices", Formula: 'Formula',
"Checkbox": "Checkbox", 'Formula description': 'Compute a value in each record based on other fields in the same record.',
"Single select": "Single select", Choices: 'Choices',
"Multiple select": "Multiple select", Checkbox: 'Checkbox',
"Radio group": "Radio group", 'Single select': 'Single select',
"Checkbox group": "Checkbox group", 'Multiple select': 'Multiple select',
"China region": "China region", 'Radio group': 'Radio group',
"Date & Time": "Date & Time", 'Checkbox group': 'Checkbox group',
"Datetime": "Datetime", 'China region': 'China region',
"Relation": "Relation", 'Date & Time': 'Date & Time',
"Link to": "Link to", Datetime: 'Datetime',
"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.", Relation: 'Relation',
"Sub-table": "Sub-table", 'Link to': 'Link to',
"Sub-details":"Sub-details", 'Link to description':
"Sub-form(Popover)":"Sub-form(Popover)", '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.',
"System info": "System info", 'Sub-table': 'Sub-table',
"Created at": "Created at", 'Sub-details': 'Sub-details',
"Last updated at": "Last updated at", 'Sub-form(Popover)': 'Sub-form(Popover)',
"Created by": "Created by", 'System info': 'System info',
"Last updated by": "Last updated by", 'Created at': 'Created at',
"Add field": "Add field", 'Last updated at': 'Last updated at',
"Field display name": "Field display name", 'Created by': 'Created by',
"Field type": "Field type", 'Last updated by': 'Last updated by',
"Field interface": "Field interface", 'Add field': 'Add field',
"Date format": "Date format", 'Field display name': 'Field display name',
"Year/Month/Day": "Year/Month/Day", 'Field type': 'Field type',
"Year-Month-Day": "Year-Month-Day", 'Field interface': 'Field interface',
"Day/Month/Year": "Day/Month/Year", 'Date format': 'Date format',
"Show time": "Show time", 'Year/Month/Day': 'Year/Month/Day',
"Time format": "Time format", 'Year-Month-Day': 'Year-Month-Day',
"12 hour": "12 hour", 'Day/Month/Year': 'Day/Month/Year',
"24 hour": "24 hour", 'Show time': 'Show time',
"Relationship type": "Relationship type", 'Time format': 'Time format',
"Inverse relationship type": "Inverse relationship type", '12 hour': '12 hour',
"Source collection": "Source collection", '24 hour': '24 hour',
"Source key": "Source key", 'Relationship type': 'Relationship type',
"Target collection": "Target collection", 'Inverse relationship type': 'Inverse relationship type',
"Through collection": "Through collection", 'Source collection': 'Source collection',
"Target key": "Target key", 'Source key': 'Source key',
"Foreign key": "Foreign key", 'Target collection': 'Target collection',
"One to one": "One to one", 'Through collection': 'Through collection',
"One to many": "One to many", 'Target key': 'Target key',
"Many to one": "Many to one", 'Foreign key': 'Foreign key',
"Many to many": "Many to many", 'One to one': 'One to one',
"Foreign key 1": "Foreign key 1", 'One to many': 'One to many',
"Foreign key 2": "Foreign key 2", 'Many to one': 'Many to one',
"One to one description": "Used to create one-to-one relationships. For example, a user has a profile.", 'Many to many': 'Many to many',
"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.", 'Foreign key 1': 'Foreign key 1',
"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.", 'Foreign key 2': 'Foreign key 2',
"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.", 'One to one description': 'Used to create one-to-one relationships. For example, a user has a profile.',
"Generated automatically if left blank": "Generated automatically if left blank", 'One to many description':
"Display association fields": "Display association fields", '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.',
"Display field title": "Display field title", 'Many to one description':
"Field component": "Field component", '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.',
"Allow multiple": "Allow multiple", 'Many to many description':
"Quick upload": "Quick upload", '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.',
"Select file": "Select file", 'Generated automatically if left blank': 'Generated automatically if left blank',
"Subtable": "Sub-table", 'Display association fields': 'Display association fields',
"Sub-form": "Sub-form", 'Display field title': 'Display field title',
"Field mode":"Field mode", 'Field component': 'Field component',
"Allow add new data":"Allow add new data", 'Allow multiple': 'Allow multiple',
"Record picker": "Record picker", 'Quick upload': 'Quick upload',
"Toggles the subfield mode": "Toggles the subfield mode", 'Select file': 'Select file',
"Selector mode": "Selector mode", Subtable: 'Sub-table',
"Subtable mode": "Sub-table mode", 'Sub-form': 'Sub-form',
"Subform mode": "Sub-form mode", 'Field mode': 'Field mode',
"Edit block title": "Edit block title", 'Allow add new data': 'Allow add new data',
"Block title": "Block title", 'Record picker': 'Record picker',
"Pattern": "Pattern", 'Toggles the subfield mode': 'Toggles the subfield mode',
"Operator": "Operator", 'Selector mode': 'Selector mode',
"Editable": "Editable", 'Subtable mode': 'Sub-table mode',
"Readonly": "Readonly", 'Subform mode': 'Sub-form mode',
"Easy-reading": "Easy-reading", 'Edit block title': 'Edit block title',
"Add filter": "Add filter", 'Block title': 'Block title',
"Add filter group": "Add filter group", Pattern: 'Pattern',
"Comparision": "Comparision", Operator: 'Operator',
"is": "is", Editable: 'Editable',
"is not": "is not", Readonly: 'Readonly',
"contains": "contains", 'Easy-reading': 'Easy-reading',
"does not contain": "does not contain", 'Add filter': 'Add filter',
"starts with": "starts with", 'Add filter group': 'Add filter group',
"not starts with": "not starts with", Comparision: 'Comparision',
"ends with": "ends with", is: 'is',
"not ends with": "not ends with", 'is not': 'is not',
"is empty": "is empty", contains: 'contains',
"is not empty": "is not empty", 'does not contain': 'does not contain',
"Edit chart": "Edit chart", 'starts with': 'starts with',
"Add text": "Add text", 'not starts with': 'not starts with',
"Filterable fields": "Filterable fields", 'ends with': 'ends with',
"Edit button": "Edit button", 'not ends with': 'not ends with',
"Hide": "Hide", 'is empty': 'is empty',
"Enable actions": "Enable actions", 'is not empty': 'is not empty',
"Import": "Import", 'Edit chart': 'Edit chart',
"Export": "Export", 'Add text': 'Add text',
"Customize": "Customize", 'Filterable fields': 'Filterable fields',
"Custom": "Custom", 'Edit button': 'Edit button',
"Function": "Function", Hide: 'Hide',
"Popup form": "Popup form", 'Enable actions': 'Enable actions',
"Flexible popup": "Flexible popup", Import: 'Import',
"Configure actions": "Configure actions", Export: 'Export',
"Display order number": "Display order number", Customize: 'Customize',
"Enable drag and drop sorting": "Enable drag and drop sorting", Custom: 'Custom',
"Triggered when the row is clicked": "Triggered when the row is clicked", Function: 'Function',
"Add tab": "Add tab", 'Popup form': 'Popup form',
"Disable tabs": "Disable tabs", 'Flexible popup': 'Flexible popup',
"Details": "Details", 'Configure actions': 'Configure actions',
"Edit tab": "Edit tab", 'Display order number': 'Display order number',
"Relationship blocks": "Relationship blocks", 'Enable drag and drop sorting': 'Enable drag and drop sorting',
"Select record": "Select record", 'Triggered when the row is clicked': 'Triggered when the row is clicked',
"Display name": "Display name", 'Add tab': 'Add tab',
"Select icon": "Select icon", 'Disable tabs': 'Disable tabs',
"Custom column name": "Custom column name", Details: 'Details',
"Edit description": "Edit description", 'Edit tab': 'Edit tab',
"Required": "Required", 'Relationship blocks': 'Relationship blocks',
"Unique": "Unique", 'Select record': 'Select record',
"Label field": "Label field", 'Display name': 'Display name',
"Default is the ID field": "Default is the ID field", 'Select icon': 'Select icon',
"Set default sorting rules": "Set default sorting rules", 'Custom column name': 'Custom column name',
"Set validation rules": "Set validation rules", 'Edit description': 'Edit description',
"Max length": "Max length", Required: 'Required',
"Min length": "Min length", Unique: 'Unique',
"Maximum": "Maximum", 'Label field': 'Label field',
"Minimum": "Minimum", 'Default is the ID field': 'Default is the ID field',
"Max length must greater than min length": "Max length must greater than min length", 'Set default sorting rules': 'Set default sorting rules',
"Min length must less than max length": "Min length must less than max length", 'Set validation rules': 'Set validation rules',
"Maximum must greater than minimum": "Maximum must greater than minimum", 'Max length': 'Max length',
"Minimum must less than maximum": "Minimum must less than maximum", 'Min length': 'Min length',
"Validation rule": "Validation rule", Maximum: 'Maximum',
"Add validation rule": "Add validation rule", Minimum: 'Minimum',
"Format": "Format", 'Max length must greater than min length': 'Max length must greater than min length',
"Regular expression": "Pattern", 'Min length must less than max length': 'Min length must less than max length',
"Error message": "Error message", 'Maximum must greater than minimum': 'Maximum must greater than minimum',
"Length": "Length", 'Minimum must less than maximum': 'Minimum must less than maximum',
"The field value cannot be greater than ": "The field value cannot be greater than ", 'Validation rule': 'Validation rule',
"The field value cannot be less than ": "The field value cannot be less than ", 'Add validation rule': 'Add validation rule',
"The field value is not an integer number": "The field value is not an integer number", Format: 'Format',
"Set default value": "Set default value", 'Regular expression': 'Pattern',
"Default value": "Default value", 'Error message': 'Error message',
"is before": "is before", Length: 'Length',
"is after": "is after", 'The field value cannot be greater than ': 'The field value cannot be greater than ',
"is on or after": "is on or after", 'The field value cannot be less than ': 'The field value cannot be less than ',
"is on or before": "is on or before", 'The field value is not an integer number': 'The field value is not an integer number',
"is between": "is between", 'Set default value': 'Set default value',
"Upload": "Upload", 'Default value': 'Default value',
"Select level": "Select level", 'is before': 'is before',
"Province": "Province", 'is after': 'is after',
"City": "City", 'is on or after': 'is on or after',
"Area": "Area", 'is on or before': 'is on or before',
"Street": "Street", 'is between': 'is between',
"Village": "Village", Upload: 'Upload',
"Must select to the last level": "Must select to the last level", 'Select level': 'Select level',
"Move {{title}} to": "Move {{title}} to", Province: 'Province',
"Target position": "Target position", City: 'City',
"After": "After", Area: 'Area',
"Before": "Before", Street: 'Street',
"Add {{type}} before \"{{title}}\"": "Add {{type}} before \"{{title}}\"", Village: 'Village',
"Add {{type}} after \"{{title}}\"": "Add {{type}} after \"{{title}}\"", 'Must select to the last level': 'Must select to the last level',
"Add {{type}} in \"{{title}}\"": "Add {{type}} in \"{{title}}\"", 'Move {{title}} to': 'Move {{title}} to',
"Original name": "Original name", 'Target position': 'Target position',
"Custom name": "Custom name", After: 'After',
"Custom Title": "Custom Title", Before: 'Before',
"Options": "Options", 'Add {{type}} before "{{title}}"': 'Add {{type}} before "{{title}}"',
"Option value": "Option value", 'Add {{type}} after "{{title}}"': 'Add {{type}} after "{{title}}"',
"Option label": "Option label", 'Add {{type}} in "{{title}}"': 'Add {{type}} in "{{title}}"',
"Color": "Color", 'Original name': 'Original name',
"Add option": "Add option", 'Custom name': 'Custom name',
"Related collection": "Related collection", 'Custom Title': 'Custom Title',
"Allow linking to multiple records": "Allow linking to multiple records", Options: 'Options',
"Allow uploading multiple files": "Allow uploading multiple files", 'Option value': 'Option value',
"Configure calendar": "Configure calendar", 'Option label': 'Option label',
"Title field": "Title field", Color: 'Color',
"Custom title": "Custom title", 'Add option': 'Add option',
"Daily": "Daily", 'Related collection': 'Related collection',
"Weekly": "Weekly", 'Allow linking to multiple records': 'Allow linking to multiple records',
"Monthly": "Monthly", 'Allow uploading multiple files': 'Allow uploading multiple files',
"Yearly": "Yearly", 'Configure calendar': 'Configure calendar',
"Repeats": "Repeats", 'Title field': 'Title field',
"Show lunar": "Show lunar", 'Custom title': 'Custom title',
"Start date field": "Start date field", Daily: 'Daily',
"End date field": "End date field", Weekly: 'Weekly',
"Navigate": "Navigate", Monthly: 'Monthly',
"Title": "Title", Yearly: 'Yearly',
"Description": "Description", Repeats: 'Repeats',
"Select view": "Select view", 'Show lunar': 'Show lunar',
"Reset": "Reset", 'Start date field': 'Start date field',
"Importable fields": "Importable fields", 'End date field': 'End date field',
"Exportable fields": "Exportable fields", Navigate: 'Navigate',
"Saved successfully": "Saved successfully", Title: 'Title',
"Nickname": "Nickname", Description: 'Description',
"Sign in": "Sign in", 'Select view': 'Select view',
"Sign in via account": "Sign in via account", Reset: 'Reset',
"Sign in via phone": "Sign in via phone", 'Importable fields': 'Importable fields',
"Create an account": "Create an account", 'Exportable fields': 'Exportable fields',
"Sign up": "Sign up", 'Saved successfully': 'Saved successfully',
"Confirm password": "Confirm password", Nickname: 'Nickname',
"Log in with an existing account": "Log in with an existing account", 'Sign in': 'Sign in',
"Signed up successfully. It will jump to the login page.": "Signed up successfully. It will jump to the login page.", 'Sign in via account': 'Sign in via account',
"Password mismatch": "Password mismatch", 'Sign in via phone': 'Sign in via phone',
"Users": "Users", 'Create an account': 'Create an account',
"Verification code": "Verification code", 'Sign up': 'Sign up',
"Send code": "Send code", 'Confirm password': 'Confirm password',
"Retry after {{count}} seconds": "Retry after {{count}} seconds", 'Log in with an existing account': 'Log in with an existing account',
"Roles": "Roles", 'Signed up successfully. It will jump to the login page.': 'Signed up successfully. It will jump to the login page.',
"Add role": "Add role", 'Password mismatch': 'Password mismatch',
"Role name": "Role name", Users: 'Users',
"Configure": "Configure", 'Verification code': 'Verification code',
"Configure permissions": "Configure permissions", 'Send code': 'Send code',
"Edit role": "Edit role", 'Retry after {{count}} seconds': 'Retry after {{count}} seconds',
"Action permissions": "Action permissions", Roles: 'Roles',
"Menu permissions": "Menu permissions", 'Add role': 'Add role',
"Menu item name": "Menu item name", 'Role name': 'Role name',
"Allow access": "Allow access", Configure: 'Configure',
"Action name": "Action name", 'Configure permissions': 'Configure permissions',
"Allow action": "Allow action", 'Edit role': 'Edit role',
"Action scope": "Action scope", 'Action permissions': 'Action permissions',
"Operate on new data": "Operate on new data", 'Menu permissions': 'Menu permissions',
"Operate on existing data": "Operate on existing data", 'Menu item name': 'Menu item name',
"Yes": "Yes", 'Allow access': 'Allow access',
"No": "No", 'Action name': 'Action name',
"Red": "Red", 'Allow action': 'Allow action',
"Magenta": "Magenta", 'Action scope': 'Action scope',
"Volcano": "Volcano", 'Operate on new data': 'Operate on new data',
"Orange": "Orange", 'Operate on existing data': 'Operate on existing data',
"Gold": "Gold", Yes: 'Yes',
"Lime": "Lime", No: 'No',
"Green": "Green", Red: 'Red',
"Cyan": "Cyan", Magenta: 'Magenta',
"Blue": "Blue", Volcano: 'Volcano',
"Geek blue": "Geek blue", Orange: 'Orange',
"Purple": "Purple", Gold: 'Gold',
"Default": "Default", Lime: 'Lime',
"Add card": "Add card", Green: 'Green',
"edit title": "edit title", Cyan: 'Cyan',
"Turn pages": "Turn pages", Blue: 'Blue',
"Others": "Others", 'Geek blue': 'Geek blue',
"Save as template": "Save as template", Purple: 'Purple',
"Save as block template": "Save as block template", Default: 'Default',
"Block templates": "Block templates", 'Add card': 'Add card',
"Convert reference to duplicate": "Convert reference to duplicate", 'edit title': 'edit title',
"Template name": "Template name", 'Turn pages': 'Turn pages',
"Block type": "Block type", Others: 'Others',
"No blocks to connect": "No blocks to connect", 'Save as template': 'Save as template',
"Action column": "Action column", 'Save as block template': 'Save as block template',
"Records per page": "Records per page", 'Block templates': 'Block templates',
"(Fields only)": "(Fields only)", 'Convert reference to duplicate': 'Convert reference to duplicate',
"Button title": "Button title", 'Template name': 'Template name',
"Button icon": "Button icon", 'Block type': 'Block type',
"Submitted successfully": "Submitted successfully", 'No blocks to connect': 'No blocks to connect',
"Operation succeeded": "Operation succeeded", 'Action column': 'Action column',
"Operation failed": "Operation failed", 'Records per page': 'Records per page',
"Open mode": "Open mode", '(Fields only)': '(Fields only)',
"Popup size": "Popup size", 'Button title': 'Button title',
"Small": "Small", 'Button icon': 'Button icon',
"Middle": "Middle", 'Submitted successfully': 'Submitted successfully',
"Large": "Large", 'Operation succeeded': 'Operation succeeded',
"Menu item title": "Menu item title", 'Operation failed': 'Operation failed',
"Menu item icon": "Menu item icon", 'Open mode': 'Open mode',
"Target": "Target", 'Popup size': 'Popup size',
"Position": "Position", Small: 'Small',
"Insert before": "Insert before", Middle: 'Middle',
"Insert after": "Insert after", Large: 'Large',
"UI Editor": "UI Editor", 'Menu item title': 'Menu item title',
"ASC": "ASC", 'Menu item icon': 'Menu item icon',
"DESC": "DESC", Target: 'Target',
"Add sort field": "Add sort field", Position: 'Position',
"ID": "ID", 'Insert before': 'Insert before',
"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.", 'Insert after': 'Insert after',
"Drawer": "Drawer", 'UI Editor': 'UI Editor',
"Dialog": "Dialog", ASC: 'ASC',
"Delete action": "Delete action", DESC: 'DESC',
"Custom column title": "Custom column title", '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', 'Column title': 'column title',
"Original title: ": "Original title: ", 'Original title: ': 'Original title: ',
"Delete table column": "Delete table column", 'Delete table column': 'Delete table column',
"Skip required validation": "Skip required validation", 'Skip required validation': 'Skip required validation',
"Form values": "Form values", 'Form values': 'Form values',
"Fields values": "Fields values", 'Fields values': 'Fields values',
'The field has been deleted': 'The field has been deleted', '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", 'When submitting the following fields, the saved values are':
"After successful submission": "After successful submission", 'When submitting the following fields, the saved values are',
"Then": "Then", 'After successful submission': 'After successful submission',
"Stay on current page": "Stay on current page", Then: 'Then',
"Redirect to": "Redirect to", 'Stay on current page': 'Stay on current page',
"Save action": "Save action", 'Redirect to': 'Redirect to',
"Exists": "Exists", 'Save action': 'Save action',
"Add condition": "Add condition", Exists: 'Exists',
"Add condition group": "Add condition group", 'Add condition': 'Add condition',
"exists": "exists", 'Add condition group': 'Add condition group',
"not exists": "not exists", exists: 'exists',
"=": "=", 'not exists': 'not exists',
"≠": "≠", '=': '=',
">": ">", '≠': '≠',
"≥": "≥", '>': '>',
"<": "<", '≥': '≥',
"≤": "≤", '<': '<',
"Role UID": "Role UID", '≤': '≤',
"Precision": "Precision", 'Role UID': 'Role UID',
"Formula mode": "Formula mode", Precision: 'Precision',
"Expression": "Expression", 'Formula mode': 'Formula mode',
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.", Expression: 'Expression',
"Formula error.": "Formula error.", 'Input +, -, *, /, ( ) to calculate, input @ to open field variables.':
"Rich Text": "Rich Text", 'Input +, -, *, /, ( ) to calculate, input @ to open field variables.',
"Junction collection": "Junction collection", 'Formula error.': 'Formula error.',
"Leave it blank, unless you need a custom intermediate table": "Leave it blank, unless you need a custom intermediate table", 'Rich Text': 'Rich Text',
"Fields": "Fields", 'Junction collection': 'Junction collection',
"Edit field title": "Edit field title", 'Leave it blank, unless you need a custom intermediate table':
"Field title": "Field title", 'Leave it blank, unless you need a custom intermediate table',
"Original field title: ": "Original field title: ", Fields: 'Fields',
"Edit tooltip": "Edit tooltip", 'Edit field title': 'Edit field title',
"Delete field": "Delete field", 'Field title': 'Field title',
"Select collection": "Select collection", 'Original field title: ': 'Original field title: ',
"Blank block": "Blank block", 'Edit tooltip': 'Edit tooltip',
"Duplicate template": "Duplicate template", 'Delete field': 'Delete field',
"Reference template": "Reference template", 'Select collection': 'Select collection',
"Create calendar block": "Create calendar block", 'Blank block': 'Blank block',
"Create kanban block": "Create kanban block", 'Duplicate template': 'Duplicate template',
"Grouping field": "Grouping field", 'Reference template': 'Reference template',
"Single select and radio fields can be used as the grouping field": "Single select and radio fields can be used as the grouping field", 'Create calendar block': 'Create calendar block',
"Tab name": "Tab name", 'Create kanban block': 'Create kanban block',
"Current record blocks": "Current record blocks", 'Grouping field': 'Grouping field',
"Popup message": "Popup message", 'Single select and radio fields can be used as the grouping field':
"Delete role": "Delete role", 'Single select and radio fields can be used as the grouping field',
"Role display name": "Role display name", 'Tab name': 'Tab name',
"Default role": "Default role", 'Current record blocks': 'Current record blocks',
"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.", 'Popup message': 'Popup message',
"Allows configuration of the whole system, including UI, collections, permissions, etc.": "Allows configuration of the whole system, including UI, collections, permissions, etc.", 'Delete role': 'Delete role',
"New menu items are allowed to be accessed by default.": "New menu items are allowed to be accessed by default.", 'Role display name': 'Role display name',
"Global permissions": "Global permissions", 'Default role': 'Default role',
"General permissions": "General permissions", 'All collections use general action permissions by default; permission configured individually will override the default one.':
"Global action permissions": "Global action permissions", 'All collections use general action permissions by default; permission configured individually will override the default one.',
"General action permissions": "General action permissions", 'Allows configuration of the whole system, including UI, collections, permissions, etc.':
"Plugin settings permissions": "Plugin settings permissions", 'Allows configuration of the whole system, including UI, collections, permissions, etc.',
"Allow to desgin pages": "Allow to desgin pages", 'New menu items are allowed to be accessed by default.': 'New menu items are allowed to be accessed by default.',
"Allow to manage plugins": "Allow to manage plugins", 'Global permissions': 'Global permissions',
"Allow to configure plugins": "Allow to configure plugins", 'General permissions': 'General permissions',
"Allows to configure interface": "Allows to configure interface", 'Global action permissions': 'Global action permissions',
"Allows to install, activate, disable plugins": "Allows to install, activate, disable plugins", 'General action permissions': 'General action permissions',
"Allows to configure plugins": "Allows to configure plugins", 'Plugin settings permissions': 'Plugin settings permissions',
"Action display name": "Action display name", 'Allow to desgin pages': 'Allow to desgin pages',
"Allow": "Allow", 'Allow to manage plugins': 'Allow to manage plugins',
"Data scope": "Data scope", 'Allow to configure plugins': 'Allow to configure plugins',
"Action on new records": "Action on new records", 'Allows to configure interface': 'Allows to configure interface',
"Action on existing records": "Action on existing records", 'Allows to install, activate, disable plugins': 'Allows to install, activate, disable plugins',
"All records": "All records", 'Allows to configure plugins': 'Allows to configure plugins',
"Own records": "Own records", 'Action display name': 'Action display name',
"Permission policy": "Permission policy", Allow: 'Allow',
"Individual": "Individual", 'Data scope': 'Data scope',
"General": "General", 'Action on new records': 'Action on new records',
"Accessible": "Accessible", 'Action on existing records': 'Action on existing records',
"Configure permission": "Configure permission", 'All records': 'All records',
"Action permission": "Action permission", 'Own records': 'Own records',
"Field permission": "Field permission", 'Permission policy': 'Permission policy',
"Scope name": "Scope name", Individual: 'Individual',
"Unsaved changes": "Unsaved changes", 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?", "Are you sure you don't want to save?": "Are you sure you don't want to save?",
"Dragging": "Dragging", "Dragging": "Dragging",
"Popup": "Popup", "Popup": "Popup",
@ -765,4 +778,7 @@ export default {
"Select all":"Select all", "Select all":"Select all",
"Restart": "Restart", "Restart": "Restart",
"Restart application": "Restart application", "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',
}; };

View File

@ -222,6 +222,7 @@ export default {
'Records can be sorted': '可以对行记录进行排序', 'Records can be sorted': '可以对行记录进行排序',
'Calendar collection': '日历数据表', 'Calendar collection': '日历数据表',
'General collection': '普通数据表', 'General collection': '普通数据表',
'SQL collection': 'SQL数据表',
'Connect to database view': '连接数据库视图', 'Connect to database view': '连接数据库视图',
'Source collections': '来源数据表', 'Source collections': '来源数据表',
'Field source': '来源字段', 'Field source': '来源字段',
@ -709,50 +710,52 @@ export default {
'Plugin manager': '插件管理器', 'Plugin manager': '插件管理器',
Local: '本地', Local: '本地',
'Built-in': '内置', 'Built-in': '内置',
'Marketplace': '插件市场', Marketplace: '插件市场',
"Add plugin": "新增插件", 'Add plugin': '新增插件',
"Upgrade": "可供更新", Upgrade: '可供更新',
"Plugin dependencies check failed": "插件依赖检查失败", 'Plugin dependencies check failed': '插件依赖检查失败',
"Remove": "移除", Remove: '移除',
"Docs": "文档", Docs: '文档',
"More details": "更多详情", 'More details': '更多详情',
"Upload new version": "上传新版", 'Upload new version': '上传新版',
"Official plugin": "官方插件", 'Official plugin': '官方插件',
"Version": "版本", Version: '版本',
"Npm package": "Npm 包", 'Npm package': 'Npm 包',
"Upload plugin": "上传插件", 'Upload plugin': '上传插件',
"Npm package name": "Npm 包名", 'Npm package name': 'Npm 包名',
"Add type": "新增方式", 'Add type': '新增方式',
"Plugin source": "插件来源", 'Plugin source': '插件来源',
"Changelog": "更新日志", Changelog: '更新日志',
"Dependencies check": "依赖检查", 'Dependencies check': '依赖检查',
"Update plugin": "更新插件", 'Update plugin': '更新插件',
"Installing": "安装中", Installing: '安装中',
"The deletion was successful.": "删除成功", 'The deletion was successful.': '删除成功',
"Plugin Zip File": "插件压缩包", 'Plugin Zip File': '插件压缩包',
"Compressed file url": "压缩包地址", 'Compressed file url': '压缩包地址',
"Last updated": "最后更新", 'Last updated': '最后更新',
"PackageName": "包名", PackageName: '包名',
"DisplayName": "显示名称", DisplayName: '显示名称',
"Readme": "说明文档", Readme: '说明文档',
"Dependencies compatibility check": "依赖兼容性检查", 'Dependencies compatibility check': '依赖兼容性检查',
"Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "插件兼容性检查失败,你应该修改依赖版本以满足版本要求。", 'Plugin dependencies check failed, you should change the dependent version to meet the version requirements.':
"Version range": "版本范围", '插件兼容性检查失败,你应该修改依赖版本以满足版本要求。',
"Plugin's version": "插件的版本", 'Version range': '版本范围',
"Result": "结果", "Plugin's version": '插件的版本',
"No CHANGELOG.md file": "没有 CHANGELOG.md 日志", Result: '结果',
"No README.md file": "没有 README.md 文件", 'No CHANGELOG.md file': '没有 CHANGELOG.md 日志',
"Homepage": "主页", 'No README.md file': '没有 README.md 文件',
'Drag and drop the file here or click to upload, file size should not exceed 30M': '将文件拖放到此处或单击上传,文件大小不应超过 30M', Homepage: '主页',
"Dependencies check failed, can't enable.": "依赖检查失败,无法启用。", 'Drag and drop the file here or click to upload, file size should not exceed 30M':
"Plugin starting...": "插件启动中...", '将文件拖放到此处或单击上传,文件大小不应超过 30M',
"Plugin stopping...": "插件停止中...", "Dependencies check failed, can't enable.": '依赖检查失败,无法启用。',
"Are you sure to delete this plugin?": "确定要删除此插件吗?", 'Plugin starting...': '插件启动中...',
"re-download file": "重新下载文件", 'Plugin stopping...': '插件停止中...',
"Not enabled": "未启用", 'Are you sure to delete this plugin?': '确定要删除此插件吗?',
"Search plugin": "搜索插件", 're-download file': '重新下载文件',
"Author": "作者", 'Not enabled': '未启用',
"Plugin loading failed. Please check the server logs.": "插件加载失败,请检查服务器日志。", 'Search plugin': '搜索插件',
Author: '作者',
'Plugin loading failed. Please check the server logs.': '插件加载失败,请检查服务器日志。',
'Coming soon...': '敬请期待...', 'Coming soon...': '敬请期待...',
'All plugin settings': '所有插件配置', 'All plugin settings': '所有插件配置',
Bookmark: '书签', Bookmark: '书签',
@ -863,11 +866,13 @@ export default {
'Date display format': '日期显示格式', 'Date display format': '日期显示格式',
'Assign data scope for the template': '为模板指定数据范围', 'Assign data scope for the template': '为模板指定数据范围',
'Table selected records': '表格中选中的记录', 'Table selected records': '表格中选中的记录',
Tag: '标签', Tag: '标签',
'Tag color field': '标签颜色字段', 'Tag color field': '标签颜色字段',
'Sync successfully': '同步成功', 'Sync successfully': '同步成功',
'Sync from form fields': '同步表单字段', 'Sync from form fields': '同步表单字段',
'Select all': '全选', 'Select all': '全选',
'Determine whether a record exists by the following fields': '通过以下字段判断记录是否存在', '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 语句',
}; };

View File

@ -76,7 +76,7 @@ export const CalendarActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
], ],

View File

@ -25,7 +25,7 @@ export const CalendarFormActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); 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 }}', useProps: '{{ useCustomizeRequestActionProps }}',
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
], ],

View File

@ -31,9 +31,9 @@ export const GridCardActionInitializers = {
skipScopeCheck: true, skipScopeCheck: true,
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); 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, skipScopeCheck: true,
}, },
}, },
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
{ {
type: 'item', type: 'item',
@ -163,9 +167,9 @@ export const GridCardItemActionInitializers = {
'x-decorator': 'ACLActionProvider', 'x-decorator': 'ACLActionProvider',
'x-align': 'left', 'x-align': 'left',
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); 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-decorator': 'ACLActionProvider',
'x-align': 'left', 'x-align': 'left',
}, },
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
], ],
}, },
@ -261,9 +269,9 @@ export const GridCardItemActionInitializers = {
useProps: '{{ useCustomizeUpdateActionProps }}', useProps: '{{ useCustomizeUpdateActionProps }}',
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); 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 }}', useProps: '{{ useCustomizeRequestActionProps }}',
}, },
}, },
visible: () => { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return (collection as any).template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
], ],

View File

@ -33,7 +33,11 @@ export const ListActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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, skipScopeCheck: true,
}, },
}, },
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
{ {
type: 'item', type: 'item',
@ -165,7 +173,7 @@ export const ListItemActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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-decorator': 'ACLActionProvider',
'x-align': 'left', 'x-align': 'left',
}, },
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
], ],
}, },
@ -263,7 +275,7 @@ export const ListItemActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return (collection as any).template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
], ],

View File

@ -2,7 +2,7 @@ import { useCollection } from '../..';
const useVisibleCollection = () => { const useVisibleCollection = () => {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}; };
// 表单的操作配置 // 表单的操作配置
export const ReadPrettyFormActionInitializers = { export const ReadPrettyFormActionInitializers = {

View File

@ -17,10 +17,11 @@ const recursiveParent = (schema: Schema) => {
const useRelationFields = () => { const useRelationFields = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const collection = useCollection();
let fields = []; let fields = [];
if (fieldSchema['x-initializer']) { if (fieldSchema['x-initializer']) {
fields = useCollection().fields; fields = collection.fields;
} else { } else {
const collection = recursiveParent(fieldSchema.parent); const collection = recursiveParent(fieldSchema.parent);
if (collection) { if (collection) {
@ -184,7 +185,18 @@ export const RecordBlockInitializers = (props: any) => {
const hasFormChildCollection = formChildrenCollections?.length > 0; const hasFormChildCollection = formChildrenCollections?.length > 0;
const detailChildrenCollections = getChildrenCollections(collection.name, true); const detailChildrenCollections = getChildrenCollections(collection.name, true);
const hasDetailChildCollection = detailChildrenCollections?.length > 0; 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 ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
wrap={gridRowColWrap} wrap={gridRowColWrap}
@ -202,11 +214,7 @@ export const RecordBlockInitializers = (props: any) => {
key: 'details', key: 'details',
type: 'subMenu', type: 'subMenu',
title: '{{t("Details")}}', title: '{{t("Details")}}',
children: useDetailCollections({ children: detailChildren,
...props,
childrenCollections: detailChildrenCollections,
collection,
}),
} }
: { : {
key: 'details', key: 'details',
@ -220,11 +228,7 @@ export const RecordBlockInitializers = (props: any) => {
key: 'form', key: 'form',
type: 'subMenu', type: 'subMenu',
title: '{{t("Form")}}', title: '{{t("Form")}}',
children: useFormCollections({ children: formChildren,
...props,
childrenCollections: formChildrenCollections,
collection,
}),
} }
: modifyFlag && { : modifyFlag && {
key: 'form', key: 'form',

View File

@ -101,7 +101,7 @@ export const TableActionColumnInitializers = (props: any) => {
'x-decorator': 'ACLActionProvider', 'x-decorator': 'ACLActionProvider',
}, },
visible: () => { 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', 'x-decorator': 'ACLActionProvider',
}, },
visible: () => { visible: () => {
return collection.template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
collection.tree && collection.tree &&
@ -139,7 +139,7 @@ export const TableActionColumnInitializers = (props: any) => {
'x-decorator': 'ACLActionProvider', 'x-decorator': 'ACLActionProvider',
}, },
visible: () => { 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: () => { 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: () => { visible: () => {
return collection.template !== 'view' || collection?.writableView; return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql';
}, },
}, },
], ],

View File

@ -34,7 +34,7 @@ export const TableActionInitializers = {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); 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', type: 'divider',
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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() { visible: function useVisible() {
const collection = useCollection(); const collection = useCollection();
return collection.template !== 'view' || collection?.writableView; return !['view', 'sql'].includes(collection.template) || collection?.writableView;
}, },
}, },
], ],

View File

@ -18,7 +18,9 @@ export const DetailsBlockInitializer = (props) => {
collection: item.name, collection: item.name,
rowKey: collection.filterTargetKey || 'id', rowKey: collection.filterTargetKey || 'id',
actionInitializers: actionInitializers:
(collection.template !== 'view' || collection?.writableView) && 'DetailsActionInitializers', (collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'sql' &&
'DetailsActionInitializers',
}); });
insert(schema); insert(schema);
}} }}

View File

@ -862,7 +862,10 @@ export const useCollectionDataSourceItems = (componentName) => {
const fields = getCollectionFields(item.name); const fields = getCollectionFields(item.name);
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) { if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
return false; 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; return false;
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
return false; return false;

View File

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

View File

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

View File

@ -10,4 +10,16 @@ describe('sql parser', () => {
expect(firstColumn['expr']['table']).toEqual('users'); expect(firstColumn['expr']['table']).toEqual('users');
expect(firstColumn['expr']['column']).toEqual('id'); 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));
// });
}); });

View File

@ -70,6 +70,7 @@ import {
import { patchSequelizeQueryInterface, snakeCase } from './utils'; import { patchSequelizeQueryInterface, snakeCase } from './utils';
import { BaseValueParser, registerFieldValueParsers } from './value-parsers'; import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
import { ViewCollection } from './view-collection'; import { ViewCollection } from './view-collection';
import { SqlCollection } from './sql-collection/sql-collection';
export type MergeOptions = merge.Options; export type MergeOptions = merge.Options;
@ -358,6 +359,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
if (collection.options.schema) { if (collection.options.schema) {
collection.model._schema = collection.options.schema; collection.model._schema = collection.options.schema;
} }
if (collection.options.sql) {
collection.modelInit();
}
}); });
this.on('beforeDefineCollection', (options) => { this.on('beforeDefineCollection', (options) => {
@ -451,6 +455,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
return ViewCollection; return ViewCollection;
} }
if (options.sql) {
return SqlCollection;
}
return Collection; return Collection;
})(); })();

View File

@ -41,4 +41,5 @@ export { snakeCase } from './utils';
export * from './value-parsers'; export * from './value-parsers';
export * from './view-collection'; export * from './view-collection';
export * from './view/view-inference'; export * from './view/view-inference';
export * from './sql-collection';
export * from './helpers'; export * from './helpers';

View File

@ -0,0 +1,2 @@
export * from './sql-model';
export * from './sql-collection';

View 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};`;
}

View 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);
},
});
}
}

View 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;
}
}

View File

@ -6,6 +6,7 @@ interface LoadOptions extends Transactionable {
// TODO // TODO
skipField?: boolean | Array<string>; skipField?: boolean | Array<string>;
skipExist?: boolean; skipExist?: boolean;
resetFields?: boolean;
} }
export class CollectionModel extends MagicAttributeModel { export class CollectionModel extends MagicAttributeModel {
@ -14,7 +15,7 @@ export class CollectionModel extends MagicAttributeModel {
} }
async load(loadOptions: LoadOptions = {}) { async load(loadOptions: LoadOptions = {}) {
const { skipExist, skipField, transaction } = loadOptions; const { skipExist, skipField, resetFields, transaction } = loadOptions;
const name = this.get('name'); const name = this.get('name');
let collection: Collection; let collection: Collection;
@ -23,7 +24,6 @@ export class CollectionModel extends MagicAttributeModel {
...this.get(), ...this.get(),
fields: [], fields: [],
}; };
if (this.db.hasCollection(name)) { if (this.db.hasCollection(name)) {
collection = this.db.getCollection(name); collection = this.db.getCollection(name);
@ -31,6 +31,10 @@ export class CollectionModel extends MagicAttributeModel {
return collection; return collection;
} }
if (resetFields) {
collection.resetFields();
}
collection.updateOptions(collectionOptions); collection.updateOptions(collectionOptions);
} else { } else {
collection = this.db.collection(collectionOptions); collection = this.db.collection(collectionOptions);

View File

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

View File

@ -17,6 +17,7 @@ import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollec
import { CollectionModel, FieldModel } from './models'; import { CollectionModel, FieldModel } from './models';
import collectionActions from './resourcers/collections'; import collectionActions from './resourcers/collections';
import viewResourcer from './resourcers/views'; import viewResourcer from './resourcers/views';
import sqlResourcer from './resourcers/sql';
export class CollectionManagerPlugin extends Plugin { export class CollectionManagerPlugin extends Plugin {
public schema: string; public schema: string;
@ -51,7 +52,7 @@ export class CollectionManagerPlugin extends Plugin {
this.app.acl.registerSnippet({ this.app.acl.registerSnippet({
name: `pm.${this.name}.collections`, 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) => { this.app.db.on('collections.beforeCreate', async (model) => {
@ -277,6 +278,7 @@ export class CollectionManagerPlugin extends Plugin {
}); });
this.app.resource(viewResourcer); this.app.resource(viewResourcer);
this.app.resource(sqlResourcer);
this.app.actions(collectionActions); this.app.actions(collectionActions);
const handleFieldSource = (fields) => { const handleFieldSource = (fields) => {

View File

@ -14,6 +14,7 @@ describe('query', () => {
beforeAll(() => { beforeAll(() => {
app = mockServer(); app = mockServer();
app.db.options.underscored = true;
app.db.collection({ app.db.collection({
name: 'orders', name: 'orders',
fields: [ fields: [

View File

@ -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: collectionName, measures, dimensions, orders, filter } = ctx.action.params.values as QueryParams;
const collection = ctx.db.getCollection(collectionName); const collection = ctx.db.getCollection(collectionName);
const fields = collection.fields; const fields = collection.fields;
const underscored = ctx.db.options.underscored; const underscored = collection.options.underscored;
const models: { const models: {
[target: string]: { [target: string]: {
type: string; type: string;

View File

@ -20,7 +20,11 @@ export const ImportInitializerProvider = (props: any) => {
}, },
visible: function useVisible() { visible: function useVisible() {
const collection = useCollection(); 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; return props.children;

View File

@ -1,3 +1,5 @@
import { useCollection } from '@nocobase/client';
// 表格操作配置 // 表格操作配置
export const MapActionInitializers = { export const MapActionInitializers = {
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
@ -29,6 +31,10 @@ export const MapActionInitializers = {
skipScopeCheck: true, skipScopeCheck: true,
}, },
}, },
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
{ {
type: 'item', type: 'item',
@ -42,6 +48,10 @@ export const MapActionInitializers = {
}, },
{ {
type: 'divider', type: 'divider',
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
{ {
type: 'subMenu', type: 'subMenu',
@ -92,6 +102,10 @@ export const MapActionInitializers = {
}, },
}, },
], ],
visible: function useVisible() {
const collection = useCollection();
return collection.template !== 'sql';
},
}, },
], ],
}; };

View File

@ -23324,7 +23324,7 @@ vite@4.3.1:
vite@^4.4.9: vite@^4.4.9:
version "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== integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
dependencies: dependencies:
esbuild "^0.18.10" esbuild "^0.18.10"