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:
Zeke Zhang 2024-08-07 14:25:40 +08:00 committed by GitHub
parent 64e53558df
commit a429b7a4b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1630 additions and 449 deletions

View File

@ -2,9 +2,7 @@
"version": "1.3.0-alpha", "version": "1.3.0-alpha",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": [ "npmClientArgs": ["--ignore-engines"],
"--ignore-engines"
],
"command": { "command": {
"version": { "version": {
"forcePublish": true, "forcePublish": true,

View File

@ -20,7 +20,7 @@ import omit from 'lodash/omit';
import qs from 'qs'; import qs from 'qs';
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'; import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; 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 { useReactToPrint } from 'react-to-print';
import { import {
AssociationFilter, AssociationFilter,
@ -1592,6 +1592,9 @@ export function useLinkActionProps(componentProps?: any) {
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow']; const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
const { parseURLAndParams } = useParseURLAndParams(); const { parseURLAndParams } = useParseURLAndParams();
// see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter
const basenameOfCurrentRouter = useHref('/');
return { return {
type: 'default', type: 'default',
async onClick() { async onClick() {
@ -1605,7 +1608,7 @@ export function useLinkActionProps(componentProps?: any) {
if (openInNewWindow) { if (openInNewWindow) {
window.open(completeURL(link), '_blank'); window.open(completeURL(link), '_blank');
} else { } else {
navigateWithinSelf(link, navigate); navigateWithinSelf(link, navigate, window.location.origin + basenameOfCurrentRouter);
} }
} else { } else {
console.error('link should be a string'); 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}`; 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)) { if (!_.isString(link)) {
return console.error('link should be a string'); return console.error('link should be a string');
} }
if (isURL(link)) { if (isURL(link)) {
if (link.startsWith(origin)) { if (link.startsWith(basePath)) {
navigate(link.replace(origin, '')); navigate(completeURL(link.replace(basePath, ''), ''));
} else { } else {
window.open(link, '_self'); window.open(link, '_self');
} }
} else { } else {
navigate(link.startsWith('/') ? link : `/${link}`); navigate(completeURL(link, ''));
} }
} }

View File

@ -9,7 +9,7 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useBlockTemplateContext } from '../../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../../schema-templates/BlockTemplateProvider';
export const useBlockHeightProps = () => { export const useBlockHeightProps = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

@ -26,11 +26,21 @@ const AppInner = memo(({ children }: { children: React.ReactNode }) => {
}); });
AppInner.displayName = 'AppInner'; AppInner.displayName = 'AppInner';
const AntdAppProvider = ({ children }: { children: React.ReactNode }) => { const AntdAppProvider = ({
children,
className,
style,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}) => {
return ( return (
<App <App
className={className}
style={{ style={{
height: '100%', height: '100%',
...style,
}} }}
> >
<AppInner>{children}</AppInner> <AppInner>{children}</AppInner>

View File

@ -44,7 +44,7 @@ const commonOptions = {
showAssociationFields: true, showAssociationFields: true,
onlyCurrentDataSource: true, onlyCurrentDataSource: true,
hideSearch: true, hideSearch: true,
componentType: 'FormItem', componentType: `FormItem`,
currentText: t('Current collection'), currentText: t('Current collection'),
otherText: t('Other collections'), otherText: t('Other collections'),
}, },

View File

@ -29,10 +29,11 @@ export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps>
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const { urlSchema, paramsSchema, openInNewWindowSchema } = useURLAndHTMLSchema(); const { urlSchema, paramsSchema, openInNewWindowSchema } = useURLAndHTMLSchema();
const componentProps = fieldSchema['x-component-props'] || {};
const initialValues = { const initialValues = {
url: field.componentProps.url, url: componentProps.url,
params: field.componentProps.params || [{}], params: componentProps.params || [{}],
openInNewWindow: field.componentProps.openInNewWindow, openInNewWindow: componentProps.openInNewWindow,
}; };
return ( return (
@ -52,7 +53,6 @@ export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps>
}, },
}} }}
onSubmit={({ url, params, openInNewWindow }) => { onSubmit={({ url, params, openInNewWindow }) => {
const componentProps = fieldSchema['x-component-props'] || {};
componentProps.url = url; componentProps.url = url;
componentProps.params = params; componentProps.params = params;
componentProps.openInNewWindow = openInNewWindow; componentProps.openInNewWindow = openInNewWindow;

View File

@ -21,6 +21,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem'; import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem';
const commonItems: SchemaSettingsItemType[] = [ const commonItems: SchemaSettingsItemType[] = [
@ -201,10 +202,11 @@ const commonItems: SchemaSettingsItemType[] = [
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Details', componentName: `${componentNamePrefix}Details`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -10,10 +10,11 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types'; 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 { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
const commonItems: SchemaSettingsItemType[] = [ const commonItems: SchemaSettingsItemType[] = [
{ {
name: 'title', name: 'title',
@ -27,7 +28,7 @@ const commonItems: SchemaSettingsItemType[] = [
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection();
return { return {
collectionName: name, collectionName: name,
readPretty: true, readPretty: true,
@ -38,13 +39,14 @@ const commonItems: SchemaSettingsItemType[] = [
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
insertAdjacentPosition: 'beforeEnd', insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem', componentName: `${componentNamePrefix}ReadPrettyFormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../../collection-manager'; import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
import { import {
SchemaSettingsDataTemplates, SchemaSettingsDataTemplates,
SchemaSettingsFormItemTemplate, SchemaSettingsFormItemTemplate,
@ -18,6 +19,7 @@ import {
} from '../../../../schema-settings'; } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
export const createFormBlockSettings = new SchemaSettings({ export const createFormBlockSettings = new SchemaSettings({
name: 'blockSettings:createForm', name: 'blockSettings:createForm',
@ -62,12 +64,13 @@ export const createFormBlockSettings = new SchemaSettings({
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { componentNamePrefix } = useBlockTemplateContext();
const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: `${componentNamePrefix}FormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -11,6 +11,7 @@ import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../../collection-manager'; import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
import { import {
SchemaSettingsDataTemplates, SchemaSettingsDataTemplates,
SchemaSettingsFormItemTemplate, SchemaSettingsFormItemTemplate,
@ -18,6 +19,7 @@ import {
} from '../../../../schema-settings'; } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
export const editFormBlockSettings = new SchemaSettings({ export const editFormBlockSettings = new SchemaSettings({
name: 'blockSettings:editForm', name: 'blockSettings:editForm',
@ -62,12 +64,13 @@ export const editFormBlockSettings = new SchemaSettings({
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { componentNamePrefix } = useBlockTemplateContext();
const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: `${componentNamePrefix}FormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -49,7 +49,7 @@ export const GridCardBlockInitializer = ({
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
icon={<OrderedListOutlined />} icon={<OrderedListOutlined />}
componentType={'GridCard'} componentType={`GridCard`}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {
return createBlockSchema(options); return createBlockSchema(options);

View File

@ -20,9 +20,9 @@ import { pageSizeOptions } from '../../../../schema-component/antd/grid-card/opt
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow'; import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow';
import { useCollection } from '../../../../data-source';
export const gridCardBlockSettings = new SchemaSettings({ export const gridCardBlockSettings = new SchemaSettings({
name: 'blockSettings:gridCard', name: 'blockSettings:gridCard',
@ -210,11 +210,12 @@ export const gridCardBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'GridCard', componentName: `${componentNamePrefix}GridCard`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -49,7 +49,7 @@ export const ListBlockInitializer = ({
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
icon={<OrderedListOutlined />} icon={<OrderedListOutlined />}
componentType={'List'} componentType={`List`}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {
return createBlockSchema(options); return createBlockSchema(options);

View File

@ -19,8 +19,8 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { useCollection } from '../../../../data-source';
export const listBlockSettings = new SchemaSettings({ export const listBlockSettings = new SchemaSettings({
name: 'blockSettings:list', name: 'blockSettings:list',
@ -212,11 +212,12 @@ export const listBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'List', componentName: `${componentNamePrefix}List`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -44,7 +44,7 @@ export const TableBlockInitializer = ({
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
icon={<TableOutlined />} icon={<TableOutlined />}
componentType={'Table'} componentType={`Table`}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {
return createBlockSchema(options); return createBlockSchema(options);

View File

@ -23,8 +23,8 @@ import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSetti
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem'; import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem'; import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { useCollection } from '../../../../data-source';
export const tableBlockSettings = new SchemaSettings({ export const tableBlockSettings = new SchemaSettings({
name: 'blockSettings:table', name: 'blockSettings:table',
@ -212,10 +212,11 @@ export const tableBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Table', componentName: `${componentNamePrefix}Table`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -11,9 +11,9 @@ import { TableOutlined } from '@ant-design/icons';
import React from 'react'; import React from 'react';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { Collection, CollectionFieldOptions } from '../../../../data-source'; import { Collection, CollectionFieldOptions } from '../../../../data-source';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema';
export const FilterCollapseBlockInitializer = ({ export const FilterCollapseBlockInitializer = ({
filterCollections, filterCollections,
@ -32,7 +32,7 @@ export const FilterCollapseBlockInitializer = ({
{...itemConfig} {...itemConfig}
onlyCurrentDataSource={onlyCurrentDataSource} onlyCurrentDataSource={onlyCurrentDataSource}
icon={<TableOutlined />} icon={<TableOutlined />}
componentType={'FilterCollapse'} componentType={`FilterCollapse`}
onCreateBlockSchema={async ({ item }) => { onCreateBlockSchema={async ({ item }) => {
const schema = createCollapseBlockSchema({ const schema = createCollapseBlockSchema({
dataSource: item.dataSource, dataSource: item.dataSource,

View File

@ -12,10 +12,11 @@ import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../../collection-manager'; import { useCollection_deprecated } from '../../../../collection-manager';
import { FilterBlockType } from '../../../../filter-provider'; import { FilterBlockType } from '../../../../filter-provider';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
export const filterCollapseBlockSettings = new SchemaSettings({ export const filterCollapseBlockSettings = new SchemaSettings({
name: 'blockSettings:filterCollapse', name: 'blockSettings:filterCollapse',
@ -34,11 +35,12 @@ export const filterCollapseBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FilterCollapse', componentName: `${componentNamePrefix}FilterCollapse`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -10,9 +10,10 @@
import { FormOutlined } from '@ant-design/icons'; import { FormOutlined } from '@ant-design/icons';
import React from 'react'; import React from 'react';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { createFilterFormBlockSchema } from './createFilterFormBlockSchema';
import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer';
import { Collection, CollectionFieldOptions } from '../../../../data-source'; 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 = ({ export const FilterFormBlockInitializer = ({
filterCollections, filterCollections,
@ -25,13 +26,14 @@ export const FilterFormBlockInitializer = ({
}) => { }) => {
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer(); const { insert } = useSchemaInitializer();
const { componentNamePrefix } = useBlockTemplateContext();
return ( return (
<FilterBlockInitializer <FilterBlockInitializer
{...itemConfig} {...itemConfig}
icon={<FormOutlined />} icon={<FormOutlined />}
onlyCurrentDataSource={onlyCurrentDataSource} onlyCurrentDataSource={onlyCurrentDataSource}
componentType={'FilterFormItem'} componentType={`${componentNamePrefix}FilterFormItem`}
templateWrap={(templateSchema, { item }) => { templateWrap={(templateSchema, { item }) => {
const s = createFilterFormBlockSchema({ const s = createFilterFormBlockSchema({
templateSchema: templateSchema, templateSchema: templateSchema,

View File

@ -11,11 +11,13 @@ import { useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../../collection-manager'; import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
import { FilterBlockType } from '../../../../filter-provider'; import { FilterBlockType } from '../../../../filter-provider';
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings'; import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem'; import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
export const filterFormBlockSettings = new SchemaSettings({ export const filterFormBlockSettings = new SchemaSettings({
name: 'blockSettings:filterForm', name: 'blockSettings:filterForm',
@ -32,12 +34,13 @@ export const filterFormBlockSettings = new SchemaSettings({
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { componentNamePrefix } = useBlockTemplateContext();
const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FilterFormItem', componentName: `${componentNamePrefix}FilterFormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -16,8 +16,8 @@ import { ComposedActionDrawer } from './types';
export const ActionContainer: ComposedActionDrawer = observer( export const ActionContainer: ComposedActionDrawer = observer(
(props: any) => { (props: any) => {
const { openMode } = useActionContext(); const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext();
const { getComponentByOpenMode } = useOpenModeContext(); const { openMode = defaultOpenMode } = useActionContext();
const { currentLevel } = useCurrentPopupContext(); const { currentLevel } = useCurrentPopupContext();
const Component = getComponentByOpenMode(openMode); const Component = getComponentByOpenMode(openMode);

View File

@ -43,19 +43,19 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
const useBlockServiceInActionButton = () => { const useBlockServiceInActionButton = () => {
const { params } = useCurrentPopupContext(); const { params } = useCurrentPopupContext();
const fieldSchema = useFieldSchema();
const popupUidWithoutOpened = useFieldSchema()?.['x-uid']; const popupUidWithoutOpened = useFieldSchema()?.['x-uid'];
const service = useDataBlockRequest(); const service = useDataBlockRequest();
const currentPopupUid = params?.popupuid; 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(() => { useEffect(() => {
// This case refers to when the current button is rendered on a page or in a popup
if (popupUidWithoutOpened && currentPopupUid !== popupUidWithoutOpened) { if (popupUidWithoutOpened && currentPopupUid !== popupUidWithoutOpened) {
storeBlockService(popupUidWithoutOpened, { service }); 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) { if (currentPopupUid === popupUidWithoutOpened) {
return getBlockService(currentPopupUid)?.service || service; return getBlockService(currentPopupUid)?.service || service;
} }

View File

@ -1,8 +1,7 @@
import { ISchema, observer, useForm } from '@formily/react'; import { ISchema, observer, useForm } from '@formily/react';
import { import {
Action, Action,
CustomRouterContextProvider,
Form, Form,
FormItem, FormItem,
Input, Input,
@ -11,6 +10,7 @@ import {
useActionContext, useActionContext,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const useCloseAction = () => { const useCloseAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -67,8 +67,12 @@ const schema: ISchema = {
export default observer(() => { export default observer(() => {
return ( return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,9 +1,8 @@
import { ISchema, observer, useForm } from '@formily/react'; import { ISchema, observer, useForm } from '@formily/react';
import { import {
Action, Action,
ActionContextProvider, ActionContextProvider,
CustomRouterContextProvider,
Form, Form,
FormItem, FormItem,
Input, Input,
@ -12,6 +11,7 @@ import {
useActionContext, useActionContext,
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Router } from 'react-router-dom';
const useCloseAction = () => { const useCloseAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -59,11 +59,15 @@ const schema: ISchema = {
export default observer(() => { export default observer(() => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}> <Router location={window.location} navigator={null}>
<ActionContextProvider value={{ visible, setVisible }}> <CustomRouterContextProvider>
<a onClick={() => setVisible(true)}>Open</a> <SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
<SchemaComponent scope={{ useCloseAction }} schema={schema} /> <ActionContextProvider value={{ visible, setVisible }}>
</ActionContextProvider> <a onClick={() => setVisible(true)}>Open</a>
</SchemaComponentProvider> <SchemaComponent scope={{ useCloseAction }} schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,8 +1,7 @@
import { ISchema, observer, useForm } from '@formily/react'; import { ISchema, observer, useForm } from '@formily/react';
import { import {
Action, Action,
CustomRouterContextProvider,
Form, Form,
FormItem, FormItem,
Input, Input,
@ -11,6 +10,7 @@ import {
useActionContext, useActionContext,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const useCloseAction = () => { const useCloseAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -55,8 +55,12 @@ const schema: ISchema = {
export default observer(() => { export default observer(() => {
return ( return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

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

View File

@ -13,14 +13,8 @@ import React from 'react';
import { SchemaComponentOptions } from '../../'; import { SchemaComponentOptions } from '../../';
import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks'; import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { useAssociationFieldModeContext } from './AssociationFieldModeProvider';
import { AssociationFieldProvider } from './AssociationFieldProvider'; 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 { CreateRecordAction } from './components/CreateRecordAction';
import { useAssociationFieldContext } from './hooks'; import { useAssociationFieldContext } from './hooks';
@ -30,6 +24,7 @@ const EditableAssociationField = observer(
const field: Field = useField(); const field: Field = useField();
const form = useForm(); const form = useForm();
const { options: collectionField, currentMode } = useAssociationFieldContext(); const { options: collectionField, currentMode } = useAssociationFieldContext();
const { getComponent } = useAssociationFieldModeContext();
const useCreateActionProps = () => { const useCreateActionProps = () => {
const { onClick } = useCAP(); const { onClick } = useCAP();
@ -57,15 +52,11 @@ const EditableAssociationField = observer(
}; };
}; };
const Component = getComponent(currentMode);
return ( return (
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}> <SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
{currentMode === 'Picker' && <InternalPicker {...props} />} <Component {...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} />}
</SchemaComponentOptions> </SchemaComponentOptions>
); );
}, },

View File

@ -10,7 +10,7 @@
import { EditOutlined } from '@ant-design/icons'; import { EditOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react'; 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 { useTranslation } from 'react-i18next';
import { ActionContext, ActionContextProvider } from '../action/context'; import { ActionContext, ActionContextProvider } from '../action/context';
import { useGetAriaLabelOfPopover } from '../action/hooks/useGetAriaLabelOfPopover'; import { useGetAriaLabelOfPopover } from '../action/hooks/useGetAriaLabelOfPopover';
@ -22,6 +22,7 @@ import { useAssociationFieldContext } from './hooks';
const InternalPopoverNesterContentCss = css` const InternalPopoverNesterContentCss = css`
min-width: 600px; min-width: 600px;
max-width: 800px;
max-height: 440px; max-height: 440px;
overflow: auto; overflow: auto;
.ant-card { .ant-card {
@ -30,7 +31,16 @@ const InternalPopoverNesterContentCss = css`
`; `;
export const InternalPopoverNester = observer( 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 { options } = useAssociationFieldContext();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -42,11 +52,7 @@ export const InternalPopoverNester = observer(
shouldMountElement: true, shouldMountElement: true,
}; };
const content = ( const content = (
<div <div ref={ref} className={`${InternalPopoverNesterContentCss} popover-subform-container`}>
ref={ref}
style={{ minWidth: '600px', maxWidth: '800px', maxHeight: '440px', overflow: 'auto' }}
className={InternalPopoverNesterContentCss}
>
<InternalNester {...nesterProps} /> <InternalNester {...nesterProps} />
</div> </div>
); );
@ -56,6 +62,11 @@ export const InternalPopoverNester = observer(
getContainer: getContainer, getContainer: getContainer,
}; };
const { getAriaLabel } = useGetAriaLabelOfPopover(); const { getAriaLabel } = useGetAriaLabelOfPopover();
const Container = props.Container || StablePopover;
const handleOpenChange = useCallback((open: boolean) => {
setVisible(open);
}, []);
const overlayStyle = useMemo(() => ({ padding: '0px' }), []);
if (process.env.__E2E__) { if (process.env.__E2E__) {
useSetAriaLabelForPopover(visible); useSetAriaLabelForPopover(visible);
@ -63,13 +74,13 @@ export const InternalPopoverNester = observer(
return ( return (
<ActionContextProvider value={{ ...ctx, modalProps }}> <ActionContextProvider value={{ ...ctx, modalProps }}>
<StablePopover <Container
overlayStyle={{ padding: '0px' }} overlayStyle={overlayStyle}
content={content} content={content}
trigger="click" trigger="click"
placement="topLeft" placement="topLeft"
open={visible} open={visible}
onOpenChange={(open) => setVisible(open)} onOpenChange={handleOpenChange}
title={t(options?.uiSchema?.rawTitle)} title={t(options?.uiSchema?.rawTitle)}
> >
<span style={{ cursor: 'pointer', display: 'flex' }}> <span style={{ cursor: 'pointer', display: 'flex' }}>
@ -82,7 +93,7 @@ export const InternalPopoverNester = observer(
</div> </div>
<EditOutlined style={{ display: 'inline-flex', margin: '5px' }} /> <EditOutlined style={{ display: 'inline-flex', margin: '5px' }} />
</span> </span>
</StablePopover> </Container>
{visible && ( {visible && (
<div <div
role="button" role="button"

View File

@ -25,6 +25,7 @@ import { getPath } from '../../../variables/utils/getPath';
import { getVariableName } from '../../../variables/utils/getVariableName'; import { getVariableName } from '../../../variables/utils/getVariableName';
import { isVariable } from '../../../variables/utils/isVariable'; import { isVariable } from '../../../variables/utils/isVariable';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { AssociationFieldMode } from './AssociationFieldModeProvider';
import { AssociationFieldContext } from './context'; import { AssociationFieldContext } from './context';
export const useInsertSchema = (component) => { export const useInsertSchema = (component) => {
@ -51,7 +52,7 @@ export function useAssociationFieldContext<F extends GeneralField>() {
return useContext(AssociationFieldContext) as { return useContext(AssociationFieldContext) as {
options: any; options: any;
field: F; field: F;
currentMode: string; currentMode: AssociationFieldMode;
allowMultiple?: boolean; allowMultiple?: boolean;
allowDissociate?: boolean; allowDissociate?: boolean;
}; };

View File

@ -15,6 +15,7 @@ import { Nester } from './Nester';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { SubTable } from './SubTable'; import { SubTable } from './SubTable';
export { AssociationFieldModeProvider } from './AssociationFieldModeProvider';
export const AssociationField: any = connect(Editable, mapReadPretty(ReadPretty)); export const AssociationField: any = connect(Editable, mapReadPretty(ReadPretty));
AssociationField.SubTable = SubTable; AssociationField.SubTable = SubTable;

View File

@ -17,19 +17,25 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet
import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
export const AssociationFilterBlockDesigner = () => { export const AssociationFilterBlockDesigner = () => {
const { name, title } = useCollection_deprecated(); const { name, title } = useCollection_deprecated();
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsBlockTitleItem /> <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')} /> <SchemaSettingsConnectDataBlocks type={FilterBlockType.COLLAPSE} emptyDescription={t('No blocks to connect')} />
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove

View File

@ -99,6 +99,8 @@ const InternalAssociationSelect = connect(
mapReadPretty(ReadPretty), mapReadPretty(ReadPretty),
); );
InternalAssociationSelect.displayName = 'InternalAssociationSelect';
interface AssociationSelectInterface { interface AssociationSelectInterface {
(props: any): React.ReactElement; (props: any): React.ReactElement;
Designer: React.FC; Designer: React.FC;

View File

@ -30,6 +30,17 @@ export type FilterActionProps<T = {}> = ActionProps & {
form?: Form; form?: Form;
onSubmit?: (values: T) => void; onSubmit?: (values: T) => void;
onReset?: (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( export const FilterAction = withDynamicSchemaProps(
@ -42,7 +53,7 @@ export const FilterAction = withDynamicSchemaProps(
const form = useMemo<Form>(() => props.form || createForm(), []); const form = useMemo<Form>(() => props.form || createForm(), []);
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.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 => { const onOpenChange = useCallback((visible: boolean): void => {
setVisible(visible); setVisible(visible);
@ -50,7 +61,7 @@ export const FilterAction = withDynamicSchemaProps(
return ( return (
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}> <FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
<StablePopover <Container
destroyTooltipOnHide destroyTooltipOnHide
placement={'bottomLeft'} placement={'bottomLeft'}
open={visible} open={visible}
@ -112,8 +123,8 @@ export const FilterAction = withDynamicSchemaProps(
</form> </form>
} }
> >
<Action {...others} title={field.title} /> <Action onClick={() => setVisible(!visible)} {...others} title={field.title} />
</StablePopover> </Container>
</FilterActionContext.Provider> </FilterActionContext.Provider>
); );
}), }),

View File

@ -62,7 +62,7 @@ export const FilterItem = observer(
return ( return (
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素 // 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
<div style={style} className="nc-filter-item"> <div style={style} className="nc-filter-item">
<Space> <Space wrap>
<Cascader <Cascader
// @ts-ignore // @ts-ignore
role="button" role="button"
@ -90,14 +90,16 @@ export const FilterItem = observer(
onChange={onOperatorsChange} onChange={onOperatorsChange}
placeholder={t('Comparision')} placeholder={t('Comparision')}
/> />
{!operator?.noValue ? ( <Space>
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} /> {!operator?.noValue ? (
) : null} <DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
{!props.disabled && ( ) : null}
<a role="button" aria-label="icon-close"> {!props.disabled && (
<CloseCircleOutlined onClick={remove} style={removeStyle} /> <a role="button" aria-label="icon-close">
</a> <CloseCircleOutlined onClick={remove} style={removeStyle} />
)} </a>
)}
</Space>
</Space> </Space>
</div> </div>
); );

View File

@ -1,8 +1,14 @@
import { ISchema } from '@formily/json-schema'; 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 React from 'react';
import { Router } from 'react-router-dom';
const options = [ const options = [
{ {
@ -99,8 +105,12 @@ const schema: ISchema = {
export default () => { export default () => {
return ( return (
<SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider components={{ FilterAction, Filter, Input }} scope={{ options }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}; };

View File

@ -15,6 +15,7 @@ import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProv
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { useSortFields } from '../../../collection-manager/action-hooks'; 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 { setDataLoadingModeSettingsItem } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
import { import {
SchemaSettingsDataTemplates, SchemaSettingsDataTemplates,
@ -25,6 +26,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../schema-settings/SchemaSe
import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSettingsBlockTitleItem'; import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
@ -74,12 +76,13 @@ export const formSettings = new SchemaSettings({
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { componentNamePrefix } = useBlockTemplateContext();
const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: `${componentNamePrefix}FormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };
@ -127,13 +130,14 @@ export const readPrettyFormSettings = new SchemaSettings({
name: 'formItemTemplate', name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate, Component: SchemaSettingsFormItemTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { componentNamePrefix } = useBlockTemplateContext();
const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
insertAdjacentPosition: 'beforeEnd', insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem', componentName: `${componentNamePrefix}ReadPrettyFormItem`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };
@ -336,10 +340,11 @@ export const formDetailsSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Details', componentName: `${componentNamePrefix}Details`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -10,6 +10,7 @@
import { SchemaSettings } from '../../../application/schema-settings'; import { SchemaSettings } from '../../../application/schema-settings';
import { useCollection_deprecated } from '../../../collection-manager'; import { useCollection_deprecated } from '../../../collection-manager';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
/** /**
* @deprecated * @deprecated
@ -22,8 +23,9 @@ export const formV1Settings = new SchemaSettings({
Component: SchemaSettingsTemplate, Component: SchemaSettingsTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const { componentNamePrefix } = useBlockTemplateContext();
return { return {
componentName: 'Form', componentName: `${componentNamePrefix}Form`,
collectionName: name, collectionName: name,
}; };
}, },

View File

@ -19,6 +19,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettingsDivider, SchemaSettingsRemove } from '../../../schema-settings';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
type Opts = Options<any, any> & { uid?: string }; type Opts = Options<any, any> & { uid?: string };
@ -130,9 +131,10 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer(
Form.Designer = function Designer() { Form.Designer = function Designer() {
const { name, title } = useCollection_deprecated(); const { name, title } = useCollection_deprecated();
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const { componentNamePrefix } = useBlockTemplateContext();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsTemplate componentName={'Form'} collectionName={name} /> <SchemaSettingsTemplate componentName={`${componentNamePrefix}Form`} collectionName={name} />
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -1,9 +1,15 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -60,8 +66,12 @@ const Output = observer(
export default observer(() => { export default observer(() => {
return ( return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,9 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 { Card } from 'antd';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -59,8 +58,12 @@ export default observer(() => {
}; };
return ( return (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,9 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 { Card } from 'antd';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -58,8 +57,12 @@ export default observer(() => {
}; };
return ( return (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,9 +1,15 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -65,8 +71,12 @@ export default observer(() => {
); );
return ( return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,9 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; import { ISchema, observer, useForm } from '@formily/react';
import { import {
APIClientProvider, APIClientProvider,
Action, Action,
CustomRouterContextProvider,
Form, Form,
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
@ -12,6 +11,7 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import { Card, Space } from 'antd'; import { Card, Space } from 'antd';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
import { apiClient } from './apiClient'; import { apiClient } from './apiClient';
const schema: ISchema = { const schema: ISchema = {
@ -105,13 +105,17 @@ const useRefresh = () => {
export default observer(() => { export default observer(() => {
return ( return (
<APIClientProvider apiClient={apiClient}> <Router location={window.location} navigator={null}>
<SchemaComponentProvider <CustomRouterContextProvider>
scope={{ useSubmit, useRefresh }} <APIClientProvider apiClient={apiClient}>
components={{ Space, Card, Output, Action, Form, Input, FormItem }} <SchemaComponentProvider
> scope={{ useSubmit, useRefresh }}
<SchemaComponent schema={schema} /> components={{ Space, Card, Output, Action, Form, Input, FormItem }}
</SchemaComponentProvider> >
</APIClientProvider> <SchemaComponent schema={schema} />
</SchemaComponentProvider>
</APIClientProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,9 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 { Card } from 'antd';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -59,8 +58,12 @@ const useSubmit = () => {
export default observer(() => { export default observer(() => {
return ( return (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}> <Router location={window.location} navigator={null}>
<SchemaComponent schema={schema} /> <CustomRouterContextProvider>
</SchemaComponentProvider> <SchemaComponentProvider scope={{ useSubmit }} components={{ Card, Output, Action, Form, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,17 @@
import { FormItem, Input } from '@formily/antd-v5'; import { FormItem, Input } from '@formily/antd-v5';
import { ISchema, observer, useForm } from '@formily/react'; 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 { Card } from 'antd';
import React from 'react'; import React from 'react';
import { Router } from 'react-router-dom';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -63,11 +70,15 @@ const useValues: FormUseValues = (opts) => {
export default observer(() => { export default observer(() => {
return ( return (
<SchemaComponentProvider <Router location={window.location} navigator={null}>
scope={{ useSubmit, useValues }} <CustomRouterContextProvider>
components={{ Card, Output, Action, Form, Input, FormItem }} <SchemaComponentProvider
> scope={{ useSubmit, useValues }}
<SchemaComponent schema={schema} /> components={{ Card, Output, Action, Form, Input, FormItem }}
</SchemaComponentProvider> >
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -1,10 +1,9 @@
import { FormItem } from '@formily/antd-v5'; import { FormItem } from '@formily/antd-v5';
import { ISchema, observer } from '@formily/react'; import { ISchema, observer } from '@formily/react';
import { import {
Action, Action,
ActionContextProvider, ActionContextProvider,
CustomRouterContextProvider,
Form, Form,
Input, Input,
SchemaComponent, SchemaComponent,
@ -15,6 +14,7 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import { Button } from 'antd'; import { Button } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Router } from 'react-router-dom';
const useValues = (options) => { const useValues = (options) => {
const { visible } = useActionContext(); const { visible } = useActionContext();
@ -72,17 +72,21 @@ export default observer(() => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
<SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}> <Router location={window.location} navigator={null}>
<ActionContextProvider value={{ visible, setVisible }}> <CustomRouterContextProvider>
<Button <SchemaComponentProvider components={{ Action, Input, FormItem, Form }} scope={{ useCloseAction }}>
onClick={() => { <ActionContextProvider value={{ visible, setVisible }}>
setVisible(true); <Button
}} onClick={() => {
> setVisible(true);
Edit }}
</Button> >
<SchemaComponent schema={schema} /> Edit
</ActionContextProvider> </Button>
</SchemaComponentProvider> <SchemaComponent schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
</CustomRouterContextProvider>
</Router>
); );
}); });

View File

@ -17,7 +17,6 @@ import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../collection-manager'; import { useCollection_deprecated, useSortFields } from '../../../collection-manager';
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
import { SetTheCountOfColumnsDisplayedInARow } from '../../../modules/blocks/data-blocks/grid-card/SetTheCountOfColumnsDisplayedInARow'; import { SetTheCountOfColumnsDisplayedInARow } from '../../../modules/blocks/data-blocks/grid-card/SetTheCountOfColumnsDisplayedInARow';
import { useRecord } from '../../../record-provider';
import { import {
GeneralSchemaDesigner, GeneralSchemaDesigner,
SchemaSettingsDivider, SchemaSettingsDivider,
@ -28,10 +27,11 @@ import {
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { SchemaComponentOptions } from '../../core'; import { SchemaComponentOptions } from '../../core';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter'; 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) => { export const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
obj[cur] = cur; obj[cur] = cur;
@ -47,11 +47,10 @@ export const GridCardDesigner = () => {
const field = useField(); const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord(); const { componentNamePrefix } = useBlockTemplateContext();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
const columnCountSchema = useMemo(() => { const columnCountSchema = useMemo(() => {
return { return {
@ -217,7 +216,11 @@ export const GridCardDesigner = () => {
}); });
}} }}
/> />
<SchemaSettingsTemplate componentName={'GridCard'} collectionName={name} resourceName={defaultResource} /> <SchemaSettingsTemplate
componentName={`${componentNamePrefix}GridCard`}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../collection-manager'; import { useCollection_deprecated, useSortFields } from '../../../collection-manager';
import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; import { SetDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
import { useRecord } from '../../../record-provider';
import { import {
GeneralSchemaDesigner, GeneralSchemaDesigner,
SchemaSettingsDivider, SchemaSettingsDivider,
@ -27,6 +26,7 @@ import { SchemaSettingsBlockTitleItem } from '../../../schema-settings/SchemaSet
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
@ -42,7 +42,6 @@ export const ListDesigner = () => {
const field = useField(); const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
@ -57,6 +56,7 @@ export const ListDesigner = () => {
direction: 'asc', direction: 'asc',
}; };
}); });
const { componentNamePrefix } = useBlockTemplateContext();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsBlockTitleItem /> <SchemaSettingsBlockTitleItem />
@ -187,7 +187,11 @@ export const ListDesigner = () => {
}); });
}} }}
/> />
<SchemaSettingsTemplate componentName={'List'} collectionName={name} resourceName={defaultResource} /> <SchemaSettingsTemplate
componentName={`${componentNamePrefix}List`}
collectionName={name}
resourceName={defaultResource}
/>
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -11,15 +11,13 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button } from 'antd';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useToken } from '../../../style'; import { useToken } from '../../../style';
import { useCurrentPopupContext } from './PagePopups'; import { useActionContext } from '../action/hooks';
import { usePagePopup } from './pagePopupUtils';
export const useBackButton = () => { export const useBackButton = () => {
const { params } = useCurrentPopupContext(); const { setVisible } = useActionContext();
const { closePopup } = usePagePopup();
const goBack = useCallback(() => { const goBack = useCallback(() => {
closePopup(params?.popupuid); setVisible?.(false);
}, [closePopup, params?.popupuid]); }, [setVisible]);
return { return {
goBack, goBack,

View File

@ -13,4 +13,5 @@ export * from './FixedBlockDesignerItem';
export * from './Page'; export * from './Page';
export * from './Page.Settings'; export * from './Page.Settings';
export { PagePopups } from './PagePopups'; export { PagePopups } from './PagePopups';
export { storePopupContext } from './pagePopupUtils';
export * from './PageTab.Settings'; export * from './PageTab.Settings';

View File

@ -44,6 +44,10 @@ export interface PopupContextStorage extends PopupContext {
/** used to refresh data for block */ /** used to refresh data for block */
service?: any; service?: any;
sourceId?: string; sourceId?: string;
/**
* if true, will not back to the previous path when closing the popup
*/
notBackToPreviousPath?: boolean;
} }
const popupsContextStorage: Record<string, PopupContextStorage> = {}; 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 popupUid
* @param params * @param params
*/ */
@ -108,6 +115,10 @@ export const getPopupPathFromParams = (params: PopupParams) => {
return `/popups/${popupPath.map((item) => encodePathValue(item)).join('/')}`; return `/popups/${popupPath.map((item) => encodePathValue(item)).join('/')}`;
}; };
/**
* Note: use this hook in a plugin is not recommended
* @returns
*/
export const usePagePopup = () => { export const usePagePopup = () => {
const navigate = useNavigateNoUpdate(); const navigate = useNavigateNoUpdate();
const location = useLocationNoUpdate(); 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; // 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, // 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; // we need to construct the URL of the previous record to return to;
if (getStoredPopupContext(currentPopupUid)) { if (getStoredPopupContext(currentPopupUid) && !getStoredPopupContext(currentPopupUid).notBackToPreviousPath) {
navigate(-1); navigate(-1);
} else { } else {
navigate(withSearchParams(removeLastPopupPath(location.pathname))); navigate(withSearchParams(removeLastPopupPath(location.pathname)));
} }
}, },
[navigate, location, isPopupVisibleControlledByURL], [isPopupVisibleControlledByURL, setVisibleFromAction, navigate, location?.pathname],
); );
const changeTab = useCallback( const changeTab = useCallback(

View File

@ -9,6 +9,7 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import { Select } from 'antd'; import { Select } from 'antd';
import { differenceBy, unionBy } from 'lodash'; import { differenceBy, unionBy } from 'lodash';
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
@ -20,10 +21,9 @@ import { CollectionProvider_deprecated, useCollection_deprecated } from '../../.
import { FormProvider, SchemaComponentOptions } from '../../core'; import { FormProvider, SchemaComponentOptions } from '../../core';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { ActionContextProvider, useActionContext } from '../action'; import { ActionContextProvider, useActionContext } from '../action';
import { Upload } from '../upload';
import { useFieldNames } from './useFieldNames'; import { useFieldNames } from './useFieldNames';
import { getLabelFormatValue, useLabelUiSchema } from './util'; import { getLabelFormatValue, useLabelUiSchema } from './util';
import { Upload } from '../upload';
import { toArr } from '@formily/shared';
export const RecordPickerContext = createContext(null); export const RecordPickerContext = createContext(null);
RecordPickerContext.displayName = 'RecordPickerContext'; 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]; return Array.isArray(value) ? value?.map((v) => v[fieldNames.value]) : value?.[fieldNames.value];
}; };
const handleSelect = () => {
setVisible(true);
setSelectedRows([]);
};
// const handleRemove = (file) => { // const handleRemove = (file) => {
// const newOptions = options.filter((option) => option.id !== file.id); // const newOptions = options.filter((option) => option.id !== file.id);
// setOptions(newOptions); // setOptions(newOptions);

View File

@ -35,6 +35,7 @@ import { SchemaSettingsConnectDataBlocks } from '../../../schema-settings/Schema
import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter'; import { removeNullCondition } from '../filter';
@ -86,6 +87,7 @@ export const TableBlockDesigner = () => {
const { service } = useTableBlockContext(); const { service } = useTableBlockContext();
const { t } = useTranslation(); const { t } = useTranslation();
const { dn } = useDesignable(); const { dn } = useDesignable();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = const defaultResource =
@ -304,7 +306,11 @@ export const TableBlockDesigner = () => {
<SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} /> <SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
{supportTemplate && <SchemaSettingsDivider />} {supportTemplate && <SchemaSettingsDivider />}
{supportTemplate && ( {supportTemplate && (
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} resourceName={defaultResource} /> <SchemaSettingsTemplate
componentName={`${componentNamePrefix}Table`}
collectionName={name}
resourceName={defaultResource}
/>
)} )}
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove

View File

@ -23,6 +23,7 @@ import {
} from '../../../schema-settings'; } from '../../../schema-settings';
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate'; import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
export const TableVoidDesigner = () => { export const TableVoidDesigner = () => {
@ -48,6 +49,7 @@ export const TableVoidDesigner = () => {
}; };
}); });
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const { componentNamePrefix } = useBlockTemplateContext();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettingsSwitchItem <SchemaSettingsSwitchItem
@ -213,7 +215,7 @@ export const TableVoidDesigner = () => {
}} }}
/> />
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsTemplate componentName={'Table'} collectionName={name} /> <SchemaSettingsTemplate componentName={`${componentNamePrefix}Table`} collectionName={name} />
<SchemaSettingsDivider /> <SchemaSettingsDivider />
<SchemaSettingsRemove <SchemaSettingsRemove
removeParentsIfNoChildren removeParentsIfNoChildren

View File

@ -10,6 +10,7 @@
import { Schema } from '@formily/react'; import { Schema } from '@formily/react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { import {
useActionAvailable,
useCollection, useCollection,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useCollection_deprecated, useCollection_deprecated,
@ -22,7 +23,6 @@ import {
useCreateEditFormBlock, useCreateEditFormBlock,
useCreateFormBlock, useCreateFormBlock,
useCreateTableBlock, useCreateTableBlock,
useActionAvailable,
} from '../..'; } from '../..';
import { CompatibleSchemaInitializer } from '../../application/schema-initializer/CompatibleSchemaInitializer'; import { CompatibleSchemaInitializer } from '../../application/schema-initializer/CompatibleSchemaInitializer';
import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer'; import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer';
@ -101,7 +101,7 @@ function useRecordBlocks() {
}, },
onlyCurrentDataSource: true, onlyCurrentDataSource: true,
hideSearch: true, hideSearch: true,
componentType: 'ReadPrettyFormItem', componentType: `ReadPrettyFormItem`,
createBlockSchema, createBlockSchema,
templateWrap: useCallback( templateWrap: useCallback(
(templateSchema, { item }) => { (templateSchema, { item }) => {
@ -140,7 +140,7 @@ function useRecordBlocks() {
onlyCurrentDataSource: true, onlyCurrentDataSource: true,
hideSearch: true, hideSearch: true,
hideOtherRecordsInPopup: true, hideOtherRecordsInPopup: true,
componentType: 'FormItem', componentType: `FormItem`,
createBlockSchema: createEditFormBlock, createBlockSchema: createEditFormBlock,
templateWrap: templateWrapEdit, templateWrap: templateWrapEdit,
showAssociationFields: true, showAssociationFields: true,
@ -166,7 +166,7 @@ function useRecordBlocks() {
}, },
onlyCurrentDataSource: true, onlyCurrentDataSource: true,
hideSearch: true, hideSearch: true,
componentType: 'FormItem', componentType: `FormItem`,
createBlockSchema: ({ item, fromOthersInPopup }) => { createBlockSchema: ({ item, fromOthersInPopup }) => {
if (fromOthersInPopup) { if (fromOthersInPopup) {
return createFormBlock({ item, fromOthersInPopup }); return createFormBlock({ item, fromOthersInPopup });

View File

@ -81,6 +81,7 @@ const TabPaneInitializers = (props?: any) => {
'x-component': 'Action.Modal', 'x-component': 'Action.Modal',
'x-component-props': { 'x-component-props': {
width: 520, width: 520,
zIndex: 2000,
}, },
type: 'void', type: 'void',
title: '{{t("Add tab")}}', title: '{{t("Add tab")}}',

View File

@ -271,7 +271,7 @@ function FinallyButton({
}), }),
React.cloneElement(rightButton as React.ReactElement<any, string>, { React.cloneElement(rightButton as React.ReactElement<any, string>, {
loading: false, loading: false,
style: props?.style, style: { ...props?.style, justifyContent: 'center' },
}), }),
]} ]}
menu={menu} menu={menu}

View File

@ -30,6 +30,7 @@ import { useDataSourceManager } from '../data-source/data-source/DataSourceManag
import { isAssocField } from '../filter-provider/utils'; import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useCompile, useDesignable } from '../schema-component'; import { useActionContext, useCompile, useDesignable } from '../schema-component';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
export const itemsMerge = (items1) => { export const itemsMerge = (items1) => {
return items1; return items1;
@ -879,11 +880,17 @@ export const useCollectionDataSourceItems = ({
currentText?: string; currentText?: string;
otherText?: string; otherText?: string;
}) => { }) => {
const { componentNamePrefix } = useBlockTemplateContext();
const { t } = useTranslation(); const { t } = useTranslation();
const dm = useDataSourceManager(); const dm = useDataSourceManager();
const dataSourceKey = useDataSourceKey(); const dataSourceKey = useDataSourceKey();
const collection = useCollection(); const collection = useCollection();
const associationFields = useAssociationFields({ componentName, filterCollections: filter, showAssociationFields }); const associationFields = useAssociationFields({
componentName: componentNamePrefix + componentName,
filterCollections: filter,
showAssociationFields,
componentNamePrefix,
});
const association = useAssociationName(); const association = useAssociationName();
let allCollections = dm.getAllCollections({ let allCollections = dm.getAllCollections({
@ -911,11 +918,12 @@ export const useCollectionDataSourceItems = ({
name, name,
association, association,
collections, collections,
componentName, componentName: componentNamePrefix + componentName,
searchValue: '', searchValue: '',
dataSource: key, dataSource: key,
getTemplatesByCollection, getTemplatesByCollection,
t, t,
componentNamePrefix,
}).sort((item) => { }).sort((item) => {
// fix https://nocobase.height.app/T-3551 // fix https://nocobase.height.app/T-3551
const inherits = _.toArray(collection?.inherits || []); const inherits = _.toArray(collection?.inherits || []);
@ -1401,6 +1409,7 @@ const getChildren = ({
searchValue, searchValue,
getTemplatesByCollection, getTemplatesByCollection,
t, t,
componentNamePrefix,
}: { }: {
name: string; name: string;
association: string; association: string;
@ -1409,7 +1418,8 @@ const getChildren = ({
searchValue: string; searchValue: string;
dataSource: string; dataSource: string;
getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any; getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any;
t; t: any;
componentNamePrefix: string;
}) => { }) => {
return collections return collections
?.filter((item) => { ?.filter((item) => {
@ -1419,11 +1429,16 @@ const getChildren = ({
if (!item.filterTargetKey) { if (!item.filterTargetKey) {
return false; return false;
} else if ( } else if (
['Kanban', 'FormItem'].includes(componentName) && [componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem'].includes(componentName) &&
((item.template === 'view' && !item.writableView) || item.template === 'sql') ((item.template === 'view' && !item.writableView) || item.template === 'sql')
) { ) {
return false; return false;
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { } else if (
item.template === 'file' &&
[componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem', componentNamePrefix + 'Calendar'].includes(
componentName,
)
) {
return false; return false;
} else { } else {
const title = item.title || item.tableName; const title = item.title || item.tableName;
@ -1483,7 +1498,10 @@ const getChildren = ({
dataSource, dataSource,
title: t('Duplicate template'), title: t('Duplicate template'),
children: templates.map((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} ${t('(Fields only)')}`
: template?.name; : template?.name;
return { return {
@ -1503,7 +1521,10 @@ const getChildren = ({
dataSource, dataSource,
title: t('Reference template'), title: t('Reference template'),
children: templates.map((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} ${t('(Fields only)')}`
: template?.name; : template?.name;
return { return {
@ -1525,9 +1546,11 @@ function useAssociationFields({
componentName, componentName,
filterCollections, filterCollections,
showAssociationFields, showAssociationFields,
componentNamePrefix,
}: { }: {
componentName: string; componentName: string;
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean; filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
componentNamePrefix: string;
showAssociationFields?: boolean; showAssociationFields?: boolean;
}) { }) {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -1566,11 +1589,11 @@ function useAssociationFields({
} }
// 针对弹窗中的详情区块 // 针对弹窗中的详情区块
if (componentName === 'ReadPrettyFormItem') { if (componentName === componentNamePrefix + 'ReadPrettyFormItem') {
if (['hasOne', 'belongsTo'].includes(field.type)) { if (['hasOne', 'belongsTo'].includes(field.type)) {
return template.componentName === 'ReadPrettyFormItem'; return template.componentName === componentNamePrefix + 'ReadPrettyFormItem';
} else { } else {
return template.componentName === 'Details'; return template.componentName === componentNamePrefix + 'Details';
} }
} }
@ -1611,7 +1634,10 @@ function useAssociationFields({
dataSource, dataSource,
title: t('Duplicate template'), title: t('Duplicate template'),
children: templates.map((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} ${t('(Fields only)')}`
: template?.name; : template?.name;
return { return {
@ -1633,7 +1659,10 @@ function useAssociationFields({
dataSource, dataSource,
title: t('Reference template'), title: t('Reference template'),
children: templates.map((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} ${t('(Fields only)')}`
: template?.name; : template?.name;
return { return {
@ -1662,5 +1691,6 @@ function useAssociationFields({
getTemplatesByCollection, getTemplatesByCollection,
showAssociationFields, showAssociationFields,
t, t,
componentNamePrefix,
]); ]);
} }

View File

@ -95,7 +95,7 @@ import { SchemaComponentOptions } from '../schema-component/core/SchemaComponent
import { useCompile } from '../schema-component/hooks/useCompile'; import { useCompile } from '../schema-component/hooks/useCompile';
import { Designable, createDesignable, useDesignable } from '../schema-component/hooks/useDesignable'; import { Designable, createDesignable, useDesignable } from '../schema-component/hooks/useDesignable';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
import { useLocalVariables, useVariables } from '../variables'; import { useLocalVariables, useVariables } from '../variables';
import { FormDataTemplates } from './DataTemplates'; import { FormDataTemplates } from './DataTemplates';
import { EnableChildCollections } from './EnableChildCollections'; import { EnableChildCollections } from './EnableChildCollections';

View File

@ -20,7 +20,7 @@ import { SchemaComponent } from '../schema-component/core/SchemaComponent';
import { useCompile } from '../schema-component/hooks/useCompile'; import { useCompile } from '../schema-component/hooks/useCompile';
import { createDesignable } from '../schema-component/hooks/useDesignable'; import { createDesignable } from '../schema-component/hooks/useDesignable';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
import { SchemaSettingsItem, useSchemaSettings } from './SchemaSettings'; import { SchemaSettingsItem, useSchemaSettings } from './SchemaSettings';
export function SchemaSettingsTemplate(props) { export function SchemaSettingsTemplate(props) {

View File

@ -8,17 +8,10 @@
*/ */
import { observer, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useMemo } from 'react'; import React, { useMemo } from 'react';
import { CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..'; import { BlockTemplateProvider, CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider'; import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
const BlockTemplateContext = createContext<any>({});
BlockTemplateContext.displayName = 'BlockTemplateContext';
export const useBlockTemplateContext = () => {
return useContext(BlockTemplateContext);
};
export const BlockTemplate = observer( export const BlockTemplate = observer(
(props: any) => { (props: any) => {
@ -36,9 +29,9 @@ export const BlockTemplate = observer(
onTemplateSuccess?.(); onTemplateSuccess?.();
}; };
return template ? ( return template ? (
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}> <BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
<RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} /> <RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
</BlockTemplateContext.Provider> </BlockTemplateProvider>
) : ( ) : (
<CollectionDeletedPlaceholder type="Block template" name={templateId} /> <CollectionDeletedPlaceholder type="Block template" name={templateId} />
); );

View File

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

View File

@ -9,4 +9,5 @@
export * from './BlockTemplateDetails'; export * from './BlockTemplateDetails';
export * from './BlockTemplatePage'; export * from './BlockTemplatePage';
export * from './BlockTemplateProvider';
export * from './SchemaTemplateManagerProvider'; export * from './SchemaTemplateManagerProvider';

View File

@ -24,9 +24,9 @@ export * from './number';
export * from './parse-filter'; export * from './parse-filter';
export * from './registry'; export * from './registry';
// export * from './toposort'; // export * from './toposort';
export * from './i18n';
export * from './isPortalInBody'; export * from './isPortalInBody';
export * from './parseHTML';
export * from './uid'; export * from './uid';
export * from './url'; export * from './url';
export { dayjs, lodash }; export { dayjs, lodash };
export * from './parseHTML';
export * from './i18n';

View File

@ -70,7 +70,7 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => {
}); });
test('i18n should not fallbackNS', async ({ page }) => { test('i18n should not fallbackNS', async ({ page }) => {
await page.goto('/admin/settings/system-settings'); await page.goto('/');
// 创建 Users 页面 // 创建 Users 页面
await page.getByTestId('schema-initializer-Menu-header').hover(); 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').click();
await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').fill('Users'); await page.getByLabel('block-item-Input-Menu item').getByRole('textbox').fill('Users');
await page.getByRole('button', { name: 'OK' }).click(); 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 expect(page.getByLabel('用户')).not.toBeVisible();
// 添加中文选项 // 添加中文选项
await page.reload(); await page.goto('/admin/settings/system-settings');
await page.getByTestId('select-multiple').click(); await page.getByTestId('select-multiple').click();
await page.getByRole('option', { name: '简体中文 (zh-CN)' }).click(); await page.getByRole('option', { name: '简体中文 (zh-CN)' }).click();
await page.getByLabel('action-Action-Submit').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.getByText('LanguageEnglish').click();
await page.getByRole('option', { name: '简体中文' }).click(); await page.getByRole('option', { name: '简体中文' }).click();
// await page.reload();
// 应该显示 Users 而非中文 “用户” // 应该显示 Users 而非中文 “用户”
await expect(page.getByLabel('Users')).toBeVisible(); await expect(page.getByLabel('Users').first()).toBeVisible();
await expect(page.getByLabel('用户')).not.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('简体中文 (zh-CN)').getByLabel('icon-close-tag').click();
await page.getByLabel('action-Action-提交').click(); await page.getByLabel('action-Action-提交').click();
// 删除 Users 页面 // 删除 Users 页面
await page.getByLabel('Users').hover(); await page.getByLabel('Users').first().hover();
await page.getByLabel('designer-schema-settings-Menu').hover(); await page.getByLabel('designer-schema-settings-Menu').first().hover();
await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();
}); });

View File

@ -7,10 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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'; import React from 'react';
export const BulkEditActionInitializer = () => { export const BulkEditActionInitializer = () => {
const { defaultOpenMode } = useOpenModeContext();
const schema = { const schema = {
type: 'void', type: 'void',
title: '{{t("Bulk edit")}}', title: '{{t("Bulk edit")}}',
@ -20,7 +22,7 @@ export const BulkEditActionInitializer = () => {
updateMode: 'selected', updateMode: 'selected',
}, },
'x-component-props': { 'x-component-props': {
openMode: 'drawer', openMode: defaultOpenMode,
icon: 'EditOutlined', icon: 'EditOutlined',
}, },
properties: { properties: {

View File

@ -7,10 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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'; import React from 'react';
export const DuplicateActionInitializer = (props) => { export const DuplicateActionInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext();
const schema = { const schema = {
type: 'void', type: 'void',
'x-action': 'duplicate', 'x-action': 'duplicate',
@ -19,7 +21,7 @@ export const DuplicateActionInitializer = (props) => {
'x-component': 'Action.Link', 'x-component': 'Action.Link',
'x-decorator': 'ACLActionProvider', 'x-decorator': 'ACLActionProvider',
'x-component-props': { 'x-component-props': {
openMode: 'drawer', openMode: defaultOpenMode,
component: 'DuplicateAction', component: 'DuplicateAction',
}, },
properties: { properties: {

View File

@ -72,7 +72,7 @@ describe('createCalendarBlockSchema', () => {
}, },
"title": "{{t('View record', { ns: 'calendar' })}}", "title": "{{t('View record', { ns: 'calendar' })}}",
"type": "void", "type": "void",
"x-component": "Action.Drawer", "x-component": "Action.Container",
"x-component-props": { "x-component-props": {
"className": "nb-action-popup", "className": "nb-action-popup",
}, },

View File

@ -18,6 +18,7 @@ import {
SchemaSettingsSwitchItem, SchemaSettingsSwitchItem,
SchemaSettingsTemplate, SchemaSettingsTemplate,
removeNullCondition, removeNullCondition,
useBlockTemplateContext,
useCollection, useCollection,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useDesignable, useDesignable,
@ -212,10 +213,11 @@ export const calendarBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection(); const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Calendar', componentName: `${componentNamePrefix}Calendar`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -62,7 +62,7 @@ export const createCalendarBlockUISchema = (options: {
properties: { properties: {
drawer: { drawer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer', 'x-component': 'Action.Container',
'x-component-props': { 'x-component-props': {
className: 'nb-action-popup', className: 'nb-action-popup',
}, },

View File

@ -45,7 +45,7 @@ export const CalendarBlockInitializer = ({
return ( return (
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
componentType={'Calendar'} componentType={`Calendar`}
icon={<FormOutlined />} icon={<FormOutlined />}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {

View File

@ -16,6 +16,7 @@ import {
SchemaSettingsTemplate, SchemaSettingsTemplate,
removeNullCondition, removeNullCondition,
setDataLoadingModeSettingsItem, setDataLoadingModeSettingsItem,
useBlockTemplateContext,
useCollection, useCollection,
useCollection_deprecated, useCollection_deprecated,
useCompile, useCompile,
@ -245,8 +246,9 @@ export const oldGanttSettings = new SchemaSettings({
Component: SchemaSettingsTemplate, Component: SchemaSettingsTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const { componentNamePrefix } = useBlockTemplateContext();
return { return {
componentName: 'Gantt', componentName: `${componentNamePrefix}Gantt`,
collectionName: name, collectionName: name,
}; };
}, },
@ -485,8 +487,9 @@ export const ganttSettings = new SchemaSettings({
Component: SchemaSettingsTemplate, Component: SchemaSettingsTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection(); const { name } = useCollection();
const { componentNamePrefix } = useBlockTemplateContext();
return { return {
componentName: 'Gantt', componentName: `${componentNamePrefix}Gantt`,
collectionName: name, collectionName: name,
}; };
}, },

View File

@ -14,16 +14,16 @@ import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
useSchemaInitializer,
useSchemaInitializerItem,
useCollectionManager_deprecated,
useGlobalTheme,
FormDialog,
SchemaComponent,
DataBlockInitializer,
SchemaComponentOptions,
Collection, Collection,
CollectionFieldOptions, CollectionFieldOptions,
DataBlockInitializer,
FormDialog,
SchemaComponent,
SchemaComponentOptions,
useCollectionManager_deprecated,
useGlobalTheme,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client'; } from '@nocobase/client';
import { createGanttBlockUISchema } from './createGanttBlockUISchema'; import { createGanttBlockUISchema } from './createGanttBlockUISchema';
@ -46,7 +46,7 @@ export const GanttBlockInitializer = ({
return ( return (
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
componentType={'Calendar'} componentType={`Calendar`}
icon={<FormOutlined />} icon={<FormOutlined />}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {

View File

@ -9,15 +9,17 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { import {
useFormBlockContext, removeNullCondition,
SchemaSettings,
SchemaSettingsBlockHeightItem,
SchemaSettingsBlockTitleItem,
SchemaSettingsDataScope, SchemaSettingsDataScope,
SchemaSettingsTemplate,
useBlockTemplateContext,
useCollection,
useCollection_deprecated, useCollection_deprecated,
useDesignable, useDesignable,
SchemaSettings, useFormBlockContext,
SchemaSettingsBlockTitleItem,
removeNullCondition,
SchemaSettingsTemplate,
SchemaSettingsBlockHeightItem,
} from '@nocobase/client'; } from '@nocobase/client';
import { useKanbanBlockContext } from './KanbanBlockProvider'; import { useKanbanBlockContext } from './KanbanBlockProvider';
export const kanbanSettings = new SchemaSettings({ export const kanbanSettings = new SchemaSettings({
@ -66,9 +68,10 @@ export const kanbanSettings = new SchemaSettings({
name: 'template', name: 'template',
Component: SchemaSettingsTemplate, Component: SchemaSettingsTemplate,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection();
const { componentNamePrefix } = useBlockTemplateContext();
return { return {
componentName: 'Kanban', componentName: `${componentNamePrefix}Kanban`,
collectionName: name, collectionName: name,
}; };
}, },

View File

@ -15,20 +15,20 @@ import { useTranslation } from 'react-i18next';
import { import {
APIClientProvider, APIClientProvider,
useCollectionManager_deprecated, Collection,
useGlobalTheme, CollectionFieldOptions,
DataBlockInitializer,
FormDialog, FormDialog,
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
DataBlockInitializer, useAPIClient,
useCollectionManager_deprecated,
useGlobalTheme,
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
useAPIClient,
Collection,
CollectionFieldOptions,
} from '@nocobase/client'; } from '@nocobase/client';
import { createKanbanBlockUISchema } from './createKanbanBlockUISchema';
import { CreateAndSelectSort } from './CreateAndSelectSort'; import { CreateAndSelectSort } from './CreateAndSelectSort';
import { createKanbanBlockUISchema } from './createKanbanBlockUISchema';
import { NAMESPACE } from './locale'; import { NAMESPACE } from './locale';
const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options, api }) => { const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options, api }) => {
@ -130,7 +130,7 @@ export const KanbanBlockInitializer = ({
return ( return (
<DataBlockInitializer <DataBlockInitializer
{...itemConfig} {...itemConfig}
componentType={'Calendar'} componentType={`Calendar`}
icon={<FormOutlined />} icon={<FormOutlined />}
onCreateBlockSchema={async (options) => { onCreateBlockSchema={async (options) => {
if (createBlockSchema) { if (createBlockSchema) {

View File

@ -65,7 +65,7 @@ test('createMapBlockSchema should return an object with expected properties', ()
}, },
"title": "{{ t("View record") }}", "title": "{{ t("View record") }}",
"type": "void", "type": "void",
"x-component": "Action.Drawer", "x-component": "Action.Container",
"x-component-props": { "x-component-props": {
"className": "nb-action-popup", "className": "nb-action-popup",
}, },

View File

@ -11,6 +11,7 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import { import {
FilterBlockType, FilterBlockType,
SchemaSettings, SchemaSettings,
SchemaSettingsBlockHeightItem,
SchemaSettingsBlockTitleItem, SchemaSettingsBlockTitleItem,
SchemaSettingsCascaderItem, SchemaSettingsCascaderItem,
SchemaSettingsConnectDataBlocks, SchemaSettingsConnectDataBlocks,
@ -20,11 +21,11 @@ import {
SchemaSettingsSelectItem, SchemaSettingsSelectItem,
SchemaSettingsTemplate, SchemaSettingsTemplate,
setDataLoadingModeSettingsItem, setDataLoadingModeSettingsItem,
useBlockTemplateContext,
useCollection, useCollection,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useDesignable, useDesignable,
useFormBlockContext, useFormBlockContext,
SchemaSettingsBlockHeightItem,
} from '@nocobase/client'; } from '@nocobase/client';
import _ from 'lodash'; import _ from 'lodash';
import { useMapTranslation } from '../locale'; import { useMapTranslation } from '../locale';
@ -231,10 +232,11 @@ export const mapBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection(); const { name } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource = const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association; fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Map', componentName: `${componentNamePrefix}Map`,
collectionName: name, collectionName: name,
resourceName: defaultResource, resourceName: defaultResource,
}; };

View File

@ -22,8 +22,8 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useMapTranslation } from '../locale'; import { useMapTranslation } from '../locale';
import { findNestedOption } from './utils';
import { createMapBlockUISchema } from './createMapBlockUISchema'; import { createMapBlockUISchema } from './createMapBlockUISchema';
import { findNestedOption } from './utils';
export const MapBlockInitializer = () => { export const MapBlockInitializer = () => {
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();
@ -32,9 +32,10 @@ export const MapBlockInitializer = () => {
const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
const { t } = useMapTranslation(); const { t } = useMapTranslation();
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
return ( return (
<DataBlockInitializer <DataBlockInitializer
componentType={'Map'} componentType={`Map`}
icon={<TableOutlined />} icon={<TableOutlined />}
onCreateBlockSchema={async ({ item }) => { onCreateBlockSchema={async ({ item }) => {
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], { const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], {

View File

@ -9,7 +9,6 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { theme } from 'antd';
export const createMapBlockUISchema = (options: { export const createMapBlockUISchema = (options: {
collectionName: string; collectionName: string;
@ -49,7 +48,7 @@ export const createMapBlockUISchema = (options: {
properties: { properties: {
drawer: { drawer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer', 'x-component': 'Action.Container',
'x-component-props': { 'x-component-props': {
className: 'nb-action-popup', className: 'nb-action-popup',
}, },

View File

@ -326,7 +326,6 @@ export const AMapBlock = (props) => {
const MapBlockDrawer = (props) => { const MapBlockDrawer = (props) => {
const { setVisible, record } = props; const { setVisible, record } = props;
const { t } = useMapTranslation();
const collection = useCollection(); const collection = useCollection();
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

@ -375,7 +375,6 @@ export const GoogleMapsBlock = (props) => {
const MapBlockDrawer = (props) => { const MapBlockDrawer = (props) => {
const { setVisible, record } = props; const { setVisible, record } = props;
const { t } = useMapTranslation();
const collection = useCollection(); const collection = useCollection();
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,5 +23,19 @@ export const useMobileActionPageStyle = createStyles(({ css, token }: any) => {
margin: 20px; 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;
`,
}; };
}); });

View File

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

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { import {
css, css,
DndContext, DndContext,
@ -23,22 +23,35 @@ import {
import { Tabs } from 'antd-mobile'; import { Tabs } from 'antd-mobile';
import { LeftOutline } from 'antd-mobile-icons'; import { LeftOutline } from 'antd-mobile-icons';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MobilePageHeader } from '../dynamic-page'; import { MobilePageHeader } from '../../pages/dynamic-page';
import { MobilePageContentContainer } from '../dynamic-page/content/MobilePageContentContainer'; import { MobilePageContentContainer } from '../../pages/dynamic-page/content/MobilePageContentContainer';
import { useStyles } from '../dynamic-page/header/tabs'; import { useStyles } from '../../pages/dynamic-page/header/tabs';
import { useMobileTabsForMobileActionPageStyle } from './MobileTabsForMobileActionPage.style'; import { useMobileTabsForMobileActionPageStyle } from './MobileTabsForMobileActionPage.style';
export const MobileTabsForMobileActionPage: any = observer( export const MobileTabsForMobileActionPage: any = observer(
(props) => { (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); 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 } = useStyles();
const { styles: mobileTabsForMobileActionPageStyle } = useMobileTabsForMobileActionPageStyle(); const { styles: mobileTabsForMobileActionPageStyle } = useMobileTabsForMobileActionPageStyle();
const { goBack } = useBackButton(); const { goBack } = useBackButton();
const keyToTabRef = useRef({}); const keyToTabRef = useRef({});
const onChange = useCallback(
(key) => {
setActiveKey(key);
_onChange?.(key);
},
[_onChange],
);
useEffect(() => {
setActiveKey(_activeKey);
}, [_activeKey]);
const items = useMemo(() => { const items = useMemo(() => {
const result = fieldSchema.mapProperties((schema, key) => { const result = fieldSchema.mapProperties((schema, key) => {
keyToTabRef.current[key] = <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />; keyToTabRef.current[key] = <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />;
@ -50,6 +63,7 @@ export const MobileTabsForMobileActionPage: any = observer(
const tabContent = useMemo(() => { const tabContent = useMemo(() => {
const list = fieldSchema.mapProperties((schema, key) => { const list = fieldSchema.mapProperties((schema, key) => {
schema = hideDivider(schema);
return { return {
key, key,
node: <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />, node: <SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />,
@ -148,3 +162,17 @@ MobileTabsForMobileActionPage.TabPane = observer(
); );
MobileTabsForMobileActionPage.Designer = TabsOfPC.Designer; 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;
}

View File

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

View File

@ -9,11 +9,11 @@
import { PagePopups, Plugin, RouterManager, createRouterManager } from '@nocobase/client'; import { PagePopups, Plugin, RouterManager, createRouterManager } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { Outlet } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { name } from '../../package.json'; import { name } from '../../package.json';
import { Outlet } from 'react-router-dom';
import { generatePluginTranslationTemplate } from './locale'; import { generatePluginTranslationTemplate } from './locale';
import { Mobile } from './mobile'; import { Mobile } from './mobile';
import { 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', { this.mobileRouter.add('mobile.schema', {
element: <Outlet />, element: <Outlet />,
}); });

View File

@ -22,6 +22,7 @@ export const MobileProviders: FC<MobileProvidersProps> = ({ children, skipLogin
useEffect(() => { useEffect(() => {
document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px'); document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px');
document.body.style.setProperty('--nb-mobile-page-header-height', '50px');
}, []); }, []);
return ( return (

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { 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'; import type { IResource } from '@nocobase/sdk';
@ -27,6 +27,7 @@ export interface MobileRouteItem {
children?: MobileRouteItem[]; children?: MobileRouteItem[];
} }
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(null);
export interface MobileRoutesContextValue { export interface MobileRoutesContextValue {
routeList?: MobileRouteItem[]; routeList?: MobileRouteItem[];
@ -37,8 +38,6 @@ export interface MobileRoutesContextValue {
activeTabItem?: MobileRouteItem; activeTabItem?: MobileRouteItem;
api: APIClient; api: APIClient;
} }
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(null);
MobileRoutesContext.displayName = 'MobileRoutesContext'; MobileRoutesContext.displayName = 'MobileRoutesContext';
export const useMobileRoutes = () => { export const useMobileRoutes = () => {
@ -97,7 +96,9 @@ export const MobileRoutesProvider = ({ children }) => {
data, data,
runAsync: refresh, runAsync: refresh,
loading, 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 routeList = useMemo(() => data?.data || [], [data]);
const { activeTabBarItem, activeTabItem } = useActiveTabBar(routeList); const { activeTabBarItem, activeTabItem } = useActiveTabBar(routeList);

View File

@ -7,21 +7,38 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 React from 'react';
import { isDesktop } from 'react-device-detect'; 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 { PageBackgroundColor } from '../constants';
import { DesktopMode } from '../desktop-mode/DesktopMode'; import { DesktopMode } from '../desktop-mode/DesktopMode';
import { PluginMobileClient } from '../index'; import { PluginMobileClient } from '../index';
import { MobileActionPage } from '../pages/mobile-action-page/MobileActionPage';
import { MobileAppProvider } from './MobileAppContext'; import { MobileAppProvider } from './MobileAppContext';
import { useStyles } from './styles'; import { useStyles } from './styles';
export const Mobile = () => { export const Mobile = () => {
useToAdaptFilterActionToMobile();
useToAdaptActionDrawerToMobile();
const { styles } = useStyles();
const mobilePlugin = usePlugin(PluginMobileClient); const mobilePlugin = usePlugin(PluginMobileClient);
const MobileRouter = mobilePlugin.getRouterComponent(); const MobileRouter = mobilePlugin.getRouterComponent();
const { styles } = useStyles();
// 设置的移动端 meta // 设置的移动端 meta
React.useEffect(() => { React.useEffect(() => {
if (!isDesktop) { if (!isDesktop) {
@ -44,36 +61,51 @@ export const Mobile = () => {
}, []); }, []);
const DesktopComponent = mobilePlugin.desktopMode === false ? React.Fragment : DesktopMode; const DesktopComponent = mobilePlugin.desktopMode === false ? React.Fragment : DesktopMode;
const modeToComponent = React.useMemo(() => {
return {
PopoverNester: _.memoize((OriginComponent) => (props) => (
<InternalPopoverNesterUsedInMobile {...props} OriginComponent={OriginComponent} />
)),
};
}, []);
return ( return (
<DesktopComponent> <DesktopComponent>
{/* 目前移动端由于和客户端的主题对不上,所以先使用 `GlobalThemeProvider` 和 `AntdAppProvider` 进行重置为默认主题 */} {/* 目前移动端由于和客户端的主题对不上,所以先使用 `GlobalThemeProvider` 和 `AntdAppProvider` 进行重置为默认主题 */}
<div className={styles.nbMobile}> <GlobalThemeProvider
<GlobalThemeProvider theme={{
theme={{ token: {
token: { marginBlock: 18,
marginBlock: 18, borderRadiusBlock: 0,
borderRadiusBlock: 0, boxShadowTertiary: 'none',
boxShadowTertiary: 'none', },
}, }}
}} >
> <AntdAppProvider className={`mobile-container ${styles.nbMobile}`}>
<AntdAppProvider> <OpenModeProvider
<OpenModeProvider defaultOpenMode="page"
defaultOpenMode="page" hideOpenMode
hideOpenMode openModeToComponent={{
openModeToComponent={{ page: MobileActionPage,
page: MobileActionPage, drawer: ActionDrawerUsedInMobile,
drawer: MobileActionPage, modal: Action.Modal,
modal: Action.Modal, }}
}} >
> <BlockTemplateProvider componentNamePrefix="mobile-">
<MobileAppProvider> <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> </MobileAppProvider>
</OpenModeProvider> </BlockTemplateProvider>
</AntdAppProvider> </OpenModeProvider>
</GlobalThemeProvider> </AntdAppProvider>
</div> </GlobalThemeProvider>
</DesktopComponent> </DesktopComponent>
); );
}; };

View File

@ -19,6 +19,19 @@ export const useStyles = createStyles(({ token, css }) => {
.ant-table-thead button[aria-label*='schema-initializer-TableV2-table:configureColumns'] > .ant-btn-icon { .ant-table-thead button[aria-label*='schema-initializer-TableV2-table:configureColumns'] > .ant-btn-icon {
margin: 0px; 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 { .ant-pagination .ant-pagination-total-text {
display: none; display: none;
} }

View File

@ -20,7 +20,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({
items: [ items: [
{ {
name: 'dataBlocks', name: 'dataBlocks',
title: '{{t("Data blocks")}}', title: '{{t("Desktop data blocks")}}',
type: 'itemGroup', type: 'itemGroup',
children: [ children: [
{ {
@ -53,7 +53,7 @@ export const mobileAddBlockInitializer = new SchemaInitializer({
{ {
name: 'otherBlocks', name: 'otherBlocks',
type: 'itemGroup', type: 'itemGroup',
title: '{{t("Other blocks")}}', title: '{{t("Other desktop blocks")}}',
children: [ children: [
{ {
name: 'markdown', name: 'markdown',

View File

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