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