mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat: plugin mobile v2 (#4777)
* feat: init * fix: mobile layout * feat: more code * feat: improve navigate bar * fix: mobile title * feat: improve code * fix: add settings and initailzer * fix: settings * fix: tabbar items settings * feat: tabbar initializer * fix: api * fix: styles * feat: navbar * feat: navigate bar tabs initializer * feat: navigate bar tab settings * feat: navigation bar actions * fix: bug * fix: bug * fix: bug * fix: tabbar active * fix: bug * fix: mobile login and layout * fix: update version * fix: build error * feat: plugin settings support link * fix: add mobile meta * fix: desktop mode * fix: remove old code and change collection name and mobile path * fix: tabbar and tabs initialer layout * fix: initializer style * fix: adjust schema position * fix: mobile style * fix: delete relation resource and home page bug * fix: support multi app * fix: not found page * fix: js bridge * fix: bug * fix: navigation bar schema flat * fix: navigation bar action style * fix: change version * fix: mobile meta and real mobile test * refactor: folder and name * fix: navigation bar sticky and zIndex * fix: full mobile schema * fix: mobile readme and package.json * fix: e2e bug * fix: bug * fix: tabbar style on productino * fix: bug * fix: rename MobileTabBar.Page * fix: support tabbar sort * fix: support page tabs sort * fix: i18n * fix: settings utils import bug * docs: api doc * fix: qrcode refresh * test: unit tests * fix: bug * fix: unit test * fix: build bug * fix: e2e test * fix: overflow scroll * fix: bug * fix: scroll and overflow * fix: bug * fix: e2e expect await * fix: e2e bug * fix: bug * fix: change name * fix: add more e2e * fix: page header * fix: tab support icon * fix: bug * fix: bug * fix: docs * fix(T-4811): scroll bar too long * fix(T-4810): desktop mode * fix: e2e * fix(T-4812): title empty * fix: unit test * feat: hide Open mode option in mobile mode * feat: change default value of Open mode on mobile * feat: add OpenModeProvider * feat: support page mode * fix: fix build * test: update unit tests * chore: remove pro-plugins * fix: bug * fix(T-4812): title is required * fix: bug * fix: bug * fix: bug * fix: bug * refactor: remove z-index * refactor: make better for subpages * fix: drag bug * fix: bug * fix: theme bug * fix(T-4859): create tab bar title empty * fix(T-4857): action too long * fix: e2e bug * fix: remove comment * fix: bug * fix: theme bug * fix: should provider modal component * fix: bug --------- Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Zeke Zhang <958414905@qq.com>
This commit is contained in:
parent
01477986ee
commit
61e9dd5cc1
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ storage/backups/*
|
|||||||
**/.dumi/tmp-production
|
**/.dumi/tmp-production
|
||||||
packages/core/client/docs/contributing.md
|
packages/core/client/docs/contributing.md
|
||||||
packages/core/app/client/src/.plugins
|
packages/core/app/client/src/.plugins
|
||||||
|
packages/pro-plugins/
|
||||||
storage/plugins
|
storage/plugins
|
||||||
storage/tar
|
storage/tar
|
||||||
storage/tmp
|
storage/tmp
|
||||||
|
@ -59,7 +59,11 @@ export function AntdConfigProvider(props) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spin />;
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AppLangContext.Provider value={data?.data}>
|
<AppLangContext.Provider value={data?.data}>
|
||||||
|
@ -39,6 +39,7 @@ import { DataSourceApplicationProvider } from '../data-source/components/DataSou
|
|||||||
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
|
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
|
||||||
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
|
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
|
||||||
|
|
||||||
|
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
||||||
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||||
import type { Plugin } from './Plugin';
|
import type { Plugin } from './Plugin';
|
||||||
import type { RequireJS } from './utils/requirejs';
|
import type { RequireJS } from './utils/requirejs';
|
||||||
@ -158,6 +159,7 @@ export class Application {
|
|||||||
});
|
});
|
||||||
this.use(AntdAppProvider);
|
this.use(AntdAppProvider);
|
||||||
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
||||||
|
this.use(OpenModeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addReactRouterComponents() {
|
private addReactRouterComponents() {
|
||||||
|
@ -12,7 +12,7 @@ import type { Application } from './Application';
|
|||||||
|
|
||||||
export class Plugin<T = any> {
|
export class Plugin<T = any> {
|
||||||
constructor(
|
constructor(
|
||||||
protected options: T,
|
public options: T,
|
||||||
protected app: Application,
|
protected app: Application,
|
||||||
) {
|
) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
@ -12,7 +12,7 @@ import type { Plugin } from './Plugin';
|
|||||||
import { getPlugins } from './utils/remotePlugins';
|
import { getPlugins } from './utils/remotePlugins';
|
||||||
|
|
||||||
export type PluginOptions<T = any> = { name?: string; packageName?: string; config?: T };
|
export type PluginOptions<T = any> = { name?: string; packageName?: string; config?: T };
|
||||||
export type PluginType<Opts = any> = typeof Plugin | [typeof Plugin, PluginOptions<Opts>];
|
export type PluginType<Opts = any> = typeof Plugin | [typeof Plugin<Opts>, PluginOptions<Opts>];
|
||||||
export type PluginData = {
|
export type PluginData = {
|
||||||
name: string;
|
name: string;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
|
@ -32,12 +32,14 @@ export interface PluginSettingOptions {
|
|||||||
*/
|
*/
|
||||||
sort?: number;
|
sort?: number;
|
||||||
aclSnippet?: string;
|
aclSnippet?: string;
|
||||||
|
link?: string;
|
||||||
[index: string]: any;
|
[index: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginSettingsPageType {
|
export interface PluginSettingsPageType {
|
||||||
label?: string | React.ReactElement;
|
label?: string | React.ReactElement;
|
||||||
title: string | React.ReactElement;
|
title: string | React.ReactElement;
|
||||||
|
link?: string;
|
||||||
key: string;
|
key: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -13,6 +13,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link, Outlet } from 'react-router-dom';
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
import { describe } from 'vitest';
|
import { describe } from 'vitest';
|
||||||
|
import { OpenModeProvider } from '../../modules/popup/OpenModeProvider';
|
||||||
import { Application } from '../Application';
|
import { Application } from '../Application';
|
||||||
import { Plugin } from '../Plugin';
|
import { Plugin } from '../Plugin';
|
||||||
import { useApp } from '../hooks';
|
import { useApp } from '../hooks';
|
||||||
@ -211,6 +212,7 @@ describe('Application', () => {
|
|||||||
it('initial', () => {
|
it('initial', () => {
|
||||||
const app = new Application({ router, providers: [Hello, [World, { name: 'aaa' }]] });
|
const app = new Application({ router, providers: [Hello, [World, { name: 'aaa' }]] });
|
||||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||||
|
[OpenModeProvider, undefined],
|
||||||
[Hello, undefined],
|
[Hello, undefined],
|
||||||
[World, { name: 'aaa' }],
|
[World, { name: 'aaa' }],
|
||||||
]);
|
]);
|
||||||
@ -220,6 +222,7 @@ describe('Application', () => {
|
|||||||
const app = new Application({ router, providers: [Hello] });
|
const app = new Application({ router, providers: [Hello] });
|
||||||
app.addProviders([[World, { name: 'aaa' }], Foo]);
|
app.addProviders([[World, { name: 'aaa' }], Foo]);
|
||||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||||
|
[OpenModeProvider, undefined],
|
||||||
[Hello, undefined],
|
[Hello, undefined],
|
||||||
[World, { name: 'aaa' }],
|
[World, { name: 'aaa' }],
|
||||||
[Foo, undefined],
|
[Foo, undefined],
|
||||||
@ -230,6 +233,7 @@ describe('Application', () => {
|
|||||||
const app = new Application({ router, providers: [Hello] });
|
const app = new Application({ router, providers: [Hello] });
|
||||||
app.addProvider(World, { name: 'aaa' });
|
app.addProvider(World, { name: 'aaa' });
|
||||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||||
|
[OpenModeProvider, undefined],
|
||||||
[Hello, undefined],
|
[Hello, undefined],
|
||||||
[World, { name: 'aaa' }],
|
[World, { name: 'aaa' }],
|
||||||
]);
|
]);
|
||||||
@ -239,6 +243,7 @@ describe('Application', () => {
|
|||||||
const app = new Application({ router, providers: [Hello] });
|
const app = new Application({ router, providers: [Hello] });
|
||||||
app.use(World, { name: 'aaa' });
|
app.use(World, { name: 'aaa' });
|
||||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||||
|
[OpenModeProvider, undefined],
|
||||||
[Hello, undefined],
|
[Hello, undefined],
|
||||||
[World, { name: 'aaa' }],
|
[World, { name: 'aaa' }],
|
||||||
]);
|
]);
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { Plugin } from '../Plugin';
|
import { Plugin } from '../Plugin';
|
||||||
import { useApp } from './useApp';
|
import { useApp } from './useApp';
|
||||||
|
|
||||||
export function usePlugin<T extends typeof Plugin>(plugin: T): InstanceType<T>;
|
export function usePlugin<T extends typeof Plugin = any>(plugin: T): InstanceType<T>;
|
||||||
export function usePlugin<T extends {}>(name: string): T;
|
export function usePlugin<T extends {}>(name: string): T;
|
||||||
export function usePlugin(name: any) {
|
export function usePlugin(name: any) {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
@ -18,10 +18,8 @@ export * from './globalType';
|
|||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './schema-initializer';
|
export * from './schema-initializer';
|
||||||
export * from './schema-settings';
|
export * from './schema-settings';
|
||||||
|
export * from './schema-settings/utils';
|
||||||
export * from './schema-settings/context/SchemaSettingItemContext';
|
export * from './schema-settings/context/SchemaSettingItemContext';
|
||||||
export * from './schema-settings/hooks/useSchemaSettingsRender';
|
export * from './schema-settings/hooks/useSchemaSettingsRender';
|
||||||
export * from './schema-settings/utils/createModalSettingsItem';
|
|
||||||
export * from './schema-settings/utils/createSelectSettingsItem';
|
|
||||||
export * from './schema-settings/utils/createSwitchSettingsItem';
|
|
||||||
export * from './schema-toolbar';
|
export * from './schema-toolbar';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
import { useActionContext, SchemaComponent } from '../../../schema-component';
|
import { useActionContext, SchemaComponent } from '../../../schema-component';
|
||||||
import { useSchemaInitializerItem } from '../context';
|
import { useSchemaInitializer, useSchemaInitializerItem } from '../context';
|
||||||
import { SchemaInitializerItem } from './SchemaInitializerItem';
|
import { SchemaInitializerItem } from './SchemaInitializerItem';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
@ -19,10 +19,12 @@ export interface SchemaInitializerActionModalProps {
|
|||||||
icon?: string | React.ReactNode;
|
icon?: string | React.ReactNode;
|
||||||
schema: any;
|
schema: any;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onSubmit?: (values: any) => void;
|
onSubmit?: (values: any) => Promise<any> | void;
|
||||||
buttonText?: any;
|
buttonText?: any;
|
||||||
component?: any;
|
component?: any;
|
||||||
isItem?: boolean;
|
isItem?: boolean;
|
||||||
|
width?: string;
|
||||||
|
btnStyles?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any, ref: any) => {
|
const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any, ref: any) => {
|
||||||
@ -31,7 +33,8 @@ const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any,
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps> = (props) => {
|
export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps> = (props) => {
|
||||||
const { title, icon, schema, buttonText, isItem, component, onCancel, onSubmit } = props;
|
const { title, icon, width, schema, buttonText, btnStyles, isItem, component, onCancel, onSubmit } = props;
|
||||||
|
const { setVisible: initializerSetVisible } = useSchemaInitializer();
|
||||||
const useCancelAction = useCallback(() => {
|
const useCancelAction = useCallback(() => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
@ -54,9 +57,13 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
|||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
await form.validate();
|
await form.validate();
|
||||||
|
try {
|
||||||
await onSubmit?.(form.values);
|
await onSubmit?.(form.values);
|
||||||
ctx.setVisible(false);
|
ctx.setVisible(false);
|
||||||
void form.reset();
|
void form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [onSubmit]);
|
}, [onSubmit]);
|
||||||
@ -92,6 +99,7 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
|||||||
style: {
|
style: {
|
||||||
borderColor: 'var(--colorSettings)',
|
borderColor: 'var(--colorSettings)',
|
||||||
color: 'var(--colorSettings)',
|
color: 'var(--colorSettings)',
|
||||||
|
...(btnStyles || {}),
|
||||||
},
|
},
|
||||||
title: buttonText,
|
title: buttonText,
|
||||||
type: 'dashed',
|
type: 'dashed',
|
||||||
@ -101,10 +109,14 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
|||||||
'x-decorator': 'Form',
|
'x-decorator': 'Form',
|
||||||
'x-component': 'Action.Modal',
|
'x-component': 'Action.Modal',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
|
width: width,
|
||||||
style: {
|
style: {
|
||||||
maxWidth: '520px',
|
maxWidth: width ? width : '520px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
afterOpenChange: () => {
|
||||||
|
initializerSetVisible(false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
title,
|
||||||
|
@ -31,11 +31,13 @@ export const SchemaInitializerButton: FC<SchemaInitializerButtonProps> = React.m
|
|||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--colorSettings)',
|
borderColor: 'var(--colorSettings)',
|
||||||
color: 'var(--colorSettings)',
|
color: 'var(--colorSettings)',
|
||||||
|
flex: 'none',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
icon={typeof options.icon === 'string' ? <Icon type={options.icon as string} /> : options.icon}
|
icon={typeof options.icon === 'string' ? <Icon type={options.icon as string} /> : options.icon}
|
||||||
{...others}
|
{...others}
|
||||||
>
|
>
|
||||||
|
{options.title && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@ -48,6 +50,7 @@ export const SchemaInitializerButton: FC<SchemaInitializerButtonProps> = React.m
|
|||||||
>
|
>
|
||||||
{compile(options.title)}
|
{compile(options.title)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,7 @@ import type {
|
|||||||
|
|
||||||
export type InsertType = (s: ISchema) => void;
|
export type InsertType = (s: ISchema) => void;
|
||||||
|
|
||||||
type SchemaInitializerItemBuiltInType<T = {}> = T & {
|
type SchemaInitializerItemBuiltInType<T = {}> = Partial<T> & {
|
||||||
name: string;
|
name: string;
|
||||||
sort?: number;
|
sort?: number;
|
||||||
componentProps?: Omit<T, 'children'>;
|
componentProps?: Omit<T, 'children'>;
|
||||||
@ -110,6 +110,7 @@ export interface SchemaInitializerOptions<P1 = ButtonProps, P2 = {}> {
|
|||||||
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
|
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
|
||||||
designable?: boolean;
|
designable?: boolean;
|
||||||
wrap?: (s: ISchema, options?: any) => ISchema;
|
wrap?: (s: ISchema, options?: any) => ISchema;
|
||||||
|
useWrap?: () => ((s: ISchema, options?: any) => ISchema);
|
||||||
onSuccess?: (data: any) => void;
|
onSuccess?: (data: any) => void;
|
||||||
insert?: InsertType;
|
insert?: InsertType;
|
||||||
useInsert?: () => InsertType;
|
useInsert?: () => InsertType;
|
||||||
|
@ -21,6 +21,7 @@ import { SchemaInitializerOptions } from './types';
|
|||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
const defaultWrap = (s: ISchema) => s;
|
const defaultWrap = (s: ISchema) => s;
|
||||||
|
const useWrapDefault = (wrap = defaultWrap) => wrap;
|
||||||
|
|
||||||
export function withInitializer<T>(C: ComponentType<T>) {
|
export function withInitializer<T>(C: ComponentType<T>) {
|
||||||
const WithInitializer = observer(
|
const WithInitializer = observer(
|
||||||
@ -30,6 +31,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
|||||||
const {
|
const {
|
||||||
insert,
|
insert,
|
||||||
useInsert,
|
useInsert,
|
||||||
|
useWrap = useWrapDefault,
|
||||||
wrap = defaultWrap,
|
wrap = defaultWrap,
|
||||||
insertPosition = 'beforeEnd',
|
insertPosition = 'beforeEnd',
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@ -43,15 +45,16 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
|||||||
|
|
||||||
// 插入 schema 的能力
|
// 插入 schema 的能力
|
||||||
const insertCallback = useInsert ? useInsert() : insert;
|
const insertCallback = useInsert ? useInsert() : insert;
|
||||||
|
const wrapCallback = useWrap(wrap);
|
||||||
const insertSchema = useCallback(
|
const insertSchema = useCallback(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
if (insertCallback) {
|
if (insertCallback) {
|
||||||
insertCallback(wrap(schema, { isInSubTable }));
|
insertCallback(wrapCallback(schema, { isInSubTable }));
|
||||||
} else {
|
} else {
|
||||||
insertAdjacent(insertPosition, wrap(schema, { isInSubTable }), { onSuccess });
|
insertAdjacent(insertPosition, wrapCallback(schema, { isInSubTable }), { onSuccess });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[insertCallback, wrap, insertAdjacent, insertPosition, onSuccess],
|
[insertCallback, wrapCallback, insertAdjacent, insertPosition, onSuccess],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { wrapSSR, hashId, componentCls } = useSchemaInitializerStyles();
|
const { wrapSSR, hashId, componentCls } = useSchemaInitializerStyles();
|
||||||
|
@ -7,22 +7,26 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SchemaSettingsItemType, useCompile, useDesignable } from '@nocobase/client';
|
|
||||||
import { ISchema, useFieldSchema } from '@formily/react';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { ISchema, useFieldSchema } from '@formily/react';
|
||||||
import { TFunction, useTranslation } from 'react-i18next';
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { SchemaSettingsItemType } from '../types';
|
||||||
import { getNewSchema, useHookDefault } from './util';
|
import { getNewSchema, useHookDefault } from './util';
|
||||||
|
import { useCompile } from '../../../schema-component/hooks/useCompile';
|
||||||
|
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
|
||||||
|
|
||||||
export interface CreateModalSchemaSettingsItemProps {
|
export interface CreateModalSchemaSettingsItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
title: string | ((t: TFunction<'translation', undefined>) => string);
|
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||||
parentSchemaKey: string;
|
parentSchemaKey?: string;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
useDefaultValue?: () => any;
|
useDefaultValue?: () => any;
|
||||||
schema: (defaultValue: any) => ISchema;
|
schema: (defaultValue: any) => ISchema;
|
||||||
valueKeys?: string[];
|
valueKeys?: string[];
|
||||||
useVisible?: () => boolean;
|
useVisible?: () => boolean;
|
||||||
|
width?: number | string;
|
||||||
|
useSubmit?: () => (values: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,9 +42,11 @@ export function createModalSettingsItem(options: CreateModalSchemaSettingsItemPr
|
|||||||
valueKeys,
|
valueKeys,
|
||||||
schema,
|
schema,
|
||||||
title,
|
title,
|
||||||
|
useSubmit = useHookDefault,
|
||||||
useVisible,
|
useVisible,
|
||||||
defaultValue: propsDefaultValue,
|
defaultValue: propsDefaultValue,
|
||||||
useDefaultValue = useHookDefault,
|
useDefaultValue = useHookDefault,
|
||||||
|
width,
|
||||||
} = options;
|
} = options;
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -50,15 +56,18 @@ export function createModalSettingsItem(options: CreateModalSchemaSettingsItemPr
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { deepMerge } = useDesignable();
|
const { deepMerge } = useDesignable();
|
||||||
const defaultValue = useDefaultValue(propsDefaultValue);
|
const defaultValue = useDefaultValue(propsDefaultValue);
|
||||||
const values = _.get(fieldSchema, parentSchemaKey);
|
const values = parentSchemaKey ? _.get(fieldSchema, parentSchemaKey) : undefined;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const onSubmit = useSubmit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: typeof title === 'function' ? title(t) : compile(title),
|
title: typeof title === 'function' ? title(t) : compile(title),
|
||||||
|
width,
|
||||||
schema: schema({ ...defaultValue, ...values }),
|
schema: schema({ ...defaultValue, ...values }),
|
||||||
onSubmit(values) {
|
onSubmit(values) {
|
||||||
deepMerge(getNewSchema({ fieldSchema, schemaKey: parentSchemaKey, value: values, valueKeys }));
|
deepMerge(getNewSchema({ fieldSchema, parentSchemaKey, value: values, valueKeys }));
|
||||||
|
return onSubmit?.(values);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -9,10 +9,14 @@
|
|||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { SchemaSettingsItemType, SelectProps, useCompile, useDesignable } from '@nocobase/client';
|
|
||||||
import { getNewSchema, useHookDefault } from './util';
|
|
||||||
import { TFunction, useTranslation } from 'react-i18next';
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { SchemaSettingsItemType } from '../types';
|
||||||
|
import { getNewSchema, useHookDefault } from './util';
|
||||||
|
import { SelectProps } from '../../../schema-component/antd/select';
|
||||||
|
import { useCompile } from '../../../schema-component/hooks/useCompile';
|
||||||
|
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
|
||||||
|
|
||||||
interface CreateSelectSchemaSettingsItemProps {
|
interface CreateSelectSchemaSettingsItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
title: string | ((t: TFunction<'translation', undefined>) => string);
|
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||||
|
@ -7,13 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SchemaSettingsItemType, useCompile, useDesignable } from '@nocobase/client';
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { getNewSchema, useHookDefault } from './util';
|
|
||||||
import { TFunction, useTranslation } from 'react-i18next';
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { SchemaSettingsItemType } from '../types';
|
||||||
|
import { getNewSchema, useHookDefault } from './util';
|
||||||
|
import { useCompile } from '../../../schema-component/hooks/useCompile';
|
||||||
|
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
|
||||||
|
|
||||||
export interface CreateSwitchSchemaSettingsItemProps {
|
export interface CreateSwitchSchemaSettingsItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
title: string | ((t: TFunction<'translation', undefined>) => string);
|
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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 { TFunction, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useHookDefault } from './util';
|
||||||
|
import { SchemaSettingsItemType } from '../types';
|
||||||
|
import { useCompile } from '../../../schema-component/hooks/useCompile';
|
||||||
|
|
||||||
|
export interface CreateTextSchemaSettingsItemProps {
|
||||||
|
name: string;
|
||||||
|
useVisible?: () => boolean;
|
||||||
|
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||||
|
useTextClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTextSettingsItem(options: CreateTextSchemaSettingsItemProps): SchemaSettingsItemType {
|
||||||
|
const { name, useVisible, title, useTextClick = useHookDefault } = options;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: 'item',
|
||||||
|
useVisible,
|
||||||
|
useComponentProps() {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const onClick = useTextClick();
|
||||||
|
return {
|
||||||
|
title: typeof title === 'function' ? title(t) : compile(title),
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './createModalSettingsItem';
|
||||||
|
export * from './createSelectSettingsItem';
|
||||||
|
export * from './createSwitchSettingsItem';
|
||||||
|
export * from './createTextSettingsItem';
|
@ -12,28 +12,24 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
type IGetNewSchema = {
|
type IGetNewSchema = {
|
||||||
fieldSchema: ISchema;
|
fieldSchema: ISchema;
|
||||||
schemaKey: string;
|
schemaKey?: string;
|
||||||
|
parentSchemaKey?: string;
|
||||||
value: any;
|
value: any;
|
||||||
valueKeys?: string[];
|
valueKeys?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getNewSchema(options: IGetNewSchema) {
|
export function getNewSchema(options: IGetNewSchema) {
|
||||||
const { fieldSchema, schemaKey, value, valueKeys } = options as any;
|
const { fieldSchema, schemaKey, value, parentSchemaKey, valueKeys } = options;
|
||||||
const schemaKeyArr = schemaKey.split('.');
|
|
||||||
const clonedSchema = _.cloneDeep(fieldSchema[schemaKeyArr[0]]);
|
|
||||||
|
|
||||||
if (value != undefined && typeof value === 'object') {
|
if (value != undefined && typeof value === 'object') {
|
||||||
Object.keys(value).forEach((key) => {
|
Object.keys(value).forEach((key) => {
|
||||||
if (valueKeys && !valueKeys.includes(key)) return;
|
if (valueKeys && !valueKeys.includes(key)) return;
|
||||||
_.set(clonedSchema, `${schemaKeyArr.slice(1)}.${key}`, value[key]);
|
_.set(fieldSchema, `${parentSchemaKey}.${key}`, value[key]);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_.set(clonedSchema, schemaKeyArr.slice(1), value);
|
_.set(fieldSchema, schemaKey, value);
|
||||||
}
|
}
|
||||||
return {
|
return fieldSchema;
|
||||||
'x-uid': fieldSchema['x-uid'],
|
|
||||||
[schemaKeyArr[0]]: clonedSchema,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHookDefault = (defaultValues?: any) => defaultValues;
|
export const useHookDefault = (defaultValues?: any) => defaultValues;
|
||||||
|
@ -7,6 +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 { css } from '@emotion/css';
|
||||||
import { Field, Form } from '@formily/core';
|
import { Field, Form } from '@formily/core';
|
||||||
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { untracked } from '@formily/reactive';
|
import { untracked } from '@formily/reactive';
|
||||||
@ -21,7 +22,6 @@ 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 } from 'react-router-dom';
|
||||||
import { useReactToPrint } from 'react-to-print';
|
import { useReactToPrint } from 'react-to-print';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import {
|
import {
|
||||||
AssociationFilter,
|
AssociationFilter,
|
||||||
useCollection,
|
useCollection,
|
||||||
@ -1582,12 +1582,13 @@ export const useParseURLAndParams = () => {
|
|||||||
return { parseURLAndParams };
|
return { parseURLAndParams };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useLinkActionProps() {
|
export function useLinkActionProps(componentProps?: any) {
|
||||||
const navigate = useNavigateNoUpdate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
const componentPropsValue = fieldSchema?.['x-component-props'] || componentProps;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const url = fieldSchema?.['x-component-props']?.['url'];
|
const url = componentPropsValue?.['url'];
|
||||||
const searchParams = fieldSchema?.['x-component-props']?.['params'] || [];
|
const searchParams = componentPropsValue?.['params'] || [];
|
||||||
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
|
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
|
||||||
const { parseURLAndParams } = useParseURLAndParams();
|
const { parseURLAndParams } = useParseURLAndParams();
|
||||||
|
|
||||||
|
@ -17,3 +17,4 @@ export * from './TableFieldProvider';
|
|||||||
export * from './TableSelectorProvider';
|
export * from './TableSelectorProvider';
|
||||||
export * from './DetailsBlockProvider';
|
export * from './DetailsBlockProvider';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
export { useLinkActionProps } from './hooks/index';
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { ConfigProvider, theme as antdTheme } from 'antd';
|
import { ConfigProvider, theme as antdTheme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { createContext, useCallback, useMemo, useRef } from 'react';
|
import React, { createContext, FC, useCallback, useMemo, useRef } from 'react';
|
||||||
import compatOldTheme from './compatOldTheme';
|
import compatOldTheme from './compatOldTheme';
|
||||||
import { addCustomAlgorithmToTheme } from './customAlgorithm';
|
import { addCustomAlgorithmToTheme } from './customAlgorithm';
|
||||||
import defaultTheme from './defaultTheme';
|
import defaultTheme from './defaultTheme';
|
||||||
@ -41,7 +41,11 @@ export const useGlobalTheme = () => {
|
|||||||
return React.useContext(GlobalThemeContext) || ({ theme: {}, isDarkTheme: false } as GlobalThemeContextProps);
|
return React.useContext(GlobalThemeContext) || ({ theme: {}, isDarkTheme: false } as GlobalThemeContextProps);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GlobalThemeProvider = ({ children, theme: themeFromProps }) => {
|
interface GlobalThemeProviderProps {
|
||||||
|
theme?: ThemeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalThemeProvider: FC<GlobalThemeProviderProps> = ({ children, theme: themeFromProps }) => {
|
||||||
const [theme, setTheme] = React.useState<ThemeConfig>(themeFromProps || defaultTheme);
|
const [theme, setTheme] = React.useState<ThemeConfig>(themeFromProps || defaultTheme);
|
||||||
const currentSettingThemeRef = useRef<ThemeConfig>(null);
|
const currentSettingThemeRef = useRef<ThemeConfig>(null);
|
||||||
const currentEditingThemeRef = useRef<ThemeItem>(null);
|
const currentEditingThemeRef = useRef<ThemeItem>(null);
|
||||||
|
@ -70,5 +70,6 @@ export * from './modules/blocks/data-blocks/table';
|
|||||||
export * from './modules/blocks/data-blocks/table-selector';
|
export * from './modules/blocks/data-blocks/table-selector';
|
||||||
export * from './modules/blocks/index';
|
export * from './modules/blocks/index';
|
||||||
export * from './modules/blocks/useParentRecordCommon';
|
export * from './modules/blocks/useParentRecordCommon';
|
||||||
|
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
|
||||||
|
|
||||||
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
|
@ -11,7 +11,9 @@ import React from 'react';
|
|||||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
export const CreateChildInitializer = (props) => {
|
export const CreateChildInitializer = (props) => {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { getPopupContext } = usePagePopup();
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -22,7 +24,7 @@ export const CreateChildInitializer = (props) => {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-visible': '{{treeTable}}',
|
'x-visible': '{{treeTable}}',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
type: 'link',
|
type: 'link',
|
||||||
addChild: true,
|
addChild: true,
|
||||||
style: { height: 'auto', lineHeight: 'normal' },
|
style: { height: 'auto', lineHeight: 'normal' },
|
||||||
|
@ -7,14 +7,14 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||||
import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsLinkageRules, SchemaSettingsEnableChildCollections } from '../../../schema-settings';
|
import { SchemaSettingsEnableChildCollections, SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const addChildActionSettings = new SchemaSettings({
|
export const addChildActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:addChild',
|
name: 'actionSettings:addChild',
|
||||||
@ -42,9 +42,12 @@ export const addChildActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -12,8 +12,10 @@ import { useSchemaInitializerItem } from '../../../application';
|
|||||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const CreateActionInitializer = () => {
|
export const CreateActionInitializer = () => {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { getPopupContext } = usePagePopup();
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -25,7 +27,7 @@ export const CreateActionInitializer = () => {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
component: 'CreateRecordAction',
|
component: 'CreateRecordAction',
|
||||||
icon: 'PlusOutlined',
|
icon: 'PlusOutlined',
|
||||||
|
@ -14,6 +14,7 @@ import { useCollection_deprecated, useCollectionManager_deprecated } from '../..
|
|||||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const addNewActionSettings = new SchemaSettings({
|
export const addNewActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:addNew',
|
name: 'actionSettings:addNew',
|
||||||
@ -29,9 +30,12 @@ export const addNewActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ import { useSchemaToolbar } from '../../../application';
|
|||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const customizeAddRecordActionSettings = new SchemaSettings({
|
export const customizeAddRecordActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:addRecord',
|
name: 'actionSettings:addRecord',
|
||||||
@ -26,9 +27,12 @@ export const customizeAddRecordActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import { ArrayItems } from '@formily/antd-v5';
|
import { ArrayItems } from '@formily/antd-v5';
|
||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCollectionRecord, useDesignable } from '../../../';
|
import { useCollectionRecord, useDesignable } from '../../../';
|
||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
@ -19,7 +19,11 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
|
|||||||
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings';
|
||||||
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
||||||
|
|
||||||
export function SchemaSettingsActionLinkItem() {
|
interface SchemaSettingsActionLinkItemProps {
|
||||||
|
afterSubmit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps> = ({ afterSubmit }) => {
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
@ -66,11 +70,12 @@ export function SchemaSettingsActionLinkItem() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
dn.refresh();
|
dn.refresh();
|
||||||
|
afterSubmit?.();
|
||||||
}}
|
}}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const customizeLinkActionSettings = new SchemaSettings({
|
export const customizeLinkActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:link',
|
name: 'actionSettings:link',
|
||||||
|
@ -12,8 +12,10 @@ import { useSchemaInitializerItem } from '../../../application';
|
|||||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { BlockInitializer } from '../../../schema-initializer/items';
|
import { BlockInitializer } from '../../../schema-initializer/items';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const PopupActionInitializer = (props) => {
|
export const PopupActionInitializer = (props) => {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { getPopupContext } = usePagePopup();
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -23,7 +25,7 @@ export const PopupActionInitializer = (props) => {
|
|||||||
'x-settings': 'actionSettings:popup',
|
'x-settings': 'actionSettings:popup',
|
||||||
'x-component': props?.['x-component'] || 'Action.Link',
|
'x-component': props?.['x-component'] || 'Action.Link',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
refreshDataBlockRequest: true,
|
refreshDataBlockRequest: true,
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -11,8 +11,10 @@ import React from 'react';
|
|||||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const UpdateActionInitializer = (props) => {
|
export const UpdateActionInitializer = (props) => {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { getPopupContext } = usePagePopup();
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -22,7 +24,7 @@ export const UpdateActionInitializer = (props) => {
|
|||||||
'x-settings': 'actionSettings:edit',
|
'x-settings': 'actionSettings:edit',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
icon: 'EditOutlined',
|
icon: 'EditOutlined',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -11,8 +11,10 @@ import React from 'react';
|
|||||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const ViewActionInitializer = (props) => {
|
export const ViewActionInitializer = (props) => {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { getPopupContext } = usePagePopup();
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -22,7 +24,7 @@ export const ViewActionInitializer = (props) => {
|
|||||||
'x-settings': 'actionSettings:view',
|
'x-settings': 'actionSettings:view',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
drawer: {
|
drawer: {
|
||||||
|
@ -10,13 +10,10 @@
|
|||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
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 {
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
ButtonEditor,
|
|
||||||
RemoveButton,
|
|
||||||
RefreshDataBlockRequest,
|
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const customizePopupActionSettings = new SchemaSettings({
|
export const customizePopupActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:popup',
|
name: 'actionSettings:popup',
|
||||||
@ -44,9 +41,12 @@ export const customizePopupActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
|
|||||||
import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const editActionSettings = new SchemaSettings({
|
export const editActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:edit',
|
name: 'actionSettings:edit',
|
||||||
@ -40,9 +41,12 @@ export const editActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ import { useCollection_deprecated } from '../../../collection-manager';
|
|||||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
|
||||||
export const viewActionSettings = new SchemaSettings({
|
export const viewActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:view',
|
name: 'actionSettings:view',
|
||||||
@ -40,9 +41,12 @@ export const viewActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
componentProps: {
|
useComponentProps() {
|
||||||
openMode: true,
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
openSize: true,
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -33,6 +33,7 @@ import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSetti
|
|||||||
import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule';
|
import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule';
|
||||||
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
||||||
import { useLocalVariables, useVariables } from '../../../../variables';
|
import { useLocalVariables, useVariables } from '../../../../variables';
|
||||||
|
import { useOpenModeContext } from '../../../popup/OpenModeProvider';
|
||||||
|
|
||||||
const enableLink = {
|
const enableLink = {
|
||||||
name: 'enableLink',
|
name: 'enableLink',
|
||||||
@ -162,6 +163,7 @@ const quickCreate: any = {
|
|||||||
name: 'quickCreate',
|
name: 'quickCreate',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -194,7 +196,7 @@ const quickCreate: any = {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
component: 'CreateRecordAction',
|
component: 'CreateRecordAction',
|
||||||
},
|
},
|
||||||
|
99
packages/core/client/src/modules/popup/OpenModeProvider.tsx
Normal file
99
packages/core/client/src/modules/popup/OpenModeProvider.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 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, { FC, useCallback, useMemo } from 'react';
|
||||||
|
import ActionDrawer from '../../schema-component/antd/action/Action.Drawer';
|
||||||
|
import ActionModal from '../../schema-component/antd/action/Action.Modal';
|
||||||
|
import ActionPage from '../../schema-component/antd/action/Action.Page';
|
||||||
|
|
||||||
|
type OpenMode = 'drawer' | 'page' | 'modal';
|
||||||
|
|
||||||
|
interface OpenModeProviderProps {
|
||||||
|
/**
|
||||||
|
* @default 'drawer'
|
||||||
|
* open mode 的全局默认值
|
||||||
|
*/
|
||||||
|
defaultOpenMode?: OpenMode;
|
||||||
|
/**
|
||||||
|
* @default { drawer: ActionDrawer, page: ActionPage, modal: ActionModal }
|
||||||
|
* 根据 open mode 获取对应的组件
|
||||||
|
*/
|
||||||
|
openModeToComponent?: Partial<Record<OpenMode, any>>;
|
||||||
|
/**
|
||||||
|
* @default false
|
||||||
|
* 隐藏 open mode 的配置选项
|
||||||
|
*/
|
||||||
|
hideOpenMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContext: OpenModeProviderProps = {
|
||||||
|
defaultOpenMode: 'drawer',
|
||||||
|
openModeToComponent: {
|
||||||
|
drawer: ActionDrawer,
|
||||||
|
page: ActionPage,
|
||||||
|
modal: ActionModal,
|
||||||
|
},
|
||||||
|
hideOpenMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OpenModeContext = React.createContext<{
|
||||||
|
defaultOpenMode: OpenModeProviderProps['defaultOpenMode'];
|
||||||
|
hideOpenMode: boolean;
|
||||||
|
getComponentByOpenMode: (openMode: OpenMode) => any;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为按钮的 Open mode 选项提供上下文
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const OpenModeProvider: FC<OpenModeProviderProps> = (props) => {
|
||||||
|
const context = useMemo(() => {
|
||||||
|
const result = { ...defaultContext };
|
||||||
|
|
||||||
|
if (props.defaultOpenMode !== undefined) {
|
||||||
|
result.defaultOpenMode = props.defaultOpenMode;
|
||||||
|
}
|
||||||
|
if (props.openModeToComponent !== undefined) {
|
||||||
|
result.openModeToComponent = props.openModeToComponent;
|
||||||
|
}
|
||||||
|
if (props.hideOpenMode !== undefined) {
|
||||||
|
result.hideOpenMode = props.hideOpenMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [props.defaultOpenMode, props.openModeToComponent, props.hideOpenMode]);
|
||||||
|
|
||||||
|
const getComponentByOpenMode = useCallback(
|
||||||
|
(openMode: OpenMode) => {
|
||||||
|
const result = context.openModeToComponent[openMode];
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(`OpenModeProvider: openModeToComponent[${openMode}] is not defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[context],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return {
|
||||||
|
defaultOpenMode: context.defaultOpenMode,
|
||||||
|
hideOpenMode: context.hideOpenMode,
|
||||||
|
getComponentByOpenMode,
|
||||||
|
};
|
||||||
|
}, [context.defaultOpenMode, context.hideOpenMode, getComponentByOpenMode]);
|
||||||
|
|
||||||
|
return <OpenModeContext.Provider value={value}>{props.children}</OpenModeContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOpenModeContext = () => {
|
||||||
|
return React.useContext(OpenModeContext);
|
||||||
|
};
|
@ -47,7 +47,8 @@ export const SettingsCenterDropdown = () => {
|
|||||||
return {
|
return {
|
||||||
key: setting.name,
|
key: setting.name,
|
||||||
icon: setting.icon,
|
icon: setting.icon,
|
||||||
label: <Link to={setting.path}>{compile(setting.title)}</Link>,
|
label: setting.link ? <div onClick={() => window.open(setting.link)}>{compile(setting.title)}</div> :
|
||||||
|
<Link to={setting.path}>{compile(setting.title)}</Link>
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [app, t]);
|
}, [app, t]);
|
||||||
|
@ -144,6 +144,12 @@ export const AdminSettingsLayout = () => {
|
|||||||
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
||||||
onClick={({ key }) => {
|
onClick={({ key }) => {
|
||||||
const plugin = settings.find((item) => item.name === key);
|
const plugin = settings.find((item) => item.name === key);
|
||||||
|
|
||||||
|
if (plugin.link) {
|
||||||
|
window.open(plugin.link, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (plugin.children?.length) {
|
if (plugin.children?.length) {
|
||||||
return navigate(getFirstDeepChildPath(plugin.children));
|
return navigate(getFirstDeepChildPath(plugin.children));
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,21 +10,19 @@
|
|||||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useActionContext } from '.';
|
import { useActionContext } from '.';
|
||||||
import { ActionDrawer } from './Action.Drawer';
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { ActionModal } from './Action.Modal';
|
import { useCurrentPopupContext } from '../page/PagePopups';
|
||||||
import { ActionPage } from './Action.Page';
|
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
|
|
||||||
export const ActionContainer: ComposedActionDrawer = observer(
|
export const ActionContainer: ComposedActionDrawer = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { openMode } = useActionContext();
|
const { openMode } = useActionContext();
|
||||||
if (openMode === 'drawer') {
|
const { getComponentByOpenMode } = useOpenModeContext();
|
||||||
return <ActionDrawer footerNodeName={'Action.Container.Footer'} {...props} />;
|
const { currentLevel } = useCurrentPopupContext();
|
||||||
}
|
|
||||||
if (openMode === 'modal') {
|
const Component = getComponentByOpenMode(openMode);
|
||||||
return <ActionModal footerNodeName={'Action.Container.Footer'} {...props} />;
|
|
||||||
}
|
return <Component footerNodeName={'Action.Container.Footer'} level={currentLevel} {...props} />;
|
||||||
return <ActionPage footerNodeName={'Action.Container.Footer'} {...props} />;
|
|
||||||
},
|
},
|
||||||
{ displayName: 'ActionContainer' },
|
{ displayName: 'ActionContainer' },
|
||||||
);
|
);
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
||||||
import { FlagProvider } from '../../../flag-provider';
|
import { FlagProvider } from '../../../flag-provider';
|
||||||
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||||
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { GeneralSchemaDesigner } from '../../../schema-settings/GeneralSchemaDesigner';
|
import { GeneralSchemaDesigner } from '../../../schema-settings/GeneralSchemaDesigner';
|
||||||
import {
|
import {
|
||||||
@ -656,6 +657,7 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
|||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const isPopupAction = [
|
const isPopupAction = [
|
||||||
'create',
|
'create',
|
||||||
@ -667,8 +669,8 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
|||||||
].includes(fieldSchema['x-action'] || '');
|
].includes(fieldSchema['x-action'] || '');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openMode: isPopupAction,
|
openMode: isPopupAction && !hideOpenMode,
|
||||||
openSize: isPopupAction,
|
openSize: isPopupAction && !hideOpenMode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -11,25 +11,26 @@ import { RecursionField, observer, useFieldSchema } from '@formily/react';
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useActionContext } from '.';
|
import { useActionContext } from '.';
|
||||||
import { useCurrentPopupContext } from '../page/PagePopups';
|
import { BackButtonUsedInSubPage } from '../page/BackButtonUsedInSubPage';
|
||||||
|
import { TabsContextProvider, useTabsContext } from '../tabs/context';
|
||||||
import { useActionPageStyle } from './Action.Page.style';
|
import { useActionPageStyle } from './Action.Page.style';
|
||||||
import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
|
|
||||||
export const ActionPage: ComposedActionDrawer = observer(
|
export const ActionPage: ComposedActionDrawer = observer(
|
||||||
() => {
|
({ level }) => {
|
||||||
const filedSchema = useFieldSchema();
|
const filedSchema = useFieldSchema();
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
const { getContainerDOM } = usePopupOrSubpagesContainerDOM();
|
const { getContainerDOM } = usePopupOrSubpagesContainerDOM();
|
||||||
const { styles } = useActionPageStyle();
|
const { styles } = useActionPageStyle();
|
||||||
const { currentLevel } = useCurrentPopupContext();
|
const tabContext = useTabsContext();
|
||||||
|
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
// 20 is the z-index value of the main page
|
// 20 is the z-index value of the main page
|
||||||
zIndex: 20 + currentLevel,
|
zIndex: 20 + level,
|
||||||
};
|
};
|
||||||
}, [currentLevel]);
|
}, [level]);
|
||||||
|
|
||||||
if (!ctx.visible) {
|
if (!ctx.visible) {
|
||||||
return null;
|
return null;
|
||||||
@ -37,11 +38,19 @@ export const ActionPage: ComposedActionDrawer = observer(
|
|||||||
|
|
||||||
const actionPageNode = (
|
const actionPageNode = (
|
||||||
<div className={styles.container} style={style}>
|
<div className={styles.container} style={style}>
|
||||||
|
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />}>
|
||||||
<RecursionField schema={filedSchema} onlyRenderProperties />
|
<RecursionField schema={filedSchema} onlyRenderProperties />
|
||||||
|
</TabsContextProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return createPortal(actionPageNode, getContainerDOM());
|
const container = getContainerDOM();
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
return createPortal(actionPageNode, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionPageNode;
|
||||||
},
|
},
|
||||||
{ displayName: 'ActionPage' },
|
{ displayName: 'ActionPage' },
|
||||||
);
|
);
|
||||||
|
@ -79,7 +79,7 @@ export const ActionBar = withDynamicSchemaProps(
|
|||||||
>
|
>
|
||||||
{props.children && (
|
{props.children && (
|
||||||
<div>
|
<div>
|
||||||
<Space {...spaceProps} style={{ flexWrap: 'wrap' }}>
|
<Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}>
|
||||||
{fieldSchema.mapProperties((schema, key) => {
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
return <RecursionField key={key} name={key} schema={schema} />;
|
return <RecursionField key={key} name={key} schema={schema} />;
|
||||||
})}
|
})}
|
||||||
@ -129,7 +129,7 @@ export const ActionBar = withDynamicSchemaProps(
|
|||||||
return <RecursionField key={key} name={key} schema={schema} />;
|
return <RecursionField key={key} name={key} schema={schema} />;
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
<Space {...spaceProps} style={{ flexWrap: 'wrap' }}>
|
<Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}>
|
||||||
{fieldSchema.mapProperties((schema, key) => {
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
if (schema['x-align'] === 'left') {
|
if (schema['x-align'] === 'left') {
|
||||||
return null;
|
return null;
|
||||||
|
@ -87,7 +87,11 @@ export type ComposedAction = React.FC<ActionProps> & {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionDrawerProps<T = DrawerProps> = T & { footerNodeName?: string };
|
export type ActionDrawerProps<T = DrawerProps> = T & {
|
||||||
|
footerNodeName?: string;
|
||||||
|
/** 当前弹窗嵌套的层级 */
|
||||||
|
level?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
|
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
|
||||||
Footer?: React.FC;
|
Footer?: React.FC;
|
||||||
|
@ -13,6 +13,7 @@ import React, { FC, Fragment, useRef, useState } from 'react';
|
|||||||
import { useDesignable } from '../../';
|
import { useDesignable } from '../../';
|
||||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||||
import { useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
import { useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
||||||
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContextProvider, useActionContext } from '../action';
|
import { ActionContextProvider, useActionContext } from '../action';
|
||||||
@ -139,6 +140,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
|||||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||||
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||||
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
|
|
||||||
const btnElement = (
|
const btnElement = (
|
||||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||||
@ -175,7 +177,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
|||||||
setVisible?.(value);
|
setVisible?.(value);
|
||||||
setVisibleWithURL?.(value);
|
setVisibleWithURL?.(value);
|
||||||
},
|
},
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
snapshot: collectionField?.interface === 'snapshot',
|
snapshot: collectionField?.interface === 'snapshot',
|
||||||
fieldSchema: fieldSchema,
|
fieldSchema: fieldSchema,
|
||||||
}}
|
}}
|
||||||
|
@ -73,13 +73,14 @@ const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => {
|
|||||||
export interface BlockItemProps {
|
export interface BlockItemProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
||||||
(props) => {
|
(props) => {
|
||||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||||
const { className, children } = useProps(props);
|
const { className, children, style } = useProps(props);
|
||||||
const { styles: blockItemCss } = useStyles();
|
const { styles: blockItemCss } = useStyles();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { render } = useSchemaToolbarRender(fieldSchema);
|
const { render } = useSchemaToolbarRender(fieldSchema);
|
||||||
@ -87,7 +88,12 @@ export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
|||||||
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
|
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableItem role="button" aria-label={label} className={cls('nb-block-item', className, blockItemCss)}>
|
<SortableItem
|
||||||
|
role="button"
|
||||||
|
aria-label={label}
|
||||||
|
className={cls('nb-block-item', className, blockItemCss)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
{render()}
|
{render()}
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={(err) => console.log(err)}>
|
<ErrorBoundary FallbackComponent={ErrorFallback} onError={(err) => console.log(err)}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -13,7 +13,7 @@ import { Select, SelectProps, Tag } from 'antd';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCompile } from '../../hooks/useCompile';
|
import { useCompile } from '../../hooks/useCompile';
|
||||||
|
|
||||||
const colors = {
|
const defaultColors = {
|
||||||
red: '{{t("Red")}}',
|
red: '{{t("Red")}}',
|
||||||
magenta: '{{t("Magenta")}}',
|
magenta: '{{t("Magenta")}}',
|
||||||
volcano: '{{t("Volcano")}}',
|
volcano: '{{t("Volcano")}}',
|
||||||
@ -30,13 +30,15 @@ const colors = {
|
|||||||
|
|
||||||
export interface ColorSelectProps extends SelectProps {
|
export interface ColorSelectProps extends SelectProps {
|
||||||
suffix?: React.ReactNode;
|
suffix?: React.ReactNode;
|
||||||
|
colors?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColorSelect = connect(
|
export const ColorSelect = connect(
|
||||||
(props: ColorSelectProps) => {
|
(props: ColorSelectProps) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const { colors = defaultColors, ...selectProps } = props;
|
||||||
return (
|
return (
|
||||||
<Select {...props}>
|
<Select {...selectProps}>
|
||||||
{Object.keys(colors).map((color) => (
|
{Object.keys(colors).map((color) => (
|
||||||
<Select.Option key={color} value={color}>
|
<Select.Option key={color} value={color}>
|
||||||
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
|
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
|
||||||
@ -53,7 +55,7 @@ export const ColorSelect = connect(
|
|||||||
}),
|
}),
|
||||||
mapReadPretty((props) => {
|
mapReadPretty((props) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { value } = props;
|
const { value, colors = defaultColors } = props;
|
||||||
if (!colors[value]) {
|
if (!colors[value]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
} from '../../../collection-manager';
|
} from '../../../collection-manager';
|
||||||
import { useCollectionManager } from '../../../data-source';
|
import { useCollectionManager } from '../../../data-source';
|
||||||
import { useFlag } from '../../../flag-provider';
|
import { useFlag } from '../../../flag-provider';
|
||||||
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { useRecord } from '../../../record-provider';
|
import { useRecord } from '../../../record-provider';
|
||||||
import { useColumnSchema } from '../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
import { useColumnSchema } from '../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
||||||
import { generalSettingsItems } from '../../../schema-items/GeneralSettings';
|
import { generalSettingsItems } from '../../../schema-items/GeneralSettings';
|
||||||
@ -54,6 +55,7 @@ export const allowAddNew: SchemaSettingsItemType = {
|
|||||||
return !flag?.isInSubTable && !readPretty && isAssociationField && ['Picker'].includes(fieldMode);
|
return !flag?.isInSubTable && !readPretty && isAssociationField && ['Picker'].includes(fieldMode);
|
||||||
},
|
},
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -83,7 +85,7 @@ export const allowAddNew: SchemaSettingsItemType = {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
component: 'CreateRecordAction',
|
component: 'CreateRecordAction',
|
||||||
},
|
},
|
||||||
@ -540,6 +542,7 @@ export const formItemSettings = new SchemaSettings({
|
|||||||
return !readPretty && isAssociationField && ['Select'].includes(fieldMode);
|
return !readPretty && isAssociationField && ['Select'].includes(fieldMode);
|
||||||
},
|
},
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -572,7 +575,7 @@ export const formItemSettings = new SchemaSettings({
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'drawer',
|
openMode: defaultOpenMode,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
component: 'CreateRecordAction',
|
component: 'CreateRecordAction',
|
||||||
},
|
},
|
||||||
|
@ -14,14 +14,25 @@ import { useToken } from '../../../style';
|
|||||||
import { useCurrentPopupContext } from './PagePopups';
|
import { useCurrentPopupContext } from './PagePopups';
|
||||||
import { usePagePopup } from './pagePopupUtils';
|
import { usePagePopup } from './pagePopupUtils';
|
||||||
|
|
||||||
|
export const useBackButton = () => {
|
||||||
|
const { params } = useCurrentPopupContext();
|
||||||
|
const { closePopup } = usePagePopup();
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
closePopup(params?.popupuid);
|
||||||
|
}, [closePopup, params?.popupuid]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
goBack,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for the back button in subpages
|
* Used for the back button in subpages
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const BackButtonUsedInSubPage = () => {
|
export const BackButtonUsedInSubPage = () => {
|
||||||
const { params } = useCurrentPopupContext();
|
|
||||||
const { closePopup } = usePagePopup();
|
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
|
const { goBack } = useBackButton();
|
||||||
// tab item gutter, this is fixed value in antd
|
// tab item gutter, this is fixed value in antd
|
||||||
const horizontalItemGutter = 32;
|
const horizontalItemGutter = 32;
|
||||||
|
|
||||||
@ -35,17 +46,7 @@ export const BackButtonUsedInSubPage = () => {
|
|||||||
};
|
};
|
||||||
}, [token.paddingXS]);
|
}, [token.paddingXS]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
closePopup(params.popupuid);
|
|
||||||
}, [params.popupuid]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button aria-label="back-button" type="text" icon={<ArrowLeftOutlined />} style={resetStyle} onClick={goBack} />
|
||||||
aria-label="back-button"
|
|
||||||
type="text"
|
|
||||||
icon={<ArrowLeftOutlined />}
|
|
||||||
style={resetStyle}
|
|
||||||
onClick={handleClick}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -110,7 +110,7 @@ export const Page = (props) => {
|
|||||||
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
||||||
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
||||||
}}
|
}}
|
||||||
onTabClick={(activeKey) => {
|
onChange={(activeKey) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
navigate(`/admin/${pageUid}/tabs/${activeKey}`, { replace: true });
|
navigate(`/admin/${pageUid}/tabs/${activeKey}`, { replace: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -19,7 +19,6 @@ import { DataBlockProvider } from '../../../data-source/data-block/DataBlockProv
|
|||||||
import { BlockRequestContext } from '../../../data-source/data-block/DataBlockRequestProvider';
|
import { BlockRequestContext } from '../../../data-source/data-block/DataBlockRequestProvider';
|
||||||
import { SchemaComponent } from '../../core';
|
import { SchemaComponent } from '../../core';
|
||||||
import { TabsContextProvider } from '../tabs/context';
|
import { TabsContextProvider } from '../tabs/context';
|
||||||
import { BackButtonUsedInSubPage } from './BackButtonUsedInSubPage';
|
|
||||||
import { usePopupSettings } from './PopupSettingsProvider';
|
import { usePopupSettings } from './PopupSettingsProvider';
|
||||||
import { deleteRandomNestedSchemaKey, getRandomNestedSchemaKey } from './nestedSchemaKeyStorage';
|
import { deleteRandomNestedSchemaKey, getRandomNestedSchemaKey } from './nestedSchemaKeyStorage';
|
||||||
import { PopupParams, getPopupParamsFromPath, getStoredPopupContext, usePagePopup } from './pagePopupUtils';
|
import { PopupParams, getPopupParamsFromPath, getStoredPopupContext, usePagePopup } from './pagePopupUtils';
|
||||||
@ -87,22 +86,20 @@ const PopupParamsProvider: FC<Omit<PopupProps, 'hidden'>> = (props) => {
|
|||||||
|
|
||||||
const PopupTabsPropsProvider: FC<{ params: PopupParams }> = ({ children, params }) => {
|
const PopupTabsPropsProvider: FC<{ params: PopupParams }> = ({ children, params }) => {
|
||||||
const { changeTab } = usePagePopup();
|
const { changeTab } = usePagePopup();
|
||||||
const onTabClick = useCallback(
|
const onChange = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
changeTab(key);
|
changeTab(key);
|
||||||
},
|
},
|
||||||
[changeTab],
|
[changeTab],
|
||||||
);
|
);
|
||||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
const { isSubPage } = useCurrentPopupContext();
|
|
||||||
const tabBarExtraContent = useMemo(() => (isSubPage ? <BackButtonUsedInSubPage /> : null), [isSubPage]);
|
|
||||||
|
|
||||||
if (!isPopupVisibleControlledByURL()) {
|
if (!isPopupVisibleControlledByURL()) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContextProvider activeKey={params.tab} onTabClick={onTabClick} tabBarExtraContent={tabBarExtraContent}>
|
<TabsContextProvider activeKey={params.tab} onChange={onChange}>
|
||||||
{children}
|
{children}
|
||||||
</TabsContextProvider>
|
</TabsContextProvider>
|
||||||
);
|
);
|
||||||
@ -276,7 +273,7 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => {
|
|||||||
context={popupPropsRef.current[0].context}
|
context={popupPropsRef.current[0].context}
|
||||||
currentLevel={1}
|
currentLevel={1}
|
||||||
>
|
>
|
||||||
<SchemaComponent components={components} schema={rootSchema} onlyRenderProperties />;
|
<SchemaComponent components={components} schema={rootSchema} onlyRenderProperties />
|
||||||
</PagePopupsItemProvider>
|
</PagePopupsItemProvider>
|
||||||
</AllPopupsPropsProviderContext.Provider>
|
</AllPopupsPropsProviderContext.Provider>
|
||||||
);
|
);
|
||||||
@ -351,6 +348,7 @@ function get404Schema() {
|
|||||||
'x-component': 'Action.Container',
|
'x-component': 'Action.Container',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
className: 'nb-action-popup',
|
className: 'nb-action-popup',
|
||||||
|
level: 99, // 确保在最上层
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -17,8 +17,15 @@ export const usePopupSettings = () => {
|
|||||||
const isPopupVisibleControlledByURL = useCallback(() => {
|
const isPopupVisibleControlledByURL = useCallback(() => {
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
return pathname?.includes('/admin/') && !hash?.includes('/mobile');
|
const isOldMobileMode = pathname?.includes('/mobile/') || hash?.includes('/mobile/');
|
||||||
|
const isNewMobileMode = pathname?.includes('/m/');
|
||||||
|
const isPCMode = pathname?.includes('/admin/');
|
||||||
|
|
||||||
|
return (isPCMode || isNewMobileMode) && !isOldMobileMode;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { isPopupVisibleControlledByURL };
|
return {
|
||||||
|
/** 弹窗窗口的显隐是否由 URL 控制 */
|
||||||
|
isPopupVisibleControlledByURL,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -7,8 +7,10 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { BackButtonUsedInSubPage, useBackButton } from './BackButtonUsedInSubPage';
|
||||||
export * from './FixedBlock';
|
export * from './FixedBlock';
|
||||||
export * from './FixedBlockDesignerItem';
|
export * from './FixedBlockDesignerItem';
|
||||||
export * from './Page';
|
export * from './Page';
|
||||||
export * from './Page.Settings';
|
export * from './Page.Settings';
|
||||||
|
export { PagePopups } from './PagePopups';
|
||||||
export * from './PageTab.Settings';
|
export * from './PageTab.Settings';
|
||||||
|
@ -129,7 +129,7 @@ export const usePagePopup = () => {
|
|||||||
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
|
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
|
||||||
[parentRecord, association],
|
[parentRecord, association],
|
||||||
);
|
);
|
||||||
const currentPopupUidWithoutOpened = fieldSchema['x-uid'];
|
const currentPopupUidWithoutOpened = fieldSchema?.['x-uid'];
|
||||||
|
|
||||||
const getNewPathname = useCallback(
|
const getNewPathname = useCallback(
|
||||||
({
|
({
|
||||||
|
@ -22,6 +22,8 @@ export const DesignableSwitch = () => {
|
|||||||
const style = {};
|
const style = {};
|
||||||
if (designable) {
|
if (designable) {
|
||||||
style['backgroundColor'] = 'var(--colorSettings)';
|
style['backgroundColor'] = 'var(--colorSettings)';
|
||||||
|
} else {
|
||||||
|
style['backgroundColor'] = 'transparent';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快捷键切换编辑状态
|
// 快捷键切换编辑状态
|
||||||
|
@ -11,7 +11,7 @@ import { createForm } from '@formily/core';
|
|||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { useSchemaComponentContext } from '../hooks';
|
import { useComponent, useSchemaComponentContext } from '../hooks';
|
||||||
import { FormProvider } from './FormProvider';
|
import { FormProvider } from './FormProvider';
|
||||||
import { SchemaComponent } from './SchemaComponent';
|
import { SchemaComponent } from './SchemaComponent';
|
||||||
import { useRequestSchema } from './useRequestSchema';
|
import { useRequestSchema } from './useRequestSchema';
|
||||||
@ -26,6 +26,12 @@ export interface RemoteSchemaComponentProps {
|
|||||||
hidden?: any;
|
hidden?: any;
|
||||||
onlyRenderProperties?: boolean;
|
onlyRenderProperties?: boolean;
|
||||||
noForm?: boolean;
|
noForm?: boolean;
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
memoized?: boolean;
|
||||||
|
NotFoundPage?: React.ComponentType | string;
|
||||||
|
onPageNotFind?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTransform = (s: Schema) => s;
|
const defaultTransform = (s: Schema) => s;
|
||||||
@ -37,9 +43,12 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
hidden,
|
hidden,
|
||||||
scope,
|
scope,
|
||||||
uid,
|
uid,
|
||||||
|
memoized = true,
|
||||||
components,
|
components,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
NotFoundPage,
|
||||||
schemaTransform = defaultTransform,
|
schemaTransform = defaultTransform,
|
||||||
|
onPageNotFind,
|
||||||
} = props;
|
} = props;
|
||||||
const { reset } = useSchemaComponentContext();
|
const { reset } = useSchemaComponentContext();
|
||||||
const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema';
|
const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema';
|
||||||
@ -55,12 +64,20 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
reset && reset();
|
reset && reset();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (loading) {
|
const NotFoundComponent = useComponent(NotFoundPage);
|
||||||
return <Spin />;
|
if (loading || hidden) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (hidden) {
|
|
||||||
return <Spin />;
|
if (!schema || Object.keys(schema).length === 0) {
|
||||||
|
onPageNotFind && onPageNotFind();
|
||||||
|
return NotFoundComponent ? <NotFoundComponent /> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return noForm ? (
|
return noForm ? (
|
||||||
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -189,6 +189,12 @@ export interface SchemaToolbarProps {
|
|||||||
*/
|
*/
|
||||||
showBorder?: boolean;
|
showBorder?: boolean;
|
||||||
showBackground?: boolean;
|
showBackground?: boolean;
|
||||||
|
toolbarClassName?: string;
|
||||||
|
toolbarStyle?: React.CSSProperties;
|
||||||
|
spaceWrapperClassName?: string;
|
||||||
|
spaceWrapperStyle?: React.CSSProperties;
|
||||||
|
spaceClassName?: string;
|
||||||
|
spaceStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||||
@ -198,11 +204,17 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
|||||||
initializer,
|
initializer,
|
||||||
settings,
|
settings,
|
||||||
showBackground,
|
showBackground,
|
||||||
|
spaceWrapperClassName,
|
||||||
|
spaceWrapperStyle,
|
||||||
showBorder = true,
|
showBorder = true,
|
||||||
draggable = true,
|
draggable = true,
|
||||||
|
spaceClassName,
|
||||||
|
spaceStyle,
|
||||||
|
toolbarClassName,
|
||||||
|
toolbarStyle = {},
|
||||||
} = {
|
} = {
|
||||||
...props,
|
...props,
|
||||||
...(fieldSchema['x-toolbar-props'] || {}),
|
...(fieldSchema?.['x-toolbar-props'] || {}),
|
||||||
} as SchemaToolbarProps;
|
} as SchemaToolbarProps;
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
@ -220,12 +232,12 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
|||||||
if (Array.isArray(title)) return title.map((item) => compile(item));
|
if (Array.isArray(title)) return title.map((item) => compile(item));
|
||||||
}, [compile, title]);
|
}, [compile, title]);
|
||||||
const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender(
|
const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender(
|
||||||
settings || fieldSchema['x-settings'],
|
settings || fieldSchema?.['x-settings'],
|
||||||
fieldSchema['x-settings-props'],
|
fieldSchema?.['x-settings-props'],
|
||||||
);
|
);
|
||||||
const { render: schemaInitializerRender, exists: schemaInitializerExists } = useSchemaInitializerRender(
|
const { render: schemaInitializerRender, exists: schemaInitializerExists } = useSchemaInitializerRender(
|
||||||
initializer || fieldSchema['x-initializer'],
|
initializer || fieldSchema?.['x-initializer'],
|
||||||
fieldSchema['x-initializer-props'],
|
fieldSchema?.['x-initializer-props'],
|
||||||
);
|
);
|
||||||
const rowCtx = useGridRowContext();
|
const rowCtx = useGridRowContext();
|
||||||
const gridContext = useGridContext();
|
const gridContext = useGridContext();
|
||||||
@ -316,8 +328,8 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={toolbarRef}
|
ref={toolbarRef}
|
||||||
className={styles.toolbar}
|
className={classNames(styles.toolbar, toolbarClassName, 'schema-toolbar')}
|
||||||
style={{ border: showBorder ? 'auto' : 0, background: showBackground ? 'auto' : 0 }}
|
style={{ border: showBorder ? 'auto' : 0, background: showBackground ? 'auto' : 0, ...toolbarStyle }}
|
||||||
>
|
>
|
||||||
{titleArr && (
|
{titleArr && (
|
||||||
<div className={styles.toolbarTitle}>
|
<div className={styles.toolbarTitle}>
|
||||||
@ -333,8 +345,8 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.toolbarIcons}>
|
<div className={classNames(styles.toolbarIcons, spaceWrapperClassName)} style={spaceWrapperStyle}>
|
||||||
<Space size={3} align={'center'}>
|
<Space size={3} align={'center'} className={spaceClassName} style={spaceStyle}>
|
||||||
{dragElement}
|
{dragElement}
|
||||||
{initializerElement}
|
{initializerElement}
|
||||||
{settingsElement}
|
{settingsElement}
|
||||||
|
@ -447,15 +447,17 @@ export const SchemaSettingsDivider = function Divider() {
|
|||||||
|
|
||||||
export interface SchemaSettingsRemoveProps {
|
export interface SchemaSettingsRemoveProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
confirm?: ModalFuncProps;
|
confirm?: ModalFuncProps;
|
||||||
removeParentsIfNoChildren?: boolean;
|
removeParentsIfNoChildren?: boolean;
|
||||||
breakRemoveOn?: ISchema | ((s: ISchema) => boolean);
|
breakRemoveOn?: ISchema | ((s: ISchema) => boolean);
|
||||||
}
|
}
|
||||||
export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
||||||
const { disabled, confirm, removeParentsIfNoChildren, breakRemoveOn } = props;
|
const { disabled, confirm, title, removeParentsIfNoChildren, breakRemoveOn } = props;
|
||||||
const { dn, template } = useSchemaSettings();
|
const { dn, template } = useSchemaSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
|
const compile = useCompile();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const ctx = useBlockTemplateContext();
|
const ctx = useBlockTemplateContext();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
@ -470,7 +472,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
|||||||
eventKey="remove"
|
eventKey="remove"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: t('Delete block'),
|
title: title ? compile(title) : t('Delete block'),
|
||||||
content: t('Are you sure you want to delete it?'),
|
content: t('Are you sure you want to delete it?'),
|
||||||
...confirm,
|
...confirm,
|
||||||
async onOk() {
|
async onOk() {
|
||||||
@ -602,10 +604,22 @@ export interface SchemaSettingsActionModalItemProps
|
|||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
beforeOpen?: () => void;
|
beforeOpen?: () => void;
|
||||||
maskClosable?: boolean;
|
maskClosable?: boolean;
|
||||||
|
width?: string | number;
|
||||||
}
|
}
|
||||||
export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProps> = React.memo((props) => {
|
export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProps> = React.memo((props) => {
|
||||||
const { title, onSubmit, initialValues, beforeOpen, initialSchema, schema, modalTip, components, scope, ...others } =
|
const {
|
||||||
props;
|
title,
|
||||||
|
onSubmit,
|
||||||
|
width = '50%',
|
||||||
|
initialValues,
|
||||||
|
beforeOpen,
|
||||||
|
initialSchema,
|
||||||
|
schema,
|
||||||
|
modalTip,
|
||||||
|
components,
|
||||||
|
scope,
|
||||||
|
...others
|
||||||
|
} = props;
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [schemaUid, setSchemaUid] = useState<string>(props.uid);
|
const [schemaUid, setSchemaUid] = useState<string>(props.uid);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -636,8 +650,12 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
|
|||||||
|
|
||||||
const submitHandler = useCallback(async () => {
|
const submitHandler = useCallback(async () => {
|
||||||
await form.submit();
|
await form.submit();
|
||||||
onSubmit?.(cloneDeep(form.values));
|
try {
|
||||||
|
await onSubmit?.(cloneDeep(form.values));
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}, [form, onSubmit]);
|
}, [form, onSubmit]);
|
||||||
|
|
||||||
const openAssignedFieldValueHandler = useCallback(async () => {
|
const openAssignedFieldValueHandler = useCallback(async () => {
|
||||||
@ -667,7 +685,7 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
|
|||||||
</SchemaSettingsItem>
|
</SchemaSettingsItem>
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<Modal
|
<Modal
|
||||||
width={'50%'}
|
width={width}
|
||||||
title={compile(title)}
|
title={compile(title)}
|
||||||
{...others}
|
{...others}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
@ -704,7 +722,7 @@ SchemaSettingsActionModalItem.displayName = 'SchemaSettingsActionModalItem';
|
|||||||
|
|
||||||
export interface SchemaSettingsModalItemProps {
|
export interface SchemaSettingsModalItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
onSubmit: (values: any) => void;
|
onSubmit: (values: any) => Promise<any> | void;
|
||||||
initialValues?: any;
|
initialValues?: any;
|
||||||
schema?: ISchema | (() => ISchema);
|
schema?: ISchema | (() => ISchema);
|
||||||
modalTip?: string;
|
modalTip?: string;
|
||||||
|
@ -183,6 +183,15 @@ export interface PageConfig {
|
|||||||
keepUid?: boolean;
|
keepUid?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MobilePageConfig extends Omit<PageConfig, 'type'> {
|
||||||
|
type?: 'page' | 'link';
|
||||||
|
/**
|
||||||
|
* 页面的基础路径
|
||||||
|
* @default '/m/'
|
||||||
|
*/
|
||||||
|
basePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePageOptions {
|
interface CreatePageOptions {
|
||||||
type?: PageConfig['type'];
|
type?: PageConfig['type'];
|
||||||
url?: PageConfig['url'];
|
url?: PageConfig['url'];
|
||||||
@ -192,6 +201,10 @@ interface CreatePageOptions {
|
|||||||
keepUid?: boolean;
|
keepUid?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateMobilePageOptions extends Omit<CreatePageOptions, 'type'> {
|
||||||
|
type?: Omit<PageConfig['type'], 'group'>;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExtendUtils {
|
interface ExtendUtils {
|
||||||
page?: Page;
|
page?: Page;
|
||||||
/**
|
/**
|
||||||
@ -200,6 +213,12 @@ interface ExtendUtils {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
mockPage: (pageConfig?: PageConfig) => NocoPage;
|
mockPage: (pageConfig?: PageConfig) => NocoPage;
|
||||||
|
/**
|
||||||
|
* 根据配置,生成一个移动端 NocoBase 的页面
|
||||||
|
* @param pageConfig 页面配置
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
mockMobilePage: (pageConfig?: MobilePageConfig) => NocoMobilePage;
|
||||||
/**
|
/**
|
||||||
* 根据配置,生成一个需要手动销毁的 NocoPage 页面
|
* 根据配置,生成一个需要手动销毁的 NocoPage 页面
|
||||||
* @param pageConfig
|
* @param pageConfig
|
||||||
@ -299,14 +318,14 @@ const PORT = process.env.APP_PORT || 20000;
|
|||||||
const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`;
|
const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`;
|
||||||
|
|
||||||
export class NocoPage {
|
export class NocoPage {
|
||||||
private url: string;
|
protected url: string;
|
||||||
private uid: string | undefined;
|
protected uid: string | undefined;
|
||||||
private collectionsName: string[] | undefined;
|
protected collectionsName: string[] | undefined;
|
||||||
private _waitForInit: Promise<void>;
|
protected _waitForInit: Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private options?: PageConfig,
|
protected options?: PageConfig,
|
||||||
private page?: Page,
|
protected page?: Page,
|
||||||
) {
|
) {
|
||||||
this._waitForInit = this.init();
|
this._waitForInit = this.init();
|
||||||
}
|
}
|
||||||
@ -374,6 +393,57 @@ export class NocoPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NocoMobilePage extends NocoPage {
|
||||||
|
protected routeId: number;
|
||||||
|
protected title: string;
|
||||||
|
constructor(
|
||||||
|
protected options?: MobilePageConfig,
|
||||||
|
protected page?: Page,
|
||||||
|
) {
|
||||||
|
super(options, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const waitList = [];
|
||||||
|
if (this.options?.collections?.length) {
|
||||||
|
const collections: any = omitSomeFields(this.options.collections);
|
||||||
|
this.collectionsName = collections.map((item) => item.name);
|
||||||
|
|
||||||
|
waitList.push(createCollections(collections));
|
||||||
|
}
|
||||||
|
|
||||||
|
waitList.push(createMobilePage(this.options));
|
||||||
|
|
||||||
|
const result = await Promise.all(waitList);
|
||||||
|
|
||||||
|
const { url, pageSchemaUid, routeId, title } = result[result.length - 1];
|
||||||
|
this.title = title;
|
||||||
|
this.routeId = routeId;
|
||||||
|
this.uid = pageSchemaUid;
|
||||||
|
if (this.options?.type == 'link') {
|
||||||
|
// 内部 URL 和外部 URL
|
||||||
|
if (url?.startsWith('/')) {
|
||||||
|
this.url = `${this.options?.basePath || '/m'}${url}`;
|
||||||
|
} else {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.url = `${this.options?.basePath || '/m'}${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mobileDestroy() {
|
||||||
|
// 移除 mobile routes
|
||||||
|
await deleteMobileRoutes(this.routeId);
|
||||||
|
// 移除 schema
|
||||||
|
await this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _page: Page;
|
let _page: Page;
|
||||||
const getPage = async (browser: Browser) => {
|
const getPage = async (browser: Browser) => {
|
||||||
if (!_page) {
|
if (!_page) {
|
||||||
@ -412,6 +482,33 @@ const _test = base.extend<ExtendUtils>({
|
|||||||
|
|
||||||
await Promise.all(waitList);
|
await Promise.all(waitList);
|
||||||
},
|
},
|
||||||
|
mockMobilePage: async ({ browser }, use) => {
|
||||||
|
// 保证每个测试运行时 faker 的随机值都是一样的
|
||||||
|
// faker.seed(1);
|
||||||
|
|
||||||
|
const page = await getPage(browser);
|
||||||
|
const nocoPages: NocoMobilePage[] = [];
|
||||||
|
const mockPage = (config?: MobilePageConfig) => {
|
||||||
|
const nocoPage = new NocoMobilePage(config, page);
|
||||||
|
nocoPages.push(nocoPage);
|
||||||
|
return nocoPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
await use(mockPage);
|
||||||
|
|
||||||
|
const waitList = [];
|
||||||
|
|
||||||
|
// 测试运行完自动销毁页面
|
||||||
|
for (const nocoPage of nocoPages) {
|
||||||
|
// 这里之所以不加入 waitList 是因为会导致 acl 的测试报错
|
||||||
|
await nocoPage.mobileDestroy();
|
||||||
|
}
|
||||||
|
waitList.push(setDefaultRole('root'));
|
||||||
|
// 删除掉 id 不是 1 的 users 和 name 不是 root admin member 的 roles
|
||||||
|
waitList.push(removeRedundantUserAndRoles());
|
||||||
|
|
||||||
|
await Promise.all(waitList);
|
||||||
|
},
|
||||||
mockManualDestroyPage: async ({ browser }, use) => {
|
mockManualDestroyPage: async ({ browser }, use) => {
|
||||||
const mockManualDestroyPage = (config?: PageConfig) => {
|
const mockManualDestroyPage = (config?: PageConfig) => {
|
||||||
const nocoPage = new NocoPage(config);
|
const nocoPage = new NocoPage(config);
|
||||||
@ -684,6 +781,196 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
return pageUid;
|
return pageUid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 NocoBase 中创建一个移动端页面
|
||||||
|
*/
|
||||||
|
const createMobilePage = async (options?: CreateMobilePageOptions) => {
|
||||||
|
const { type = 'page', url, name, pageSchema, keepUid } = options || {};
|
||||||
|
function randomStr() {
|
||||||
|
return Math.random().toString(36).substring(2);
|
||||||
|
}
|
||||||
|
const api = await request.newContext({
|
||||||
|
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||||
|
});
|
||||||
|
const state = await api.storageState();
|
||||||
|
const headers = getHeaders(state);
|
||||||
|
const pageSchemaUid = name || uid();
|
||||||
|
const schemaUrl = `/page/${pageSchemaUid}`;
|
||||||
|
const firstTabUid = uid();
|
||||||
|
const title = name || randomStr();
|
||||||
|
|
||||||
|
// 创建路由
|
||||||
|
const routerResponse: any = await api.post(`/api/mobileRoutes:create`, {
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
type: type,
|
||||||
|
schemaUid: pageSchemaUid,
|
||||||
|
title: title,
|
||||||
|
icon: 'appstoreoutlined',
|
||||||
|
options: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await routerResponse.json();
|
||||||
|
const routeId = responseData.data.id;
|
||||||
|
if (!routerResponse.ok()) {
|
||||||
|
throw new Error(await routerResponse.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'link') return { url, routeId, title };
|
||||||
|
|
||||||
|
// 创建空页面
|
||||||
|
const createSchemaResult = await api.post(`/api/uiSchemas:insertAdjacent?resourceIndex=mobile&position=beforeEnd`, {
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
name: pageSchemaUid,
|
||||||
|
'x-uid': pageSchemaUid,
|
||||||
|
'x-component': 'MobilePageProvider',
|
||||||
|
'x-settings': 'mobile:page',
|
||||||
|
'x-decorator': 'BlockItem',
|
||||||
|
'x-toolbar-props': {
|
||||||
|
draggable: false,
|
||||||
|
spaceWrapperStyle: {
|
||||||
|
right: -15,
|
||||||
|
top: -15,
|
||||||
|
},
|
||||||
|
spaceClassName: 'css-m1q7xw',
|
||||||
|
toolbarStyle: {
|
||||||
|
overflowX: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
header: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageHeader',
|
||||||
|
properties: {
|
||||||
|
pageNavigationBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageNavigationBar',
|
||||||
|
properties: {
|
||||||
|
actionBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobileNavigationActionBar',
|
||||||
|
'x-initializer': 'mobile:navigation-bar:actions',
|
||||||
|
'x-component-props': {
|
||||||
|
spaceProps: {
|
||||||
|
style: {
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'actionBar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'pageNavigationBar',
|
||||||
|
},
|
||||||
|
pageTabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageTabs',
|
||||||
|
name: 'pageTabs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'header',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageContent',
|
||||||
|
properties: {
|
||||||
|
[firstTabUid]: {
|
||||||
|
...((keepUid ? pageSchema : updateUidOfPageSchema(pageSchema)) || {
|
||||||
|
type: 'void',
|
||||||
|
'x-uid': firstTabUid,
|
||||||
|
'x-async': true,
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'mobile:addBlock',
|
||||||
|
}),
|
||||||
|
name: firstTabUid,
|
||||||
|
'x-uid': firstTabUid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createSchemaResult.ok()) {
|
||||||
|
throw new Error(await createSchemaResult.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建第一个 tab
|
||||||
|
const createTabResponse = await api.post(`/api/mobileRoutes:create`, {
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
parentId: routeId,
|
||||||
|
type: 'tabs',
|
||||||
|
title: 'Unnamed',
|
||||||
|
schemaUid: firstTabUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createTabResponse.ok()) {
|
||||||
|
throw new Error(await createTabResponse.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: schemaUrl, pageSchemaUid, routeId, title };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeAllMobileRoutes = async () => {
|
||||||
|
const api = await request.newContext({
|
||||||
|
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = await api.storageState();
|
||||||
|
const headers = getHeaders(state);
|
||||||
|
|
||||||
|
const result = await api.post(
|
||||||
|
`/api/mobileRoutes:destroy?filter=%7B%22%24and%22%3A%5B%7B%22id%22%3A%7B%22%24ne%22%3A0%7D%7D%5D%7D`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.ok()) {
|
||||||
|
throw new Error(await result.text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据页面 id 删除一个 Mobile Routes 的页面
|
||||||
|
*/
|
||||||
|
const deleteMobileRoutes = async (mobileRouteId: number) => {
|
||||||
|
if (!mobileRouteId) return;
|
||||||
|
const api = await request.newContext({
|
||||||
|
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = await api.storageState();
|
||||||
|
const headers = getHeaders(state);
|
||||||
|
|
||||||
|
const result = await api.post(`/api/mobileRoutes:destroy?filterByTk=${mobileRouteId}`, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok()) {
|
||||||
|
throw new Error(await result.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
const result2 = await api.post(
|
||||||
|
`/api/mobileRoutes:destroy?filter=${encodeURIComponent(JSON.stringify({ parentId: mobileRouteId }))}`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result2.ok()) {
|
||||||
|
throw new Error(await result2.text());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据页面 uid 删除一个 NocoBase 的页面
|
* 根据页面 uid 删除一个 NocoBase 的页面
|
||||||
*/
|
*/
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
SchemaSettingsRemove,
|
SchemaSettingsRemove,
|
||||||
SchemaSettingsSelectItem,
|
SchemaSettingsSelectItem,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
|
useOpenModeContext,
|
||||||
useSchemaToolbar,
|
useSchemaToolbar,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { ModalProps } from 'antd';
|
import { ModalProps } from 'antd';
|
||||||
@ -137,14 +138,15 @@ export const bulkEditActionSettings = new SchemaSettings({
|
|||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaInitializerOpenModeSchemaItems,
|
Component: SchemaInitializerOpenModeSchemaItems,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate', 'customize:create'].includes(
|
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate', 'customize:create'].includes(
|
||||||
fieldSchema['x-action'] || '',
|
fieldSchema['x-action'] || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openMode: isPopupAction,
|
openMode: isPopupAction && !hideOpenMode,
|
||||||
openSize: isPopupAction,
|
openSize: isPopupAction && !hideOpenMode,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
useCollectionState,
|
useCollectionState,
|
||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
|
useOpenModeContext,
|
||||||
useRecord,
|
useRecord,
|
||||||
useSchemaToolbar,
|
useSchemaToolbar,
|
||||||
useSyncFromForm,
|
useSyncFromForm,
|
||||||
@ -357,6 +358,7 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
|||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const modeOptions = useMemo(() => {
|
const modeOptions = useMemo(() => {
|
||||||
@ -367,8 +369,8 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
|||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openMode: true,
|
openMode: !hideOpenMode,
|
||||||
openSize: true,
|
openSize: !hideOpenMode,
|
||||||
modeOptions,
|
modeOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -30,7 +30,11 @@ export const AuthenticatorsContextProvider: FC<{ children: React.ReactNode }> =
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spin />;
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -44,6 +44,11 @@ export class PluginBlockIframeClient extends Plugin {
|
|||||||
title: '{{t("Iframe")}}',
|
title: '{{t("Iframe")}}',
|
||||||
Component: 'IframeBlockInitializer',
|
Component: 'IframeBlockInitializer',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'otherBlocks.iframe', {
|
||||||
|
title: '{{t("Iframe")}}',
|
||||||
|
Component: 'IframeBlockInitializer',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,11 @@ export class PluginBlockWorkbenchClient extends Plugin {
|
|||||||
`otherBlocks.${workbenchBlockInitializerItem.name}`,
|
`otherBlocks.${workbenchBlockInitializerItem.name}`,
|
||||||
workbenchBlockInitializerItem,
|
workbenchBlockInitializerItem,
|
||||||
);
|
);
|
||||||
|
this.app.schemaInitializerManager.addItem(
|
||||||
|
'mobile:addBlock',
|
||||||
|
`otherBlocks.${workbenchBlockInitializerItem.name}`,
|
||||||
|
workbenchBlockInitializerItem,
|
||||||
|
);
|
||||||
|
|
||||||
// link 操作
|
// link 操作
|
||||||
this.app.schemaSettingsManager.add(workbenchActionSettingsLink);
|
this.app.schemaSettingsManager.add(workbenchActionSettingsLink);
|
||||||
|
@ -34,6 +34,10 @@ export class PluginCalendarClient extends Plugin {
|
|||||||
title: generateNTemplate('Calendar'),
|
title: generateNTemplate('Calendar'),
|
||||||
Component: 'CalendarBlockInitializer',
|
Component: 'CalendarBlockInitializer',
|
||||||
});
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'dataBlocks.calendar', {
|
||||||
|
title: generateNTemplate('Calendar'),
|
||||||
|
Component: 'CalendarBlockInitializer',
|
||||||
|
});
|
||||||
this.app.schemaInitializerManager.addItem('popup:common:addBlock', 'dataBlocks.calendar', {
|
this.app.schemaInitializerManager.addItem('popup:common:addBlock', 'dataBlocks.calendar', {
|
||||||
title: generateNTemplate('Calendar'),
|
title: generateNTemplate('Calendar'),
|
||||||
Component: 'CalendarBlockInitializer',
|
Component: 'CalendarBlockInitializer',
|
||||||
|
@ -50,6 +50,10 @@ class PluginDataVisualiztionClient extends Plugin {
|
|||||||
title: lang('Charts'),
|
title: lang('Charts'),
|
||||||
Component: 'ChartV2BlockInitializer',
|
Component: 'ChartV2BlockInitializer',
|
||||||
});
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'dataBlocks.chartV2', {
|
||||||
|
title: lang('Charts'),
|
||||||
|
Component: 'ChartV2BlockInitializer',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,12 +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 { ActionInitializerItem, useCollection_deprecated } from '@nocobase/client';
|
import { ActionInitializerItem } from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const UploadActionInitializer = (props) => {
|
export const UploadActionInitializer = (props) => {
|
||||||
const collection = useCollection_deprecated();
|
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-action': 'create',
|
'x-action': 'create',
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
useDesignable,
|
useDesignable,
|
||||||
useFormItemInitializerFields,
|
useFormItemInitializerFields,
|
||||||
useGetAriaLabelOfDesigner,
|
useGetAriaLabelOfDesigner,
|
||||||
|
useOpenModeContext,
|
||||||
useSchemaInitializerRender,
|
useSchemaInitializerRender,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Space } from 'antd';
|
import { Space } from 'antd';
|
||||||
@ -137,6 +138,13 @@ const commonOptions = {
|
|||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaInitializerOpenModeSchemaItems,
|
Component: SchemaInitializerOpenModeSchemaItems,
|
||||||
|
useComponentProps() {
|
||||||
|
const { hideOpenMode } = useOpenModeContext();
|
||||||
|
return {
|
||||||
|
openMode: !hideOpenMode,
|
||||||
|
openSize: !hideOpenMode,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -98,7 +98,7 @@ const components = {
|
|||||||
google: GoogleMapConfiguration,
|
google: GoogleMapConfiguration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabList = MapTypes.map((item) => {
|
const routeList = MapTypes.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
component: components[item.value],
|
component: components[item.value],
|
||||||
@ -112,7 +112,7 @@ export const Configuration = () => {
|
|||||||
return (
|
return (
|
||||||
<Card bordered>
|
<Card bordered>
|
||||||
<Tabs type="card" defaultActiveKey={search.get('tab')}>
|
<Tabs type="card" defaultActiveKey={search.get('tab')}>
|
||||||
{tabList.map((tab) => {
|
{routeList.map((tab) => {
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
|
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
|
||||||
<tab.component type={tab.value} />
|
<tab.component type={tab.value} />
|
||||||
|
@ -47,7 +47,10 @@ export class PluginMapClient extends Plugin {
|
|||||||
title: generateNTemplate('Map'),
|
title: generateNTemplate('Map'),
|
||||||
Component: 'MapBlockInitializer',
|
Component: 'MapBlockInitializer',
|
||||||
});
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'dataBlocks.map', {
|
||||||
|
title: generateNTemplate('Map'),
|
||||||
|
Component: 'MapBlockInitializer',
|
||||||
|
});
|
||||||
this.app.pluginSettingsManager.add(NAMESPACE, {
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
|
||||||
icon: 'EnvironmentOutlined',
|
icon: 'EnvironmentOutlined',
|
||||||
|
30
packages/plugins/@nocobase/plugin-mobile/.dumirc.ts
Normal file
30
packages/plugins/@nocobase/plugin-mobile/.dumirc.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { defineConfig } from 'dumi';
|
||||||
|
import { getUmiConfig } from '@nocobase/devtools/umiConfig';
|
||||||
|
const umiConfig = getUmiConfig();
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
hash: true,
|
||||||
|
fastRefresh: false,
|
||||||
|
mfsu: false,
|
||||||
|
cacheDirectoryPath: `node_modules/.docs-mobile-cache`,
|
||||||
|
alias: {
|
||||||
|
...umiConfig.alias
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
atomDirs: [
|
||||||
|
{ type: 'component', dir: 'src/client' },
|
||||||
|
{ type: 'component', dir: 'src/server' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
styles: [`
|
||||||
|
.dumi-mobile-demo-layout { padding: 0 !important; }
|
||||||
|
.dumi-default-previewer-sources{ flex: 0 !important; margin-top: 50px; }
|
||||||
|
`],
|
||||||
|
metas: [
|
||||||
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content:
|
||||||
|
'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
2
packages/plugins/@nocobase/plugin-mobile/.npmignore
Normal file
2
packages/plugins/@nocobase/plugin-mobile/.npmignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
15
packages/plugins/@nocobase/plugin-mobile/README.md
Normal file
15
packages/plugins/@nocobase/plugin-mobile/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Mobile
|
||||||
|
|
||||||
|
English | [中文](./README.zh-CN.md)
|
||||||
|
|
||||||
|
多应用管理插件。
|
||||||
|
|
||||||
|
## 安装激活
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn pm enable @nocobase/plugin-mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
[使用文档](https://docs.nocobase.com/handbook/mobile)
|
15
packages/plugins/@nocobase/plugin-mobile/README.zh-CN.md
Normal file
15
packages/plugins/@nocobase/plugin-mobile/README.zh-CN.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Mobile
|
||||||
|
|
||||||
|
[English](./README.md) | 中文
|
||||||
|
|
||||||
|
多应用管理插件。
|
||||||
|
|
||||||
|
## 安装激活
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn pm enable @nocobase/plugin-mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
[使用文档](https://docs.nocobase.com/handbook/mobile)
|
2
packages/plugins/@nocobase/plugin-mobile/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-mobile/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
1
packages/plugins/@nocobase/plugin-mobile/client.js
Normal file
1
packages/plugins/@nocobase/plugin-mobile/client.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
28
packages/plugins/@nocobase/plugin-mobile/package.json
Normal file
28
packages/plugins/@nocobase/plugin-mobile/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-mobile",
|
||||||
|
"version": "1.3.0-alpha",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"homepage": "https://docs.nocobase.com/handbook/mobile",
|
||||||
|
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/mobile",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"displayName": "Mobile",
|
||||||
|
"displayName.zh-CN": "移动端",
|
||||||
|
"description": "Provides the ability to configure mobile pages.",
|
||||||
|
"description.zh-CN": "提供移动端页面配置的能力。",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "17.x",
|
||||||
|
"@types/react-dom": "17.x",
|
||||||
|
"@ant-design/icons": "5.x",
|
||||||
|
"@formily/antd-v5": "1.x",
|
||||||
|
"@formily/react": "2.x",
|
||||||
|
"@formily/shared": "2.x",
|
||||||
|
"antd-mobile": "^5.36.1",
|
||||||
|
"react-device-detect": "2.2.3",
|
||||||
|
"re-resizable": "6.6.0"
|
||||||
|
}
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-mobile/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-mobile/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
1
packages/plugins/@nocobase/plugin-mobile/server.js
Normal file
1
packages/plugins/@nocobase/plugin-mobile/server.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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 { expect, test } from '@nocobase/test/e2e';
|
||||||
|
|
||||||
|
test.describe('desktop-mode', () => {
|
||||||
|
test.beforeAll(async ({ page }) => {
|
||||||
|
await page.goto('/m');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('desktop should have back link to admin', async ({ page }) => {
|
||||||
|
await page.getByRole('link', { name: 'Back' }).click();
|
||||||
|
// 跳转到 /admin
|
||||||
|
expect(page.url()).toContain('/admin');
|
||||||
|
|
||||||
|
await page.goto('/m');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ui editor should work', async ({ page }) => {
|
||||||
|
// 默认 designer 开启
|
||||||
|
await expect(page.getByTestId('schema-initializer-MobileTabBar')).toBeVisible();
|
||||||
|
|
||||||
|
// 再次点击应该隐藏
|
||||||
|
await page.getByTestId('ui-editor-button').click();
|
||||||
|
await expect(page.getByTestId('schema-initializer-MobileTabBar')).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('ui-editor-button').click();
|
||||||
|
await expect(page.getByTestId('schema-initializer-MobileTabBar')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change mobile size', async ({ page }) => {
|
||||||
|
await page.getByTestId('desktop-mode-size-pad').click();
|
||||||
|
await expect(page.getByTestId('desktop-mode-resizable')).toHaveCSS('width', '768px');
|
||||||
|
await expect(page.getByTestId('desktop-mode-resizable')).toHaveCSS('height', '667px');
|
||||||
|
|
||||||
|
await page.getByTestId('desktop-mode-size-mobile').click();
|
||||||
|
await expect(page.getByTestId('desktop-mode-resizable')).toHaveCSS('width', '375px');
|
||||||
|
await expect(page.getByTestId('desktop-mode-resizable')).toHaveCSS('height', '667px');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('show qrcode', async ({ page }) => {
|
||||||
|
await expect(page.getByRole('button', { name: 'qrcode' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'qrcode' }).click();
|
||||||
|
await expect(page.getByRole('tooltip')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* 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 { expect, test } from '@nocobase/test/e2e';
|
||||||
|
|
||||||
|
test.describe('PageHeader', () => {
|
||||||
|
test.describe('PageHeader', () => {
|
||||||
|
test('Display page header', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
// 默认有 header
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await expect(page.getByTestId('mobile-page-header')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('mobile-page-header')).toContainText(nocoPage.getTitle());
|
||||||
|
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
|
||||||
|
// 四个选项都显示
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page header' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display navigation bar' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page title' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display tabs' })).toBeVisible();
|
||||||
|
|
||||||
|
// 启用 tabs
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tabs', exact: true }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-header')).toContainText('Unnamed');
|
||||||
|
|
||||||
|
// 点击后隐藏 tabs 和 title
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display page header', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('block-item-MobilePageProvider')).not.toContainText(nocoPage.getTitle());
|
||||||
|
await expect(page.getByLabel('block-item-MobilePageProvider')).not.toContainText('Unnamed');
|
||||||
|
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
|
||||||
|
// 仅有 Display page header 显示
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page header' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display navigation bar' })).not.toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page title' })).not.toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display tabs' })).not.toBeVisible();
|
||||||
|
|
||||||
|
// 再次点击显示
|
||||||
|
await page.getByRole('menuitem', { name: 'Display page header', exact: true }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-header')).toContainText(nocoPage.getTitle());
|
||||||
|
await expect(page.getByTestId('mobile-page-header')).toContainText('Unnamed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('PageNavigationBar', () => {
|
||||||
|
test('Display navigation bar', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
// 默认有 navigation bar
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toContainText(nocoPage.getTitle());
|
||||||
|
|
||||||
|
// 点击后隐藏
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
|
||||||
|
// 显示 Display navigation bar 和 Display page title
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page title' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display navigation bar' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Display navigation bar', exact: true }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 再次点击显示
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
|
||||||
|
// Display navigation bar 为 false 时,Display page title 应该不显示
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display navigation bar' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display page title' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Display navigation bar' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toContainText(nocoPage.getTitle());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Display page title', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
// 默认有 title
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toContainText(nocoPage.getTitle());
|
||||||
|
|
||||||
|
// 点击后隐藏
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display page title' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).not.toContainText(nocoPage.getTitle());
|
||||||
|
|
||||||
|
// 再次点击显示
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display page title' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toContainText(nocoPage.getTitle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('tabs', () => {
|
||||||
|
test.beforeEach(async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tabs' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).toContainText('Unnamed');
|
||||||
|
|
||||||
|
await page.getByTestId('mobile-page-tabs').click();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Display tabs' })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Display tabs', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
// 默认没有 tabs
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 点击后现实
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tabs' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).toContainText('Unnamed');
|
||||||
|
|
||||||
|
// 再次点击隐藏
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tabs' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item:settings', async ({ page }) => {
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-Unnamed`).click();
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByLabel('designer-schema-settings-MobilePageTabs').click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Edit', exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).not.toBeVisible(); // 仅有一项的时候不显示删除
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||||
|
const newTitle = Math.random().toString(36).substring(2);
|
||||||
|
await page.getByRole('textbox').fill(newTitle);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-${newTitle}`)).toHaveText(
|
||||||
|
newTitle,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-${newTitle}`).click();
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByLabel('designer-schema-settings-MobilePageTabs').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
await expect(page.getByRole('textbox')).toHaveValue(newTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item:add and remove', async ({ page }) => {
|
||||||
|
// 添加页面内容
|
||||||
|
await page.getByLabel('schema-initializer-Grid-mobile:addBlock').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'form Markdown' }).click();
|
||||||
|
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('action-Action-undefined').click();
|
||||||
|
const title = Math.random().toString(36).substring(2);
|
||||||
|
await page.getByRole('textbox').fill(title);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-${title}`)).toHaveText(title);
|
||||||
|
|
||||||
|
// 第一项也显示删除了
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-Unnamed`).click();
|
||||||
|
await page.getByTestId('mobile-page-tabs-Unnamed').getByLabel('designer-schema-settings-MobilePageTabs').click();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Edit', exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// 新增显示删除和编辑
|
||||||
|
await page.getByTestId('mobile-page-tabs').getByTestId(`mobile-page-tabs-${title}`).click();
|
||||||
|
await page.getByTestId(`mobile-page-tabs-${title}`).getByLabel('designer-schema-settings-MobilePageTabs').click();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Edit', exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// 切换页面,第一个 tab 的内容不显示
|
||||||
|
await expect(page.getByLabel('block-item-Markdown.Void-')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-page-tabs')).not.toContainText(title);
|
||||||
|
// 等待删除完成
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// 删除后第一个 tab 的内容显示
|
||||||
|
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('actions', () => {
|
||||||
|
test('link', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
await expect(page.getByTestId('mobile-page-navigation-bar')).toBeVisible();
|
||||||
|
async function doPosition(position: 'left' | 'right') {
|
||||||
|
// 添加左侧 Action
|
||||||
|
await expect(page.getByTestId(`mobile-navigation-action-bar-${position}`)).toBeVisible();
|
||||||
|
const navigationBarPositionElement = page.getByTestId(`mobile-navigation-action-bar-${position}`);
|
||||||
|
await navigationBarPositionElement
|
||||||
|
.getByLabel('schema-initializer-MobileNavigationActionBar-mobile:navigation-bar:actions')
|
||||||
|
.click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Link' }).click();
|
||||||
|
await page.getByRole('textbox').fill('Test________');
|
||||||
|
await page.getByLabel('action-Action-Submit').click();
|
||||||
|
await expect(navigationBarPositionElement).toContainText('Test________');
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
await navigationBarPositionElement.getByRole('button', { name: 'Test________' }).hover();
|
||||||
|
await navigationBarPositionElement
|
||||||
|
.getByLabel('designer-schema-settings-Action-mobile:navigation-bar:actions:link')
|
||||||
|
.hover();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit button' }).click();
|
||||||
|
await page.getByRole('textbox').fill('Test_changed');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await expect(navigationBarPositionElement).toContainText('Test_changed');
|
||||||
|
|
||||||
|
// 编辑 URL
|
||||||
|
await navigationBarPositionElement.getByText('Test_changed').hover();
|
||||||
|
await navigationBarPositionElement
|
||||||
|
.getByLabel('designer-schema-settings-Action-mobile:navigation-bar:actions:link')
|
||||||
|
.hover();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit link' }).click();
|
||||||
|
await page.getByLabel('block-item-URL').getByLabel('textbox').fill('https://github.com');
|
||||||
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await navigationBarPositionElement.getByText('Test_changed').hover();
|
||||||
|
await navigationBarPositionElement
|
||||||
|
.getByLabel('designer-schema-settings-Action-mobile:navigation-bar:actions:link')
|
||||||
|
.hover();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByText('Delete action').click();
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'OK' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||||||
|
await expect(navigationBarPositionElement).not.toContainText('Test_changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
await doPosition('left');
|
||||||
|
await doPosition('right');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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 { expect, removeAllMobileRoutes, test } from '@nocobase/test/e2e';
|
||||||
|
|
||||||
|
function randomStr() {
|
||||||
|
return Math.random().toString(36).substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('TabBar', () => {
|
||||||
|
test('schema page & settings', async ({ page }) => {
|
||||||
|
await removeAllMobileRoutes();
|
||||||
|
await page.goto('/m');
|
||||||
|
|
||||||
|
// hover initializer
|
||||||
|
await expect(page.getByTestId('schema-initializer-MobileTabBar')).toBeVisible();
|
||||||
|
await page.getByTestId('schema-initializer-MobileTabBar').click();
|
||||||
|
|
||||||
|
// 添加页面
|
||||||
|
const Title = randomStr();
|
||||||
|
await page.getByRole('menuitem', { name: 'Page' }).click();
|
||||||
|
await page.getByRole('textbox').click();
|
||||||
|
await page.getByRole('textbox').fill(Title);
|
||||||
|
await page.getByRole('button', { name: 'Select icon' }).click();
|
||||||
|
await page.getByRole('tooltip').getByLabel('account-book').locator('svg').click();
|
||||||
|
await page.getByLabel('action-Action-Submit').click();
|
||||||
|
await expect(page.getByTestId('modal-Action.Modal-Add page')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 确认添加成功 data-testid="mobile-tab-bar-r5fb94wgkra"
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}`).click();
|
||||||
|
const count = await page.locator(`text=${Title}`).count(); // title and tabBar
|
||||||
|
await expect(count).toBe(2);
|
||||||
|
await expect(page.getByLabel('block-item-MobilePageProvider')).toBeVisible();
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}`).click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobileTabBar.Page-mobile:tab-bar:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit button' }).click();
|
||||||
|
await page.getByRole('textbox').fill(`${Title}_change`);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await expect(page.getByTestId('modal-Action.Modal-Add page')).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('block-item-MobileTabBar.Page').click();
|
||||||
|
const count_changed = await page.locator(`text=${Title}_change`).count();
|
||||||
|
await expect(count_changed).toBe(2);
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}_change`).click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobileTabBar.Page-mobile:tab-bar:page').click();
|
||||||
|
await page.getByText('Delete').click();
|
||||||
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
await expect(page.getByText('Delete action')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 确认删除成功
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await expect(page.getByText(`${Title}_change`)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add link page & settings', async ({ page }) => {
|
||||||
|
await page.goto('/m');
|
||||||
|
await removeAllMobileRoutes();
|
||||||
|
|
||||||
|
// hover initializer
|
||||||
|
await expect(page.getByTestId('schema-initializer-MobileTabBar')).toBeVisible();
|
||||||
|
await page.getByTestId('schema-initializer-MobileTabBar').hover();
|
||||||
|
|
||||||
|
// 添加页面
|
||||||
|
const Title = randomStr();
|
||||||
|
await page.getByRole('menuitem', { name: 'Link' }).click();
|
||||||
|
await page.getByRole('textbox').click();
|
||||||
|
await page.getByRole('textbox').fill(Title);
|
||||||
|
await page.getByRole('button', { name: 'Select icon' }).click();
|
||||||
|
await page.getByRole('tooltip').getByLabel('account-book').locator('svg').click();
|
||||||
|
await page.getByLabel('action-Action-Submit').click();
|
||||||
|
await expect(page.getByTestId('modal-Action.Modal-Add page')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 确认添加成功
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}`).click();
|
||||||
|
await expect(page.getByText('Please configure the URL')).toBeVisible();
|
||||||
|
const count = await page.locator(`text=${Title}`).count();
|
||||||
|
await expect(count).toBe(1);
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}`).hover();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobileTabBar.Link-mobile:tab-bar:link').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit button' }).click();
|
||||||
|
await page.getByRole('textbox').fill(`${Title}_change`);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await expect(page.getByTestId('modal-Action.Modal-Add page')).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}_change`).click();
|
||||||
|
const count_changed = await page.locator(`text=${Title}_change`).count();
|
||||||
|
expect(count_changed).toBe(1);
|
||||||
|
|
||||||
|
// 编辑 URL
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}_change`).hover();
|
||||||
|
// 如果有多个,点击获取 display 不为 none 的元素
|
||||||
|
await page.getByLabel('designer-schema-settings-MobileTabBar.Link-mobile:tab-bar:link').hover();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit link' }).click();
|
||||||
|
console.log('page.url()', page.url());
|
||||||
|
await page.getByLabel('block-item-URL').getByLabel('textbox').fill(page.url().replace('/m', '/admin'));
|
||||||
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
const page2Promise = page.waitForEvent('popup');
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}_change`).click();
|
||||||
|
const page2 = await page2Promise;
|
||||||
|
expect(page2.url()).toBe(page.url().replace('/m', '/admin'));
|
||||||
|
await page2.close();
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await page.getByTestId(`mobile-tab-bar-${Title}_change`).hover();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobileTabBar.Link-mobile:tab-bar:link').click();
|
||||||
|
await page.getByText('Delete').click();
|
||||||
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
await expect(page.getByText('Delete action')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 确认删除成功
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await expect(page.getByText(`${Title}_change`)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TabBar settings', async ({ page, mockMobilePage }) => {
|
||||||
|
const nocoPage = mockMobilePage();
|
||||||
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
// 默认有 title
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await expect(page.getByTestId('mobile-tab-bar')).toBeVisible();
|
||||||
|
|
||||||
|
// 点击后隐藏
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tab bar' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-tab-bar')).not.toBeVisible();
|
||||||
|
|
||||||
|
// 再次点击显示
|
||||||
|
await page.getByLabel('block-item-MobilePageProvider').click();
|
||||||
|
await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Display tab bar' }).click();
|
||||||
|
await expect(page.getByTestId('mobile-tab-bar')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import App from '../demos/DesktopMode-basic';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
|
||||||
|
describe('DesktopMode', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<App />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
// back
|
||||||
|
expect(screen.queryByRole('link')).toHaveAttribute('href', '/admin');
|
||||||
|
|
||||||
|
// ui-editor
|
||||||
|
expect(screen.queryByTestId('ui-editor-button')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// size
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.queryByTestId('desktop-mode-size-pad'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('desktop-mode-resizable').style.width).toBe('768px');
|
||||||
|
expect(screen.queryByTestId('desktop-mode-resizable').style.height).toBe('667px');
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.queryByTestId('desktop-mode-size-mobile'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('desktop-mode-resizable').style.width).toBe('375px');
|
||||||
|
expect(screen.queryByTestId('desktop-mode-resizable').style.height).toBe('667px');
|
||||||
|
});
|
||||||
|
|
||||||
|
// qrcode
|
||||||
|
expect(screen.queryByTestId('desktop-mode-qrcode')).toBeInTheDocument();
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.hover(screen.queryByTestId('desktop-mode-qrcode'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector('canvas')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// content
|
||||||
|
expect(screen.queryByText('demo content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import Basic from '../../demos/pages-dynamic-page-basic';
|
||||||
|
import NotFound from '../../demos/pages-dynamic-page-404';
|
||||||
|
import Schema from '../../demos/pages-dynamic-page-schema';
|
||||||
|
// import Settings from '../../demos/pages-dynamic-page-settings'
|
||||||
|
|
||||||
|
describe('MobilePage', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<Basic />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Schema Test Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('not found', async () => {
|
||||||
|
render(<NotFound />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('404')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByText('Back Home'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('schema', async () => {
|
||||||
|
render(<Schema />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Tab1 Content')).toBeInTheDocument();
|
||||||
|
expect(screen.queryAllByRole('button')).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('settings', async () => {
|
||||||
|
// render(<Settings />);
|
||||||
|
// await waitForApp();
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(screen.queryByText('Settings')).toBeInTheDocument();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// await userEvent.hover(screen.getByText('Settings'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(document.querySelector('span[aria-label="designer-schema-settings-div-mobile:page"]')).toBeInTheDocument();
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// await userEvent.hover(document.querySelector('span[aria-label="designer-schema-settings-div-mobile:page"]'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(screen.queryByText('Display page header')).toBeInTheDocument();
|
||||||
|
// expect(screen.queryByText('Display page title')).not.toBeInTheDocument();
|
||||||
|
// expect(screen.queryByText('Display tabs')).not.toBeInTheDocument();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// await userEvent.click(screen.getByText('Display page header'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(screen.queryByText('Display page title')).toBeInTheDocument();
|
||||||
|
// expect(screen.queryByText('Display tabs')).toBeInTheDocument();
|
||||||
|
// expect(screen.queryByTestId('schema-json')).toHaveTextContent(JSON.stringify({
|
||||||
|
// "displayNavigationBar": true
|
||||||
|
// }));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// await userEvent.click(screen.getByText('Display page title'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(screen.queryByTestId('schema-json')).toHaveTextContent(JSON.stringify({
|
||||||
|
// "displayNavigationBar": true,
|
||||||
|
// "displayPageTitle": false
|
||||||
|
// }));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// await userEvent.click(screen.getByText('Display tabs'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// expect(screen.queryByTestId('schema-json')).toHaveTextContent(JSON.stringify({
|
||||||
|
// "displayNavigationBar": true,
|
||||||
|
// "displayPageTitle": false,
|
||||||
|
// "displayTabs": true
|
||||||
|
// }));
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import Basic from '../../demos/pages-page-content-basic';
|
||||||
|
import FirstRoute from '../../demos/pages-page-content-first-route';
|
||||||
|
import NotFound from '../../demos/pages-page-content-404';
|
||||||
|
|
||||||
|
describe('MobilePageContent', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<Basic />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Schema Test Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render first route', async () => {
|
||||||
|
render(<FirstRoute />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('First Route Content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not found', async () => {
|
||||||
|
render(<NotFound />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('404')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByText('Back Home'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import Basic from '../../demos/pages-navigation-bar-basic';
|
||||||
|
import NavFalse from '../../demos/pages-navigation-bar-false';
|
||||||
|
import NavTitleFalse from '../../demos/pages-navigation-bar-title-false';
|
||||||
|
import NavTabs from '../../demos/pages-page-tabs';
|
||||||
|
|
||||||
|
describe('MobilePageNavigationBar', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<Basic />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displayNavigationBar: false', async () => {
|
||||||
|
render(<NavFalse />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Title')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displayPageTitle: false', async () => {
|
||||||
|
render(<NavTitleFalse />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Title')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displayTabs: true', async () => {
|
||||||
|
render(<NavTabs />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Tab1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Tab2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('/page/page1/tabs/tab1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByText('Tab2'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Tab1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Tab2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('/page/page1/tabs/tab2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { render, screen, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import Basic from '../../demos/pages-navigation-bar-actions';
|
||||||
|
|
||||||
|
describe('MobilePage', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
render(<Basic />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Title')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(document.querySelector('.adm-nav-bar-left')).toHaveTextContent('Left');
|
||||||
|
expect(document.querySelector('.adm-nav-bar-right')).toHaveTextContent('Right1');
|
||||||
|
expect(document.querySelector('.adm-nav-bar-right')).toHaveTextContent('Right2');
|
||||||
|
|
||||||
|
expect(screen.queryByText('Bottom')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import App from '../demos/Mobile-basic';
|
||||||
|
|
||||||
|
describe('Mobile', () => {
|
||||||
|
test('desktop mode', async () => {
|
||||||
|
render(<App />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('ui-editor-button')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryAllByText('Test1')).toHaveLength(2);
|
||||||
|
expect(screen.queryByText('Test2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Tab1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Tab2')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Tab1 Content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.queryByText('Tab2'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Tab2 Content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { render, screen, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import BasicApp from '../demos/MobileTabBar-basic';
|
||||||
|
import FalseApp from '../demos/MobileTabBar-false';
|
||||||
|
import InnerPageApp from '../demos/MobileTabBar-inner-page';
|
||||||
|
|
||||||
|
describe('MobileTabBar', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
render(<BasicApp />);
|
||||||
|
await waitForApp();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Test1')).toBeInTheDocument(); // title
|
||||||
|
expect(screen.queryByText('Test2')).toBeInTheDocument(); // title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableTabBar: false', async () => {
|
||||||
|
render(<FalseApp />);
|
||||||
|
await waitForApp();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Test1')).not.toBeInTheDocument(); // title
|
||||||
|
expect(screen.queryByText('Test2')).not.toBeInTheDocument(); // title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('inner page', async () => {
|
||||||
|
render(<InnerPageApp />);
|
||||||
|
await waitForApp();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('inner page')).toBeInTheDocument(); // custom page content
|
||||||
|
expect(screen.queryByText('Test1')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Test2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import MobileTitleProviderApp from '../demos/MobileTitleProvider-basic';
|
||||||
|
import MobileRoutesProviderApp from '../demos/MobileRoutesProvider-basic';
|
||||||
|
|
||||||
|
describe('MobileProviders', () => {
|
||||||
|
test('MobileTitleProvider', async () => {
|
||||||
|
render(<MobileTitleProviderApp />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Set Title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.queryByText('Set Title'));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Hello World')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MobileRoutesProvider', async () => {
|
||||||
|
render(<MobileRoutesProviderApp />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Test1')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent } from '@nocobase/test/client';
|
||||||
|
import BasicApp from '../demos/MobileTabBar.Item-basic';
|
||||||
|
import OnClickApp from '../demos/MobileTabBar.Item-on-click';
|
||||||
|
import SelectedApp from '../demos/MobileTabBar.Item-selected';
|
||||||
|
import SelectedIconApp from '../demos/MobileTabBar.Item-selected-icon';
|
||||||
|
import WithIconApp from '../demos/MobileTabBar.Item-with-icon';
|
||||||
|
import WithIconReactNodeApp from '../demos/MobileTabBar.Item-with-icon-node';
|
||||||
|
|
||||||
|
describe('MobileTabBar.Item', () => {
|
||||||
|
test('Basic', () => {
|
||||||
|
render(<BasicApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With Icon: string', () => {
|
||||||
|
render(<WithIconApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With Icon: React.Node', () => {
|
||||||
|
render(<WithIconReactNodeApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selected', () => {
|
||||||
|
render(<SelectedApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.adm-tab-bar-item-active')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selected Icon', () => {
|
||||||
|
render(<SelectedIconApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.adm-tab-bar-item-active')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toHaveAttribute('aria-label', 'appstore-add');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onClick', async () => {
|
||||||
|
render(<OnClickApp />);
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByText('Test'));
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Clicked')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -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 React from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import InnerApp from '../demos/MobileTabBar.Link-inner';
|
||||||
|
import OuterApp from '../demos/MobileTabBar.Link-outer';
|
||||||
|
import SelectedApp from '../demos/MobileTabBar.Link-selected';
|
||||||
|
import SchemaApp from '../demos/MobileTabBar.Link-schema';
|
||||||
|
|
||||||
|
describe('MobileTabBar.Item', () => {
|
||||||
|
test('Inner Link', async () => {
|
||||||
|
render(<InnerApp />);
|
||||||
|
await waitForApp();
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Test'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Test Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Outer Link', async () => {
|
||||||
|
render(<OuterApp />);
|
||||||
|
await waitForApp();
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const originOpen = window.open;
|
||||||
|
const origin = vitest.fn();
|
||||||
|
window.open = origin;
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Test'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(origin).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open = originOpen;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selected', async () => {
|
||||||
|
render(<SelectedApp />);
|
||||||
|
await waitForApp();
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.adm-tab-bar-item-active')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Schema', async () => {
|
||||||
|
render(<SchemaApp />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
expect(screen.getByText('Link')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('schema-json')).toMatchInlineSnapshot(`
|
||||||
|
<pre
|
||||||
|
data-testid="schema-json"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
"_isJSONSchemaObject": true,
|
||||||
|
"version": "2.0",
|
||||||
|
"name": "schema",
|
||||||
|
"type": "void",
|
||||||
|
"x-decorator": "BlockItem",
|
||||||
|
"x-settings": "mobile:tab-bar:link",
|
||||||
|
"x-component": "MobileTabBar.Link",
|
||||||
|
"x-toolbar-props": {
|
||||||
|
"showBorder": false,
|
||||||
|
"showBackground": true
|
||||||
|
},
|
||||||
|
"x-component-props": {
|
||||||
|
"title": "Link",
|
||||||
|
"icon": "AppstoreOutlined",
|
||||||
|
"url": "https://github.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { act, render, screen, userEvent, waitFor, waitForApp } from '@nocobase/test/client';
|
||||||
|
import BasicApp from '../demos/MobileTabBar.Page-basic';
|
||||||
|
import SelectedApp from '../demos/MobileTabBar.Page-selected';
|
||||||
|
import SchemaApp from '../demos/MobileTabBar.Page-schema';
|
||||||
|
|
||||||
|
describe('MobileTabBar.Item', () => {
|
||||||
|
test('Basic', async () => {
|
||||||
|
render(<BasicApp />);
|
||||||
|
await waitForApp();
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Test'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Schema Test Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Selected', async () => {
|
||||||
|
render(<SelectedApp />);
|
||||||
|
await waitForApp();
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.adm-tab-bar-item-active')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Schema', async () => {
|
||||||
|
render(<SchemaApp />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('img')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('schema-json')).toMatchInlineSnapshot(`
|
||||||
|
<pre
|
||||||
|
data-testid="schema-json"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
"_isJSONSchemaObject": true,
|
||||||
|
"version": "2.0",
|
||||||
|
"name": "schema",
|
||||||
|
"type": "void",
|
||||||
|
"x-decorator": "BlockItem",
|
||||||
|
"x-settings": "mobile:tab-bar:page",
|
||||||
|
"x-component": "MobileTabBar.Page",
|
||||||
|
"x-toolbar-props": {
|
||||||
|
"showBorder": false,
|
||||||
|
"showBackground": true
|
||||||
|
},
|
||||||
|
"x-component-props": {
|
||||||
|
"title": "Test",
|
||||||
|
"icon": "AppstoreOutlined",
|
||||||
|
"schemaUid": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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 { invoke, isJSBridge } from '../js-bridge';
|
||||||
|
|
||||||
|
describe('invoke function', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
(window as any).JsBridge = { invoke: vitest.fn() };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke scan correctly', () => {
|
||||||
|
const cb = vitest.fn();
|
||||||
|
invoke({ action: 'scan' }, cb);
|
||||||
|
expect((window as any).JsBridge.invoke).toHaveBeenCalledWith({ action: 'scan' }, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should invoke moveTaskToBack correctly', () => {
|
||||||
|
invoke({ action: 'moveTaskToBack' });
|
||||||
|
expect((window as any).JsBridge.invoke).toHaveBeenCalledWith({ action: 'moveTaskToBack' }, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle callbacks on moveTaskToBack action', () => {
|
||||||
|
const cb = vitest.fn();
|
||||||
|
invoke({ action: 'moveTaskToBack' }, cb);
|
||||||
|
expect((window as any).JsBridge.invoke).toHaveBeenCalledWith({ action: 'moveTaskToBack' }, cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isJSBridge function', () => {
|
||||||
|
it('should return true if JsBridge is available', () => {
|
||||||
|
expect(isJSBridge()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if JsBridge is not defined', () => {
|
||||||
|
const originalJsBridge = (window as any).JsBridge;
|
||||||
|
delete (window as any).JsBridge;
|
||||||
|
expect(isJSBridge()).toBe(false);
|
||||||
|
(window as any).JsBridge = originalJsBridge; // Restore original state
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { render, waitForApp, screen, waitFor } from '@nocobase/test/client';
|
||||||
|
import BasicApp from '../../demos/pages-home-basic';
|
||||||
|
import CustomApp from '../../demos/pages-home-custom';
|
||||||
|
import NullApp from '../../demos/pages-home-null';
|
||||||
|
|
||||||
|
describe('Home page', () => {
|
||||||
|
it('rewrite to first page', async () => {
|
||||||
|
render(<BasicApp />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Test Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if custom home page, not rewrite', async () => {
|
||||||
|
render(<CustomApp />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('mobile-loading')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Custom Home Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no routes render null', async () => {
|
||||||
|
render(<NullApp />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector('.ant-app').innerHTML).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 from 'react';
|
||||||
|
import { render, waitForApp, screen, userEvent, waitFor, act } from '@nocobase/test/client';
|
||||||
|
import App from '../../demos/pages-not-found';
|
||||||
|
|
||||||
|
describe('NotFound page', () => {
|
||||||
|
it('basic page', async () => {
|
||||||
|
render(<App />);
|
||||||
|
await waitForApp();
|
||||||
|
|
||||||
|
expect(screen.queryByText('404')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByText('Back Home'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Home Page')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
259
packages/plugins/@nocobase/plugin-mobile/src/client/client.d.ts
vendored
Normal file
259
packages/plugins/@nocobase/plugin-mobile/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module 'demos' {
|
||||||
|
import type { FC } from 'react'
|
||||||
|
export const DemoBlock: FC<{
|
||||||
|
title: string
|
||||||
|
padding?: string
|
||||||
|
background?: string
|
||||||
|
children?: ReactNode
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' { }
|
||||||
|
declare module '*.scss' { }
|
||||||
|
declare module '*.sass' { }
|
||||||
|
declare module '*.less' { }
|
||||||
|
declare module '*.styl' { }
|
||||||
|
declare module '*.stylus' { }
|
||||||
|
declare module '*.pcss' { }
|
||||||
|
declare module '*.sss' { }
|
||||||
|
|
||||||
|
// Built-in asset types
|
||||||
|
// see `src/node/constants.ts`
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.apng' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jfif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.opus' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mov' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.m4a' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.vtt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm?init
|
||||||
|
declare module '*.wasm?init' {
|
||||||
|
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||||
|
export default initWasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web worker
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&inline' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&inline' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?inline' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PluginName = 'mobile';
|
||||||
|
export const NavigationBarHeight = 50;
|
||||||
|
export const PageBackgroundColor = '#f5f5f5';
|
@ -0,0 +1,19 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { DesktopMode } from '@nocobase/plugin-mobile/client';
|
||||||
|
|
||||||
|
import { mockApp } from '@nocobase/client/demo-utils';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Demo = () => {
|
||||||
|
return <DesktopMode>demo content</DesktopMode>;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DemoPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.router.add('root', { path: '/', Component: Demo });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = mockApp({ plugins: [DemoPlugin] });
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
@ -0,0 +1,290 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import PluginMobileClient, { Mobile } from '@nocobase/plugin-mobile/client';
|
||||||
|
import { mockApp } from '@nocobase/client/demo-utils';
|
||||||
|
|
||||||
|
class DemoPlugin extends Plugin {
|
||||||
|
async beforeLoad(): Promise<void> {
|
||||||
|
await this.app.pluginManager.add(PluginMobileClient, {
|
||||||
|
config: {
|
||||||
|
router: {
|
||||||
|
type: 'memory',
|
||||||
|
basename: '/m',
|
||||||
|
initialEntries: ['/m'],
|
||||||
|
},
|
||||||
|
skipLogin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.app.router.add('root', { path: '/m', Component: Mobile });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = mockApp({
|
||||||
|
router: {
|
||||||
|
type: 'memory',
|
||||||
|
initialEntries: ['/m'],
|
||||||
|
},
|
||||||
|
plugins: [DemoPlugin],
|
||||||
|
apis: {
|
||||||
|
'mobileRoutes:list': {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
createdAt: '2024-07-08T13:22:33.763Z',
|
||||||
|
updatedAt: '2024-07-08T13:22:33.763Z',
|
||||||
|
parentId: null,
|
||||||
|
title: 'Test1',
|
||||||
|
icon: 'AppstoreOutlined',
|
||||||
|
schemaUid: 'd4o6esth2ik',
|
||||||
|
type: 'page',
|
||||||
|
options: null,
|
||||||
|
sort: 1,
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
createdAt: '2024-07-08T13:22:33.800Z',
|
||||||
|
updatedAt: '2024-07-08T13:22:45.084Z',
|
||||||
|
parentId: 10,
|
||||||
|
title: 'Tab1',
|
||||||
|
icon: null,
|
||||||
|
schemaUid: 'pm65m9y0o2y',
|
||||||
|
type: 'tabs',
|
||||||
|
options: null,
|
||||||
|
sort: 2,
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
__index: '0.children.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
createdAt: '2024-07-08T13:22:48.564Z',
|
||||||
|
updatedAt: '2024-07-08T13:22:48.564Z',
|
||||||
|
parentId: 10,
|
||||||
|
title: 'Tab2',
|
||||||
|
icon: null,
|
||||||
|
schemaUid: '1mcth1tfcb6',
|
||||||
|
type: 'tabs',
|
||||||
|
options: null,
|
||||||
|
sort: 3,
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
__index: '0.children.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__index: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
createdAt: '2024-07-08T13:23:01.929Z',
|
||||||
|
updatedAt: '2024-07-08T13:23:12.433Z',
|
||||||
|
parentId: null,
|
||||||
|
title: 'Test2',
|
||||||
|
icon: 'aliwangwangoutlined',
|
||||||
|
schemaUid: null,
|
||||||
|
type: 'link',
|
||||||
|
options: {
|
||||||
|
schemaUid: null,
|
||||||
|
url: 'https://github.com',
|
||||||
|
params: [{}],
|
||||||
|
},
|
||||||
|
sort: 4,
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
__index: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'applicationPlugins:update': {
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
'uiSchemas:getJsonSchema/d4o6esth2ik': {
|
||||||
|
data: {
|
||||||
|
'x-uid': 'd4o6esth2ik',
|
||||||
|
name: 'd4o6esth2ik',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageProvider',
|
||||||
|
'x-settings': 'mobile:page',
|
||||||
|
'x-decorator': 'BlockItem',
|
||||||
|
'x-decorator-props': {
|
||||||
|
style: {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-toolbar-props': {
|
||||||
|
draggable: false,
|
||||||
|
spaceWrapperStyle: {
|
||||||
|
right: -15,
|
||||||
|
top: -15,
|
||||||
|
},
|
||||||
|
spaceClassName: 'css-m1q7xw',
|
||||||
|
toolbarStyle: {
|
||||||
|
overflowX: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
'x-async': false,
|
||||||
|
'x-component-props': {
|
||||||
|
displayPageTitle: true,
|
||||||
|
displayTabs: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
header: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageHeader',
|
||||||
|
properties: {
|
||||||
|
pageNavigationBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageNavigationBar',
|
||||||
|
properties: {
|
||||||
|
actionBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobileNavigationActionBar',
|
||||||
|
'x-initializer': 'mobile:navigation-bar:actions',
|
||||||
|
'x-component-props': {
|
||||||
|
spaceProps: {
|
||||||
|
style: {
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'actionBar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'pageNavigationBar',
|
||||||
|
},
|
||||||
|
pageTabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageTabs',
|
||||||
|
name: 'pageTabs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'header',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MobilePageContent',
|
||||||
|
'x-uid': 'ooteekvdis8',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'uiSchemas:getJsonSchema/1mcth1tfcb6': {
|
||||||
|
data: {
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'mobile:addBlock',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
properties: {
|
||||||
|
mbds3xuxm48: {
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
properties: {
|
||||||
|
jfe4z693cji: {
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
properties: {
|
||||||
|
'01rowxmritv': {
|
||||||
|
'x-uid': 'pj9gi5yfpza',
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-settings': 'blockSettings:markdown',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-decorator-props': {
|
||||||
|
name: 'markdown',
|
||||||
|
},
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Tab2 Content',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '4twpusksaod',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ktou2snt890',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'nuh60rxntix',
|
||||||
|
'x-uid': '1mcth1tfcb6',
|
||||||
|
'x-async': true,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'uiSchemas:getJsonSchema/pm65m9y0o2y': {
|
||||||
|
data: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'mobile:addBlock',
|
||||||
|
properties: {
|
||||||
|
lxtx5t4hh2x: {
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
properties: {
|
||||||
|
yn6ojyount2: {
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
properties: {
|
||||||
|
mgsz7z1ibu0: {
|
||||||
|
'x-uid': '1dpiddwlasg',
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-settings': 'blockSettings:markdown',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-decorator-props': {
|
||||||
|
name: 'markdown',
|
||||||
|
},
|
||||||
|
'x-component': 'div',
|
||||||
|
'x-content': 'Tab1 Content',
|
||||||
|
'x-app-version': '1.2.12-alpha',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ip7l9yu8v37',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '48ilv5bcdz1',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'pm65m9y0o2y',
|
||||||
|
'x-uid': 'pm65m9y0o2y',
|
||||||
|
'x-async': true,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.getRootComponent();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user