mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-29 18:58:26 +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
|
||||
packages/core/client/docs/contributing.md
|
||||
packages/core/app/client/src/.plugins
|
||||
packages/pro-plugins/
|
||||
storage/plugins
|
||||
storage/tar
|
||||
storage/tmp
|
||||
|
@ -59,7 +59,11 @@ export function AntdConfigProvider(props) {
|
||||
},
|
||||
);
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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 { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
|
||||
|
||||
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
||||
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||
import type { Plugin } from './Plugin';
|
||||
import type { RequireJS } from './utils/requirejs';
|
||||
@ -158,6 +159,7 @@ export class Application {
|
||||
});
|
||||
this.use(AntdAppProvider);
|
||||
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
||||
this.use(OpenModeProvider);
|
||||
}
|
||||
|
||||
private addReactRouterComponents() {
|
||||
|
@ -12,7 +12,7 @@ import type { Application } from './Application';
|
||||
|
||||
export class Plugin<T = any> {
|
||||
constructor(
|
||||
protected options: T,
|
||||
public options: T,
|
||||
protected app: Application,
|
||||
) {
|
||||
this.options = options;
|
||||
|
@ -12,7 +12,7 @@ import type { Plugin } from './Plugin';
|
||||
import { getPlugins } from './utils/remotePlugins';
|
||||
|
||||
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 = {
|
||||
name: string;
|
||||
packageName: string;
|
||||
|
@ -32,12 +32,14 @@ export interface PluginSettingOptions {
|
||||
*/
|
||||
sort?: number;
|
||||
aclSnippet?: string;
|
||||
link?: string;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export interface PluginSettingsPageType {
|
||||
label?: string | React.ReactElement;
|
||||
title: string | React.ReactElement;
|
||||
link?: string;
|
||||
key: string;
|
||||
icon: any;
|
||||
path: string;
|
||||
|
@ -13,6 +13,7 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import React, { Component } from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { describe } from 'vitest';
|
||||
import { OpenModeProvider } from '../../modules/popup/OpenModeProvider';
|
||||
import { Application } from '../Application';
|
||||
import { Plugin } from '../Plugin';
|
||||
import { useApp } from '../hooks';
|
||||
@ -211,6 +212,7 @@ describe('Application', () => {
|
||||
it('initial', () => {
|
||||
const app = new Application({ router, providers: [Hello, [World, { name: 'aaa' }]] });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[OpenModeProvider, undefined],
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
@ -220,6 +222,7 @@ describe('Application', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.addProviders([[World, { name: 'aaa' }], Foo]);
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[OpenModeProvider, undefined],
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
[Foo, undefined],
|
||||
@ -230,6 +233,7 @@ describe('Application', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.addProvider(World, { name: 'aaa' });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[OpenModeProvider, undefined],
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
@ -239,6 +243,7 @@ describe('Application', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.use(World, { name: 'aaa' });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[OpenModeProvider, undefined],
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { Plugin } from '../Plugin';
|
||||
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(name: any) {
|
||||
const app = useApp();
|
||||
|
@ -18,10 +18,8 @@ export * from './globalType';
|
||||
export * from './hooks';
|
||||
export * from './schema-initializer';
|
||||
export * from './schema-settings';
|
||||
export * from './schema-settings/utils';
|
||||
export * from './schema-settings/context/SchemaSettingItemContext';
|
||||
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 './utils';
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { useForm } from '@formily/react';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useActionContext, SchemaComponent } from '../../../schema-component';
|
||||
import { useSchemaInitializerItem } from '../context';
|
||||
import { useSchemaInitializer, useSchemaInitializerItem } from '../context';
|
||||
import { SchemaInitializerItem } from './SchemaInitializerItem';
|
||||
import { uid } from '@formily/shared';
|
||||
|
||||
@ -19,10 +19,12 @@ export interface SchemaInitializerActionModalProps {
|
||||
icon?: string | React.ReactNode;
|
||||
schema: any;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (values: any) => void;
|
||||
onSubmit?: (values: any) => Promise<any> | void;
|
||||
buttonText?: any;
|
||||
component?: any;
|
||||
isItem?: boolean;
|
||||
width?: string;
|
||||
btnStyles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any, ref: any) => {
|
||||
@ -31,7 +33,8 @@ const SchemaInitializerActionModalItemComponent = React.forwardRef((props: any,
|
||||
});
|
||||
|
||||
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(() => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const form = useForm();
|
||||
@ -54,9 +57,13 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
||||
return {
|
||||
async run() {
|
||||
await form.validate();
|
||||
await onSubmit?.(form.values);
|
||||
ctx.setVisible(false);
|
||||
void form.reset();
|
||||
try {
|
||||
await onSubmit?.(form.values);
|
||||
ctx.setVisible(false);
|
||||
void form.reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [onSubmit]);
|
||||
@ -92,6 +99,7 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
||||
style: {
|
||||
borderColor: 'var(--colorSettings)',
|
||||
color: 'var(--colorSettings)',
|
||||
...(btnStyles || {}),
|
||||
},
|
||||
title: buttonText,
|
||||
type: 'dashed',
|
||||
@ -101,10 +109,14 @@ export const SchemaInitializerActionModal: FC<SchemaInitializerActionModalProps>
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
width: width,
|
||||
style: {
|
||||
maxWidth: '520px',
|
||||
maxWidth: width ? width : '520px',
|
||||
width: '100%',
|
||||
},
|
||||
afterOpenChange: () => {
|
||||
initializerSetVisible(false);
|
||||
},
|
||||
},
|
||||
type: 'void',
|
||||
title,
|
||||
|
@ -31,23 +31,26 @@ export const SchemaInitializerButton: FC<SchemaInitializerButtonProps> = React.m
|
||||
style={{
|
||||
borderColor: 'var(--colorSettings)',
|
||||
color: 'var(--colorSettings)',
|
||||
flex: 'none',
|
||||
...style,
|
||||
}}
|
||||
icon={typeof options.icon === 'string' ? <Icon type={options.icon as string} /> : options.icon}
|
||||
{...others}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'inline-block',
|
||||
width: options.icon ? 'calc(100% - 14px)' : '100%',
|
||||
verticalAlign: 'bottom',
|
||||
}}
|
||||
>
|
||||
{compile(options.title)}
|
||||
</span>
|
||||
{options.title && (
|
||||
<span
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'inline-block',
|
||||
width: options.icon ? 'calc(100% - 14px)' : '100%',
|
||||
verticalAlign: 'bottom',
|
||||
}}
|
||||
>
|
||||
{compile(options.title)}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
@ -21,7 +21,7 @@ import type {
|
||||
|
||||
export type InsertType = (s: ISchema) => void;
|
||||
|
||||
type SchemaInitializerItemBuiltInType<T = {}> = T & {
|
||||
type SchemaInitializerItemBuiltInType<T = {}> = Partial<T> & {
|
||||
name: string;
|
||||
sort?: number;
|
||||
componentProps?: Omit<T, 'children'>;
|
||||
@ -110,6 +110,7 @@ export interface SchemaInitializerOptions<P1 = ButtonProps, P2 = {}> {
|
||||
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
|
||||
designable?: boolean;
|
||||
wrap?: (s: ISchema, options?: any) => ISchema;
|
||||
useWrap?: () => ((s: ISchema, options?: any) => ISchema);
|
||||
onSuccess?: (data: any) => void;
|
||||
insert?: InsertType;
|
||||
useInsert?: () => InsertType;
|
||||
|
@ -21,6 +21,7 @@ import { SchemaInitializerOptions } from './types';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
const defaultWrap = (s: ISchema) => s;
|
||||
const useWrapDefault = (wrap = defaultWrap) => wrap;
|
||||
|
||||
export function withInitializer<T>(C: ComponentType<T>) {
|
||||
const WithInitializer = observer(
|
||||
@ -30,6 +31,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
const {
|
||||
insert,
|
||||
useInsert,
|
||||
useWrap = useWrapDefault,
|
||||
wrap = defaultWrap,
|
||||
insertPosition = 'beforeEnd',
|
||||
onSuccess,
|
||||
@ -43,15 +45,16 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
|
||||
// 插入 schema 的能力
|
||||
const insertCallback = useInsert ? useInsert() : insert;
|
||||
const wrapCallback = useWrap(wrap);
|
||||
const insertSchema = useCallback(
|
||||
(schema) => {
|
||||
if (insertCallback) {
|
||||
insertCallback(wrap(schema, { isInSubTable }));
|
||||
insertCallback(wrapCallback(schema, { isInSubTable }));
|
||||
} 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();
|
||||
@ -105,7 +108,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
{...popoverProps}
|
||||
arrow={false}
|
||||
overlayClassName={overlayClassName}
|
||||
open={visible}
|
||||
open={visible}
|
||||
onOpenChange={setVisible}
|
||||
content={wrapSSR(
|
||||
<div
|
||||
|
@ -7,22 +7,26 @@
|
||||
* 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 { ISchema, useFieldSchema } from '@formily/react';
|
||||
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 CreateModalSchemaSettingsItemProps {
|
||||
name: string;
|
||||
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||
parentSchemaKey: string;
|
||||
parentSchemaKey?: string;
|
||||
defaultValue?: any;
|
||||
useDefaultValue?: () => any;
|
||||
schema: (defaultValue: any) => ISchema;
|
||||
valueKeys?: string[];
|
||||
useVisible?: () => boolean;
|
||||
width?: number | string;
|
||||
useSubmit?: () => (values: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,9 +42,11 @@ export function createModalSettingsItem(options: CreateModalSchemaSettingsItemPr
|
||||
valueKeys,
|
||||
schema,
|
||||
title,
|
||||
useSubmit = useHookDefault,
|
||||
useVisible,
|
||||
defaultValue: propsDefaultValue,
|
||||
useDefaultValue = useHookDefault,
|
||||
width,
|
||||
} = options;
|
||||
return {
|
||||
name,
|
||||
@ -50,15 +56,18 @@ export function createModalSettingsItem(options: CreateModalSchemaSettingsItemPr
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { deepMerge } = useDesignable();
|
||||
const defaultValue = useDefaultValue(propsDefaultValue);
|
||||
const values = _.get(fieldSchema, parentSchemaKey);
|
||||
const values = parentSchemaKey ? _.get(fieldSchema, parentSchemaKey) : undefined;
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const onSubmit = useSubmit();
|
||||
|
||||
return {
|
||||
title: typeof title === 'function' ? title(t) : compile(title),
|
||||
width,
|
||||
schema: schema({ ...defaultValue, ...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 { useFieldSchema } from '@formily/react';
|
||||
import { SchemaSettingsItemType, SelectProps, useCompile, useDesignable } from '@nocobase/client';
|
||||
import { getNewSchema, useHookDefault } from './util';
|
||||
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 {
|
||||
name: string;
|
||||
title: string | ((t: TFunction<'translation', undefined>) => string);
|
||||
|
@ -7,13 +7,15 @@
|
||||
* 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 { getNewSchema, useHookDefault } from './util';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
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 {
|
||||
name: 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 = {
|
||||
fieldSchema: ISchema;
|
||||
schemaKey: string;
|
||||
schemaKey?: string;
|
||||
parentSchemaKey?: string;
|
||||
value: any;
|
||||
valueKeys?: string[];
|
||||
};
|
||||
|
||||
export function getNewSchema(options: IGetNewSchema) {
|
||||
const { fieldSchema, schemaKey, value, valueKeys } = options as any;
|
||||
const schemaKeyArr = schemaKey.split('.');
|
||||
const clonedSchema = _.cloneDeep(fieldSchema[schemaKeyArr[0]]);
|
||||
const { fieldSchema, schemaKey, value, parentSchemaKey, valueKeys } = options;
|
||||
|
||||
if (value != undefined && typeof value === 'object') {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (valueKeys && !valueKeys.includes(key)) return;
|
||||
_.set(clonedSchema, `${schemaKeyArr.slice(1)}.${key}`, value[key]);
|
||||
_.set(fieldSchema, `${parentSchemaKey}.${key}`, value[key]);
|
||||
});
|
||||
} else {
|
||||
_.set(clonedSchema, schemaKeyArr.slice(1), value);
|
||||
_.set(fieldSchema, schemaKey, value);
|
||||
}
|
||||
return {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
[schemaKeyArr[0]]: clonedSchema,
|
||||
};
|
||||
return fieldSchema;
|
||||
}
|
||||
|
||||
export const useHookDefault = (defaultValues?: any) => defaultValues;
|
||||
|
@ -7,6 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { Field, Form } from '@formily/core';
|
||||
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { untracked } from '@formily/reactive';
|
||||
@ -21,7 +22,6 @@ import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
AssociationFilter,
|
||||
useCollection,
|
||||
@ -1582,12 +1582,13 @@ export const useParseURLAndParams = () => {
|
||||
return { parseURLAndParams };
|
||||
};
|
||||
|
||||
export function useLinkActionProps() {
|
||||
export function useLinkActionProps(componentProps?: any) {
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const componentPropsValue = fieldSchema?.['x-component-props'] || componentProps;
|
||||
const { t } = useTranslation();
|
||||
const url = fieldSchema?.['x-component-props']?.['url'];
|
||||
const searchParams = fieldSchema?.['x-component-props']?.['params'] || [];
|
||||
const url = componentPropsValue?.['url'];
|
||||
const searchParams = componentPropsValue?.['params'] || [];
|
||||
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
|
||||
const { parseURLAndParams } = useParseURLAndParams();
|
||||
|
||||
|
@ -17,3 +17,4 @@ export * from './TableFieldProvider';
|
||||
export * from './TableSelectorProvider';
|
||||
export * from './DetailsBlockProvider';
|
||||
export * from './hooks';
|
||||
export { useLinkActionProps } from './hooks/index';
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { ConfigProvider, theme as antdTheme } from 'antd';
|
||||
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 { addCustomAlgorithmToTheme } from './customAlgorithm';
|
||||
import defaultTheme from './defaultTheme';
|
||||
@ -41,7 +41,11 @@ export const useGlobalTheme = () => {
|
||||
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 currentSettingThemeRef = useRef<ThemeConfig>(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/index';
|
||||
export * from './modules/blocks/useParentRecordCommon';
|
||||
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
|
||||
|
||||
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
@ -11,7 +11,9 @@ import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
export const CreateChildInitializer = (props) => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
@ -22,7 +24,7 @@ export const CreateChildInitializer = (props) => {
|
||||
'x-component': 'Action',
|
||||
'x-visible': '{{treeTable}}',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
type: 'link',
|
||||
addChild: true,
|
||||
style: { height: 'auto', lineHeight: 'normal' },
|
||||
|
@ -7,14 +7,14 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useMemo } from 'react';
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer';
|
||||
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({
|
||||
name: 'actionSettings:addChild',
|
||||
@ -42,9 +42,12 @@ export const addChildActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
return {
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -12,8 +12,10 @@ import { useSchemaInitializerItem } from '../../../application';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const CreateActionInitializer = () => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
@ -25,7 +27,7 @@ export const CreateActionInitializer = () => {
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
type: 'primary',
|
||||
component: 'CreateRecordAction',
|
||||
icon: 'PlusOutlined',
|
||||
|
@ -14,6 +14,7 @@ import { useCollection_deprecated, useCollectionManager_deprecated } from '../..
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const addNewActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:addNew',
|
||||
@ -29,9 +30,12 @@ export const addNewActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
return {
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const customizeAddRecordActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:addRecord',
|
||||
@ -26,9 +27,12 @@ export const customizeAddRecordActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
return {
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -9,7 +9,7 @@
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionRecord, useDesignable } from '../../../';
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
@ -19,7 +19,11 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
|
||||
import { SchemaSettingsLinkageRules, SchemaSettingsModalItem } from '../../../schema-settings';
|
||||
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
||||
|
||||
export function SchemaSettingsActionLinkItem() {
|
||||
interface SchemaSettingsActionLinkItemProps {
|
||||
afterSubmit?: () => void;
|
||||
}
|
||||
|
||||
export const SchemaSettingsActionLinkItem: FC<SchemaSettingsActionLinkItemProps> = ({ afterSubmit }) => {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
@ -66,11 +70,12 @@ export function SchemaSettingsActionLinkItem() {
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
afterSubmit?.();
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const customizeLinkActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:link',
|
||||
|
@ -12,8 +12,10 @@ import { useSchemaInitializerItem } from '../../../application';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { BlockInitializer } from '../../../schema-initializer/items';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const PopupActionInitializer = (props) => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
@ -23,7 +25,7 @@ export const PopupActionInitializer = (props) => {
|
||||
'x-settings': 'actionSettings:popup',
|
||||
'x-component': props?.['x-component'] || 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
refreshDataBlockRequest: true,
|
||||
},
|
||||
properties: {
|
||||
|
@ -11,8 +11,10 @@ import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const UpdateActionInitializer = (props) => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
@ -22,7 +24,7 @@ export const UpdateActionInitializer = (props) => {
|
||||
'x-settings': 'actionSettings:edit',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
properties: {
|
||||
|
@ -11,8 +11,10 @@ import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const ViewActionInitializer = (props) => {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
@ -22,7 +24,7 @@ export const ViewActionInitializer = (props) => {
|
||||
'x-settings': 'actionSettings:view',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
|
@ -10,13 +10,10 @@
|
||||
import { useSchemaToolbar } from '../../../application';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useCollection_deprecated } from '../../../collection-manager';
|
||||
import {
|
||||
ButtonEditor,
|
||||
RemoveButton,
|
||||
RefreshDataBlockRequest,
|
||||
} from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const customizePopupActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:popup',
|
||||
@ -44,9 +41,12 @@ export const customizePopupActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
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 { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const editActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:edit',
|
||||
@ -40,9 +41,12 @@ export const editActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
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 { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||
|
||||
export const viewActionSettings = new SchemaSettings({
|
||||
name: 'actionSettings:view',
|
||||
@ -40,9 +41,12 @@ export const viewActionSettings = new SchemaSettings({
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
componentProps: {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
return {
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -33,6 +33,7 @@ import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSetti
|
||||
import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule';
|
||||
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
||||
import { useLocalVariables, useVariables } from '../../../../variables';
|
||||
import { useOpenModeContext } from '../../../popup/OpenModeProvider';
|
||||
|
||||
const enableLink = {
|
||||
name: 'enableLink',
|
||||
@ -162,6 +163,7 @@ const quickCreate: any = {
|
||||
name: 'quickCreate',
|
||||
type: 'select',
|
||||
useComponentProps() {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { t } = useTranslation();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -194,7 +196,7 @@ const quickCreate: any = {
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
type: 'default',
|
||||
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 {
|
||||
key: setting.name,
|
||||
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]);
|
||||
|
@ -144,6 +144,12 @@ export const AdminSettingsLayout = () => {
|
||||
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
||||
onClick={({ key }) => {
|
||||
const plugin = settings.find((item) => item.name === key);
|
||||
|
||||
if (plugin.link) {
|
||||
window.open(plugin.link, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.children?.length) {
|
||||
return navigate(getFirstDeepChildPath(plugin.children));
|
||||
} else {
|
||||
|
@ -10,21 +10,19 @@
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useActionContext } from '.';
|
||||
import { ActionDrawer } from './Action.Drawer';
|
||||
import { ActionModal } from './Action.Modal';
|
||||
import { ActionPage } from './Action.Page';
|
||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||
import { useCurrentPopupContext } from '../page/PagePopups';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
|
||||
export const ActionContainer: ComposedActionDrawer = observer(
|
||||
(props: any) => {
|
||||
const { openMode } = useActionContext();
|
||||
if (openMode === 'drawer') {
|
||||
return <ActionDrawer footerNodeName={'Action.Container.Footer'} {...props} />;
|
||||
}
|
||||
if (openMode === 'modal') {
|
||||
return <ActionModal footerNodeName={'Action.Container.Footer'} {...props} />;
|
||||
}
|
||||
return <ActionPage footerNodeName={'Action.Container.Footer'} {...props} />;
|
||||
const { getComponentByOpenMode } = useOpenModeContext();
|
||||
const { currentLevel } = useCurrentPopupContext();
|
||||
|
||||
const Component = getComponentByOpenMode(openMode);
|
||||
|
||||
return <Component footerNodeName={'Action.Container.Footer'} level={currentLevel} {...props} />;
|
||||
},
|
||||
{ displayName: 'ActionContainer' },
|
||||
);
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
||||
import { FlagProvider } from '../../../flag-provider';
|
||||
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
import { GeneralSchemaDesigner } from '../../../schema-settings/GeneralSchemaDesigner';
|
||||
import {
|
||||
@ -656,6 +657,7 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isPopupAction = [
|
||||
'create',
|
||||
@ -667,8 +669,8 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
||||
].includes(fieldSchema['x-action'] || '');
|
||||
|
||||
return {
|
||||
openMode: isPopupAction,
|
||||
openSize: isPopupAction,
|
||||
openMode: isPopupAction && !hideOpenMode,
|
||||
openSize: isPopupAction && !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -11,25 +11,26 @@ import { RecursionField, observer, useFieldSchema } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
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 { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
|
||||
export const ActionPage: ComposedActionDrawer = observer(
|
||||
() => {
|
||||
({ level }) => {
|
||||
const filedSchema = useFieldSchema();
|
||||
const ctx = useActionContext();
|
||||
const { getContainerDOM } = usePopupOrSubpagesContainerDOM();
|
||||
const { styles } = useActionPageStyle();
|
||||
const { currentLevel } = useCurrentPopupContext();
|
||||
const tabContext = useTabsContext();
|
||||
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
// 20 is the z-index value of the main page
|
||||
zIndex: 20 + currentLevel,
|
||||
zIndex: 20 + level,
|
||||
};
|
||||
}, [currentLevel]);
|
||||
}, [level]);
|
||||
|
||||
if (!ctx.visible) {
|
||||
return null;
|
||||
@ -37,11 +38,19 @@ export const ActionPage: ComposedActionDrawer = observer(
|
||||
|
||||
const actionPageNode = (
|
||||
<div className={styles.container} style={style}>
|
||||
<RecursionField schema={filedSchema} onlyRenderProperties />
|
||||
<TabsContextProvider {...tabContext} tabBarExtraContent={<BackButtonUsedInSubPage />}>
|
||||
<RecursionField schema={filedSchema} onlyRenderProperties />
|
||||
</TabsContextProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(actionPageNode, getContainerDOM());
|
||||
const container = getContainerDOM();
|
||||
|
||||
if (container) {
|
||||
return createPortal(actionPageNode, container);
|
||||
}
|
||||
|
||||
return actionPageNode;
|
||||
},
|
||||
{ displayName: 'ActionPage' },
|
||||
);
|
||||
|
@ -79,7 +79,7 @@ export const ActionBar = withDynamicSchemaProps(
|
||||
>
|
||||
{props.children && (
|
||||
<div>
|
||||
<Space {...spaceProps} style={{ flexWrap: 'wrap' }}>
|
||||
<Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
@ -129,7 +129,7 @@ export const ActionBar = withDynamicSchemaProps(
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
</Space>
|
||||
<Space {...spaceProps} style={{ flexWrap: 'wrap' }}>
|
||||
<Space {...spaceProps} style={{ flexWrap: 'wrap', ...(spaceProps?.style || {}) }}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
if (schema['x-align'] === 'left') {
|
||||
return null;
|
||||
|
@ -87,7 +87,11 @@ export type ComposedAction = React.FC<ActionProps> & {
|
||||
[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>> & {
|
||||
Footer?: React.FC;
|
||||
|
@ -13,6 +13,7 @@ import React, { FC, Fragment, useRef, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
@ -139,6 +140,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
|
||||
const btnElement = (
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
@ -175,7 +177,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
setVisible?.(value);
|
||||
setVisibleWithURL?.(value);
|
||||
},
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
snapshot: collectionField?.interface === 'snapshot',
|
||||
fieldSchema: fieldSchema,
|
||||
}}
|
||||
|
@ -73,13 +73,14 @@ const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => {
|
||||
export interface BlockItemProps {
|
||||
name?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { className, children } = useProps(props);
|
||||
const { className, children, style } = useProps(props);
|
||||
const { styles: blockItemCss } = useStyles();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaToolbarRender(fieldSchema);
|
||||
@ -87,7 +88,12 @@ export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
||||
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
|
||||
|
||||
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()}
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={(err) => console.log(err)}>
|
||||
{children}
|
||||
|
@ -13,7 +13,7 @@ import { Select, SelectProps, Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { useCompile } from '../../hooks/useCompile';
|
||||
|
||||
const colors = {
|
||||
const defaultColors = {
|
||||
red: '{{t("Red")}}',
|
||||
magenta: '{{t("Magenta")}}',
|
||||
volcano: '{{t("Volcano")}}',
|
||||
@ -30,13 +30,15 @@ const colors = {
|
||||
|
||||
export interface ColorSelectProps extends SelectProps {
|
||||
suffix?: React.ReactNode;
|
||||
colors?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const ColorSelect = connect(
|
||||
(props: ColorSelectProps) => {
|
||||
const compile = useCompile();
|
||||
const { colors = defaultColors, ...selectProps } = props;
|
||||
return (
|
||||
<Select {...props}>
|
||||
<Select {...selectProps}>
|
||||
{Object.keys(colors).map((color) => (
|
||||
<Select.Option key={color} value={color}>
|
||||
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
|
||||
@ -53,7 +55,7 @@ export const ColorSelect = connect(
|
||||
}),
|
||||
mapReadPretty((props) => {
|
||||
const compile = useCompile();
|
||||
const { value } = props;
|
||||
const { value, colors = defaultColors } = props;
|
||||
if (!colors[value]) {
|
||||
return null;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
} from '../../../collection-manager';
|
||||
import { useCollectionManager } from '../../../data-source';
|
||||
import { useFlag } from '../../../flag-provider';
|
||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { useColumnSchema } from '../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
||||
import { generalSettingsItems } from '../../../schema-items/GeneralSettings';
|
||||
@ -54,6 +55,7 @@ export const allowAddNew: SchemaSettingsItemType = {
|
||||
return !flag?.isInSubTable && !readPretty && isAssociationField && ['Picker'].includes(fieldMode);
|
||||
},
|
||||
useComponentProps() {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { t } = useTranslation();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -83,7 +85,7 @@ export const allowAddNew: SchemaSettingsItemType = {
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
type: 'default',
|
||||
component: 'CreateRecordAction',
|
||||
},
|
||||
@ -540,6 +542,7 @@ export const formItemSettings = new SchemaSettings({
|
||||
return !readPretty && isAssociationField && ['Select'].includes(fieldMode);
|
||||
},
|
||||
useComponentProps() {
|
||||
const { defaultOpenMode } = useOpenModeContext();
|
||||
const { t } = useTranslation();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -572,7 +575,7 @@ export const formItemSettings = new SchemaSettings({
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
openMode: defaultOpenMode,
|
||||
type: 'default',
|
||||
component: 'CreateRecordAction',
|
||||
},
|
||||
|
@ -14,14 +14,25 @@ import { useToken } from '../../../style';
|
||||
import { useCurrentPopupContext } from './PagePopups';
|
||||
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
|
||||
* @returns
|
||||
*/
|
||||
export const BackButtonUsedInSubPage = () => {
|
||||
const { params } = useCurrentPopupContext();
|
||||
const { closePopup } = usePagePopup();
|
||||
const { token } = useToken();
|
||||
const { goBack } = useBackButton();
|
||||
// tab item gutter, this is fixed value in antd
|
||||
const horizontalItemGutter = 32;
|
||||
|
||||
@ -35,17 +46,7 @@ export const BackButtonUsedInSubPage = () => {
|
||||
};
|
||||
}, [token.paddingXS]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
closePopup(params.popupuid);
|
||||
}, [params.popupuid]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="back-button"
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
style={resetStyle}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Button aria-label="back-button" type="text" icon={<ArrowLeftOutlined />} style={resetStyle} onClick={goBack} />
|
||||
);
|
||||
};
|
||||
|
@ -110,7 +110,7 @@ export const Page = (props) => {
|
||||
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
||||
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
||||
}}
|
||||
onTabClick={(activeKey) => {
|
||||
onChange={(activeKey) => {
|
||||
setLoading(true);
|
||||
navigate(`/admin/${pageUid}/tabs/${activeKey}`, { replace: true });
|
||||
setTimeout(() => {
|
||||
|
@ -19,7 +19,6 @@ import { DataBlockProvider } from '../../../data-source/data-block/DataBlockProv
|
||||
import { BlockRequestContext } from '../../../data-source/data-block/DataBlockRequestProvider';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { TabsContextProvider } from '../tabs/context';
|
||||
import { BackButtonUsedInSubPage } from './BackButtonUsedInSubPage';
|
||||
import { usePopupSettings } from './PopupSettingsProvider';
|
||||
import { deleteRandomNestedSchemaKey, getRandomNestedSchemaKey } from './nestedSchemaKeyStorage';
|
||||
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 { changeTab } = usePagePopup();
|
||||
const onTabClick = useCallback(
|
||||
const onChange = useCallback(
|
||||
(key: string) => {
|
||||
changeTab(key);
|
||||
},
|
||||
[changeTab],
|
||||
);
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
const { isSubPage } = useCurrentPopupContext();
|
||||
const tabBarExtraContent = useMemo(() => (isSubPage ? <BackButtonUsedInSubPage /> : null), [isSubPage]);
|
||||
|
||||
if (!isPopupVisibleControlledByURL()) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabsContextProvider activeKey={params.tab} onTabClick={onTabClick} tabBarExtraContent={tabBarExtraContent}>
|
||||
<TabsContextProvider activeKey={params.tab} onChange={onChange}>
|
||||
{children}
|
||||
</TabsContextProvider>
|
||||
);
|
||||
@ -276,7 +273,7 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => {
|
||||
context={popupPropsRef.current[0].context}
|
||||
currentLevel={1}
|
||||
>
|
||||
<SchemaComponent components={components} schema={rootSchema} onlyRenderProperties />;
|
||||
<SchemaComponent components={components} schema={rootSchema} onlyRenderProperties />
|
||||
</PagePopupsItemProvider>
|
||||
</AllPopupsPropsProviderContext.Provider>
|
||||
);
|
||||
@ -351,6 +348,7 @@ function get404Schema() {
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
level: 99, // 确保在最上层
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
|
@ -17,8 +17,15 @@ export const usePopupSettings = () => {
|
||||
const isPopupVisibleControlledByURL = useCallback(() => {
|
||||
const pathname = window.location.pathname;
|
||||
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.
|
||||
*/
|
||||
|
||||
export { BackButtonUsedInSubPage, useBackButton } from './BackButtonUsedInSubPage';
|
||||
export * from './FixedBlock';
|
||||
export * from './FixedBlockDesignerItem';
|
||||
export * from './Page';
|
||||
export * from './Page.Settings';
|
||||
export { PagePopups } from './PagePopups';
|
||||
export * from './PageTab.Settings';
|
||||
|
@ -129,7 +129,7 @@ export const usePagePopup = () => {
|
||||
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
|
||||
[parentRecord, association],
|
||||
);
|
||||
const currentPopupUidWithoutOpened = fieldSchema['x-uid'];
|
||||
const currentPopupUidWithoutOpened = fieldSchema?.['x-uid'];
|
||||
|
||||
const getNewPathname = useCallback(
|
||||
({
|
||||
|
@ -22,6 +22,8 @@ export const DesignableSwitch = () => {
|
||||
const style = {};
|
||||
if (designable) {
|
||||
style['backgroundColor'] = 'var(--colorSettings)';
|
||||
} else {
|
||||
style['backgroundColor'] = 'transparent';
|
||||
}
|
||||
|
||||
// 快捷键切换编辑状态
|
||||
|
@ -11,7 +11,7 @@ import { createForm } from '@formily/core';
|
||||
import { Schema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useSchemaComponentContext } from '../hooks';
|
||||
import { useComponent, useSchemaComponentContext } from '../hooks';
|
||||
import { FormProvider } from './FormProvider';
|
||||
import { SchemaComponent } from './SchemaComponent';
|
||||
import { useRequestSchema } from './useRequestSchema';
|
||||
@ -26,6 +26,12 @@ export interface RemoteSchemaComponentProps {
|
||||
hidden?: any;
|
||||
onlyRenderProperties?: boolean;
|
||||
noForm?: boolean;
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
memoized?: boolean;
|
||||
NotFoundPage?: React.ComponentType | string;
|
||||
onPageNotFind?: () => void;
|
||||
}
|
||||
|
||||
const defaultTransform = (s: Schema) => s;
|
||||
@ -37,9 +43,12 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
hidden,
|
||||
scope,
|
||||
uid,
|
||||
memoized = true,
|
||||
components,
|
||||
onSuccess,
|
||||
NotFoundPage,
|
||||
schemaTransform = defaultTransform,
|
||||
onPageNotFind,
|
||||
} = props;
|
||||
const { reset } = useSchemaComponentContext();
|
||||
const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema';
|
||||
@ -55,12 +64,20 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
reset && reset();
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
const NotFoundComponent = useComponent(NotFoundPage);
|
||||
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 ? (
|
||||
<SchemaComponent components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||
) : (
|
||||
|
@ -189,6 +189,12 @@ export interface SchemaToolbarProps {
|
||||
*/
|
||||
showBorder?: boolean;
|
||||
showBackground?: boolean;
|
||||
toolbarClassName?: string;
|
||||
toolbarStyle?: React.CSSProperties;
|
||||
spaceWrapperClassName?: string;
|
||||
spaceWrapperStyle?: React.CSSProperties;
|
||||
spaceClassName?: string;
|
||||
spaceStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
@ -198,11 +204,17 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
initializer,
|
||||
settings,
|
||||
showBackground,
|
||||
spaceWrapperClassName,
|
||||
spaceWrapperStyle,
|
||||
showBorder = true,
|
||||
draggable = true,
|
||||
spaceClassName,
|
||||
spaceStyle,
|
||||
toolbarClassName,
|
||||
toolbarStyle = {},
|
||||
} = {
|
||||
...props,
|
||||
...(fieldSchema['x-toolbar-props'] || {}),
|
||||
...(fieldSchema?.['x-toolbar-props'] || {}),
|
||||
} as SchemaToolbarProps;
|
||||
const { designable } = useDesignable();
|
||||
const compile = useCompile();
|
||||
@ -220,12 +232,12 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
if (Array.isArray(title)) return title.map((item) => compile(item));
|
||||
}, [compile, title]);
|
||||
const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender(
|
||||
settings || fieldSchema['x-settings'],
|
||||
fieldSchema['x-settings-props'],
|
||||
settings || fieldSchema?.['x-settings'],
|
||||
fieldSchema?.['x-settings-props'],
|
||||
);
|
||||
const { render: schemaInitializerRender, exists: schemaInitializerExists } = useSchemaInitializerRender(
|
||||
initializer || fieldSchema['x-initializer'],
|
||||
fieldSchema['x-initializer-props'],
|
||||
initializer || fieldSchema?.['x-initializer'],
|
||||
fieldSchema?.['x-initializer-props'],
|
||||
);
|
||||
const rowCtx = useGridRowContext();
|
||||
const gridContext = useGridContext();
|
||||
@ -316,8 +328,8 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className={styles.toolbar}
|
||||
style={{ border: showBorder ? 'auto' : 0, background: showBackground ? 'auto' : 0 }}
|
||||
className={classNames(styles.toolbar, toolbarClassName, 'schema-toolbar')}
|
||||
style={{ border: showBorder ? 'auto' : 0, background: showBackground ? 'auto' : 0, ...toolbarStyle }}
|
||||
>
|
||||
{titleArr && (
|
||||
<div className={styles.toolbarTitle}>
|
||||
@ -333,8 +345,8 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.toolbarIcons}>
|
||||
<Space size={3} align={'center'}>
|
||||
<div className={classNames(styles.toolbarIcons, spaceWrapperClassName)} style={spaceWrapperStyle}>
|
||||
<Space size={3} align={'center'} className={spaceClassName} style={spaceStyle}>
|
||||
{dragElement}
|
||||
{initializerElement}
|
||||
{settingsElement}
|
||||
|
@ -447,15 +447,17 @@ export const SchemaSettingsDivider = function Divider() {
|
||||
|
||||
export interface SchemaSettingsRemoveProps {
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
confirm?: ModalFuncProps;
|
||||
removeParentsIfNoChildren?: boolean;
|
||||
breakRemoveOn?: ISchema | ((s: ISchema) => boolean);
|
||||
}
|
||||
export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
||||
const { disabled, confirm, removeParentsIfNoChildren, breakRemoveOn } = props;
|
||||
const { disabled, confirm, title, removeParentsIfNoChildren, breakRemoveOn } = props;
|
||||
const { dn, template } = useSchemaSettings();
|
||||
const { t } = useTranslation();
|
||||
const field = useField<Field>();
|
||||
const compile = useCompile();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const ctx = useBlockTemplateContext();
|
||||
const form = useForm();
|
||||
@ -470,7 +472,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
||||
eventKey="remove"
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
title: t('Delete block'),
|
||||
title: title ? compile(title) : t('Delete block'),
|
||||
content: t('Are you sure you want to delete it?'),
|
||||
...confirm,
|
||||
async onOk() {
|
||||
@ -602,10 +604,22 @@ export interface SchemaSettingsActionModalItemProps
|
||||
schema?: ISchema;
|
||||
beforeOpen?: () => void;
|
||||
maskClosable?: boolean;
|
||||
width?: string | number;
|
||||
}
|
||||
export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProps> = React.memo((props) => {
|
||||
const { title, onSubmit, initialValues, beforeOpen, initialSchema, schema, modalTip, components, scope, ...others } =
|
||||
props;
|
||||
const {
|
||||
title,
|
||||
onSubmit,
|
||||
width = '50%',
|
||||
initialValues,
|
||||
beforeOpen,
|
||||
initialSchema,
|
||||
schema,
|
||||
modalTip,
|
||||
components,
|
||||
scope,
|
||||
...others
|
||||
} = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [schemaUid, setSchemaUid] = useState<string>(props.uid);
|
||||
const { t } = useTranslation();
|
||||
@ -636,8 +650,12 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
|
||||
|
||||
const submitHandler = useCallback(async () => {
|
||||
await form.submit();
|
||||
onSubmit?.(cloneDeep(form.values));
|
||||
setVisible(false);
|
||||
try {
|
||||
await onSubmit?.(cloneDeep(form.values));
|
||||
setVisible(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, [form, onSubmit]);
|
||||
|
||||
const openAssignedFieldValueHandler = useCallback(async () => {
|
||||
@ -667,7 +685,7 @@ export const SchemaSettingsActionModalItem: FC<SchemaSettingsActionModalItemProp
|
||||
</SchemaSettingsItem>
|
||||
{createPortal(
|
||||
<Modal
|
||||
width={'50%'}
|
||||
width={width}
|
||||
title={compile(title)}
|
||||
{...others}
|
||||
destroyOnClose
|
||||
@ -704,7 +722,7 @@ SchemaSettingsActionModalItem.displayName = 'SchemaSettingsActionModalItem';
|
||||
|
||||
export interface SchemaSettingsModalItemProps {
|
||||
title: string;
|
||||
onSubmit: (values: any) => void;
|
||||
onSubmit: (values: any) => Promise<any> | void;
|
||||
initialValues?: any;
|
||||
schema?: ISchema | (() => ISchema);
|
||||
modalTip?: string;
|
||||
|
@ -183,6 +183,15 @@ export interface PageConfig {
|
||||
keepUid?: boolean;
|
||||
}
|
||||
|
||||
export interface MobilePageConfig extends Omit<PageConfig, 'type'> {
|
||||
type?: 'page' | 'link';
|
||||
/**
|
||||
* 页面的基础路径
|
||||
* @default '/m/'
|
||||
*/
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
interface CreatePageOptions {
|
||||
type?: PageConfig['type'];
|
||||
url?: PageConfig['url'];
|
||||
@ -192,6 +201,10 @@ interface CreatePageOptions {
|
||||
keepUid?: boolean;
|
||||
}
|
||||
|
||||
interface CreateMobilePageOptions extends Omit<CreatePageOptions, 'type'> {
|
||||
type?: Omit<PageConfig['type'], 'group'>;
|
||||
}
|
||||
|
||||
interface ExtendUtils {
|
||||
page?: Page;
|
||||
/**
|
||||
@ -200,6 +213,12 @@ interface ExtendUtils {
|
||||
* @returns
|
||||
*/
|
||||
mockPage: (pageConfig?: PageConfig) => NocoPage;
|
||||
/**
|
||||
* 根据配置,生成一个移动端 NocoBase 的页面
|
||||
* @param pageConfig 页面配置
|
||||
* @returns
|
||||
*/
|
||||
mockMobilePage: (pageConfig?: MobilePageConfig) => NocoMobilePage;
|
||||
/**
|
||||
* 根据配置,生成一个需要手动销毁的 NocoPage 页面
|
||||
* @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}`;
|
||||
|
||||
export class NocoPage {
|
||||
private url: string;
|
||||
private uid: string | undefined;
|
||||
private collectionsName: string[] | undefined;
|
||||
private _waitForInit: Promise<void>;
|
||||
protected url: string;
|
||||
protected uid: string | undefined;
|
||||
protected collectionsName: string[] | undefined;
|
||||
protected _waitForInit: Promise<void>;
|
||||
|
||||
constructor(
|
||||
private options?: PageConfig,
|
||||
private page?: Page,
|
||||
protected options?: PageConfig,
|
||||
protected page?: Page,
|
||||
) {
|
||||
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;
|
||||
const getPage = async (browser: Browser) => {
|
||||
if (!_page) {
|
||||
@ -412,6 +482,33 @@ const _test = base.extend<ExtendUtils>({
|
||||
|
||||
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) => {
|
||||
const mockManualDestroyPage = (config?: PageConfig) => {
|
||||
const nocoPage = new NocoPage(config);
|
||||
@ -684,6 +781,196 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
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 的页面
|
||||
*/
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
SchemaSettingsRemove,
|
||||
SchemaSettingsSelectItem,
|
||||
useDesignable,
|
||||
useOpenModeContext,
|
||||
useSchemaToolbar,
|
||||
} from '@nocobase/client';
|
||||
import { ModalProps } from 'antd';
|
||||
@ -137,14 +138,15 @@ export const bulkEditActionSettings = new SchemaSettings({
|
||||
name: 'openMode',
|
||||
Component: SchemaInitializerOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate', 'customize:create'].includes(
|
||||
fieldSchema['x-action'] || '',
|
||||
);
|
||||
|
||||
return {
|
||||
openMode: isPopupAction,
|
||||
openSize: isPopupAction,
|
||||
openMode: isPopupAction && !hideOpenMode,
|
||||
openSize: isPopupAction && !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
useCollectionState,
|
||||
useCollection_deprecated,
|
||||
useDesignable,
|
||||
useOpenModeContext,
|
||||
useRecord,
|
||||
useSchemaToolbar,
|
||||
useSyncFromForm,
|
||||
@ -357,6 +358,7 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const modeOptions = useMemo(() => {
|
||||
@ -367,8 +369,8 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
||||
}, [t]);
|
||||
|
||||
return {
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
modeOptions,
|
||||
};
|
||||
},
|
||||
|
@ -30,7 +30,11 @@ export const AuthenticatorsContextProvider: FC<{ children: React.ReactNode }> =
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
@ -44,6 +44,11 @@ export class PluginBlockIframeClient extends Plugin {
|
||||
title: '{{t("Iframe")}}',
|
||||
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}`,
|
||||
workbenchBlockInitializerItem,
|
||||
);
|
||||
this.app.schemaInitializerManager.addItem(
|
||||
'mobile:addBlock',
|
||||
`otherBlocks.${workbenchBlockInitializerItem.name}`,
|
||||
workbenchBlockInitializerItem,
|
||||
);
|
||||
|
||||
// link 操作
|
||||
this.app.schemaSettingsManager.add(workbenchActionSettingsLink);
|
||||
|
@ -34,6 +34,10 @@ export class PluginCalendarClient extends Plugin {
|
||||
title: generateNTemplate('Calendar'),
|
||||
Component: 'CalendarBlockInitializer',
|
||||
});
|
||||
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'dataBlocks.calendar', {
|
||||
title: generateNTemplate('Calendar'),
|
||||
Component: 'CalendarBlockInitializer',
|
||||
});
|
||||
this.app.schemaInitializerManager.addItem('popup:common:addBlock', 'dataBlocks.calendar', {
|
||||
title: generateNTemplate('Calendar'),
|
||||
Component: 'CalendarBlockInitializer',
|
||||
|
@ -50,6 +50,10 @@ class PluginDataVisualiztionClient extends Plugin {
|
||||
title: lang('Charts'),
|
||||
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.
|
||||
*/
|
||||
|
||||
import { ActionInitializerItem, useCollection_deprecated } from '@nocobase/client';
|
||||
import { ActionInitializerItem } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
export const UploadActionInitializer = (props) => {
|
||||
const collection = useCollection_deprecated();
|
||||
|
||||
const schema = {
|
||||
type: 'void',
|
||||
'x-action': 'create',
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
useDesignable,
|
||||
useFormItemInitializerFields,
|
||||
useGetAriaLabelOfDesigner,
|
||||
useOpenModeContext,
|
||||
useSchemaInitializerRender,
|
||||
} from '@nocobase/client';
|
||||
import { Space } from 'antd';
|
||||
@ -137,6 +138,13 @@ const commonOptions = {
|
||||
{
|
||||
name: 'openMode',
|
||||
Component: SchemaInitializerOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const { hideOpenMode } = useOpenModeContext();
|
||||
return {
|
||||
openMode: !hideOpenMode,
|
||||
openSize: !hideOpenMode,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -98,7 +98,7 @@ const components = {
|
||||
google: GoogleMapConfiguration,
|
||||
};
|
||||
|
||||
const tabList = MapTypes.map((item) => {
|
||||
const routeList = MapTypes.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
component: components[item.value],
|
||||
@ -112,7 +112,7 @@ export const Configuration = () => {
|
||||
return (
|
||||
<Card bordered>
|
||||
<Tabs type="card" defaultActiveKey={search.get('tab')}>
|
||||
{tabList.map((tab) => {
|
||||
{routeList.map((tab) => {
|
||||
return (
|
||||
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
|
||||
<tab.component type={tab.value} />
|
||||
|
@ -47,7 +47,10 @@ export class PluginMapClient extends Plugin {
|
||||
title: generateNTemplate('Map'),
|
||||
Component: 'MapBlockInitializer',
|
||||
});
|
||||
|
||||
this.app.schemaInitializerManager.addItem('mobile:addBlock', 'dataBlocks.map', {
|
||||
title: generateNTemplate('Map'),
|
||||
Component: 'MapBlockInitializer',
|
||||
});
|
||||
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||
title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
|
||||
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…
Reference in New Issue
Block a user