mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
feat: adapt desktop blocks to mobile (#4945)
* feat: register workflow blocks to mobile page * fix: should hide Divider in subpage * refactor: rename 'Data blocks' to 'Desktop data blocks' * feat: adapt blocks within subpages for mobile * feat: adapt Filter action * feat: isolate block templates between desktop and mobile * refactor: export storePopupContext * feat: support popup URL for 'Workflow todos' * chore: update e2e tests * chore: make e2e tests pass * chore: add comment * fix: make popup style of duplicate and bulk edit right * fix(GridCard): ensure single column display in mobile * fix: fix goBack * refactor: make more stable * refactor: change name for add blocks menu * fix: fix block template for mobile * feat: adapt Apply action of approval block to mobile * fix(Map): use window.open to redirect to configuration page * Revert "fix(Map): use window.open to redirect to configuration page" This reverts commit 248ae8b68cfd78415184dfab2442081363872fb0. * fix: redirect to the main app page when URL is starts with 'admin' * fix(Link): make path right * fix: refactor Popup to fix draging bug * fix: should auto refresh when submiting in Manual popup * fix(Action.Container): should return null when visible is false (T-4949) * fix: increase z-index of subpage to cover Amap elements * fix: fix tab switching not work (T-4985) * fix(Link): should be change Link's URL of all table rows after editing URL (T-4981) * fix: fix URL not changed after closing popup (T-4987) * fix: make unit tests pass * fix: make unit tests pass * chore: get e2e tests to pass * fix: use Popup to display data picker (T-4965) * fix: use mobile Popup in some bloks * refactor: use local isMobile * fix: increase Popup's z-index to cover subpage * fix: optimize Popup for mobile * style: createRecordAction style improve * refactor(AssociationField): get Component from AssociationFieldModeProvider * refactor(InternalPopoverNester): support custom Container component * feat: adapt PopoverNester to mobile * chore: update unit tests * fix: get e2e tests to pass * chore: make e2e more stable * refactor: move mobile-action-page in adaptor-of-desktop folder * fix: get the z-index of popups and subpages correct * feat: unify the styles of popups * chore: make e2e more stable --------- Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
64e53558df
commit
a429b7a4b3
@ -2,9 +2,7 @@
|
||||
"version": "1.3.0-alpha",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -20,7 +20,7 @@ import omit from 'lodash/omit';
|
||||
import qs from 'qs';
|
||||
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
import { NavigateFunction, useHref } from 'react-router-dom';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import {
|
||||
AssociationFilter,
|
||||
@ -1592,6 +1592,9 @@ export function useLinkActionProps(componentProps?: any) {
|
||||
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
|
||||
const { parseURLAndParams } = useParseURLAndParams();
|
||||
|
||||
// see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter
|
||||
const basenameOfCurrentRouter = useHref('/');
|
||||
|
||||
return {
|
||||
type: 'default',
|
||||
async onClick() {
|
||||
@ -1605,7 +1608,7 @@ export function useLinkActionProps(componentProps?: any) {
|
||||
if (openInNewWindow) {
|
||||
window.open(completeURL(link), '_blank');
|
||||
} else {
|
||||
navigateWithinSelf(link, navigate);
|
||||
navigateWithinSelf(link, navigate, window.location.origin + basenameOfCurrentRouter);
|
||||
}
|
||||
} else {
|
||||
console.error('link should be a string');
|
||||
@ -1719,18 +1722,18 @@ export function completeURL(url: string, origin = window.location.origin) {
|
||||
return url.startsWith('/') ? `${origin}${url}` : `${origin}/${url}`;
|
||||
}
|
||||
|
||||
export function navigateWithinSelf(link: string, navigate: NavigateFunction, origin = window.location.origin) {
|
||||
export function navigateWithinSelf(link: string, navigate: NavigateFunction, basePath = window.location.origin) {
|
||||
if (!_.isString(link)) {
|
||||
return console.error('link should be a string');
|
||||
}
|
||||
|
||||
if (isURL(link)) {
|
||||
if (link.startsWith(origin)) {
|
||||
navigate(link.replace(origin, ''));
|
||||
if (link.startsWith(basePath)) {
|
||||
navigate(completeURL(link.replace(basePath, ''), ''));
|
||||
} else {
|
||||
window.open(link, '_self');
|
||||
}
|
||||
} else {
|
||||
navigate(link.startsWith('/') ? link : `/${link}`);
|
||||
navigate(completeURL(link, ''));
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useMemo } from 'react';
|
||||
import { useBlockTemplateContext } from '../../schema-templates/BlockTemplate';
|
||||
import { useBlockTemplateContext } from '../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const useBlockHeightProps = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -26,11 +26,21 @@ const AppInner = memo(({ children }: { children: React.ReactNode }) => {
|
||||
});
|
||||
AppInner.displayName = 'AppInner';
|
||||
|
||||
const AntdAppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const AntdAppProvider = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
return (
|
||||
<App
|
||||
className={className}
|
||||
style={{
|
||||
height: '100%',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<AppInner>{children}</AppInner>
|
||||
|
@ -44,7 +44,7 @@ const commonOptions = {
|
||||
showAssociationFields: true,
|
||||
onlyCurrentDataSource: true,
|
||||
hideSearch: true,
|
||||
componentType: 'FormItem',
|
||||
componentType: `FormItem`,
|
||||
currentText: t('Current collection'),
|
||||
otherText: t('Other collections'),
|
||||
},
|
||||
|
@ -29,10 +29,11 @@ export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps>
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const { urlSchema, paramsSchema, openInNewWindowSchema } = useURLAndHTMLSchema();
|
||||
const componentProps = fieldSchema['x-component-props'] || {};
|
||||
const initialValues = {
|
||||
url: field.componentProps.url,
|
||||
params: field.componentProps.params || [{}],
|
||||
openInNewWindow: field.componentProps.openInNewWindow,
|
||||
url: componentProps.url,
|
||||
params: componentProps.params || [{}],
|
||||
openInNewWindow: componentProps.openInNewWindow,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -52,7 +53,6 @@ export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps>
|
||||
},
|
||||
}}
|
||||
onSubmit={({ url, params, openInNewWindow }) => {
|
||||
const componentProps = fieldSchema['x-component-props'] || {};
|
||||
componentProps.url = url;
|
||||
componentProps.params = params;
|
||||
componentProps.openInNewWindow = openInNewWindow;
|
||||
|
@ -21,6 +21,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem';
|
||||
|
||||
const commonItems: SchemaSettingsItemType[] = [
|
||||
@ -201,10 +202,11 @@ const commonItems: SchemaSettingsItemType[] = [
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Details',
|
||||
componentName: `${componentNamePrefix}Details`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -10,10 +10,11 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
|
||||
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
const commonItems: SchemaSettingsItemType[] = [
|
||||
{
|
||||
name: 'title',
|
||||
@ -27,7 +28,7 @@ const commonItems: SchemaSettingsItemType[] = [
|
||||
name: 'linkageRules',
|
||||
Component: SchemaSettingsLinkageRules,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { name } = useCollection();
|
||||
return {
|
||||
collectionName: name,
|
||||
readPretty: true,
|
||||
@ -38,13 +39,14 @@ const commonItems: SchemaSettingsItemType[] = [
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
insertAdjacentPosition: 'beforeEnd',
|
||||
componentName: 'ReadPrettyFormItem',
|
||||
componentName: `${componentNamePrefix}ReadPrettyFormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
|
||||
import {
|
||||
SchemaSettingsDataTemplates,
|
||||
SchemaSettingsFormItemTemplate,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from '../../../../schema-settings';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const createFormBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:createForm',
|
||||
@ -62,12 +64,13 @@ export const createFormBlockSettings = new SchemaSettings({
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
componentName: `${componentNamePrefix}FormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
|
||||
import {
|
||||
SchemaSettingsDataTemplates,
|
||||
SchemaSettingsFormItemTemplate,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from '../../../../schema-settings';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const editFormBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:editForm',
|
||||
@ -62,12 +64,13 @@ export const editFormBlockSettings = new SchemaSettings({
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
componentName: `${componentNamePrefix}FormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ export const GridCardBlockInitializer = ({
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
icon={<OrderedListOutlined />}
|
||||
componentType={'GridCard'}
|
||||
componentType={`GridCard`}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
return createBlockSchema(options);
|
||||
|
@ -20,9 +20,9 @@ import { pageSizeOptions } from '../../../../schema-component/antd/grid-card/opt
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||
import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow';
|
||||
import { useCollection } from '../../../../data-source';
|
||||
|
||||
export const gridCardBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:gridCard',
|
||||
@ -210,11 +210,12 @@ export const gridCardBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'GridCard',
|
||||
componentName: `${componentNamePrefix}GridCard`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ export const ListBlockInitializer = ({
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
icon={<OrderedListOutlined />}
|
||||
componentType={'List'}
|
||||
componentType={`List`}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
return createBlockSchema(options);
|
||||
|
@ -19,8 +19,8 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||
import { useCollection } from '../../../../data-source';
|
||||
|
||||
export const listBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:list',
|
||||
@ -212,11 +212,12 @@ export const listBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'List',
|
||||
componentName: `${componentNamePrefix}List`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -44,7 +44,7 @@ export const TableBlockInitializer = ({
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
icon={<TableOutlined />}
|
||||
componentType={'Table'}
|
||||
componentType={`Table`}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
return createBlockSchema(options);
|
||||
|
@ -23,8 +23,8 @@ import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSetti
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
|
||||
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||
import { useCollection } from '../../../../data-source';
|
||||
|
||||
export const tableBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:table',
|
||||
@ -212,10 +212,11 @@ export const tableBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Table',
|
||||
componentName: `${componentNamePrefix}Table`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -11,9 +11,9 @@ import { TableOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
|
||||
import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema';
|
||||
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
|
||||
import { Collection, CollectionFieldOptions } from '../../../../data-source';
|
||||
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
|
||||
import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema';
|
||||
|
||||
export const FilterCollapseBlockInitializer = ({
|
||||
filterCollections,
|
||||
@ -32,7 +32,7 @@ export const FilterCollapseBlockInitializer = ({
|
||||
{...itemConfig}
|
||||
onlyCurrentDataSource={onlyCurrentDataSource}
|
||||
icon={<TableOutlined />}
|
||||
componentType={'FilterCollapse'}
|
||||
componentType={`FilterCollapse`}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
const schema = createCollapseBlockSchema({
|
||||
dataSource: item.dataSource,
|
||||
|
@ -12,10 +12,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { FilterBlockType } from '../../../../filter-provider';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const filterCollapseBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:filterCollapse',
|
||||
@ -34,11 +35,12 @@ export const filterCollapseBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'FilterCollapse',
|
||||
componentName: `${componentNamePrefix}FilterCollapse`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -10,9 +10,10 @@
|
||||
import { FormOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
|
||||
import { createFilterFormBlockSchema } from './createFilterFormBlockSchema';
|
||||
import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer';
|
||||
import { Collection, CollectionFieldOptions } from '../../../../data-source';
|
||||
import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
import { createFilterFormBlockSchema } from './createFilterFormBlockSchema';
|
||||
|
||||
export const FilterFormBlockInitializer = ({
|
||||
filterCollections,
|
||||
@ -25,13 +26,14 @@ export const FilterFormBlockInitializer = ({
|
||||
}) => {
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
const { insert } = useSchemaInitializer();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
|
||||
return (
|
||||
<FilterBlockInitializer
|
||||
{...itemConfig}
|
||||
icon={<FormOutlined />}
|
||||
onlyCurrentDataSource={onlyCurrentDataSource}
|
||||
componentType={'FilterFormItem'}
|
||||
componentType={`${componentNamePrefix}FilterFormItem`}
|
||||
templateWrap={(templateSchema, { item }) => {
|
||||
const s = createFilterFormBlockSchema({
|
||||
templateSchema: templateSchema,
|
||||
|
@ -11,11 +11,13 @@ import { useFieldSchema } from '@formily/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
|
||||
import { FilterBlockType } from '../../../../filter-provider';
|
||||
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
||||
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
|
||||
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const filterFormBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:filterForm',
|
||||
@ -32,12 +34,13 @@ export const filterFormBlockSettings = new SchemaSettings({
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FilterFormItem',
|
||||
componentName: `${componentNamePrefix}FilterFormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -16,8 +16,8 @@ import { ComposedActionDrawer } from './types';
|
||||
|
||||
export const ActionContainer: ComposedActionDrawer = observer(
|
||||
(props: any) => {
|
||||
const { openMode } = useActionContext();
|
||||
const { getComponentByOpenMode } = useOpenModeContext();
|
||||
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext();
|
||||
const { openMode = defaultOpenMode } = useActionContext();
|
||||
const { currentLevel } = useCurrentPopupContext();
|
||||
|
||||
const Component = getComponentByOpenMode(openMode);
|
||||
|
@ -43,19 +43,19 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
|
||||
|
||||
const useBlockServiceInActionButton = () => {
|
||||
const { params } = useCurrentPopupContext();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const popupUidWithoutOpened = useFieldSchema()?.['x-uid'];
|
||||
const service = useDataBlockRequest();
|
||||
const currentPopupUid = params?.popupuid;
|
||||
|
||||
// By using caching, we solve the problem of not being able to obtain the correct service when closing a popup through a URL
|
||||
// 把 service 存起来
|
||||
useEffect(() => {
|
||||
// This case refers to when the current button is rendered on a page or in a popup
|
||||
if (popupUidWithoutOpened && currentPopupUid !== popupUidWithoutOpened) {
|
||||
storeBlockService(popupUidWithoutOpened, { service });
|
||||
}
|
||||
}, [popupUidWithoutOpened, service, currentPopupUid]);
|
||||
}, [popupUidWithoutOpened, service, currentPopupUid, fieldSchema]);
|
||||
|
||||
// This case refers to when the current button is closed as a popup (the button's uid is the same as the popup's uid)
|
||||
// 关闭弹窗时,获取到对应的 service
|
||||
if (currentPopupUid === popupUidWithoutOpened) {
|
||||
return getBlockService(currentPopupUid)?.service || service;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
|
||||
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import {
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
useActionContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -67,8 +67,12 @@ const schema: ISchema = {
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,9 +1,8 @@
|
||||
|
||||
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import {
|
||||
Action,
|
||||
ActionContextProvider,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
@ -12,6 +11,7 @@ import {
|
||||
useActionContext,
|
||||
} from '@nocobase/client';
|
||||
import React, { useState } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -59,11 +59,15 @@ const schema: ISchema = {
|
||||
export default observer(() => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<a onClick={() => setVisible(true)}>Open</a>
|
||||
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
|
||||
</ActionContextProvider>
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<a onClick={() => setVisible(true)}>Open</a>
|
||||
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
|
||||
</ActionContextProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
|
||||
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import {
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
useActionContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -55,8 +55,12 @@ const schema: ISchema = {
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { createContext, useCallback, useMemo } from 'react';
|
||||
import { AssociationSelect } from './AssociationSelect';
|
||||
import { InternalFileManager } from './FileManager';
|
||||
import { InternalCascadeSelect } from './InternalCascadeSelect';
|
||||
import { InternalNester } from './InternalNester';
|
||||
import { InternalPicker } from './InternalPicker';
|
||||
import { InternalPopoverNester } from './InternalPopoverNester';
|
||||
import { InternalSubTable } from './InternalSubTable';
|
||||
|
||||
export enum AssociationFieldMode {
|
||||
Picker = 'Picker',
|
||||
Nester = 'Nester',
|
||||
PopoverNester = 'PopoverNester',
|
||||
Select = 'Select',
|
||||
SubTable = 'SubTable',
|
||||
FileManager = 'FileManager',
|
||||
CascadeSelect = 'CascadeSelect',
|
||||
Tag = 'Tag',
|
||||
}
|
||||
|
||||
interface AssociationFieldModeProviderProps {
|
||||
modeToComponent: Partial<Record<AssociationFieldMode, React.FC | ((originalCom: React.FC) => React.FC)>>;
|
||||
}
|
||||
|
||||
const defaultModeToComponent = {
|
||||
Picker: InternalPicker,
|
||||
Nester: InternalNester,
|
||||
PopoverNester: InternalPopoverNester,
|
||||
Select: AssociationSelect,
|
||||
SubTable: InternalSubTable,
|
||||
FileManager: InternalFileManager,
|
||||
CascadeSelect: InternalCascadeSelect,
|
||||
};
|
||||
|
||||
const AssociationFieldModeContext = createContext<{
|
||||
modeToComponent: AssociationFieldModeProviderProps['modeToComponent'];
|
||||
getComponent: (mode: AssociationFieldMode) => React.FC;
|
||||
}>({
|
||||
modeToComponent: defaultModeToComponent,
|
||||
getComponent: (mode: AssociationFieldMode) => {
|
||||
return defaultModeToComponent[mode];
|
||||
},
|
||||
});
|
||||
|
||||
export const AssociationFieldModeProvider: React.FC<AssociationFieldModeProviderProps> = (props) => {
|
||||
const modeContext = useAssociationFieldModeContext();
|
||||
const modeToComponent = useMemo(
|
||||
() => ({ ...modeContext.modeToComponent, ...props.modeToComponent }),
|
||||
[modeContext.modeToComponent, props.modeToComponent],
|
||||
);
|
||||
|
||||
const getComponent = useCallback(
|
||||
(mode: AssociationFieldMode) => {
|
||||
const component = modeToComponent[mode];
|
||||
|
||||
if (_.isFunction(component)) {
|
||||
return component(defaultModeToComponent[mode]) as React.FC;
|
||||
}
|
||||
|
||||
return component || defaultModeToComponent[mode];
|
||||
},
|
||||
[modeToComponent],
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({ modeToComponent, getComponent }), [getComponent, modeToComponent]);
|
||||
return <AssociationFieldModeContext.Provider value={value}>{props.children}</AssociationFieldModeContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAssociationFieldModeContext = () => {
|
||||
return React.useContext(AssociationFieldModeContext);
|
||||
};
|
@ -13,14 +13,8 @@ import React from 'react';
|
||||
import { SchemaComponentOptions } from '../../';
|
||||
import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { useAssociationFieldModeContext } from './AssociationFieldModeProvider';
|
||||
import { AssociationFieldProvider } from './AssociationFieldProvider';
|
||||
import { AssociationSelect } from './AssociationSelect';
|
||||
import { InternalFileManager } from './FileManager';
|
||||
import { InternalCascadeSelect } from './InternalCascadeSelect';
|
||||
import { InternalNester } from './InternalNester';
|
||||
import { InternalPicker } from './InternalPicker';
|
||||
import { InternalPopoverNester } from './InternalPopoverNester';
|
||||
import { InternalSubTable } from './InternalSubTable';
|
||||
import { CreateRecordAction } from './components/CreateRecordAction';
|
||||
import { useAssociationFieldContext } from './hooks';
|
||||
|
||||
@ -30,6 +24,7 @@ const EditableAssociationField = observer(
|
||||
const field: Field = useField();
|
||||
const form = useForm();
|
||||
const { options: collectionField, currentMode } = useAssociationFieldContext();
|
||||
const { getComponent } = useAssociationFieldModeContext();
|
||||
|
||||
const useCreateActionProps = () => {
|
||||
const { onClick } = useCAP();
|
||||
@ -57,15 +52,11 @@ const EditableAssociationField = observer(
|
||||
};
|
||||
};
|
||||
|
||||
const Component = getComponent(currentMode);
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
|
||||
{currentMode === 'Picker' && <InternalPicker {...props} />}
|
||||
{currentMode === 'Nester' && <InternalNester {...props} />}
|
||||
{currentMode === 'PopoverNester' && <InternalPopoverNester {...props} />}
|
||||
{currentMode === 'Select' && <AssociationSelect {...props} />}
|
||||
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
||||
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
|
||||
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
|
||||
<Component {...props} />
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
},
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import React, { useContext, useRef, useState } from 'react';
|
||||
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, ActionContextProvider } from '../action/context';
|
||||
import { useGetAriaLabelOfPopover } from '../action/hooks/useGetAriaLabelOfPopover';
|
||||
@ -22,6 +22,7 @@ import { useAssociationFieldContext } from './hooks';
|
||||
|
||||
const InternalPopoverNesterContentCss = css`
|
||||
min-width: 600px;
|
||||
max-width: 800px;
|
||||
max-height: 440px;
|
||||
overflow: auto;
|
||||
.ant-card {
|
||||
@ -30,7 +31,16 @@ const InternalPopoverNesterContentCss = css`
|
||||
`;
|
||||
|
||||
export const InternalPopoverNester = observer(
|
||||
(props) => {
|
||||
(props: {
|
||||
Container?: (props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger: 'click' | 'hover';
|
||||
content: React.ReactElement;
|
||||
children: React.ReactElement;
|
||||
}) => React.ReactElement;
|
||||
children?: React.ReactElement;
|
||||
}) => {
|
||||
const { options } = useAssociationFieldContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@ -42,11 +52,7 @@ export const InternalPopoverNester = observer(
|
||||
shouldMountElement: true,
|
||||
};
|
||||
const content = (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ minWidth: '600px', maxWidth: '800px', maxHeight: '440px', overflow: 'auto' }}
|
||||
className={InternalPopoverNesterContentCss}
|
||||
>
|
||||
<div ref={ref} className={`${InternalPopoverNesterContentCss} popover-subform-container`}>
|
||||
<InternalNester {...nesterProps} />
|
||||
</div>
|
||||
);
|
||||
@ -56,6 +62,11 @@ export const InternalPopoverNester = observer(
|
||||
getContainer: getContainer,
|
||||
};
|
||||
const { getAriaLabel } = useGetAriaLabelOfPopover();
|
||||
const Container = props.Container || StablePopover;
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setVisible(open);
|
||||
}, []);
|
||||
const overlayStyle = useMemo(() => ({ padding: '0px' }), []);
|
||||
|
||||
if (process.env.__E2E__) {
|
||||
useSetAriaLabelForPopover(visible);
|
||||
@ -63,13 +74,13 @@ export const InternalPopoverNester = observer(
|
||||
|
||||
return (
|
||||
<ActionContextProvider value={{ ...ctx, modalProps }}>
|
||||
<StablePopover
|
||||
overlayStyle={{ padding: '0px' }}
|
||||
<Container
|
||||
overlayStyle={overlayStyle}
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="topLeft"
|
||||
open={visible}
|
||||
onOpenChange={(open) => setVisible(open)}
|
||||
onOpenChange={handleOpenChange}
|
||||
title={t(options?.uiSchema?.rawTitle)}
|
||||
>
|
||||
<span style={{ cursor: 'pointer', display: 'flex' }}>
|
||||
@ -82,7 +93,7 @@ export const InternalPopoverNester = observer(
|
||||
</div>
|
||||
<EditOutlined style={{ display: 'inline-flex', margin: '5px' }} />
|
||||
</span>
|
||||
</StablePopover>
|
||||
</Container>
|
||||
{visible && (
|
||||
<div
|
||||
role="button"
|
||||
|
@ -25,6 +25,7 @@ import { getPath } from '../../../variables/utils/getPath';
|
||||
import { getVariableName } from '../../../variables/utils/getVariableName';
|
||||
import { isVariable } from '../../../variables/utils/isVariable';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { AssociationFieldMode } from './AssociationFieldModeProvider';
|
||||
import { AssociationFieldContext } from './context';
|
||||
|
||||
export const useInsertSchema = (component) => {
|
||||
@ -51,7 +52,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
|
||||
return useContext(AssociationFieldContext) as {
|
||||
options: any;
|
||||
field: F;
|
||||
currentMode: string;
|
||||
currentMode: AssociationFieldMode;
|
||||
allowMultiple?: boolean;
|
||||
allowDissociate?: boolean;
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import { Nester } from './Nester';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import { SubTable } from './SubTable';
|
||||
|
||||
export { AssociationFieldModeProvider } from './AssociationFieldModeProvider';
|
||||
export const AssociationField: any = connect(Editable, mapReadPretty(ReadPretty));
|
||||
|
||||
AssociationField.SubTable = SubTable;
|
||||
|
@ -17,19 +17,25 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet
|
||||
import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const AssociationFilterBlockDesigner = () => {
|
||||
const { name, title } = useCollection_deprecated();
|
||||
const template = useSchemaTemplate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
<SchemaSettingsTemplate componentName={'FilterCollapse'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsTemplate
|
||||
componentName={`${componentNamePrefix}FilterCollapse`}
|
||||
collectionName={name}
|
||||
resourceName={defaultResource}
|
||||
/>
|
||||
<SchemaSettingsConnectDataBlocks type={FilterBlockType.COLLAPSE} emptyDescription={t('No blocks to connect')} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
|
@ -99,6 +99,8 @@ const InternalAssociationSelect = connect(
|
||||
mapReadPretty(ReadPretty),
|
||||
);
|
||||
|
||||
InternalAssociationSelect.displayName = 'InternalAssociationSelect';
|
||||
|
||||
interface AssociationSelectInterface {
|
||||
(props: any): React.ReactElement;
|
||||
Designer: React.FC;
|
||||
|
@ -30,6 +30,17 @@ export type FilterActionProps<T = {}> = ActionProps & {
|
||||
form?: Form;
|
||||
onSubmit?: (values: T) => void;
|
||||
onReset?: (values: T) => void;
|
||||
/**
|
||||
* @default Popover
|
||||
* 在移动端中,会使用移动端的 Popup 组件
|
||||
*/
|
||||
Container?: (props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger: 'click' | 'hover';
|
||||
content: React.ReactElement;
|
||||
children: React.ReactElement;
|
||||
}) => React.ReactElement;
|
||||
};
|
||||
|
||||
export const FilterAction = withDynamicSchemaProps(
|
||||
@ -42,7 +53,7 @@ export const FilterAction = withDynamicSchemaProps(
|
||||
const form = useMemo<Form>(() => props.form || createForm(), []);
|
||||
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { options, onSubmit, onReset, ...others } = useProps(props);
|
||||
const { options, onSubmit, onReset, Container = StablePopover, ...others } = useProps(props);
|
||||
|
||||
const onOpenChange = useCallback((visible: boolean): void => {
|
||||
setVisible(visible);
|
||||
@ -50,7 +61,7 @@ export const FilterAction = withDynamicSchemaProps(
|
||||
|
||||
return (
|
||||
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
|
||||
<StablePopover
|
||||
<Container
|
||||
destroyTooltipOnHide
|
||||
placement={'bottomLeft'}
|
||||
open={visible}
|
||||
@ -112,8 +123,8 @@ export const FilterAction = withDynamicSchemaProps(
|
||||
</form>
|
||||
}
|
||||
>
|
||||
<Action {...others} title={field.title} />
|
||||
</StablePopover>
|
||||
<Action onClick={() => setVisible(!visible)} {...others} title={field.title} />
|
||||
</Container>
|
||||
</FilterActionContext.Provider>
|
||||
);
|
||||
}),
|
||||
|
@ -62,7 +62,7 @@ export const FilterItem = observer(
|
||||
return (
|
||||
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
|
||||
<div style={style} className="nc-filter-item">
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Cascader
|
||||
// @ts-ignore
|
||||
role="button"
|
||||
@ -90,14 +90,16 @@ export const FilterItem = observer(
|
||||
onChange={onOperatorsChange}
|
||||
placeholder={t('Comparision')}
|
||||
/>
|
||||
{!operator?.noValue ? (
|
||||
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
|
||||
) : null}
|
||||
{!props.disabled && (
|
||||
<a role="button" aria-label="icon-close">
|
||||
<CloseCircleOutlined onClick={remove} style={removeStyle} />
|
||||
</a>
|
||||
)}
|
||||
<Space>
|
||||
{!operator?.noValue ? (
|
||||
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
|
||||
) : null}
|
||||
{!props.disabled && (
|
||||
<a role="button" aria-label="icon-close">
|
||||
<CloseCircleOutlined onClick={remove} style={removeStyle} />
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,8 +1,14 @@
|
||||
|
||||
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
import { Filter, FilterAction, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import {
|
||||
CustomRouterContextProvider,
|
||||
Filter,
|
||||
FilterAction,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const options = [
|
||||
{
|
||||
@ -99,8 +105,12 @@ const schema: ISchema = {
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProv
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { useCollection } from '../../../data-source/collection/CollectionProvider';
|
||||
import { setDataLoadingModeSettingsItem } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import {
|
||||
SchemaSettingsDataTemplates,
|
||||
@ -25,6 +26,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../schema-settings/SchemaSe
|
||||
import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSettingsBlockTitleItem';
|
||||
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
|
||||
@ -74,12 +76,13 @@ export const formSettings = new SchemaSettings({
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
componentName: `${componentNamePrefix}FormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
@ -127,13 +130,14 @@ export const readPrettyFormSettings = new SchemaSettings({
|
||||
name: 'formItemTemplate',
|
||||
Component: SchemaSettingsFormItemTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
insertAdjacentPosition: 'beforeEnd',
|
||||
componentName: 'ReadPrettyFormItem',
|
||||
componentName: `${componentNamePrefix}ReadPrettyFormItem`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
@ -336,10 +340,11 @@ export const formDetailsSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Details',
|
||||
componentName: `${componentNamePrefix}Details`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { SchemaSettings } from '../../../application/schema-settings';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
@ -22,8 +23,9 @@ export const formV1Settings = new SchemaSettings({
|
||||
Component: SchemaSettingsTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return {
|
||||
componentName: 'Form',
|
||||
componentName: `${componentNamePrefix}Form`,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
|
||||
type Opts = Options<any, any> & { uid?: string };
|
||||
|
||||
@ -130,9 +131,10 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer(
|
||||
Form.Designer = function Designer() {
|
||||
const { name, title } = useCollection_deprecated();
|
||||
const template = useSchemaTemplate();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsTemplate componentName={'Form'} collectionName={name} />
|
||||
<SchemaSettingsTemplate componentName={`${componentNamePrefix}Form`} collectionName={name} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
|
@ -1,9 +1,15 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useCloseAction } from '@nocobase/client';
|
||||
import {
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useCloseAction,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -60,8 +66,12 @@ const Output = observer(
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,9 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -59,8 +58,12 @@ export default observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,9 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -58,8 +57,12 @@ export default observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,9 +1,15 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider, useCloseAction } from '@nocobase/client';
|
||||
import {
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useCloseAction,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -65,8 +71,12 @@ export default observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,9 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import {
|
||||
APIClientProvider,
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
@ -12,6 +11,7 @@ import {
|
||||
} from '@nocobase/client';
|
||||
import { Card, Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { apiClient } from './apiClient';
|
||||
|
||||
const schema: ISchema = {
|
||||
@ -105,13 +105,17 @@ const useRefresh = () => {
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<SchemaComponentProvider
|
||||
scope={{ useSubmit, useRefresh }}
|
||||
components={{ Space, Card, Output, Action, Form, Input, FormItem }}
|
||||
>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</APIClientProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<SchemaComponentProvider
|
||||
scope={{ useSubmit, useRefresh }}
|
||||
components={{ Space, Card, Output, Action, Form, Input, FormItem }}
|
||||
>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</APIClientProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,9 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Action, CustomRouterContextProvider, Form, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -59,8 +58,12 @@ const useSubmit = () => {
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,17 @@
|
||||
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Action, Form, FormUseValues, SchemaComponent, SchemaComponentProvider, useRequest } from '@nocobase/client';
|
||||
import {
|
||||
Action,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
FormUseValues,
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
useRequest,
|
||||
} from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -63,11 +70,15 @@ const useValues: FormUseValues = (opts) => {
|
||||
|
||||
export default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider
|
||||
scope={{ useSubmit, useValues }}
|
||||
components={{ Card, Output, Action, Form, Input, FormItem }}
|
||||
>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider
|
||||
scope={{ useSubmit, useValues }}
|
||||
components={{ Card, Output, Action, Form, Input, FormItem }}
|
||||
>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,9 @@
|
||||
|
||||
|
||||
import { FormItem } from '@formily/antd-v5';
|
||||
import { ISchema, observer } from '@formily/react';
|
||||
import {
|
||||
Action,
|
||||
ActionContextProvider,
|
||||
CustomRouterContextProvider,
|
||||
Form,
|
||||
Input,
|
||||
SchemaComponent,
|
||||
@ -15,6 +14,7 @@ import {
|
||||
} from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
const useValues = (options) => {
|
||||
const { visible } = useActionContext();
|
||||
@ -72,17 +72,21 @@ export default observer(() => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<SchemaComponent schema={schema} />
|
||||
</ActionContextProvider>
|
||||
</SchemaComponentProvider>
|
||||
<Router location={window.location} navigator={null}>
|
||||
<CustomRouterContextProvider>
|
||||
<SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<SchemaComponent schema={schema} />
|
||||
</ActionContextProvider>
|
||||
</SchemaComponentProvider>
|
||||
</CustomRouterContextProvider>
|
||||
</Router>
|
||||
);
|
||||
});
|
||||
|
@ -17,7 +17,6 @@ import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import { useCollection_deprecated, useSortFields } from '../../../collection-manager';
|
||||
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import { SetTheCountOfColumnsDisplayedInARow } from '../../../modules/blocks/data-blocks/grid-card/SetTheCountOfColumnsDisplayedInARow';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettingsDivider,
|
||||
@ -28,10 +27,11 @@ import {
|
||||
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
|
||||
import { gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
|
||||
|
||||
export const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
|
||||
obj[cur] = cur;
|
||||
@ -47,11 +47,10 @@ export const GridCardDesigner = () => {
|
||||
const field = useField();
|
||||
const { dn } = useDesignable();
|
||||
const sortFields = useSortFields(name);
|
||||
const record = useRecord();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
|
||||
|
||||
const columnCountSchema = useMemo(() => {
|
||||
return {
|
||||
@ -217,7 +216,11 @@ export const GridCardDesigner = () => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsTemplate componentName={'GridCard'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsTemplate
|
||||
componentName={`${componentNamePrefix}GridCard`}
|
||||
collectionName={name}
|
||||
resourceName={defaultResource}
|
||||
/>
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
|
@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import { useCollection_deprecated, useSortFields } from '../../../collection-manager';
|
||||
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettingsDivider,
|
||||
@ -27,6 +26,7 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet
|
||||
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
|
||||
@ -42,7 +42,6 @@ export const ListDesigner = () => {
|
||||
const field = useField();
|
||||
const { dn } = useDesignable();
|
||||
const sortFields = useSortFields(name);
|
||||
const record = useRecord();
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
@ -57,6 +56,7 @@ export const ListDesigner = () => {
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
@ -187,7 +187,11 @@ export const ListDesigner = () => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsTemplate componentName={'List'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsTemplate
|
||||
componentName={`${componentNamePrefix}List`}
|
||||
collectionName={name}
|
||||
resourceName={defaultResource}
|
||||
/>
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
|
@ -11,15 +11,13 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useToken } from '../../../style';
|
||||
import { useCurrentPopupContext } from './PagePopups';
|
||||
import { usePagePopup } from './pagePopupUtils';
|
||||
import { useActionContext } from '../action/hooks';
|
||||
|
||||
export const useBackButton = () => {
|
||||
const { params } = useCurrentPopupContext();
|
||||
const { closePopup } = usePagePopup();
|
||||
const { setVisible } = useActionContext();
|
||||
const goBack = useCallback(() => {
|
||||
closePopup(params?.popupuid);
|
||||
}, [closePopup, params?.popupuid]);
|
||||
setVisible?.(false);
|
||||
}, [setVisible]);
|
||||
|
||||
return {
|
||||
goBack,
|
||||
|
@ -13,4 +13,5 @@ export * from './FixedBlockDesignerItem';
|
||||
export * from './Page';
|
||||
export * from './Page.Settings';
|
||||
export { PagePopups } from './PagePopups';
|
||||
export { storePopupContext } from './pagePopupUtils';
|
||||
export * from './PageTab.Settings';
|
||||
|
@ -44,6 +44,10 @@ export interface PopupContextStorage extends PopupContext {
|
||||
/** used to refresh data for block */
|
||||
service?: any;
|
||||
sourceId?: string;
|
||||
/**
|
||||
* if true, will not back to the previous path when closing the popup
|
||||
*/
|
||||
notBackToPreviousPath?: boolean;
|
||||
}
|
||||
|
||||
const popupsContextStorage: Record<string, PopupContextStorage> = {};
|
||||
@ -53,7 +57,10 @@ export const getStoredPopupContext = (popupUid: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to store the context of the current popup when a button is clicked.
|
||||
* Used to store the context of the current popup.
|
||||
*
|
||||
* The context that has already been stored, when displaying the popup,
|
||||
* will directly retrieve the context information from the cache instead of making an API request.
|
||||
* @param popupUid
|
||||
* @param params
|
||||
*/
|
||||
@ -108,6 +115,10 @@ export const getPopupPathFromParams = (params: PopupParams) => {
|
||||
return `/popups/${popupPath.map((item) => encodePathValue(item)).join('/')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: use this hook in a plugin is not recommended
|
||||
* @returns
|
||||
*/
|
||||
export const usePagePopup = () => {
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const location = useLocationNoUpdate();
|
||||
@ -228,13 +239,13 @@ export const usePagePopup = () => {
|
||||
// 1. If there is a value in the cache, it means that the current popup was opened by manual click, so we can simply return to the previous record;
|
||||
// 2. If there is no value in the cache, it means that the current popup was opened by clicking the URL elsewhere, and since there is no history,
|
||||
// we need to construct the URL of the previous record to return to;
|
||||
if (getStoredPopupContext(currentPopupUid)) {
|
||||
if (getStoredPopupContext(currentPopupUid) && !getStoredPopupContext(currentPopupUid).notBackToPreviousPath) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate(withSearchParams(removeLastPopupPath(location.pathname)));
|
||||
}
|
||||
},
|
||||
[navigate, location, isPopupVisibleControlledByURL],
|
||||
[isPopupVisibleControlledByURL, setVisibleFromAction, navigate, location?.pathname],
|
||||
);
|
||||
|
||||
const changeTab = useCallback(
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import { Select } from 'antd';
|
||||
import { differenceBy, unionBy } from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
@ -20,10 +21,9 @@ import { CollectionProvider_deprecated, useCollection_deprecated } from '../../.
|
||||
import { FormProvider, SchemaComponentOptions } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { Upload } from '../upload';
|
||||
import { useFieldNames } from './useFieldNames';
|
||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||
import { Upload } from '../upload';
|
||||
import { toArr } from '@formily/shared';
|
||||
|
||||
export const RecordPickerContext = createContext(null);
|
||||
RecordPickerContext.displayName = 'RecordPickerContext';
|
||||
@ -148,11 +148,6 @@ export const InputRecordPicker: React.FC<any> = (props: IRecordPickerProps) => {
|
||||
return Array.isArray(value) ? value?.map((v) => v[fieldNames.value]) : value?.[fieldNames.value];
|
||||
};
|
||||
|
||||
const handleSelect = () => {
|
||||
setVisible(true);
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
// const handleRemove = (file) => {
|
||||
// const newOptions = options.filter((option) => option.id !== file.id);
|
||||
// setOptions(newOptions);
|
||||
|
@ -35,6 +35,7 @@ import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/Schema
|
||||
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
|
||||
@ -86,6 +87,7 @@ export const TableBlockDesigner = () => {
|
||||
const { service } = useTableBlockContext();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource =
|
||||
@ -304,7 +306,11 @@ export const TableBlockDesigner = () => {
|
||||
<SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
|
||||
{supportTemplate && <SchemaSettingsDivider />}
|
||||
{supportTemplate && (
|
||||
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettingsTemplate
|
||||
componentName={`${componentNamePrefix}Table`}
|
||||
collectionName={name}
|
||||
resourceName={defaultResource}
|
||||
/>
|
||||
)}
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
} from '../../../schema-settings';
|
||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||
import { useDesignable } from '../../hooks';
|
||||
|
||||
export const TableVoidDesigner = () => {
|
||||
@ -48,6 +49,7 @@ export const TableVoidDesigner = () => {
|
||||
};
|
||||
});
|
||||
const template = useSchemaTemplate();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettingsSwitchItem
|
||||
@ -213,7 +215,7 @@ export const TableVoidDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} />
|
||||
<SchemaSettingsTemplate componentName={`${componentNamePrefix}Table`} collectionName={name} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { Schema } from '@formily/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useActionAvailable,
|
||||
useCollection,
|
||||
useCollectionManager_deprecated,
|
||||
useCollection_deprecated,
|
||||
@ -22,7 +23,6 @@ import {
|
||||
useCreateEditFormBlock,
|
||||
useCreateFormBlock,
|
||||
useCreateTableBlock,
|
||||
useActionAvailable,
|
||||
} from '../..';
|
||||
import { CompatibleSchemaInitializer } from '../../application/schema-initializer/CompatibleSchemaInitializer';
|
||||
import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer';
|
||||
@ -101,7 +101,7 @@ function useRecordBlocks() {
|
||||
},
|
||||
onlyCurrentDataSource: true,
|
||||
hideSearch: true,
|
||||
componentType: 'ReadPrettyFormItem',
|
||||
componentType: `ReadPrettyFormItem`,
|
||||
createBlockSchema,
|
||||
templateWrap: useCallback(
|
||||
(templateSchema, { item }) => {
|
||||
@ -140,7 +140,7 @@ function useRecordBlocks() {
|
||||
onlyCurrentDataSource: true,
|
||||
hideSearch: true,
|
||||
hideOtherRecordsInPopup: true,
|
||||
componentType: 'FormItem',
|
||||
componentType: `FormItem`,
|
||||
createBlockSchema: createEditFormBlock,
|
||||
templateWrap: templateWrapEdit,
|
||||
showAssociationFields: true,
|
||||
@ -166,7 +166,7 @@ function useRecordBlocks() {
|
||||
},
|
||||
onlyCurrentDataSource: true,
|
||||
hideSearch: true,
|
||||
componentType: 'FormItem',
|
||||
componentType: `FormItem`,
|
||||
createBlockSchema: ({ item, fromOthersInPopup }) => {
|
||||
if (fromOthersInPopup) {
|
||||
return createFormBlock({ item, fromOthersInPopup });
|
||||
|
@ -81,6 +81,7 @@ const TabPaneInitializers = (props?: any) => {
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
width: 520,
|
||||
zIndex: 2000,
|
||||
},
|
||||
type: 'void',
|
||||
title: '{{t("Add tab")}}',
|
||||
|
@ -271,7 +271,7 @@ function FinallyButton({
|
||||
}),
|
||||
React.cloneElement(rightButton as React.ReactElement<any, string>, {
|
||||
loading: false,
|
||||
style: props?.style,
|
||||
style: { ...props?.style, justifyContent: 'center' },
|
||||
}),
|
||||
]}
|
||||
menu={menu}
|
||||
|
@ -30,6 +30,7 @@ import { useDataSourceManager } from '../data-source/data-source/DataSourceManag
|
||||
import { isAssocField } from '../filter-provider/utils';
|
||||
import { useActionContext, useCompile, useDesignable } from '../schema-component';
|
||||
import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
|
||||
|
||||
export const itemsMerge = (items1) => {
|
||||
return items1;
|
||||
@ -879,11 +880,17 @@ export const useCollectionDataSourceItems = ({
|
||||
currentText?: string;
|
||||
otherText?: string;
|
||||
}) => {
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const { t } = useTranslation();
|
||||
const dm = useDataSourceManager();
|
||||
const dataSourceKey = useDataSourceKey();
|
||||
const collection = useCollection();
|
||||
const associationFields = useAssociationFields({ componentName, filterCollections: filter, showAssociationFields });
|
||||
const associationFields = useAssociationFields({
|
||||
componentName: componentNamePrefix + componentName,
|
||||
filterCollections: filter,
|
||||
showAssociationFields,
|
||||
componentNamePrefix,
|
||||
});
|
||||
const association = useAssociationName();
|
||||
|
||||
let allCollections = dm.getAllCollections({
|
||||
@ -911,11 +918,12 @@ export const useCollectionDataSourceItems = ({
|
||||
name,
|
||||
association,
|
||||
collections,
|
||||
componentName,
|
||||
componentName: componentNamePrefix + componentName,
|
||||
searchValue: '',
|
||||
dataSource: key,
|
||||
getTemplatesByCollection,
|
||||
t,
|
||||
componentNamePrefix,
|
||||
}).sort((item) => {
|
||||
// fix https://nocobase.height.app/T-3551
|
||||
const inherits = _.toArray(collection?.inherits || []);
|
||||
@ -1401,6 +1409,7 @@ const getChildren = ({
|
||||
searchValue,
|
||||
getTemplatesByCollection,
|
||||
t,
|
||||
componentNamePrefix,
|
||||
}: {
|
||||
name: string;
|
||||
association: string;
|
||||
@ -1409,7 +1418,8 @@ const getChildren = ({
|
||||
searchValue: string;
|
||||
dataSource: string;
|
||||
getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any;
|
||||
t;
|
||||
t: any;
|
||||
componentNamePrefix: string;
|
||||
}) => {
|
||||
return collections
|
||||
?.filter((item) => {
|
||||
@ -1419,11 +1429,16 @@ const getChildren = ({
|
||||
if (!item.filterTargetKey) {
|
||||
return false;
|
||||
} else if (
|
||||
['Kanban', 'FormItem'].includes(componentName) &&
|
||||
[componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem'].includes(componentName) &&
|
||||
((item.template === 'view' && !item.writableView) || item.template === 'sql')
|
||||
) {
|
||||
return false;
|
||||
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
||||
} else if (
|
||||
item.template === 'file' &&
|
||||
[componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem', componentNamePrefix + 'Calendar'].includes(
|
||||
componentName,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
const title = item.title || item.tableName;
|
||||
@ -1483,7 +1498,10 @@ const getChildren = ({
|
||||
dataSource,
|
||||
title: t('Duplicate template'),
|
||||
children: templates.map((template) => {
|
||||
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
|
||||
const templateName = [
|
||||
componentNamePrefix + 'FormItem',
|
||||
componentNamePrefix + 'ReadPrettyFormItem',
|
||||
].includes(template?.componentName)
|
||||
? `${template?.name} ${t('(Fields only)')}`
|
||||
: template?.name;
|
||||
return {
|
||||
@ -1503,7 +1521,10 @@ const getChildren = ({
|
||||
dataSource,
|
||||
title: t('Reference template'),
|
||||
children: templates.map((template) => {
|
||||
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
|
||||
const templateName = [
|
||||
componentNamePrefix + 'FormItem',
|
||||
componentNamePrefix + 'ReadPrettyFormItem',
|
||||
].includes(template?.componentName)
|
||||
? `${template?.name} ${t('(Fields only)')}`
|
||||
: template?.name;
|
||||
return {
|
||||
@ -1525,9 +1546,11 @@ function useAssociationFields({
|
||||
componentName,
|
||||
filterCollections,
|
||||
showAssociationFields,
|
||||
componentNamePrefix,
|
||||
}: {
|
||||
componentName: string;
|
||||
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
|
||||
componentNamePrefix: string;
|
||||
showAssociationFields?: boolean;
|
||||
}) {
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -1566,11 +1589,11 @@ function useAssociationFields({
|
||||
}
|
||||
|
||||
// 针对弹窗中的详情区块
|
||||
if (componentName === 'ReadPrettyFormItem') {
|
||||
if (componentName === componentNamePrefix + 'ReadPrettyFormItem') {
|
||||
if (['hasOne', 'belongsTo'].includes(field.type)) {
|
||||
return template.componentName === 'ReadPrettyFormItem';
|
||||
return template.componentName === componentNamePrefix + 'ReadPrettyFormItem';
|
||||
} else {
|
||||
return template.componentName === 'Details';
|
||||
return template.componentName === componentNamePrefix + 'Details';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1611,7 +1634,10 @@ function useAssociationFields({
|
||||
dataSource,
|
||||
title: t('Duplicate template'),
|
||||
children: templates.map((template) => {
|
||||
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
|
||||
const templateName = [
|
||||
componentNamePrefix + 'FormItem',
|
||||
componentNamePrefix + 'ReadPrettyFormItem',
|
||||
].includes(template?.componentName)
|
||||
? `${template?.name} ${t('(Fields only)')}`
|
||||
: template?.name;
|
||||
return {
|
||||
@ -1633,7 +1659,10 @@ function useAssociationFields({
|
||||
dataSource,
|
||||
title: t('Reference template'),
|
||||
children: templates.map((template) => {
|
||||
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
|
||||
const templateName = [
|
||||
componentNamePrefix + 'FormItem',
|
||||
componentNamePrefix + 'ReadPrettyFormItem',
|
||||
].includes(template?.componentName)
|
||||
? `${template?.name} ${t('(Fields only)')}`
|
||||
: template?.name;
|
||||
return {
|
||||
@ -1662,5 +1691,6 @@ function useAssociationFields({
|
||||
getTemplatesByCollection,
|
||||
showAssociationFields,
|
||||
t,
|
||||
componentNamePrefix,
|
||||
]);
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ import { SchemaComponentOptions } from '../schema-component/core/SchemaComponent
|
||||
import { useCompile } from '../schema-component/hooks/useCompile';
|
||||
import { Designable, createDesignable, useDesignable } from '../schema-component/hooks/useDesignable';
|
||||
import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
|
||||
import { useLocalVariables, useVariables } from '../variables';
|
||||
import { FormDataTemplates } from './DataTemplates';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
|
@ -20,7 +20,7 @@ import { SchemaComponent } from '../schema-component/core/SchemaComponent';
|
||||
import { useCompile } from '../schema-component/hooks/useCompile';
|
||||
import { createDesignable } from '../schema-component/hooks/useDesignable';
|
||||
import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
|
||||
import { SchemaSettingsItem, useSchemaSettings } from './SchemaSettings';
|
||||
|
||||
export function SchemaSettingsTemplate(props) {
|
||||
|
@ -8,17 +8,10 @@
|
||||
*/
|
||||
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
|
||||
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BlockTemplateProvider, CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
|
||||
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
|
||||
|
||||
const BlockTemplateContext = createContext<any>({});
|
||||
BlockTemplateContext.displayName = 'BlockTemplateContext';
|
||||
|
||||
export const useBlockTemplateContext = () => {
|
||||
return useContext(BlockTemplateContext);
|
||||
};
|
||||
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
||||
|
||||
export const BlockTemplate = observer(
|
||||
(props: any) => {
|
||||
@ -36,9 +29,9 @@ export const BlockTemplate = observer(
|
||||
onTemplateSuccess?.();
|
||||
};
|
||||
return template ? (
|
||||
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}>
|
||||
<BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
|
||||
<RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
|
||||
</BlockTemplateContext.Provider>
|
||||
</BlockTemplateProvider>
|
||||
) : (
|
||||
<CollectionDeletedPlaceholder type="Block template" name={templateId} />
|
||||
);
|
||||
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
import React, { createContext, FC, useContext } from 'react';
|
||||
|
||||
interface BlockTemplateProviderProps {
|
||||
/**
|
||||
* 为模板中的 componentName 参数设置一个前缀,用于实现相同区块的模板在不同的上下文中不会互相引用
|
||||
*/
|
||||
componentNamePrefix?: string;
|
||||
dn?: any;
|
||||
field?: any;
|
||||
fieldSchema?: ISchema;
|
||||
template?: any;
|
||||
}
|
||||
|
||||
export const BlockTemplateContext = createContext<BlockTemplateProviderProps>({ componentNamePrefix: '' });
|
||||
|
||||
export const BlockTemplateProvider: FC<BlockTemplateProviderProps> = (props) => {
|
||||
return (
|
||||
<BlockTemplateContext.Provider value={{ ...props, componentNamePrefix: props.componentNamePrefix || '' }}>
|
||||
{props.children}
|
||||
</BlockTemplateContext.Provider>
|
||||
);
|
||||
};
|
||||
export const useBlockTemplateContext = () => {
|
||||
return useContext(BlockTemplateContext);
|
||||
};
|
@ -9,4 +9,5 @@
|
||||
|
||||
export * from './BlockTemplateDetails';
|
||||
export * from './BlockTemplatePage';
|
||||
export * from './BlockTemplateProvider';
|
||||
export * from './SchemaTemplateManagerProvider';
|
||||
|
@ -24,9 +24,9 @@ export * from './number';
|
||||
export * from './parse-filter';
|
||||
export * from './registry';
|
||||
// export * from './toposort';
|
||||
export * from './i18n';
|
||||
export * from './isPortalInBody';
|
||||
export * from './parseHTML';
|
||||
export * from './uid';
|
||||
export * from './url';
|
||||
export { dayjs, lodash };
|
||||
export * from './parseHTML';
|
||||
export * from './i18n';
|
||||
|
@ -70,7 +70,7 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
});
|
||||
|
||||
test('i18n should not fallbackNS', async ({ page }) => {
|
||||
await page.goto('/admin/settings/system-settings');
|
||||
await page.goto('/');
|
||||
|
||||
// 创建 Users 页面
|
||||
await page.getByTestId('schema-initializer-Menu-header').hover();
|
||||
@ -78,11 +78,12 @@ test('i18n should not fallbackNS', async ({ page }) => {
|
||||
await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').click();
|
||||
await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').fill('Users');
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
await expect(page.getByLabel('Users')).toBeVisible();
|
||||
await page.getByLabel('Users').first().click();
|
||||
await expect(page.getByLabel('Users').first()).toBeVisible();
|
||||
await expect(page.getByLabel('用户')).not.toBeVisible();
|
||||
|
||||
// 添加中文选项
|
||||
await page.reload();
|
||||
await page.goto('/admin/settings/system-settings');
|
||||
await page.getByTestId('select-multiple').click();
|
||||
await page.getByRole('option', { name: '简体中文 (zh-CN)' }).click();
|
||||
await page.getByLabel('action-Action-Submit').click();
|
||||
@ -92,19 +93,18 @@ test('i18n should not fallbackNS', async ({ page }) => {
|
||||
await page.getByText('LanguageEnglish').click();
|
||||
await page.getByRole('option', { name: '简体中文' }).click();
|
||||
|
||||
// await page.reload();
|
||||
|
||||
// 应该显示 Users 而非中文 “用户”
|
||||
await expect(page.getByLabel('Users')).toBeVisible();
|
||||
await expect(page.getByLabel('Users').first()).toBeVisible();
|
||||
await expect(page.getByLabel('用户')).not.toBeVisible();
|
||||
|
||||
// 删除中文
|
||||
await page.goto('/admin/settings/system-settings');
|
||||
await page.getByLabel('简体中文 (zh-CN)').getByLabel('icon-close-tag').click();
|
||||
await page.getByLabel('action-Action-提交').click();
|
||||
|
||||
// 删除 Users 页面
|
||||
await page.getByLabel('Users').hover();
|
||||
await page.getByLabel('designer-schema-settings-Menu').hover();
|
||||
await page.getByLabel('Users').first().hover();
|
||||
await page.getByLabel('designer-schema-settings-Menu').first().hover();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
});
|
||||
|
@ -7,10 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||
import { BlockInitializer, useOpenModeContext, useSchemaInitializerItem } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
export const BulkEditActionInitializer = () => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
@ -20,7 +22,7 @@ export const BulkEditActionInitializer = () => {
|
||||
updateMode: 'selected',
|
||||
},
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
properties: {
|
||||
|
@ -7,10 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ActionInitializerItem } from '@nocobase/client';
|
||||
import { ActionInitializerItem, useOpenModeContext } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
export const DuplicateActionInitializer = (props) => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
|
||||
const schema = {
|
||||
type: 'void',
|
||||
'x-action': 'duplicate',
|
||||
@ -19,7 +21,7 @@ export const DuplicateActionInitializer = (props) => {
|
||||
'x-component': 'Action.Link',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
component: 'DuplicateAction',
|
||||
},
|
||||
properties: {
|
||||
|
@ -72,7 +72,7 @@ describe('createCalendarBlockSchema', () => {
|
||||
},
|
||||
"title": "{{t('View record', { ns: 'calendar' })}}",
|
||||
"type": "void",
|
||||
"x-component": "Action.Drawer",
|
||||
"x-component": "Action.Container",
|
||||
"x-component-props": {
|
||||
"className": "nb-action-popup",
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
SchemaSettingsSwitchItem,
|
||||
SchemaSettingsTemplate,
|
||||
removeNullCondition,
|
||||
useBlockTemplateContext,
|
||||
useCollection,
|
||||
useCollectionManager_deprecated,
|
||||
useDesignable,
|
||||
@ -212,10 +213,11 @@ export const calendarBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Calendar',
|
||||
componentName: `${componentNamePrefix}Calendar`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export const createCalendarBlockUISchema = (options: {
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
|
@ -45,7 +45,7 @@ export const CalendarBlockInitializer = ({
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
componentType={'Calendar'}
|
||||
componentType={`Calendar`}
|
||||
icon={<FormOutlined />}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
SchemaSettingsTemplate,
|
||||
removeNullCondition,
|
||||
setDataLoadingModeSettingsItem,
|
||||
useBlockTemplateContext,
|
||||
useCollection,
|
||||
useCollection_deprecated,
|
||||
useCompile,
|
||||
@ -245,8 +246,9 @@ export const oldGanttSettings = new SchemaSettings({
|
||||
Component: SchemaSettingsTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return {
|
||||
componentName: 'Gantt',
|
||||
componentName: `${componentNamePrefix}Gantt`,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
@ -485,8 +487,9 @@ export const ganttSettings = new SchemaSettings({
|
||||
Component: SchemaSettingsTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return {
|
||||
componentName: 'Gantt',
|
||||
componentName: `${componentNamePrefix}Gantt`,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
|
@ -14,16 +14,16 @@ import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
useCollectionManager_deprecated,
|
||||
useGlobalTheme,
|
||||
FormDialog,
|
||||
SchemaComponent,
|
||||
DataBlockInitializer,
|
||||
SchemaComponentOptions,
|
||||
Collection,
|
||||
CollectionFieldOptions,
|
||||
DataBlockInitializer,
|
||||
FormDialog,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
useCollectionManager_deprecated,
|
||||
useGlobalTheme,
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
} from '@nocobase/client';
|
||||
import { createGanttBlockUISchema } from './createGanttBlockUISchema';
|
||||
|
||||
@ -46,7 +46,7 @@ export const GanttBlockInitializer = ({
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
componentType={'Calendar'}
|
||||
componentType={`Calendar`}
|
||||
icon={<FormOutlined />}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
|
@ -9,15 +9,17 @@
|
||||
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
useFormBlockContext,
|
||||
removeNullCondition,
|
||||
SchemaSettings,
|
||||
SchemaSettingsBlockHeightItem,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsDataScope,
|
||||
SchemaSettingsTemplate,
|
||||
useBlockTemplateContext,
|
||||
useCollection,
|
||||
useCollection_deprecated,
|
||||
useDesignable,
|
||||
SchemaSettings,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
removeNullCondition,
|
||||
SchemaSettingsTemplate,
|
||||
SchemaSettingsBlockHeightItem,
|
||||
useFormBlockContext,
|
||||
} from '@nocobase/client';
|
||||
import { useKanbanBlockContext } from './KanbanBlockProvider';
|
||||
export const kanbanSettings = new SchemaSettings({
|
||||
@ -66,9 +68,10 @@ export const kanbanSettings = new SchemaSettings({
|
||||
name: 'template',
|
||||
Component: SchemaSettingsTemplate,
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const { name } = useCollection();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
return {
|
||||
componentName: 'Kanban',
|
||||
componentName: `${componentNamePrefix}Kanban`,
|
||||
collectionName: name,
|
||||
};
|
||||
},
|
||||
|
@ -15,20 +15,20 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
APIClientProvider,
|
||||
useCollectionManager_deprecated,
|
||||
useGlobalTheme,
|
||||
Collection,
|
||||
CollectionFieldOptions,
|
||||
DataBlockInitializer,
|
||||
FormDialog,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
DataBlockInitializer,
|
||||
useAPIClient,
|
||||
useCollectionManager_deprecated,
|
||||
useGlobalTheme,
|
||||
useSchemaInitializer,
|
||||
useSchemaInitializerItem,
|
||||
useAPIClient,
|
||||
Collection,
|
||||
CollectionFieldOptions,
|
||||
} from '@nocobase/client';
|
||||
import { createKanbanBlockUISchema } from './createKanbanBlockUISchema';
|
||||
import { CreateAndSelectSort } from './CreateAndSelectSort';
|
||||
import { createKanbanBlockUISchema } from './createKanbanBlockUISchema';
|
||||
import { NAMESPACE } from './locale';
|
||||
|
||||
const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options, api }) => {
|
||||
@ -130,7 +130,7 @@ export const KanbanBlockInitializer = ({
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...itemConfig}
|
||||
componentType={'Calendar'}
|
||||
componentType={`Calendar`}
|
||||
icon={<FormOutlined />}
|
||||
onCreateBlockSchema={async (options) => {
|
||||
if (createBlockSchema) {
|
||||
|
@ -65,7 +65,7 @@ test('createMapBlockSchema should return an object with expected properties', ()
|
||||
},
|
||||
"title": "{{ t("View record") }}",
|
||||
"type": "void",
|
||||
"x-component": "Action.Drawer",
|
||||
"x-component": "Action.Container",
|
||||
"x-component-props": {
|
||||
"className": "nb-action-popup",
|
||||
},
|
||||
|
@ -11,6 +11,7 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
FilterBlockType,
|
||||
SchemaSettings,
|
||||
SchemaSettingsBlockHeightItem,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsCascaderItem,
|
||||
SchemaSettingsConnectDataBlocks,
|
||||
@ -20,11 +21,11 @@ import {
|
||||
SchemaSettingsSelectItem,
|
||||
SchemaSettingsTemplate,
|
||||
setDataLoadingModeSettingsItem,
|
||||
useBlockTemplateContext,
|
||||
useCollection,
|
||||
useCollectionManager_deprecated,
|
||||
useDesignable,
|
||||
useFormBlockContext,
|
||||
SchemaSettingsBlockHeightItem,
|
||||
} from '@nocobase/client';
|
||||
import _ from 'lodash';
|
||||
import { useMapTranslation } from '../locale';
|
||||
@ -231,10 +232,11 @@ export const mapBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { componentNamePrefix } = useBlockTemplateContext();
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Map',
|
||||
componentName: `${componentNamePrefix}Map`,
|
||||
collectionName: name,
|
||||
resourceName: defaultResource,
|
||||
};
|
||||
|
@ -22,8 +22,8 @@ import {
|
||||
} from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { useMapTranslation } from '../locale';
|
||||
import { findNestedOption } from './utils';
|
||||
import { createMapBlockUISchema } from './createMapBlockUISchema';
|
||||
import { findNestedOption } from './utils';
|
||||
|
||||
export const MapBlockInitializer = () => {
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
@ -32,9 +32,10 @@ export const MapBlockInitializer = () => {
|
||||
const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
||||
const { t } = useMapTranslation();
|
||||
const { theme } = useGlobalTheme();
|
||||
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
componentType={'Map'}
|
||||
componentType={`Map`}
|
||||
icon={<TableOutlined />}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], {
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { theme } from 'antd';
|
||||
|
||||
export const createMapBlockUISchema = (options: {
|
||||
collectionName: string;
|
||||
@ -49,7 +48,7 @@ export const createMapBlockUISchema = (options: {
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
|
@ -326,7 +326,6 @@ export const AMapBlock = (props) => {
|
||||
|
||||
const MapBlockDrawer = (props) => {
|
||||
const { setVisible, record } = props;
|
||||
const { t } = useMapTranslation();
|
||||
const collection = useCollection();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -375,7 +375,6 @@ export const GoogleMapsBlock = (props) => {
|
||||
|
||||
const MapBlockDrawer = (props) => {
|
||||
const { setVisible, record } = props;
|
||||
const { t } = useMapTranslation();
|
||||
const collection = useCollection();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createStyles } from '@nocobase/client';
|
||||
|
||||
export const useMobileActionDrawerStyle = createStyles(({ css, token }: any) => {
|
||||
return {
|
||||
header: css`
|
||||
height: var(--nb-mobile-page-header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${token.colorSplit};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
z-index: 1000;
|
||||
|
||||
// to match the button named 'Add block'
|
||||
& + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn {
|
||||
// 18px is the token marginBlock value
|
||||
margin: 12px 12px calc(12px + 18px);
|
||||
}
|
||||
`,
|
||||
|
||||
placeholder: css`
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
visibility: hidden;
|
||||
`,
|
||||
|
||||
closeIcon: css`
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
`,
|
||||
|
||||
body: css`
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
max-height: calc(100% - var(--nb-mobile-page-header-height));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: ${token.colorBgLayout};
|
||||
`,
|
||||
|
||||
footer: css`
|
||||
padding: 8px var(--nb-mobile-page-tabs-content-padding);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
border-top: 1px solid ${token.colorSplit};
|
||||
background-color: ${token.colorBgLayout};
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { Action, SchemaComponent, useActionContext } from '@nocobase/client';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { Popup } from 'antd-mobile';
|
||||
import { CloseOutline } from 'antd-mobile-icons';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useMobileActionDrawerStyle } from './ActionDrawer.style';
|
||||
import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider';
|
||||
import { usePopupContainer } from './FilterAction';
|
||||
|
||||
export const ActionDrawerUsedInMobile = observer((props: { footerNodeName?: string }) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { visible, setVisible } = useActionContext();
|
||||
const { popupContainerRef, visiblePopup } = usePopupContainer(visible);
|
||||
const { styles } = useMobileActionDrawerStyle();
|
||||
const { basicZIndex } = useBasicZIndex();
|
||||
|
||||
const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT;
|
||||
|
||||
const zIndexStyle = useMemo(() => {
|
||||
return {
|
||||
zIndex: newZIndex,
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
const footerSchema = fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === props.footerNodeName) {
|
||||
return s;
|
||||
}
|
||||
return buf;
|
||||
});
|
||||
|
||||
const title = field.title || '';
|
||||
const marginBlock = 18;
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return {
|
||||
token: {
|
||||
marginBlock,
|
||||
borderRadiusBlock: 0,
|
||||
boxShadowTertiary: 'none',
|
||||
zIndexPopupBase: newZIndex,
|
||||
},
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
return (
|
||||
<BasicZIndexProvider basicZIndex={newZIndex}>
|
||||
<ConfigProvider theme={theme}>
|
||||
<Popup
|
||||
visible={visiblePopup}
|
||||
onClose={closePopup}
|
||||
onMaskClick={closePopup}
|
||||
getContainer={() => popupContainerRef.current}
|
||||
bodyClassName={styles.body}
|
||||
bodyStyle={zIndexStyle}
|
||||
maskStyle={zIndexStyle}
|
||||
closeOnSwipe
|
||||
>
|
||||
<div className={styles.header}>
|
||||
{/* used to make the title center */}
|
||||
<span className={styles.placeholder}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
<span className={styles.closeIcon} onClick={closePopup}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
</div>
|
||||
<SchemaComponent
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== props.footerNodeName;
|
||||
}}
|
||||
/>
|
||||
{/* used to offset the margin-bottom of the last block */}
|
||||
{/* The number 1 is to prevent the scroll bar from appearing */}
|
||||
<div style={{ marginBottom: 1 - marginBlock }}></div>
|
||||
{footerSchema ? (
|
||||
<div className={styles.footer}>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === props.footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</Popup>
|
||||
</ConfigProvider>
|
||||
</BasicZIndexProvider>
|
||||
);
|
||||
});
|
||||
|
||||
ActionDrawerUsedInMobile.displayName = 'ActionDrawerUsedInMobile';
|
||||
|
||||
const originalActionDrawer = Action.Drawer;
|
||||
|
||||
/**
|
||||
* adapt Action.Drawer to mobile
|
||||
*/
|
||||
export const useToAdaptActionDrawerToMobile = () => {
|
||||
Action.Drawer = ActionDrawerUsedInMobile;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Action.Drawer = originalActionDrawer;
|
||||
};
|
||||
}, []);
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
const BasicZIndexContext = React.createContext<{
|
||||
basicZIndex: number;
|
||||
}>({
|
||||
basicZIndex: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* used to accumulate z-index in nested popups
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const BasicZIndexProvider: React.FC<{ basicZIndex: number }> = (props) => {
|
||||
const value = useMemo(() => ({ basicZIndex: props.basicZIndex }), [props.basicZIndex]);
|
||||
return <BasicZIndexContext.Provider value={value}>{props.children}</BasicZIndexContext.Provider>;
|
||||
};
|
||||
|
||||
export const useBasicZIndex = () => {
|
||||
return React.useContext(BasicZIndexContext);
|
||||
};
|
||||
|
||||
// minimum z-index increment
|
||||
export const MIN_Z_INDEX_INCREMENT = 10;
|
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Filter, withDynamicSchemaProps } from '@nocobase/client';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { Popup } from 'antd-mobile';
|
||||
import { CloseOutline } from 'antd-mobile-icons';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMobileActionDrawerStyle } from './ActionDrawer.style';
|
||||
import { MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider';
|
||||
|
||||
const OriginFilterAction = Filter.Action;
|
||||
|
||||
export const FilterAction = withDynamicSchemaProps((props) => {
|
||||
return (
|
||||
<OriginFilterAction
|
||||
{...props}
|
||||
Container={(props) => {
|
||||
const { visiblePopup, popupContainerRef } = usePopupContainer(props.open);
|
||||
const { basicZIndex } = useBasicZIndex();
|
||||
const { styles } = useMobileActionDrawerStyle();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const closePopup = useCallback(() => {
|
||||
props.onOpenChange(false);
|
||||
}, [props]);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return {
|
||||
token: {
|
||||
zIndexPopupBase: newZIndex,
|
||||
},
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
const bodyStyle = useMemo(
|
||||
() => ({
|
||||
borderTopLeftRadius: '8px',
|
||||
borderTopRightRadius: '8px',
|
||||
maxHeight: 'calc(100% - var(--nb-mobile-page-header-height))',
|
||||
overflow: 'auto',
|
||||
zIndex: newZIndex,
|
||||
}),
|
||||
[newZIndex],
|
||||
);
|
||||
|
||||
const zIndexStyle = useMemo(() => ({ zIndex: newZIndex }), [newZIndex]);
|
||||
|
||||
const getContainer = useCallback(() => popupContainerRef.current, [popupContainerRef]);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={theme}>
|
||||
{props.children}
|
||||
<Popup
|
||||
visible={visiblePopup}
|
||||
onClose={closePopup}
|
||||
onMaskClick={closePopup}
|
||||
getContainer={getContainer}
|
||||
bodyStyle={bodyStyle}
|
||||
maskStyle={zIndexStyle}
|
||||
closeOnSwipe
|
||||
>
|
||||
<div className={styles.header}>
|
||||
{/* used to make the title center */}
|
||||
<span className={styles.placeholder}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
<span>{t('Filter')}</span>
|
||||
<span className={styles.closeIcon} onClick={closePopup}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>{props.content}</div>
|
||||
<div style={{ height: 150 }}></div>
|
||||
</Popup>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FilterAction.displayName = 'FilterAction';
|
||||
|
||||
const originalFilterAction = Filter.Action;
|
||||
|
||||
/**
|
||||
* adapt Filter.Action to mobile
|
||||
*/
|
||||
export const useToAdaptFilterActionToMobile = () => {
|
||||
Filter.Action = FilterAction;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Filter.Action = originalFilterAction;
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* 之所以不直接使用 mobile-container 作为容器,是因为会影响到区块的拖拽功能。详见:https://nocobase.height.app/T-4959
|
||||
* @param visible
|
||||
* @returns
|
||||
*/
|
||||
export const usePopupContainer = (visible: boolean) => {
|
||||
const [mobileContainer] = useState<HTMLElement>(() => document.querySelector('.mobile-container'));
|
||||
const [visiblePopup, setVisiblePopup] = useState(false);
|
||||
const popupContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const { basicZIndex } = useBasicZIndex();
|
||||
|
||||
const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT;
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setVisiblePopup(false);
|
||||
if (popupContainerRef.current) {
|
||||
// Popup 动画都结束的时候再移除
|
||||
setTimeout(() => {
|
||||
mobileContainer.contains(popupContainerRef.current) && mobileContainer.removeChild(popupContainerRef.current);
|
||||
popupContainerRef.current = null;
|
||||
}, 300);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const popupContainer = document.createElement('div');
|
||||
popupContainer.style.transform = 'translateZ(0)';
|
||||
popupContainer.style.position = 'absolute';
|
||||
popupContainer.style.top = '0';
|
||||
popupContainer.style.left = '0';
|
||||
popupContainer.style.right = '0';
|
||||
popupContainer.style.bottom = '0';
|
||||
popupContainer.style.overflow = 'hidden';
|
||||
popupContainer.style.zIndex = newZIndex.toString();
|
||||
|
||||
mobileContainer.appendChild(popupContainer);
|
||||
popupContainerRef.current = popupContainer;
|
||||
|
||||
setVisiblePopup(true);
|
||||
|
||||
return () => {
|
||||
if (popupContainerRef.current) {
|
||||
// Popup 动画都结束的时候再移除
|
||||
setTimeout(() => {
|
||||
mobileContainer.contains(popupContainerRef.current) && mobileContainer.removeChild(popupContainerRef.current);
|
||||
popupContainerRef.current = null;
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
}, [mobileContainer, newZIndex, visible]);
|
||||
|
||||
return {
|
||||
visiblePopup,
|
||||
popupContainerRef,
|
||||
};
|
||||
};
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createStyles } from '@nocobase/client';
|
||||
|
||||
export const useInternalPopoverNesterUsedInMobileStyle = createStyles(({ css, token }: any) => {
|
||||
return {
|
||||
header: css`
|
||||
height: var(--nb-mobile-page-header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${token.colorSplit};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
z-index: 1000;
|
||||
|
||||
// to match the button named 'Add block'
|
||||
& + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn {
|
||||
// 18px is the token marginBlock value
|
||||
margin: 12px 12px calc(12px + 18px);
|
||||
}
|
||||
`,
|
||||
|
||||
placeholder: css`
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
visibility: hidden;
|
||||
`,
|
||||
|
||||
closeIcon: css`
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
`,
|
||||
|
||||
body: css`
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
max-height: calc(100% - var(--nb-mobile-page-header-height));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
// background-color: ${token.colorBgLayout};
|
||||
|
||||
.popover-subform-container {
|
||||
min-width: initial;
|
||||
max-width: initial;
|
||||
max-height: initial;
|
||||
overflow: hidden;
|
||||
.ant-card {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
footer: css`
|
||||
padding: 8px var(--nb-mobile-page-tabs-content-padding);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
border-top: 1px solid ${token.colorSplit};
|
||||
background-color: ${token.colorBgLayout};
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useField } from '@formily/react';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { Popup } from 'antd-mobile';
|
||||
import { CloseOutline } from 'antd-mobile-icons';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from './BasicZIndexProvider';
|
||||
import { usePopupContainer } from './FilterAction';
|
||||
import { useInternalPopoverNesterUsedInMobileStyle } from './InternalPopoverNester.style';
|
||||
|
||||
const Container = (props) => {
|
||||
const { onOpenChange } = props;
|
||||
const { visiblePopup, popupContainerRef } = usePopupContainer(props.open);
|
||||
const { styles } = useInternalPopoverNesterUsedInMobileStyle();
|
||||
const field = useField();
|
||||
const { basicZIndex } = useBasicZIndex();
|
||||
|
||||
const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT;
|
||||
const title = field.title || '';
|
||||
|
||||
const zIndexStyle = useMemo(() => {
|
||||
return {
|
||||
zIndex: newZIndex,
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const openPopup = useCallback(() => {
|
||||
onOpenChange(true);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return {
|
||||
token: {
|
||||
zIndexPopupBase: newZIndex,
|
||||
},
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
return (
|
||||
<BasicZIndexProvider basicZIndex={newZIndex}>
|
||||
<ConfigProvider theme={theme}>
|
||||
<div onClick={openPopup}>{props.children}</div>
|
||||
<Popup
|
||||
visible={visiblePopup}
|
||||
onClose={closePopup}
|
||||
onMaskClick={closePopup}
|
||||
getContainer={() => popupContainerRef.current as HTMLElement}
|
||||
bodyClassName={styles.body}
|
||||
bodyStyle={zIndexStyle}
|
||||
maskStyle={zIndexStyle}
|
||||
showCloseButton
|
||||
closeOnSwipe
|
||||
>
|
||||
<div className={styles.header}>
|
||||
{/* used to make the title center */}
|
||||
<span className={styles.placeholder}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
<span className={styles.closeIcon} onClick={closePopup}>
|
||||
<CloseOutline />
|
||||
</span>
|
||||
</div>
|
||||
{props.content}
|
||||
<div style={{ height: 50 }}></div>
|
||||
</Popup>
|
||||
</ConfigProvider>
|
||||
</BasicZIndexProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const InternalPopoverNesterUsedInMobile: React.FC<{ OriginComponent: React.FC<any> }> = (props) => {
|
||||
const { OriginComponent } = props;
|
||||
|
||||
return <OriginComponent {...props} Container={Container} />;
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaComponentOptions, useSchemaOptionsContext } from '@nocobase/client';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useGridCardBlockDecoratorProps } from './useGridCardBlockDecoratorProps';
|
||||
|
||||
/* 使用移动端专属的 scope 覆盖桌面端的 scope,用于在移动端适配桌面端区块 */
|
||||
export const ResetSchemaOptionsProvider: FC = (props) => {
|
||||
const { scope: desktopScopes } = useSchemaOptionsContext();
|
||||
const scopes = useMemo(
|
||||
() => ({
|
||||
useGridCardBlockDecoratorProps: (props) =>
|
||||
useGridCardBlockDecoratorProps(props, desktopScopes?.useGridCardBlockDecoratorProps),
|
||||
}),
|
||||
[desktopScopes?.useGridCardBlockDecoratorProps],
|
||||
);
|
||||
return <SchemaComponentOptions scope={scopes}>{props.children}</SchemaComponentOptions>;
|
||||
};
|
@ -23,5 +23,19 @@ export const useMobileActionPageStyle = createStyles(({ css, token }: any) => {
|
||||
margin: 20px;
|
||||
}
|
||||
`,
|
||||
|
||||
footer: css`
|
||||
height: var(--nb-mobile-page-header-height);
|
||||
padding-right: var(--nb-mobile-page-tabs-content-padding);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
z-index: 1000;
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
BackButtonUsedInSubPage,
|
||||
SchemaComponent,
|
||||
SchemaInitializer,
|
||||
TabsContextProvider,
|
||||
useActionContext,
|
||||
useApp,
|
||||
useTabsContext,
|
||||
} from '@nocobase/client';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { usePluginTranslation } from '../../locale';
|
||||
import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from '../BasicZIndexProvider';
|
||||
import { useMobileActionPageStyle } from './MobileActionPage.style';
|
||||
import { MobileTabsForMobileActionPage } from './MobileTabsForMobileActionPage';
|
||||
|
||||
const components = { Tabs: MobileTabsForMobileActionPage };
|
||||
|
||||
/**
|
||||
* 把 popup:common:addBlock 替换为移动端专属的值。当退出子页面时,再换回来。
|
||||
*
|
||||
* 之所以要把这个过程放到子页面组件这里,是因为 dataBlocks 的 useChildren 必须要在子页面的上下文中运行。
|
||||
*
|
||||
* @param supportsDataBlocks 支持在子页面中使用的数据区块 name
|
||||
*/
|
||||
const useMobileBlockInitializersInSubpage = (
|
||||
supportsDataBlocks = ['details', 'editForm', 'createForm', 'table', 'gridCard'],
|
||||
) => {
|
||||
const app = useApp();
|
||||
const [originalInitializers] = useState<SchemaInitializer>(() =>
|
||||
app.schemaInitializerManager.get('popup:common:addBlock'),
|
||||
);
|
||||
const { t } = usePluginTranslation();
|
||||
const { visible } = useActionContext();
|
||||
|
||||
const dataBlocks = originalInitializers.options.items.find((item) => item.name === 'dataBlocks');
|
||||
const dataBlocksChildren = [...dataBlocks.useChildren(), ...dataBlocks.children];
|
||||
|
||||
const [newInitializers] = useState<SchemaInitializer>(() => {
|
||||
const options = _.cloneDeep(originalInitializers.options);
|
||||
options.items = options.items.filter((item) => {
|
||||
if (item.name === 'dataBlocks') {
|
||||
item.title = t('Desktop data blocks');
|
||||
item.children = dataBlocksChildren.filter((child) => {
|
||||
return supportsDataBlocks.includes(child.name);
|
||||
});
|
||||
item.useChildren = () => [];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.name === 'otherBlocks') {
|
||||
item.title = t('Other desktop blocks');
|
||||
}
|
||||
|
||||
return item.name !== 'filterBlocks';
|
||||
});
|
||||
|
||||
return new SchemaInitializer(options);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
app.schemaInitializerManager.add(originalInitializers);
|
||||
};
|
||||
}, [app, originalInitializers]);
|
||||
|
||||
if (visible) {
|
||||
// 把 PC 端子页面的 Add block 按钮换成移动端的。在退出移动端时,再换回来
|
||||
app.schemaInitializerManager.add(newInitializers);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在移动端通过 Action 按钮打开的页面
|
||||
* @returns
|
||||
*/
|
||||
export const MobileActionPage = ({ level, footerNodeName }) => {
|
||||
useMobileBlockInitializersInSubpage();
|
||||
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const ctx = useActionContext();
|
||||
const { styles } = useMobileActionPageStyle();
|
||||
const tabContext = useTabsContext();
|
||||
const containerDOM = useMemo(() => document.querySelector('.nb-mobile-subpages-slot'), []);
|
||||
const { basicZIndex } = useBasicZIndex();
|
||||
|
||||
// in nested popups, basicZIndex is an accumulated value to ensure that
|
||||
// the z-index of the current level is always higher than the previous level
|
||||
const newZIndex = basicZIndex + MIN_Z_INDEX_INCREMENT + (level || 1);
|
||||
|
||||
const footerSchema = fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === footerNodeName) {
|
||||
return s;
|
||||
}
|
||||
return buf;
|
||||
});
|
||||
|
||||
const zIndexStyle = useMemo(() => {
|
||||
return {
|
||||
zIndex: newZIndex,
|
||||
};
|
||||
}, [newZIndex]);
|
||||
|
||||
if (!ctx.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actionPageNode = (
|
||||
<BasicZIndexProvider basicZIndex={newZIndex}>
|
||||
<div className={styles.container} style={zIndexStyle}>
|
||||
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />} tabBarGutter={48}>
|
||||
<SchemaComponent components={components} schema={fieldSchema} onlyRenderProperties />
|
||||
</TabsContextProvider>
|
||||
{footerSchema && (
|
||||
<div className={styles.footer} style={zIndexStyle}>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BasicZIndexProvider>
|
||||
);
|
||||
|
||||
if (containerDOM) {
|
||||
return createPortal(actionPageNode, containerDOM);
|
||||
}
|
||||
|
||||
return actionPageNode;
|
||||
};
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
css,
|
||||
DndContext,
|
||||
@ -23,22 +23,35 @@ import {
|
||||
import { Tabs } from 'antd-mobile';
|
||||
import { LeftOutline } from 'antd-mobile-icons';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { MobilePageHeader } from '../dynamic-page';
|
||||
import { MobilePageContentContainer } from '../dynamic-page/content/MobilePageContentContainer';
|
||||
import { useStyles } from '../dynamic-page/header/tabs';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MobilePageHeader } from '../../pages/dynamic-page';
|
||||
import { MobilePageContentContainer } from '../../pages/dynamic-page/content/MobilePageContentContainer';
|
||||
import { useStyles } from '../../pages/dynamic-page/header/tabs';
|
||||
import { useMobileTabsForMobileActionPageStyle } from './MobileTabsForMobileActionPage.style';
|
||||
|
||||
export const MobileTabsForMobileActionPage: any = observer(
|
||||
(props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||
const { activeKey, onChange } = useTabsContext() || {};
|
||||
const { activeKey: _activeKey, onChange: _onChange } = useTabsContext() || {};
|
||||
const [activeKey, setActiveKey] = useState(_activeKey);
|
||||
const { styles } = useStyles();
|
||||
const { styles: mobileTabsForMobileActionPageStyle } = useMobileTabsForMobileActionPageStyle();
|
||||
const { goBack } = useBackButton();
|
||||
const keyToTabRef = useRef({});
|
||||
|
||||
const onChange = useCallback(
|
||||
(key) => {
|
||||
setActiveKey(key);
|
||||
_onChange?.(key);
|
||||
},
|
||||
[_onChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveKey(_activeKey);
|
||||
}, [_activeKey]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const result = fieldSchema.mapProperties((schema, key) => {
|
||||
keyToTabRef.current[key] = <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />;
|
||||
@ -50,6 +63,7 @@ export const MobileTabsForMobileActionPage: any = observer(
|
||||
|
||||
const tabContent = useMemo(() => {
|
||||
const list = fieldSchema.mapProperties((schema, key) => {
|
||||
schema = hideDivider(schema);
|
||||
return {
|
||||
key,
|
||||
node: <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />,
|
||||
@ -148,3 +162,17 @@ MobileTabsForMobileActionPage.TabPane = observer(
|
||||
);
|
||||
|
||||
MobileTabsForMobileActionPage.Designer = TabsOfPC.Designer;
|
||||
|
||||
// 隐藏 Grid 组件的左右 divider,因为移动端不需要在一行中并列展示两个区块
|
||||
function hideDivider(tabPaneSchema: Schema) {
|
||||
tabPaneSchema?.mapProperties((schema) => {
|
||||
if (schema['x-component'] === 'Grid') {
|
||||
schema['x-component-props'] = {
|
||||
...schema['x-component-props'],
|
||||
showDivider: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return tabPaneSchema;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用于设置 Grid Card 区块的 props,使其样式适配移动端;
|
||||
* @param oldUseGridCardBlockDecoratorProps
|
||||
*/
|
||||
export const useGridCardBlockDecoratorProps = (props, useGridCardBlockDecoratorPropsOfDesktop: any) => {
|
||||
const oldProps = useGridCardBlockDecoratorPropsOfDesktop(props);
|
||||
|
||||
return {
|
||||
...oldProps,
|
||||
// 在移动端中,无论是什么屏幕尺寸,都只显示一列
|
||||
columnCount: {
|
||||
lg: 1,
|
||||
md: 1,
|
||||
xs: 1,
|
||||
xxl: 1,
|
||||
},
|
||||
};
|
||||
};
|
@ -9,11 +9,11 @@
|
||||
|
||||
import { PagePopups, Plugin, RouterManager, createRouterManager } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { name } from '../../package.json';
|
||||
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { generatePluginTranslationTemplate } from './locale';
|
||||
import { Mobile } from './mobile';
|
||||
import {
|
||||
@ -184,6 +184,15 @@ export class PluginMobileClient extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
// 跳转到主应用的页面
|
||||
this.mobileRouter.add('admin', {
|
||||
path: `/admin/*`,
|
||||
Component: () => {
|
||||
window.location.replace(window.location.href.replace(this.mobilePath, ''));
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
this.mobileRouter.add('mobile.schema', {
|
||||
element: <Outlet />,
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ export const MobileProviders: FC<MobileProvidersProps> = ({ children, skipLogin
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px');
|
||||
document.body.style.setProperty('--nb-mobile-page-header-height', '50px');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -7,10 +7,10 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Spin } from 'antd';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { APIClient, useAPIClient, useRequest } from '@nocobase/client';
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import type { IResource } from '@nocobase/sdk';
|
||||
|
||||
@ -27,6 +27,7 @@ export interface MobileRouteItem {
|
||||
children?: MobileRouteItem[];
|
||||
}
|
||||
|
||||
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(null);
|
||||
|
||||
export interface MobileRoutesContextValue {
|
||||
routeList?: MobileRouteItem[];
|
||||
@ -37,8 +38,6 @@ export interface MobileRoutesContextValue {
|
||||
activeTabItem?: MobileRouteItem;
|
||||
api: APIClient;
|
||||
}
|
||||
|
||||
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(null);
|
||||
MobileRoutesContext.displayName = 'MobileRoutesContext';
|
||||
|
||||
export const useMobileRoutes = () => {
|
||||
@ -97,7 +96,9 @@ export const MobileRoutesProvider = ({ children }) => {
|
||||
data,
|
||||
runAsync: refresh,
|
||||
loading,
|
||||
} = useRequest<{ data: MobileRouteItem[] }>(() => resource.list({ tree: true, sort: 'sort' }).then((res) => res.data));
|
||||
} = useRequest<{ data: MobileRouteItem[] }>(() =>
|
||||
resource.list({ tree: true, sort: 'sort' }).then((res) => res.data),
|
||||
);
|
||||
const routeList = useMemo(() => data?.data || [], [data]);
|
||||
const { activeTabBarItem, activeTabItem } = useActiveTabBar(routeList);
|
||||
|
||||
|
@ -7,21 +7,38 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Action, AntdAppProvider, GlobalThemeProvider, OpenModeProvider, usePlugin } from '@nocobase/client';
|
||||
import {
|
||||
Action,
|
||||
AntdAppProvider,
|
||||
AssociationFieldModeProvider,
|
||||
BlockTemplateProvider,
|
||||
GlobalThemeProvider,
|
||||
OpenModeProvider,
|
||||
usePlugin,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { isDesktop } from 'react-device-detect';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from '../adaptor-of-desktop/ActionDrawer';
|
||||
import { BasicZIndexProvider } from '../adaptor-of-desktop/BasicZIndexProvider';
|
||||
import { useToAdaptFilterActionToMobile } from '../adaptor-of-desktop/FilterAction';
|
||||
import { InternalPopoverNesterUsedInMobile } from '../adaptor-of-desktop/InternalPopoverNester';
|
||||
import { MobileActionPage } from '../adaptor-of-desktop/mobile-action-page/MobileActionPage';
|
||||
import { ResetSchemaOptionsProvider } from '../adaptor-of-desktop/ResetSchemaOptionsProvider';
|
||||
import { PageBackgroundColor } from '../constants';
|
||||
import { DesktopMode } from '../desktop-mode/DesktopMode';
|
||||
import { PluginMobileClient } from '../index';
|
||||
import { MobileActionPage } from '../pages/mobile-action-page/MobileActionPage';
|
||||
import { MobileAppProvider } from './MobileAppContext';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
export const Mobile = () => {
|
||||
useToAdaptFilterActionToMobile();
|
||||
useToAdaptActionDrawerToMobile();
|
||||
|
||||
const { styles } = useStyles();
|
||||
const mobilePlugin = usePlugin(PluginMobileClient);
|
||||
const MobileRouter = mobilePlugin.getRouterComponent();
|
||||
const { styles } = useStyles();
|
||||
// 设置的移动端 meta
|
||||
React.useEffect(() => {
|
||||
if (!isDesktop) {
|
||||
@ -44,36 +61,51 @@ export const Mobile = () => {
|
||||
}, []);
|
||||
|
||||
const DesktopComponent = mobilePlugin.desktopMode === false ? React.Fragment : DesktopMode;
|
||||
const modeToComponent = React.useMemo(() => {
|
||||
return {
|
||||
PopoverNester: _.memoize((OriginComponent) => (props) => (
|
||||
<InternalPopoverNesterUsedInMobile {...props} OriginComponent={OriginComponent} />
|
||||
)),
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DesktopComponent>
|
||||
{/* 目前移动端由于和客户端的主题对不上,所以先使用 `GlobalThemeProvider` 和 `AntdAppProvider` 进行重置为默认主题 */}
|
||||
<div className={styles.nbMobile}>
|
||||
<GlobalThemeProvider
|
||||
theme={{
|
||||
token: {
|
||||
marginBlock: 18,
|
||||
borderRadiusBlock: 0,
|
||||
boxShadowTertiary: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AntdAppProvider>
|
||||
<OpenModeProvider
|
||||
defaultOpenMode="page"
|
||||
hideOpenMode
|
||||
openModeToComponent={{
|
||||
page: MobileActionPage,
|
||||
drawer: MobileActionPage,
|
||||
modal: Action.Modal,
|
||||
}}
|
||||
>
|
||||
<GlobalThemeProvider
|
||||
theme={{
|
||||
token: {
|
||||
marginBlock: 18,
|
||||
borderRadiusBlock: 0,
|
||||
boxShadowTertiary: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AntdAppProvider className={`mobile-container ${styles.nbMobile}`}>
|
||||
<OpenModeProvider
|
||||
defaultOpenMode="page"
|
||||
hideOpenMode
|
||||
openModeToComponent={{
|
||||
page: MobileActionPage,
|
||||
drawer: ActionDrawerUsedInMobile,
|
||||
modal: Action.Modal,
|
||||
}}
|
||||
>
|
||||
<BlockTemplateProvider componentNamePrefix="mobile-">
|
||||
<MobileAppProvider>
|
||||
<MobileRouter />
|
||||
<ResetSchemaOptionsProvider>
|
||||
<AssociationFieldModeProvider modeToComponent={modeToComponent}>
|
||||
{/* the z-index of all popups and subpages will be based on this value */}
|
||||
<BasicZIndexProvider basicZIndex={1000}>
|
||||
<MobileRouter />
|
||||
</BasicZIndexProvider>
|
||||
</AssociationFieldModeProvider>
|
||||
</ResetSchemaOptionsProvider>
|
||||
</MobileAppProvider>
|
||||
</OpenModeProvider>
|
||||
</AntdAppProvider>
|
||||
</GlobalThemeProvider>
|
||||
</div>
|
||||
</BlockTemplateProvider>
|
||||
</OpenModeProvider>
|
||||
</AntdAppProvider>
|
||||
</GlobalThemeProvider>
|
||||
</DesktopComponent>
|
||||
);
|
||||
};
|
||||
|
@ -19,6 +19,19 @@ export const useStyles = createStyles(({ token, css }) => {
|
||||
.ant-table-thead button[aria-label*='schema-initializer-TableV2-table:configureColumns'] > .ant-btn-icon {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
// reset Select record popup
|
||||
.ant-table-thead
|
||||
button[aria-label*='schema-initializer-TableV2.Selector-table:configureColumns']
|
||||
> span:last-child {
|
||||
display: none !important;
|
||||
}
|
||||
.ant-table-thead
|
||||
button[aria-label*='schema-initializer-TableV2.Selector-table:configureColumns']
|
||||
> .ant-btn-icon {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-total-text {
|
||||
display: none;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({
|
||||
items: [
|
||||
{
|
||||
name: 'dataBlocks',
|
||||
title: '{{t("Data blocks")}}',
|
||||
title: '{{t("Desktop data blocks")}}',
|
||||
type: 'itemGroup',
|
||||
children: [
|
||||
{
|
||||
@ -53,7 +53,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({
|
||||
{
|
||||
name: 'otherBlocks',
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Other blocks")}}',
|
||||
title: '{{t("Other desktop blocks")}}',
|
||||
children: [
|
||||
{
|
||||
name: 'markdown',
|
||||
|
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
BackButtonUsedInSubPage,
|
||||
SchemaComponent,
|
||||
TabsContextProvider,
|
||||
useActionContext,
|
||||
useTabsContext,
|
||||
} from '@nocobase/client';
|
||||
import React, { useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useMobileActionPageStyle } from './MobileActionPage.style';
|
||||
import { MobileTabsForMobileActionPage } from './MobileTabsForMobileActionPage';
|
||||
|
||||
const components = { Tabs: MobileTabsForMobileActionPage };
|
||||
|
||||
/**
|
||||
* 在移动端通过 Action 按钮打开的页面
|
||||
* @returns
|
||||
*/
|
||||
export const MobileActionPage = ({ level }) => {
|
||||
const filedSchema = useFieldSchema();
|
||||
const ctx = useActionContext();
|
||||
const { styles } = useMobileActionPageStyle();
|
||||
const tabContext = useTabsContext();
|
||||
const containerDOM = useMemo(() => document.querySelector('.nb-mobile-subpages-slot'), []);
|
||||
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
// 10 为基数,是为了要确保能大于 Table 中的悬浮行的 z-index
|
||||
zIndex: 10 + level,
|
||||
};
|
||||
}, [level]);
|
||||
|
||||
if (!ctx.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actionPageNode = (
|
||||
<div className={styles.container} style={style}>
|
||||
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />} tabBarGutter={48}>
|
||||
<SchemaComponent components={components} schema={filedSchema} onlyRenderProperties />
|
||||
</TabsContextProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (containerDOM) {
|
||||
return createPortal(actionPageNode, containerDOM);
|
||||
}
|
||||
|
||||
return actionPageNode;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user