feat(plugin-mobile-client): support mobile-side client (#1879)

* feat: init mobile client

* feat: add plugin

* feat: ready to develop

* feat: update pm styels

* feat: add mobile center

* feat: router ready

* feat: support menu block, then menu ready

* fix: incorrect path

* feat: support TabBar

* feat: tabbar, menu support dragging

* feat: support page and header

* feat: mobile view

* fix: optional schema

* feat: improve styles

* fix: user-scalable

* feat: support pc component show in mobile

* feat: hidden divider

* fix: improve drawer props

* feat: support list block

* feat: rename to details list

* feat: page support tabs

* feat: improve designer css

* feat: complete enable/disabled header of page

* feat: some improve

* feat: improve empty data

* fix: header info cannot displayed

* chore: update deps

* fix: incorrect spacing

* fix: menu designer

* refactor: re implement

* feat: support page template

* feat: clean code

* feat: support i18n

* chore: update lock

* feat: support GirdCard in mobile

* fix: build failed

* feat: only render one column in mobile interface

* fix: back button should not display in container

* fix: switch to padding

* fix: fixedBlockDesignRItem shouldn't display in dosen't support block

* fix: update font family

* fix: remove gridcard title

* fix: dragging scope is too wide

* fix: add menu cannot direct display

* refactor: improve tabbar schema usage

* refactor: improve menu schema

* feat: should to use simple pagination

* feat: the tag should pre-wrap

* feat: improve the configuration button

* feat: improve name

* fix: clear data when modal is closed

* fix: the tag is too long

* fix: i18n

* fix: font incorrect

* feat: add map block

* fix: some maps error

* feat: support global action in page

* feat: improve border color

* feat: improve performance, the count stop early

* style: improve

* fix: incorrect font

* fix: style conflict

* chore: update version

* chore: missing dep

* feat: support setting block

* feat: improve settings block and improve

* feat: support onBackPressed

* fix: ts error

* feat: improve cannot find tab should navigate to mobile

* docs: update

* chore: update deps

* fix: showTitle state is incorrect

* feat: improve jsbridge apis

* fix: navigate to admin after signout

* chore: remove mgrid block

* fix: ts error

* fix: switch role will reload to root page

* fix: update deps

* fix: upgrade formily to 2.2.24

---------

Co-authored-by: dream2023 <1098626505@qq.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Dunqing 2023-06-08 19:54:00 +08:00 committed by GitHub
parent b401c54442
commit 9c165db0f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 2807 additions and 451 deletions

View File

@ -2,12 +2,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, shrink-to-fit=no">
<title>Loading...</title>
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/site.webmanifest">
<style>
#root {
width: 100%;
height: 100%;
}
/* width */
.win ::-webkit-scrollbar {
width: 8px;

View File

@ -0,0 +1 @@
export { default } from '@nocobase/plugin-mobile-client/client';

View File

@ -53,9 +53,7 @@ const App = React.memo((props: any) => {
const C = compose(...props.providers)(() => {
const routes = useRoutes();
return (
<div>
<RouteSwitch routes={routes} />
</div>
<RouteSwitch routes={routes} />
);
});
return <C />;

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
import { sortBy } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
@ -262,8 +262,21 @@ const SettingsCenter = (props) => {
};
});
return (
<div>
<Layout>
<div
className={cx(
'nb-setting-center',
css`
&.nb-setting-center {
flex: 1;
}
`,
)}
>
<Layout
className={css`
height: 100%;
`}
>
<div
style={
{
@ -302,7 +315,12 @@ const SettingsCenter = (props) => {
items={menuItems as any}
/>
</Layout.Sider>
<Layout.Content>
<Layout.Content
className={css`
display: flex;
flex-direction: column;
`}
>
{aclPluginTabCheck && (
<PageHeader
ghost={false}
@ -321,7 +339,7 @@ const SettingsCenter = (props) => {
}
/>
)}
<div className={'m24'} style={{ margin: 24 }}>
<div className={'m24'} style={{ margin: 24, flex: 1 }}>
{aclPluginTabCheck ? (
component && React.createElement(component)
) : (

View File

@ -1,11 +1,13 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import React, { useMemo } from 'react';
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom';
import { RouteContext } from './context';
import { useRouteComponent } from './hooks';
import { useRoute, useRouteComponent } from './hooks';
import { RouteSwitchProps } from './types';
export function RouteSwitch(props: RouteSwitchProps) {
const { routes = [] } = props;
const { url: base, path } = useRouteMatch();
if (!routes.length) {
return null;
}
@ -28,21 +30,20 @@ export function RouteSwitch(props: RouteSwitchProps) {
if (!route.path && Array.isArray(route.routes)) {
route.path = route.routes.map((r) => r.path) as any;
}
let nextPath = route.path;
if (route.path && typeof route.path === 'string' && !route.path.startsWith('/')) {
nextPath = `${base.endsWith('/') ? base : base + '/'}${route.path}`;
}
return (
<Route
key={index}
path={route.path}
key={[nextPath].flat().join(',') as string}
path={nextPath}
exact={route.exact}
strict={route.strict}
sensitive={route.sensitive}
render={(props) => {
return (
<RouteContext.Provider value={route}>
<ComponentRenderer {...props} route={route} />
</RouteContext.Provider>
);
}}
/>
>
<ComponentRenderer route={route} />
</Route>
);
})}
</Switch>
@ -50,10 +51,16 @@ export function RouteSwitch(props: RouteSwitchProps) {
}
function ComponentRenderer(props) {
const Component = useRouteComponent(props?.route?.component);
const { route } = props;
const Component = useRouteComponent(route?.component);
if (React.isValidElement(Component)) {
return React.cloneElement(Component, props, <RouteSwitch routes={route.routes} />);
}
return (
<Component {...props}>
<RouteSwitch routes={props.route.routes} />
</Component>
<RouteContext.Provider value={route}>
<Component {...props}>
<RouteSwitch routes={route?.routes} />
</Component>
</RouteContext.Provider>
);
}

View File

@ -286,19 +286,25 @@ export const InternalAdminLayout = (props: any) => {
);
};
export const AdminProvider = (props) => {
return <CurrentAppInfoProvider>
<CurrentUserProvider>
<RemoteSchemaTemplateManagerProvider>
<RemoteCollectionManagerProvider>
<ACLRolesCheckProvider>
{props.children}
</ACLRolesCheckProvider>
</RemoteCollectionManagerProvider>
</RemoteSchemaTemplateManagerProvider>
</CurrentUserProvider>
</CurrentAppInfoProvider>
}
export const AdminLayout = (props) => {
return (
<CurrentAppInfoProvider>
<CurrentUserProvider>
<RemoteSchemaTemplateManagerProvider>
<RemoteCollectionManagerProvider>
<ACLRolesCheckProvider>
<InternalAdminLayout {...props} />
</ACLRolesCheckProvider>
</RemoteCollectionManagerProvider>
</RemoteSchemaTemplateManagerProvider>
</CurrentUserProvider>
</CurrentAppInfoProvider>
<AdminProvider>
<InternalAdminLayout {...props} />
</AdminProvider>
);
};

View File

@ -1,4 +1,4 @@
import { createContext } from 'react';
import { createContext, useContext } from 'react';
import { RouteProps } from './types';
export const RouteSwitchContext = createContext({
@ -7,3 +7,5 @@ export const RouteSwitchContext = createContext({
});
export const RouteContext = createContext<RouteProps>(null);
export const useRouteSwitchContext = () => useContext(RouteSwitchContext);

View File

@ -31,4 +31,5 @@ export type RouteRedirectProps = RedirectProps | RouteProps;
export interface RouteSwitchProps {
routes?: RouteRedirectProps[];
components?: any;
base?: string;
}

View File

@ -3,11 +3,10 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { Drawer } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { OpenSize } from './';
import { useActionContext } from './hooks';
import { ComposedActionDrawer } from './types';
import { OpenSize } from './';
const openSizeWidthMap = new Map<OpenSize, string>([
['small', '30%'],
@ -18,7 +17,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
(props) => {
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
const { t } = useTranslation();
const { visible, setVisible, openSize = 'middle' } = useActionContext();
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
const schema = useFieldSchema();
const field = useField();
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
@ -29,90 +28,86 @@ export const ActionDrawer: ComposedActionDrawer = observer(
}
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Drawer
width={openSizeWidthMap.get(finalOpenSize)}
title={field.title}
{...others}
destroyOnClose
open={visible}
onClose={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-drawer-header {
display: none;
}
.ant-drawer-body {
padding-top: 14px;
}
.ant-drawer-content {
background: #f0f2f5;
}
}
&.nb-record-picker-selector {
.nb-block-item {
margin-bottom: 24px;
.general-schema-designer {
top: -8px;
bottom: -8px;
left: -8px;
right: -8px;
}
}
}
`,
)}
footer={
footerSchema && (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)
<Drawer
width={openSizeWidthMap.get(openSize)}
title={field.title}
{...others}
{...drawerProps}
style={{
...drawerProps?.style,
...others?.style,
}}
destroyOnClose
open={visible}
onClose={() => setVisible(false, true)}
className={classNames(
drawerProps?.className,
others.className,
css`
&.nb-action-popup {
.ant-drawer-header {
display: none;
}
.ant-drawer-body {
padding-top: 14px;
}
.ant-drawer-content {
background: #f0f2f5;
}
}
&.nb-record-picker-selector {
.nb-block-item {
margin-bottom: 24px;
.general-schema-designer {
top: -8px;
bottom: -8px;
left: -8px;
right: -8px;
}
}
}
`,
)}
footer={
footerSchema && (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
return s['x-component'] === footerNodeName;
}}
/>
</Drawer>
</div>,
document.body,
)}
</>
</div>
)
}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Drawer>
);
},
{ displayName: 'ActionDrawer' },

View File

@ -3,7 +3,6 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { Modal, ModalProps } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import { OpenSize, useActionContext } from '.';
import { ComposedActionDrawer } from './types';
@ -15,7 +14,7 @@ const openSizeWidthMap = new Map<OpenSize, string>([
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
(props) => {
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
const { visible, setVisible, openSize = 'large' } = useActionContext();
const { visible, setVisible, openSize = 'large', modalProps } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize);
const schema = useFieldSchema();
const field = useField();
@ -26,84 +25,79 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Modal
width={actualWidth}
title={field.title}
{...(others as ModalProps)}
destroyOnClose
visible={visible}
onCancel={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-modal-header {
display: none;
}
.ant-modal-body {
padding-top: 16px;
}
.ant-modal-body {
background: #f0f2f5;
}
.ant-modal-close-x {
width: 32px;
height: 32px;
line-height: 32px;
}
}
`,
)}
footer={
footerSchema ? (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
) : (
false
)
<Modal
width={actualWidth}
title={field.title}
{...(others as ModalProps)}
{...modalProps}
style={{
...modalProps?.style,
...others?.style,
}}
destroyOnClose
open={visible}
onCancel={() => setVisible(false, true)}
className={classNames(
others.className,
modalProps?.className,
css`
&.nb-action-popup {
.ant-modal-header {
display: none;
}
.ant-modal-body {
padding-top: 16px;
}
.ant-modal-body {
background: #f0f2f5;
}
.ant-modal-close-x {
width: 32px;
height: 32px;
line-height: 32px;
}
}
`,
)}
footer={
footerSchema ? (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
return s['x-component'] === footerNodeName;
}}
/>
</Modal>
</div>,
document.body,
)}
</>
</div>
) : (
false
)
}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Modal>
);
},
{ displayName: 'ActionModal' },

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Modal, Popover } from 'antd';
import classnames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useActionContext } from '../..';
import { useDesignable } from '../../';
import { Icon } from '../../../icon';
@ -16,7 +16,7 @@ import { ActionDrawer } from './Action.Drawer';
import { ActionLink } from './Action.Link';
import { ActionModal } from './Action.Modal';
import { ActionPage } from './Action.Page';
import { ActionContext } from './context';
import { ActionContextProvider } from './context';
import { useA } from './hooks';
import { ComposedAction } from './types';
import { linkageAction } from './utils';
@ -95,16 +95,6 @@ export const Action: ComposedAction = observer(
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
const renderTitle = useMemo(() => {
if (title) {
return title;
}
if (fieldSchema.title) {
return compile(fieldSchema.title);
}
return null;
}, [compile, fieldSchema.title, title]);
useEffect(() => {
field.linkageProperty = {};
linkageRules
@ -127,6 +117,7 @@ export const Action: ComposedAction = observer(
icon={<Icon type={icon} />}
disabled={disabled}
style={{
...others.style,
opacity: designable && field?.data?.hidden && 0.1,
}}
onClick={(e: React.MouseEvent) => {
@ -151,30 +142,28 @@ export const Action: ComposedAction = observer(
component={tarComponent || Button}
className={classnames(className, actionDesignerCss)}
>
{renderTitle}
{title || compile(fieldSchema.title)}
<Designer {...designerProps} />
</SortableItem>
);
};
return (
<ActionContext.Provider
value={{
button: renderButton(),
visible,
setVisible,
formValueChanged,
setFormValueChanged,
openMode,
openSize,
containerRefKey,
fieldSchema,
}}
<ActionContextProvider
button={renderButton()}
visible={visible}
setVisible={setVisible}
formValueChanged={formValueChanged}
setFormValueChanged={setFormValueChanged}
openMode={openMode}
openSize={openSize}
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && props.children}
</ActionContext.Provider>
</ActionContextProvider>
);
},
{ displayName: 'Action' },

View File

@ -1,37 +1,81 @@
import { css, cx } from '@emotion/css';
import { cx } from '@emotion/css';
import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { Space } from 'antd';
import React from 'react';
import React, { CSSProperties, useContext } from 'react';
import { createPortal } from 'react-dom';
import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext } from '../../common';
import { useDesignable, useProps } from '../../hooks';
interface ActionBarContextForceProps {
layout?: 'one-column' | 'tow-columns';
style?: CSSProperties;
className?: string;
}
export interface ActionBarContextValue {
container?: Element | DocumentFragment;
/**
* override props
*/
forceProps?: ActionBarContextForceProps;
parentComponents?: string[];
}
const ActionBarContext = React.createContext<ActionBarContextValue>({
container: null,
});
export const ActionBarProvider: React.FC<ActionBarContextValue> = ({ children, ...props }) => {
return <ActionBarContext.Provider value={props}>{children}</ActionBarContext.Provider>;
};
export const useActionBarContext = () => {
return useContext(ActionBarContext);
};
const Portal: React.FC = (props) => {
const filedSchema = useFieldSchema();
const { container, parentComponents = ['BlockItem', 'CardItem'] } = useActionBarContext();
return (
<>
{container && parentComponents.includes(filedSchema.parent['x-component'])
? createPortal(props.children, container)
: props.children}
</>
);
};
export const ActionBar = observer(
(props: any) => {
const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
const { forceProps = {} } = useActionBarContext();
const { layout = 'tow-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
const fieldSchema = useFieldSchema();
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable();
if (layout === 'one-column') {
return (
<DndContext>
<div
style={{ display: 'flex', alignItems: 'center', ...style }}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
{props.children && (
<div style={{ marginRight: 8 }}>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</div>
)}
<InitializerComponent />
</div>
</DndContext>
<Portal>
<DndContext>
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style }}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
{props.children && (
<div>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</div>
)}
<InitializerComponent style={{ margin: '0 !important' }} />
</div>
</DndContext>
</Portal>
);
}
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
@ -41,25 +85,18 @@ export const ActionBar = observer(
!designable && !hasActions
? undefined
: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
overflowX: 'auto',
flexShrink: 0,
...style,
}
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
overflowX: 'auto',
flexShrink: 0,
...style,
}
}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
<div
className={css`
.ant-space:last-child {
margin-left: 8px;
}
`}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<DndContext>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {

View File

@ -1,8 +1,19 @@
import { Schema } from '@formily/react';
import { createContext } from 'react';
import { DrawerProps, ModalProps } from 'antd';
import React, { createContext } from 'react';
import { useActionContext } from './hooks';
export const ActionContext = createContext<ActionContextProps>({});
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
const contextProps = useActionContext();
return (
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>
{props.children}
</ActionContext.Provider>
);
};
export type OpenSize = 'small' | 'middle' | 'large';
export interface ActionContextProps {
button?: any;
@ -15,4 +26,6 @@ export interface ActionContextProps {
formValueChanged?: boolean;
setFormValueChanged?: (v: boolean) => void;
fieldSchema?: Schema;
drawerProps?: DrawerProps;
modalProps?: ModalProps;
}

View File

@ -2,15 +2,16 @@ import { css } from '@emotion/css';
import cls from 'classnames';
import React from 'react';
import { SortableItem } from '../../common';
import { useDesigner } from '../../hooks';
import { useDesigner, useProps } from '../../hooks';
export const BlockItem: React.FC<any> = (props) => {
const { className, children } = useProps(props);
const Designer = useDesigner();
return (
<SortableItem
className={cls(
'nb-block-item',
props.className,
className,
css`
position: relative;
&:hover {
@ -57,7 +58,7 @@ export const BlockItem: React.FC<any> = (props) => {
)}
>
<Designer />
{props.children}
{children}
</SortableItem>
);
};

View File

@ -12,7 +12,7 @@ export const CardItem: React.FC = (props) => {
return templateKey && !template ? null : (
<BlockItem className={'noco-card-item'}>
<Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
<Card style={{ marginBottom: 'var(--nb-spacing)' }} bordered={false} {...restProps}>
{props.children}
</Card>
</BlockItem>

View File

@ -74,7 +74,6 @@ export const GridCardDesigner = () => {
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaComponentOptions components={{ Slider }}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the count of columns displayed in a row')}
initialValues={columnCount}
@ -155,7 +154,7 @@ export const GridCardDesigner = () => {
field: {
type: 'string',
enum: sortFields,
required:true,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {

View File

@ -5,7 +5,7 @@ import { List as AntdList, PaginationProps, Col } from 'antd';
import { useGridCardActionBarProps } from './hooks';
import { SortableItem } from '../../common';
import { SchemaComponentOptions } from '../../core';
import { useDesigner } from '../../hooks';
import { useDesigner, useProps } from '../../hooks';
import { GridCardItem } from './GridCard.Item';
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
import { GridCardDesigner } from './GridCard.Designer';
@ -55,7 +55,9 @@ const designerCss = css`
`;
const InternalGridCard = (props) => {
const { service, columnCount = defaultColumnCount } = useGridCardBlockContext();
const { columnCount: columnCountProp, pagination } = useProps(props);
const { service, _columnCount = defaultColumnCount } = useGridCardBlockContext();
const columnCount = columnCountProp || _columnCount;
const { run, params } = service;
const meta = service?.data?.meta;
const fieldSchema = useFieldSchema();
@ -70,7 +72,9 @@ const InternalGridCard = (props) => {
new Schema({
type: 'object',
properties: {
[key]: fieldSchema.properties['item'],
[key]: {
...fieldSchema.properties['item'],
},
},
}),
);
@ -104,6 +108,7 @@ const InternalGridCard = (props) => {
!meta || meta.count <= meta.pageSize
? false
: {
...pagination,
onChange: onPaginationChange,
total: meta?.count || 0,
pageSize: meta?.pageSize || 10,

View File

@ -332,6 +332,7 @@ export const useGridRowContext = () => {
export const Grid: any = observer(
(props: any) => {
const { showDivider = true } = props;
const gridRef = useRef(null);
const field = useField();
const fieldSchema = useFieldSchema();
@ -346,36 +347,40 @@ export const Grid: any = observer(
return (
<GridContext.Provider
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent, showDivider }}
>
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
<DndWrapper dndContext={props.dndContext}>
<RowDivider
rows={rows}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
{showDivider ? (
<RowDivider
rows={rows}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
) : null}
{rows.map((schema, index) => {
return (
<React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} />
<RowDivider
rows={rows}
index={index}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
{showDivider ? (
<RowDivider
rows={rows}
index={index}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
) : null}
</React.Fragment>
);
})}
@ -394,6 +399,7 @@ Grid.Row = observer(
const fieldSchema = useFieldSchema();
const addr = field.address.toString();
const cols = useColProperties();
const { showDivider } = useGridContext();
return (
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
@ -402,40 +408,46 @@ Grid.Row = observer(
'nb-grid-row',
css`
overflow-x: hidden;
margin: 0 calc(-1 * var(--nb-spacing));
display: flex;
position: relative;
/* z-index: 0; */
`,
)}
style={{
margin: showDivider ? '0 calc(-1 * var(--nb-spacing))' : null,
}}
>
<ColDivider
cols={cols}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
{showDivider && (
<ColDivider
cols={cols}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
)}
{cols.map((schema, index) => {
return (
<React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} />
<ColDivider
cols={cols}
index={index}
last={index === cols.length - 1}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
{showDivider && (
<ColDivider
cols={cols}
index={index}
last={index === cols.length - 1}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
)}
</React.Fragment>
);
})}
@ -449,6 +461,7 @@ Grid.Row = observer(
Grid.Col = observer(
(props: any) => {
const { cols = [] } = useContext(GridRowContext);
const { showDivider } = useGridContext();
const schema = useFieldSchema();
const field = useField();
@ -456,7 +469,7 @@ Grid.Col = observer(
let width = '';
if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`;
width = `calc(${w}% - var(--nb-spacing) * ${(showDivider ? cols.length + 1 : 0) / cols.length})`;
}
return width;
}, [cols?.length, schema?.['x-component-props']?.['width']]);
@ -477,5 +490,5 @@ Grid.Col = observer(
</GridColContext.Provider>
);
},
{ displayName: 'Grid.Col' },
{ displayName: 'Grid.Row' },
);

View File

@ -21,12 +21,22 @@
--nb-designer-offset: -10px;
}
.theme-compact {
--nb-spacing: 16px;
--nb-spacing: 12px;
--nb-designer-offset: -6px;
.ant-formily-item {
margin-bottom: 16px;
margin-bottom: 12px;
}
.ant-card {
margin-bottom: 16px !important;
margin-bottom: 12px !important;
}
}
.ant-formily-item-control-content-component .ant-tag {
white-space: pre-wrap;
}
html body {
--adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol;
}

View File

@ -11,11 +11,13 @@ const FixedBlockContext = React.createContext<{
height: number | string;
fixedBlockUID: boolean | string;
fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
inFixedBlock: boolean;
}>({
setFixedBlock: () => {},
height: 0,
fixedBlockUID: false,
fixedBlockUIDRef: { current: false },
inFixedBlock: false,
});
export const useFixedSchema = () => {
@ -69,8 +71,9 @@ export const FixedBlockDesignerItem = () => {
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const record = useRecord();
const { inFixedBlock } = useFixedBlock();
if (Object.keys(record).length) {
if (Object.keys(record).length || !inFixedBlock) {
return null;
}
return (
@ -127,7 +130,7 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
_setFixedBlock(v);
};
return (
<FixedBlockContext.Provider value={{ height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
<FixedBlockContext.Provider value={{ inFixedBlock: true, height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
<div
className={fixedBlockUID ? fixedBlockCss : ''}
style={{

View File

@ -245,7 +245,7 @@ export const Page = (props) => {
<Tabs.TabPane
tab={
<SortableItem
id={schema.name}
id={schema.name as string}
schema={schema}
className={classNames('nb-action-link', designerCss, props.className)}
>

View File

@ -10,7 +10,7 @@ import {
import { CollectionProvider, useCollection } from '../../../collection-manager';
import { FormProvider, SchemaComponentOptions } from '../../core';
import { useCompile } from '../../hooks';
import { ActionContext, useActionContext } from '../action';
import { ActionContextProvider, useActionContext } from '../action';
import { FileSelector } from '../preview';
import { useFieldNames } from './useFieldNames';
import { getLabelFormatValue, useLabelUiSchema } from './util';
@ -269,7 +269,7 @@ const Drawer: React.FunctionComponent<{
return (
<RecordPickerProvider {...recordPickerProps}>
<CollectionProvider allowNull name={collectionField?.target}>
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
<ActionContextProvider openMode="drawer" visible={visible} setVisible={setVisible}>
<FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
@ -283,7 +283,7 @@ const Drawer: React.FunctionComponent<{
</SchemaComponentOptions>
</TableSelectorParamsProvider>
</FormProvider>
</ActionContext.Provider>
</ActionContextProvider>
</CollectionProvider>
</RecordPickerProvider>
);

View File

@ -1,13 +1,12 @@
import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import React, { Fragment, useRef, useState } from 'react';
import { WithoutTableFieldResource } from '../../../block-provider';
import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core';
import { useCompile } from '../../hooks';
import { ActionContext, useActionContext } from '../action';
import { ActionContextProvider, useActionContext } from '../action';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { Preview } from '../preview';
import { isShowFilePicker } from './InputRecordPicker';
@ -56,8 +55,7 @@ export const ReadPrettyRecordPicker: React.FC = observer(
const text = getLabelFormatValue(labelUiSchema, val, true);
return (
<Fragment key={`${record.id}_${index}`}>
{/* test-record-picker-read-pretty-item 用于在单元测试中方便选中元素 */}
<span className="test-record-picker-read-pretty-item">
<span>
{snapshot || isTagsMode ? (
text
) : (
@ -112,11 +110,11 @@ export const ReadPrettyRecordPicker: React.FC = observer(
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider
<ActionContextProvider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</ActionContextProvider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>

View File

@ -10,6 +10,7 @@ import { TableSelector } from './TableSelector';
export * from './TableBlockDesigner';
export * from './TableField';
export * from './TableSelectorDesigner';
export * from './FilterDynamicComponent';
export const TableV2 = Table;

View File

@ -7,15 +7,18 @@ import { Icon } from '../../../icon';
import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext, SortableItem } from '../../common';
import { useDesigner } from '../../hooks/useDesigner';
import { useTabsContext } from './context';
import { TabsDesigner } from './Tabs.Designer';
export const Tabs: any = observer(
(props: TabsProps) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const contextProps = useTabsContext();
return (
<DndContext>
<AntdTabs
{...contextProps}
style={props.style}
tabBarExtraContent={{
right: render(),
@ -93,7 +96,7 @@ Tabs.TabPane = observer(
</SortableItem>
);
},
{ displayName: 'TabPane' },
{ displayName: 'Tabs.TabPane' },
);
Tabs.Designer = TabsDesigner;

View File

@ -0,0 +1,12 @@
import { TabsProps } from 'antd';
import React from 'react';
const TabsContext = React.createContext<TabsProps>({});
export const TabsContextProvider: React.FC<TabsProps> = ({ children, ...props }) => {
return <TabsContext.Provider value={props}>{children}</TabsContext.Provider>;
};
export const useTabsContext = () => {
return React.useContext(TabsContext);
};

View File

@ -1 +1,2 @@
export * from './Tabs';
export * from './context';

View File

@ -1,6 +1,7 @@
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { observer, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext } from 'react';
import { cx } from '@emotion/css';
import { observer, Schema, useField, useFieldSchema } from '@formily/react';
import React, { createContext, HTMLAttributes, useContext } from 'react';
export const DraggableContext = createContext(null);
export const SortableContext = createContext(null);
@ -19,18 +20,21 @@ export const SortableProvider = (props) => {
};
export const Sortable = (props: any) => {
const { component, style, children, openMode, ...others } = props;
const { droppable } = useContext(SortableContext);
const { component, overStyle, style, children, openMode, ...others } = props;
const { draggable, droppable } = useContext(SortableContext);
const { isOver, setNodeRef } = droppable;
const droppableStyle = { ...style };
if (isOver) {
droppableStyle['color'] = 'rgba(241, 139, 98, .1)';
if (isOver && draggable?.active?.id !== droppable?.over?.id) {
droppableStyle[component === 'a' ? 'color' : 'background'] = 'rgba(241, 139, 98, .15)';
Object.assign(droppableStyle, overStyle);
}
return React.createElement(
component || 'div',
{
...others,
className: cx('nb-sortable-designer', props.className),
ref: setNodeRef,
style: droppableStyle,
},
@ -55,9 +59,15 @@ const useSortableItemId = (props) => {
return field.address.toString();
};
export const SortableItem: React.FC<any> = observer(
interface SortableItemProps extends HTMLAttributes<HTMLDivElement> {
eid?: string;
schema?: Schema;
removeParentsIfNoChildren?: boolean;
}
export const SortableItem: React.FC<SortableItemProps> = observer(
(props) => {
const { schema, id, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
const { schema, id, eid, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
return (
<SortableProvider
id={id}
@ -67,7 +77,9 @@ export const SortableItem: React.FC<any> = observer(
removeParentsIfNoChildren: removeParentsIfNoChildren ?? true,
}}
>
<Sortable {...others}>{props.children}</Sortable>
<Sortable id={eid} {...others}>
{props.children}
</Sortable>
</SortableProvider>
);
},

View File

@ -144,7 +144,7 @@ export class Designable {
if (!current['x-uid']) {
return;
}
await api.request({
const res = await api.request({
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
method: 'post',
data: {
@ -165,7 +165,7 @@ export class Designable {
method: 'post',
});
}
onSuccess?.();
onSuccess?.(res?.data?.data);
message.success(t('Saved successfully'), 0.2);
});
this.on('patch', async ({ schema }) => {
@ -321,7 +321,7 @@ export class Designable {
removed = parent;
}
}
this.emit('remove', { removed });
return this.emit('remove', { removed });
}
removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
@ -501,7 +501,7 @@ export class Designable {
const s = this.current.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current;
const [schema1, schema2] = splitWrapSchema(s, schema);
this.emit('insertAdjacent', {
return this.emit('insertAdjacent', {
position: 'beforeEnd',
schema: schema2,
wrap: schema1,

View File

@ -1,9 +1,10 @@
import { css } from '@emotion/css';
import { ISchema, observer } from '@formily/react';
import { ISchema, observer, useForm } from '@formily/react';
import { Button, Dropdown, Menu, Switch } from 'antd';
import classNames from 'classnames';
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { Icon } from '../icon';
import { SchemaComponent, useActionContext } from '../schema-component';
import { useCompile, useDesignable } from '../schema-component/hooks';
import './style.less';
import {
@ -106,6 +107,24 @@ SchemaInitializer.Button = observer(
}
});
};
const buttonDom = (
<Button
type={'dashed'}
style={{
borderColor: '#f18b62',
color: '#f18b62',
...style,
}}
{...others}
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
>
{compile(props.children || props.title)}
</Button>
);
if (!items.length) {
return buttonDom;
}
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
if (!designable && props.designable !== true) {
return null;
@ -131,22 +150,7 @@ SchemaInitializer.Button = observer(
{...dropdown}
overlay={menu}
>
{component ? (
component
) : (
<Button
type={'dashed'}
style={{
borderColor: '#f18b62',
color: '#f18b62',
...style,
}}
{...others}
icon={<Icon type={icon as string} />}
>
{compile(props.children || props.title)}
</Button>
)}
{component ? component : buttonDom}
</Dropdown>
</SchemaInitializerButtonContext.Provider>
);
@ -242,6 +246,102 @@ SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
return component;
};
interface SchemaInitializerActionModalProps {
title: string;
schema: any;
onCancel?: () => void;
onSubmit?: (values: any) => void;
buttonText?: any;
}
SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
const { title, schema, buttonText, onCancel, onSubmit } = props;
const useCancelAction = useCallback(() => {
const form = useForm();
const ctx = useActionContext();
return {
async run() {
await onCancel?.();
ctx.setVisible(false);
form.reset();
},
};
}, [onCancel]);
const useSubmitAction = useCallback(() => {
const form = useForm();
const ctx = useActionContext();
return {
async run() {
await form.validate();
await onSubmit?.(form.values);
ctx.setVisible(false);
form.reset();
},
};
}, [onSubmit]);
const defaultSchema = useMemo(() => {
return {
type: 'void',
properties: {
action1: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
icon: 'PlusOutlined',
style: {
borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)',
},
title: buttonText,
type: 'dashed',
},
properties: {
drawer1: {
'x-decorator': 'Form',
'x-component': 'Action.Modal',
'x-component-props': {
style: {
maxWidth: '520px',
width: '100%',
},
},
type: 'void',
title,
properties: {
...(schema?.properties || schema),
footer: {
'x-component': 'Action.Modal.Footer',
type: 'void',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: useCancelAction,
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: useSubmitAction,
},
},
},
},
},
},
},
},
},
};
}, [buttonText, schema, title, useCancelAction, useSubmitAction]);
return <SchemaComponent schema={defaultSchema as any} />;
};
SchemaInitializer.SwitchItem = (props) => {
return (
<SchemaInitializer.Item onClick={props.onClick}>

View File

@ -4,9 +4,7 @@ import { SchemaComponent, useActionContext, useDesignable, useRecordIndex } from
export const TabPaneInitializers = (props?: any) => {
const { designable, insertBeforeEnd } = useDesignable();
if (!designable) {
return null;
}
const useSubmitAction = () => {
const form = useForm();
const ctx = useActionContext();
@ -118,6 +116,11 @@ export const TabPaneInitializers = (props?: any) => {
},
};
}, []);
if (!designable) {
return null;
}
return <SchemaComponent schema={schema} />;
};

View File

@ -1064,6 +1064,9 @@ export const createGridCardBlockSchema = (options) => {
...others,
},
'x-component': 'BlockItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-designer': 'GridCard.Designer',
properties: {
actionBar: {
@ -1081,7 +1084,7 @@ export const createGridCardBlockSchema = (options) => {
type: 'array',
'x-component': 'GridCard',
'x-component-props': {
props: '{{ useGridCardBlockProps }}',
useProps: '{{ useGridCardBlockProps }}',
},
properties: {
item: {

View File

@ -56,7 +56,7 @@ export const GeneralSchemaItems: React.FC<{
/>
)}
<SchemaSettings.SwitchItem
checked={field.decoratorProps.showTitle ?? true}
checked={fieldSchema['x-decorator-props']?.['showTitle'] ?? true}
title={t('Display title')}
onChange={(checked) => {
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};

View File

@ -31,6 +31,11 @@ const titleCss = css`
}
`;
const overrideAntdCSS = css`
& .ant-space-item .anticon {
margin: 0;
}
`;
export const GeneralSchemaDesigner = (props: any) => {
const { disableInitializer, title, template, draggable = true } = props;
const { dn, designable } = useDesignable();
@ -62,7 +67,7 @@ export const GeneralSchemaDesigner = (props: any) => {
}
return (
<div className={'general-schema-designer'}>
<div className={classNames('general-schema-designer', overrideAntdCSS)}>
{title && (
<div className={classNames('general-schema-designer-title', titleCss)}>
<Space size={2}>

View File

@ -148,7 +148,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
};
SchemaSettings.Template = function Template(props) {
const { componentName, collectionName, resourceName } = props;
const { componentName, collectionName, resourceName, needRender } = props;
const { t } = useTranslation();
const { getCollection } = useCollectionManager();
const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
@ -156,7 +156,7 @@ SchemaSettings.Template = function Template(props) {
const api = useAPIClient();
const { dn: tdn } = useBlockTemplateContext();
const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
if (!collectionName) {
if (!collectionName && !needRender) {
return null;
}
if (template) {
@ -182,7 +182,7 @@ SchemaSettings.Template = function Template(props) {
<SchemaSettings.Item
onClick={async () => {
setVisible(false);
const { title } = getCollection(collectionName);
const { title } = collectionName ? getCollection(collectionName) : { title: '' };
const values = await FormDialog(t('Save as template'), () => {
return (
<FormLayout layout={'vertical'}>
@ -194,7 +194,7 @@ SchemaSettings.Template = function Template(props) {
name: {
title: t('Template name'),
required: true,
default: `${compile(title)}_${t(componentName)}`,
default: title ? `${compile(title)}_${t(componentName)}` : t(componentName),
'x-decorator': 'FormItem',
'x-component': 'Input',
},

View File

@ -123,6 +123,10 @@ export const useSchemaTemplateManager = () => {
const items = templates?.filter?.((template) => template.collectionName === collectionName);
return items || [];
},
getTemplatesByComponentName(componentName: string): Array<any> {
const items = templates?.filter?.((template) => template.componentName === componentName);
return items || [];
},
};
};

View File

@ -3,7 +3,7 @@ import { uid } from '@formily/shared';
import { Menu } from 'antd';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionContext, SchemaComponent, useActionContext } from '../';
import { ActionContextProvider, SchemaComponent, useActionContext } from '../';
import { useAPIClient } from '../api-client';
import { DropdownVisibleContext } from './CurrentUser';
@ -119,7 +119,7 @@ export const ChangePassword = () => {
const { t } = useTranslation();
const ctx = useContext(DropdownVisibleContext);
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<ActionContextProvider value={{ visible, setVisible }}>
<Menu.Item
key="password"
eventKey={'ChangePassword'}
@ -131,6 +131,6 @@ export const ChangePassword = () => {
{t('Change password')}
</Menu.Item>
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</ActionContext.Provider>
</ActionContextProvider>
);
};

View File

@ -10,7 +10,6 @@ import { EditProfile } from './EditProfile';
import { LanguageSettings } from './LanguageSettings';
import { SwitchRole } from './SwitchRole';
import { ThemeSettings } from './ThemeSettings';
const ApplicationVersion = () => {
const data = useCurrentAppInfo();
return (
@ -20,16 +19,18 @@ const ApplicationVersion = () => {
);
};
export const DropdownVisibleContext = createContext(null);
export const CurrentUser = () => {
/**
* @note If you want to change here, Note the Setting block on the mobile side
*/
export const SettingsMenu: React.FC<{
redirectUrl?: string;
}> = (props) => {
const { redirectUrl = '' } = props;
const { allowAll, snippets } = useACLRoleContext();
const appAllowed = allowAll || snippets?.includes('app');
const history = useHistory();
const api = useAPIClient();
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const { data } = useCurrentUserContext();
const { allowAll, snippets } = useACLRoleContext();
const appAllowed = allowAll || snippets?.includes('app');
const silenceApi = useAPIClient();
const check = async () => {
return await new Promise((resolve) => {
@ -52,6 +53,69 @@ export const CurrentUser = () => {
}, 3000);
});
};
return (
<Menu>
<ApplicationVersion />
<Menu.Divider />
<EditProfile />
<ChangePassword />
<Menu.Divider />
<SwitchRole />
<LanguageSettings />
<ThemeSettings />
<Menu.Divider />
{appAllowed && (
<>
<Menu.Item
key="cache"
onClick={async () => {
await api.resource('app').clearCache();
window.location.reload();
}}
>
{t('Clear cache')}
</Menu.Item>
<Menu.Item
key="reboot"
onClick={async () => {
Modal.confirm({
title: t('Reboot application'),
content: t(
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
),
okText: t('Reboot'),
okButtonProps: {
danger: true,
},
onOk: async () => {
await api.resource('app').reboot();
await check();
window.location.reload();
},
});
}}
>
{t('Reboot application')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item
key="signout"
onClick={async () => {
await api.auth.signOut();
history.push(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
}}
>
{t('Sign out')}
</Menu.Item>
</Menu>
);
};
export const DropdownVisibleContext = createContext(null);
export const CurrentUser = () => {
const [visible, setVisible] = useState(false);
const { data } = useCurrentUserContext();
return (
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
@ -60,64 +124,7 @@ export const CurrentUser = () => {
onOpenChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<ApplicationVersion />
<Menu.Divider />
<EditProfile />
<ChangePassword />
<Menu.Divider />
<SwitchRole />
<LanguageSettings />
<ThemeSettings />
<Menu.Divider />
{appAllowed && (
<>
<Menu.Item
key="cache"
onClick={async () => {
await api.resource('app').clearCache();
window.location.reload();
}}
>
{t('Clear cache')}
</Menu.Item>
<Menu.Item
key="reboot"
onClick={async () => {
Modal.confirm({
title: t('Reboot application'),
content: t(
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
),
okText: t('Reboot'),
okButtonProps: {
danger: true,
},
onOk: async () => {
await api.resource('app').reboot();
await check();
window.location.reload();
},
});
}}
>
{t('Reboot application')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item
key="signout"
onClick={async () => {
await api.auth.signOut();
history.push('/signin');
}}
>
{t('Sign out')}
</Menu.Item>
</Menu>
}
overlay={<SettingsMenu />}
>
<span
className={css`

View File

@ -4,7 +4,7 @@ import { Menu } from 'antd';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActionContext,
ActionContextProvider,
DropdownVisibleContext,
SchemaComponent,
useActionContext,
@ -119,18 +119,18 @@ export const EditProfile = () => {
const { t } = useTranslation();
const ctx = useContext(DropdownVisibleContext);
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<ActionContextProvider value={{ visible, setVisible }}>
<Menu.Item
key="profile"
eventKey={'EditProfile'}
onClick={() => {
setVisible(true);
ctx.setVisible(false);
ctx?.setVisible(false);
}}
>
{t('Edit profile')}
</Menu.Item>
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</ActionContext.Provider>
</ActionContextProvider>
);
};

View File

@ -54,7 +54,7 @@ export function useRedirect(next = '/admin') {
const history = useHistory();
const redirect = location?.['query']?.redirect;
return useCallback(() => {
history.push(redirect || '/admin');
history.replace(redirect || '/admin');
}, [redirect]);
}

View File

@ -31,7 +31,6 @@ export const SwitchRole = () => {
const api = useAPIClient();
const roles = useCurrentRoles();
const { t } = useTranslation();
const history = useHistory();
if (roles.length <= 1) {
return null;
}
@ -57,7 +56,7 @@ export const SwitchRole = () => {
onChange={async (roleName) => {
api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } });
history.push('/');
location.reload();
window.location.reload();
}}
/>

View File

@ -5,7 +5,7 @@ import { css } from '@emotion/css';
import { useFieldSchema } from '@formily/react';
import { useCollection } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Alert, Button, Modal } from 'antd';
import { Alert, Button, Modal, Spin } from 'antd';
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
import { useHistory } from 'react-router';
import { useMapConfiguration } from '../hooks';
@ -102,7 +102,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
const overlay = useRef<AMap.Polygon>();
const editor = useRef(null);
const history = useHistory();
const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
const id = useRef(`nocobase-map-${type || ''}-${Date.now().toString(32)}`);
const [commonOptions] = useState<AMap.PolylineOptions & AMap.PolygonOptions>({
strokeWeight: 5,
@ -316,7 +316,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
})
.then((amap) => {
requestIdleCallback(() => {
return requestIdleCallback(() => {
map.current = new amap.Map(id.current, {
resizeEnable: true,
zoom,
@ -327,10 +327,14 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
});
})
.catch((err) => {
if (err.includes('多个不一致的 key')) {
setErrMessage(t('The AccessKey is incorrect, please check it'));
} else {
setErrMessage(err);
if (typeof err === 'string') {
if (err.includes('多个不一致的 key')) {
setErrMessage(t('The AccessKey is incorrect, please check it'));
} else {
setErrMessage(err);
}
} else if (err?.type === 'error') {
setErrMessage('Something went wrong, please refresh the page and try again');
}
});
@ -379,6 +383,19 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
id={id.current}
style={props?.style}
>
{!aMap.current && (
<div
className={css`
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
`}
>
<Spin />
</div>
)}
{!disabled ? (
<>
<Search toCenter={toCenter} aMap={aMap.current} />

4
packages/plugins/mobile-client/client.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,7 @@
# Mobile-client overview
Provides a mobile client for the desktop client.
## How to visit mobile client?
When your desktop client is started, you can visit the mobile client in the browser by entering `http://localhost:3000/mobile`. Note that `/mobile` is the route of our mobile client.

View File

@ -0,0 +1,13 @@
# Mobile-client Installation
## Installation
```bash
yarn add @nocobase/plugin-mobile-client
```
## Activate the plugin
```bash
yarn pm enable mobile-client
```

View File

@ -0,0 +1,14 @@
[
{
"title": "Introduction",
"path": "index"
},
{
"title": "Installation",
"path": "installation"
},
{
"title": "Usage",
"path": "usage"
}
]

View File

@ -0,0 +1 @@
# Mobile-client Usage

View File

@ -0,0 +1,7 @@
# 移动端
提供移动端应用,独立于桌面端。
## 如何访问?
当你的桌面端启动后,可以在浏览器中输入 `http://localhost:3000/mobile` 访问移动端。注意,`/mobile` 就是我们移动端的路由。

View File

@ -0,0 +1,13 @@
# 移动端安装方法
## 安装
```bash
yarn add @nocobase/plugin-mobile-client
```
## 激活插件
```bash
yarn pm enable mobile-client
```

View File

@ -0,0 +1,14 @@
[
{
"title": "介绍",
"path": "index"
},
{
"title": "安装",
"path": "installation"
},
{
"title": "用法",
"path": "usage"
}
]

View File

@ -0,0 +1,2 @@
# 移动端用法

View File

@ -0,0 +1,27 @@
{
"name": "@nocobase/plugin-mobile-client",
"version": "0.9.4-alpha.2",
"main": "lib/server/index.js",
"displayName": "Mobile-client",
"displayName.zh-CN": "移动端",
"description": "Provide mobile client access",
"description.zh-CN": "提供移动端访问",
"devDependencies": {
"@emotion/css": "^11.7.1",
"@formily/react": "2.2.24",
"@formily/shared": "2.2.24",
"@formily/antd": "2.2.24",
"@nocobase/server": "0.9.4-alpha.2",
"@nocobase/test": "0.9.4-alpha.2",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"classnames": "^2.3.1",
"react": "17.x",
"react-dom": "17.x",
"react-router-dom": "^5.2.0",
"antd": "^4.24.8"
},
"dependencies": {
"antd-mobile": "^5.29.1"
}
}

4
packages/plugins/mobile-client/server.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,5 @@
import React from 'react';
export const AppConfiguration = () => {
return <>App Configuration</>;
};

View File

@ -0,0 +1,11 @@
import React from 'react';
import { InterfaceRouter } from '../router';
import { MobileDevice } from '../devices';
export const InterfaceConfiguration = () => {
return (
<MobileDevice>
<InterfaceRouter></InterfaceRouter>
</MobileDevice>
);
};

View File

@ -0,0 +1,2 @@
export * from './App';
export * from './Interface';

View File

@ -0,0 +1 @@
import './native-call';

View File

@ -0,0 +1,12 @@
interface InvokeFunction {
(params: { action: 'scan' }, cb: (data: { url: string }) => void): void;
(params: { action: 'moveTaskToBack' }, cb?: () => void): void;
}
const jsBridge = (window as any).jsBridge as {
invoke: InvokeFunction;
};
export const invoke: InvokeFunction = (params, cb) => {
jsBridge.invoke(params, cb);
};

View File

@ -0,0 +1,23 @@
import { invoke } from './injects';
/**
* App
* @param cb true web false表示 app
*/
const JSBridgeFunction = {
/**
* @description JSBridge injects
*/
onBackPressed: () => {
if (history.length === 1) {
invoke({ action: 'moveTaskToBack' });
} else {
history.back();
}
},
};
Object.keys(JSBridgeFunction).forEach((key) => {
window[key] = JSBridgeFunction[key];
});

View File

@ -0,0 +1,45 @@
import { SchemaComponentOptions, SchemaInitializerProvider } from '@nocobase/client';
import React from 'react';
import {
MBlockInitializers,
MMenuBlockInitializer,
MMenu,
MContainer,
MTabBar,
MPage,
MHeader,
MSettingsBlockInitializer,
MSettings,
useGridCardBlockItemProps,
useGridCardBlockProps,
} from './schema';
import './bridge';
export const MobileCore: React.FC = (props) => {
return (
<SchemaInitializerProvider
initializers={{
MBlockInitializers,
}}
>
<SchemaComponentOptions
components={{
MMenuBlockInitializer,
MSettingsBlockInitializer,
MContainer,
MMenu,
MTabBar,
MPage,
MHeader,
MSettings,
}}
scope={{
useGridCardBlockItemProps,
useGridCardBlockProps,
}}
>
{props.children}
</SchemaComponentOptions>
</SchemaInitializerProvider>
);
};

View File

@ -0,0 +1,85 @@
import { SchemaSettings, useDesignable } from '@nocobase/client';
import React from 'react';
import { useTranslation } from '../../../../locale';
import { Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { useHistory } from 'react-router-dom';
import { findSchema } from '../../helpers';
import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
export const ContainerDesigner = () => {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const tabBarSchema = fieldSchema.reduceProperties(
(schema, next) => schema || (next['x-component'] === 'MTabBar' && next),
) as Schema;
const history = useHistory();
const field = useField();
const schemaSettingsProps = {
dn,
field,
fieldSchema,
};
return (
<SchemaSettings
title={
<Button
style={{
borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)',
}}
icon={<MenuOutlined />}
type="dashed"
>
{t('App level Configuration')}
</Button>
}
{...schemaSettingsProps}
>
<SchemaSettings.SwitchItem
checked={!!tabBarSchema}
title={t('Enable TabBar')}
onChange={async (v) => {
if (v) {
const pageSchema = findSchema(fieldSchema, 'MPage');
if (!pageSchema) return;
await dn.remove(pageSchema);
await dn.insertBeforeEnd({
type: 'void',
'x-component': 'MTabBar',
'x-component-props': {},
name: 'tabBar',
properties: {
[uid()]: {
type: 'void',
'x-component': 'MTabBar.Item',
'x-designer': 'MTabBar.Item.Designer',
'x-component-props': {
icon: 'HomeOutlined',
title: t('Untitled'),
},
properties: {
page: pageSchema.toJSON(),
},
},
},
});
} else {
const pageSchema = findSchema(tabBarSchema.properties[Object.keys(tabBarSchema.properties)[0]], 'MPage');
if (!pageSchema) return;
await dn.remove(tabBarSchema);
await dn.insertBeforeEnd(pageSchema, {
onSuccess() {
history.push('../');
},
});
}
}}
/>
</SchemaSettings>
);
};

View File

@ -0,0 +1,130 @@
import React, { useMemo } from 'react';
import { css, cx } from '@emotion/css';
import { ContainerDesigner } from './Container.Designer';
import { RouteSwitch, SchemaComponent, SortableItem, useDesigner } from '@nocobase/client';
import { useFieldSchema } from '@formily/react';
import { Redirect, RouteProps, useParams, useRouteMatch } from 'react-router-dom';
const findGrid = (schema, uid) => {
return schema.reduceProperties((final, next) => {
if (final) return final;
if (next['x-component'] === 'MTabBar') {
return findGrid(next, uid);
}
if (next['x-component'] === 'MTabBar.Item' && uid === next['x-uid']) {
return next;
}
});
};
const TabContentComponent = () => {
const { name } = useParams<{ name: string }>();
const fieldSchema = useFieldSchema();
if (!name) return <></>;
const gridSchema = findGrid(fieldSchema.properties['tabBar'], name.replace('tab_', ''));
if (!gridSchema) {
return <Redirect to="../" />;
}
return <SchemaComponent schema={gridSchema} />;
};
const InternalContainer: React.FC = (props) => {
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const params = useParams<{ name: string }>();
const match = useRouteMatch();
const tabBarSchema = fieldSchema?.properties?.['tabBar'];
const redirectToUid = tabBarSchema?.properties[Object.keys(tabBarSchema.properties)[0]]['x-uid'];
const tabRoutes = useMemo<RouteProps[]>(() => {
if (!redirectToUid) {
return [];
}
return [
!params.name
? {
type: 'redirect',
to: `${match.url}/tab_${redirectToUid}`,
from: match.url,
exact: true,
}
: null,
{
type: 'route',
path: match.path,
component: TabContentComponent,
},
].filter(Boolean) as any[];
}, [redirectToUid, params.name, match.url, match.path]);
return (
<SortableItem
eid="nb-mobile-scroll-wrapper"
className={cx(
'nb-mobile-container',
css`
& > .general-schema-designer > .general-schema-designer-icons {
right: unset;
left: 2px;
}
background: #f0f2f5;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-y: scroll;
position: initial !important;
`,
)}
>
<Designer></Designer>
<div
style={{
paddingBottom: tabRoutes.length ? '50px' : '0px',
}}
className={cx('nb-mobile-container-content')}
>
{tabRoutes.length ? (
<RouteSwitch routes={tabRoutes as any} />
) : (
<SchemaComponent
filterProperties={(schema) => {
return schema['x-component'] !== 'MTabBar';
}}
schema={fieldSchema}
/>
)}
</div>
<div
className={cx(
'nb-mobile-container-tab-bar',
css`
& > .general-schema-designer {
--nb-designer-top: 20px;
}
position: absolute;
background: #ffffff;
width: 100%;
bottom: 0;
left: 0;
z-index: 1000;
`,
)}
>
<SchemaComponent
onlyRenderProperties
filterProperties={(schema) => {
return schema['x-component'] === 'MTabBar';
}}
schema={fieldSchema}
></SchemaComponent>
</div>
</SortableItem>
);
};
export const MContainer = InternalContainer as unknown as typeof InternalContainer & {
Designer: typeof ContainerDesigner;
};
MContainer.Designer = ContainerDesigner;
MContainer.displayName = 'MContainer';

View File

@ -0,0 +1 @@
export * from './Container';

View File

@ -0,0 +1,39 @@
import { Schema, useField, useFieldSchema } from '@formily/react';
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
import React from 'react';
import { Switch } from '@formily/antd';
import { useTranslation } from '../../../../locale';
import { useSchemaPatch } from '../../hooks';
export const HeaderDesigner = () => {
const field = useField();
const { onUpdateComponentProps } = useSchemaPatch();
const { t } = useTranslation();
return (
<GeneralSchemaDesigner draggable={false}>
<SchemaSettings.ModalItem
title={t('Edit info')}
components={{ Switch }}
initialValues={field.componentProps}
schema={{
properties: {
title: {
type: 'string',
title: t('Title'),
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
showBack: {
type: 'boolean',
title: t('Display back button'),
'x-component': 'Switch',
'x-decorator': 'FormItem',
},
},
}}
onSubmit={onUpdateComponentProps}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,41 @@
import { css, cx } from '@emotion/css';
import { SortableItem, useCompile, useDesigner } from '@nocobase/client';
import { NavBar, NavBarProps } from 'antd-mobile';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { HeaderDesigner } from './Header.Designer';
import { useField, useFieldSchema } from '@formily/react';
export interface HeaderProps extends NavBarProps {
title?: string;
showBack?: boolean;
}
const InternalHeader = (props: HeaderProps) => {
const field = useField();
const { title = '{{ t("Untitled") }}', showBack = false } = { ...props, ...field?.componentProps };
const Designer = useDesigner();
const compile = useCompile();
const history = useHistory();
return (
<SortableItem
className={cx(
'nb-mobile-header',
css`
width: 100%;
background: #fff;
`,
)}
>
<NavBar backArrow={showBack} onBack={history.goBack}>
{compile(title)}
</NavBar>
<Designer />
</SortableItem>
);
};
export const MHeader = InternalHeader as unknown as typeof InternalHeader & {
Designer: typeof HeaderDesigner;
};
MHeader.Designer = HeaderDesigner;

View File

@ -0,0 +1 @@
export * from './Header';

View File

@ -0,0 +1,6 @@
export * from './menu';
export * from './container';
export * from './tab-bar';
export * from './page';
export * from './header';
export * from './settings';

View File

@ -0,0 +1,46 @@
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
import { MenuOutlined } from '@ant-design/icons';
import React from 'react';
import { useTranslation } from '../../../../locale';
import { Button } from 'antd';
import { useFieldSchema, useField } from '@formily/react';
export const MenuDesigner: React.FC = (props) => {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const field = useField();
const schemaSettingsProps = {
dn,
field,
fieldSchema,
};
return (
<SchemaSettings
title={
<Button
style={{
borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)',
}}
icon={<MenuOutlined />}
type="dashed"
>
{t('Menu configuration')}
</Button>
}
{...schemaSettingsProps}
>
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete menu block'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</SchemaSettings>
);
};

View File

@ -0,0 +1,104 @@
import { css, cx } from '@emotion/css';
import { useField, useFieldSchema } from '@formily/react';
import {
GeneralSchemaDesigner,
Icon,
SchemaSettings,
SortableItem,
useCompile,
useDesignable,
useDesigner,
} from '@nocobase/client';
import { List, ListItemProps } from 'antd-mobile';
import React from 'react';
import { useHistory, useParams, useRouteMatch } from 'react-router-dom';
import { useTranslation } from '../../../../locale';
import { useSchemaPatch } from '../../hooks';
interface MMenuItemProps extends ListItemProps {
name: string;
icon: string;
}
const InternalMenuItem: React.FC<MMenuItemProps> = (props) => {
const { icon, name } = props;
const Designer = useDesigner();
const history = useHistory();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const match = useRouteMatch();
const params = useParams<{ name: string }>();
const onToPage = () => {
history.push(params.name ? fieldSchema['x-uid'] : `${match.url}/${fieldSchema['x-uid']}`);
};
return (
<SortableItem
className={cx(
'nb-mobile-menu-item',
css`
width: 100%;
background: var(--adm-color-background);
> .adm-list-item {
background: inherit;
}
`,
)}
>
<List.Item arrow clickable {...props} prefix={<Icon type={icon} />} onClick={onToPage}>
{compile(name)}
</List.Item>
<Designer></Designer>
</SortableItem>
);
};
const MenuItemDesigner: React.FC = () => {
const { t } = useTranslation();
const { onUpdateComponentProps } = useSchemaPatch();
const field = useField();
return (
<GeneralSchemaDesigner>
<SchemaSettings.ModalItem
title={t('Edit menu info')}
initialValues={field.componentProps}
schema={{
properties: {
name: {
type: 'string',
title: t('Menu name'),
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
required: true,
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
title: t('Icon'),
'x-component-props': {},
},
},
}}
onSubmit={onUpdateComponentProps}
/>
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete menu item?'),
}}
breakRemoveOn={{
'x-component': 'MMenu',
}}
/>
</GeneralSchemaDesigner>
);
};
export const MenuItem = InternalMenuItem as typeof InternalMenuItem as unknown as {
Designer: typeof MenuItemDesigner;
};
MenuItem.Designer = MenuItemDesigner;

View File

@ -0,0 +1,105 @@
import React from 'react';
import { MenuItem } from './Menu.Item';
import {
DndContext,
SchemaComponent,
SchemaInitializer,
SortableItem,
useDesignable,
useDesigner,
} from '@nocobase/client';
import { css, cx } from '@emotion/css';
import { MenuDesigner } from './Menu.Designer';
import { useFieldSchema } from '@formily/react';
import { List } from 'antd-mobile';
import { useTranslation } from '../../../../locale';
import { menuItemSchema } from './schema';
const InternalMenu: React.FC = (props) => {
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const { insertBeforeEnd, designable } = useDesignable();
const { t } = useTranslation();
const onAddMenuItem = (values: any) => {
const properties = {
page: {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
header: {
type: 'void',
'x-component': 'MHeader',
'x-designer': 'MHeader.Designer',
'x-component-props': {
title: values.name,
showBack: true,
},
},
grid: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
showDivider: false,
},
'x-initializer': 'MBlockInitializers',
},
},
},
};
return insertBeforeEnd({
type: 'void',
title: values.name,
'x-component': 'MMenu.Item',
'x-component-props': values,
'x-designer': 'MMenu.Item.Designer',
properties,
});
};
return (
<SortableItem
className={cx(
'nb-mobile-menu',
css`
background: #ffffff;
width: 100%;
margin-bottom: var(--nb-spacing);
`,
)}
>
<List>
{designable && (
<List.Item>
<Designer />
</List.Item>
)}
<DndContext>
<SchemaComponent onlyRenderProperties schema={fieldSchema}></SchemaComponent>
</DndContext>
{designable ? (
<List.Item>
<SchemaInitializer.ActionModal
buttonText={t('Add menu item')}
title={t('Add menu item')}
schema={menuItemSchema}
onSubmit={onAddMenuItem}
/>
</List.Item>
) : null}
</List>
</SortableItem>
);
};
export const MMenu = InternalMenu as unknown as typeof InternalMenu & {
Item: typeof MenuItem;
Designer: typeof MenuDesigner;
};
MMenu.Item = MenuItem;
MMenu.Designer = MenuDesigner;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { MenuOutlined } from '@ant-design/icons';
import { SchemaInitializer } from '@nocobase/client';
export const MMenuBlockInitializer = (props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<MenuOutlined />}
onClick={async () => {
insert({
type: 'void',
'x-component': 'MMenu',
'x-designer': 'MMenu.Designer',
'x-component-props': {},
});
}}
/>
);
};

View File

@ -0,0 +1,2 @@
export * from './MenuBlockInitializer';
export * from './Menu';

View File

@ -0,0 +1,17 @@
export const menuItemSchema = {
properties: {
name: {
type: 'string',
title: `{{t('Menu name')}}`,
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
title: `{{t('Icon')}}`,
'x-component-props': {},
},
},
};

View File

@ -0,0 +1,106 @@
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
import React from 'react';
import { useTranslation } from '../../../../locale';
import { useField, useFieldSchema } from '@formily/react';
import { findGridSchema } from '../../helpers';
import { uid } from '@formily/shared';
import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
export const PageDesigner = (props) => {
const { showBack } = props;
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const headerSchema = fieldSchema?.properties?.['header'];
const tabsSchema = fieldSchema?.properties?.['tabs'];
const field = useField();
const schemaSettingsProps = {
dn,
field,
fieldSchema,
};
return (
<SchemaSettings
title={
<Button
style={{
borderColor: 'rgb(241, 139, 98)',
color: 'rgb(241, 139, 98)',
}}
icon={<MenuOutlined />}
type="dashed"
>
{t('Page configuration')}
</Button>
}
{...schemaSettingsProps}
>
<SchemaSettings.SwitchItem
checked={!!headerSchema}
title={t('Enable Header')}
onChange={async (v) => {
if (v) {
await dn.insertAfterBegin({
type: 'void',
name: 'header',
'x-component': 'MHeader',
'x-designer': 'MHeader.Designer',
'x-component-props': {
title: fieldSchema.parent['x-component-props']?.name,
showBack,
},
});
} else {
await dn.remove(headerSchema);
}
dn.refresh();
}}
/>
<SchemaSettings.SwitchItem
checked={!!tabsSchema}
title={t('Enable Tabs')}
onChange={async (v) => {
if (v) {
const gridSchema = findGridSchema(fieldSchema);
if (gridSchema) {
return dn.remove(gridSchema).then(() => {
return dn.insertBeforeEnd({
type: 'void',
name: 'tabs',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
'x-initializer-props': {
gridInitializer: 'MBlockInitializers',
},
properties: {
tab1: {
type: 'void',
title: '{{t("Untitled")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
...gridSchema,
'x-uid': uid(),
},
},
},
},
});
});
}
} else {
const gridSchema = findGridSchema(tabsSchema.properties[Object.keys(tabsSchema.properties)[0]]);
if (gridSchema) {
return dn.remove(tabsSchema).then(() => dn.insertBeforeEnd(gridSchema, {}));
}
}
}}
/>
</SchemaSettings>
);
};

View File

@ -0,0 +1,140 @@
import React, { useCallback, useMemo } from 'react';
import { css, cx } from '@emotion/css';
import { PageDesigner } from './Page.Designer';
import { ActionBarProvider, SortableItem, TabsContextProvider, useDesigner } from '@nocobase/client';
import { RecursionField, useFieldSchema } from '@formily/react';
import { countGridCol } from '../../helpers';
import { TabsProps } from 'antd';
import { useHistory, useLocation, useParams } from 'react-router-dom';
const globalActionCSS = css`
#nb-position-container > & {
height: 49px;
border-top: 1px solid #f0f2f5;
margin-bottom: 0px !important;
padding: 0 var(--nb-spacing);
align-items: center;
overflow-x: auto;
background: #ffffff;
z-index: 100;
}
`;
const InternalPage: React.FC = (props) => {
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const history = useHistory();
const location = useLocation();
const query = new URLSearchParams(location.search);
const tabsSchema = fieldSchema.properties?.['tabs'];
// Only support globalActions in page
const onlyInPage = fieldSchema.root === fieldSchema.parent;
let hasGlobalActions = false;
if (!tabsSchema) {
hasGlobalActions = countGridCol(fieldSchema.properties['grid'], 2) === 1;
} else if (query.has('tab') && tabsSchema.properties[query.get('tab')]) {
hasGlobalActions = countGridCol(tabsSchema.properties[query.get('tab')]?.properties?.['grid'], 2) === 1;
} else {
const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0];
history.replace({
pathname: location.pathname,
search: new URLSearchParams({
tab: schema.name.toString(),
}).toString(),
});
}
const onTabsChange = useCallback<TabsProps['onChange']>(
(key) => {
history.replace({
pathname: history.location.pathname,
search: new URLSearchParams({
tab: key,
}).toString(),
});
},
[history],
);
const GlobalActionProvider = useMemo(() => {
if (hasGlobalActions) {
return ActionBarProvider;
}
return React.Fragment;
}, [hasGlobalActions]);
return (
<GlobalActionProvider
container={hasGlobalActions && onlyInPage ? document.getElementById('nb-position-container') : null}
forceProps={{
layout: 'one-column',
className: globalActionCSS,
}}
>
<SortableItem
eid="nb-mobile-scroll-wrapper"
className={cx(
'nb-mobile-page',
css`
background: #f0f2f5;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
padding-bottom: var(--nb-spacing);
`,
)}
>
<Designer {...fieldSchema?.['x-designer-props']}></Designer>
<div
style={{
paddingBottom: tabsSchema ? null : 'var(--nb-spacing)',
}}
className={cx(
'nb-mobile-page-header',
css`
& > .ant-tabs > .ant-tabs-nav {
background: #fff;
padding: 0 var(--nb-spacing);
}
display: flex;
flex-direction: column;
`,
)}
>
<RecursionField
schema={fieldSchema}
filterProperties={(s) => {
return 'MHeader' === s['x-component'];
}}
></RecursionField>
<TabsContextProvider activeKey={query.get('tab')} onChange={onTabsChange}>
<RecursionField
schema={fieldSchema}
filterProperties={(s) => {
return 'Tabs' === s['x-component'];
}}
></RecursionField>
</TabsContextProvider>
</div>
{!tabsSchema && (
<RecursionField
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] !== 'MHeader';
}}
></RecursionField>
)}
</SortableItem>
</GlobalActionProvider>
);
};
export const MPage = InternalPage as unknown as typeof InternalPage & {
Designer: typeof PageDesigner;
};
MPage.Designer = PageDesigner;
MPage.displayName = 'MPage';

View File

@ -0,0 +1 @@
export * from './Page';

View File

@ -0,0 +1,22 @@
import { GeneralSchemaDesigner, SchemaSettings } from '@nocobase/client';
import React from 'react';
import { useTranslation } from '../../../../locale';
export const SettingsDesigner = () => {
const { t } = useTranslation();
return (
<GeneralSchemaDesigner>
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete settings block'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,25 @@
import React from 'react';
import { SettingsMenu, SortableItem, useDesigner } from '@nocobase/client';
import { SettingsDesigner } from './Settings.Designer';
import { css, cx } from '@emotion/css';
export const InternalSettings = () => {
const Designer = useDesigner();
return (
<SortableItem
className={cx(
'nb-mobile-setting',
css`
margin-bottom: var(--nb-spacing);
`,
)}
>
<Designer />
<SettingsMenu redirectUrl="/mobile" />
</SortableItem>
);
};
export const MSettings = InternalSettings as unknown as typeof InternalSettings & {
Designer: typeof SettingsDesigner;
};
MSettings.Designer = SettingsDesigner;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { SettingOutlined } from '@ant-design/icons';
import { SchemaInitializer } from '@nocobase/client';
export const MSettingsBlockInitializer = (props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<SettingOutlined />}
onClick={async () => {
insert({
type: 'void',
'x-component': 'MSettings',
'x-designer': 'MSettings.Designer',
'x-component-props': {},
});
}}
/>
);
};

View File

@ -0,0 +1,2 @@
export * from './Settings';
export * from './SettingsBlockInitializer';

View File

@ -0,0 +1,67 @@
import React, { useEffect, useMemo } from 'react';
import { TabBar, TabBarItemProps } from 'antd-mobile';
import { GeneralSchemaDesigner, SchemaSettings, SortableItem, useDesigner } from '@nocobase/client';
import { useTranslation } from '../../../../locale';
import { Schema, useField, useFieldSchema } from '@formily/react';
import { useSchemaPatch } from '../../hooks';
import { css, cx } from '@emotion/css';
import { tabItemSchema } from './schema';
const InternalItem: React.FC<TabBarItemProps> = () => {
// NOTE: nothing to do
// return <TabBar.Item {...props}></TabBar.Item>;
const Designer = useDesigner();
return (
<SortableItem
className={cx(
'nb-mobile-tab-bar-item',
css`
position: absolute !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
`,
)}
>
<Designer />
</SortableItem>
);
};
export const Designer = () => {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { onUpdateComponentProps } = useSchemaPatch();
const field = useField();
const tabItems = Object.keys(fieldSchema.parent.properties).length;
return (
<GeneralSchemaDesigner>
<SchemaSettings.ModalItem
title={t('Edit info')}
initialValues={field.componentProps}
schema={tabItemSchema}
onSubmit={onUpdateComponentProps}
></SchemaSettings.ModalItem>
{tabItems > 1 ? (
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete tab item?'),
}}
breakRemoveOn={{
'x-component': 'MTabBar',
}}
></SchemaSettings.Remove>
) : null}
</GeneralSchemaDesigner>
);
};
export const TabBarItem = InternalItem as unknown as typeof InternalItem & {
Designer: typeof Designer;
};
TabBarItem.Designer = Designer;

View File

@ -0,0 +1,113 @@
import { TabBar } from 'antd-mobile';
import { TabBarItem } from './TabBar.Item';
import React, { useCallback, useContext } from 'react';
import { SchemaOptionsContext, useFieldSchema } from '@formily/react';
import { DndContext, Icon, SchemaComponent, SchemaInitializer, SortableItem, useDesignable } from '@nocobase/client';
import { useTranslation } from '../../../../locale';
import { css, cx } from '@emotion/css';
import { uid } from '@formily/shared';
import { useHistory, useParams } from 'react-router-dom';
import { tabItemSchema } from './schema';
export const InternalTabBar: React.FC = (props) => {
const fieldSchema = useFieldSchema();
const { designable } = useDesignable();
const { t } = useTranslation();
const options = useContext(SchemaOptionsContext);
const { insertBeforeEnd, dn } = useDesignable();
const history = useHistory();
const params = useParams<{ name: string }>();
const onAddTab = useCallback((values: any) => {
return insertBeforeEnd({
type: 'void',
'x-component': 'MTabBar.Item',
'x-component-props': values,
'x-designer': 'MTabBar.Item.Designer',
properties: {
[uid()]: {
type: 'void',
'x-component': 'MPage',
'x-designer': 'MPage.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'MBlockInitializers',
'x-component-props': {
showDivider: false,
},
},
},
},
},
});
}, []);
return (
<SortableItem
className={cx(
'nb-mobile-tab-bar',
css`
position: relative;
width: 100%;
display: flex;
align-items: center;
`,
)}
>
<DndContext>
<TabBar
activeKey={params.name}
onChange={(key) => {
if (key === 'add-tab') {
return;
}
history.push(key);
}}
safeArea
className={cx(
css`
width: 100%;
`,
)}
>
{fieldSchema.mapProperties((schema, name) => {
const cp = schema['x-component-props'];
return (
<TabBar.Item
{...cp}
key={`tab_${schema['x-uid']}`}
title={
<>
{cp.title}
<SchemaComponent schema={schema} name={name} />
</>
}
icon={cp.icon ? <Icon type={cp.icon} /> : undefined}
></TabBar.Item>
);
})}
{designable && Object.keys(fieldSchema.properties).length < 5 ? (
<TabBar.Item
className={css`
.adm-tab-bar-item-icon {
height: auto;
}
`}
icon={<SchemaInitializer.ActionModal title={t('Add tab')} onSubmit={onAddTab} schema={tabItemSchema} />}
key="add-tab"
></TabBar.Item>
) : null}
</TabBar>
</DndContext>
</SortableItem>
);
};
export const MTabBar = InternalTabBar as unknown as typeof InternalTabBar & {
Item: typeof TabBarItem;
};
MTabBar.Item = TabBarItem;
MTabBar.displayName = 'MTabBar';

View File

@ -0,0 +1 @@
export * from './TabBar';

View File

@ -0,0 +1,18 @@
export const tabItemSchema = {
properties: {
title: {
type: 'string',
title: `{{ t('Title') }}`,
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
required: true,
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
title: `{{ t('Icon') }}`,
'x-component-props': {},
},
},
};

View File

@ -0,0 +1,38 @@
import { ISchema, Schema } from '@formily/react';
import { uid } from '@formily/shared';
export const gridItemWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'MGrid.Item',
properties: {
[schema.name || uid()]: schema,
},
};
};
export const findSchema = (schema: Schema, component: string) => {
const gridSchema = schema.reduceProperties(
(schema, next) => schema || (next['x-component'] === component && next),
) as Schema;
return gridSchema;
};
export const findGridSchema = (schema: Schema) => {
return findSchema(schema, 'Grid');
};
const allowComponents = ['Grid', 'Grid.Row'];
const plusComponent = ['Grid.Col'];
export const countGridCol = (schema: Schema, countToStop?: number) => {
if (!schema) return 0;
let count = 0;
if (plusComponent.includes(schema['x-component'])) {
count += 1;
}
if (typeof countToStop === 'number' && count >= countToStop) return count;
if (allowComponents.includes(schema['x-component'])) {
schema.mapProperties((schema) => {
count += countGridCol(schema, countToStop);
});
}
return count;
};

View File

@ -0,0 +1 @@
export * from './useSchemaPatch';

View File

@ -0,0 +1,24 @@
import { useField, useFieldSchema } from '@formily/react';
import { useDesignable } from '@nocobase/client';
import _ from 'lodash';
import { useCallback } from 'react';
export const useSchemaPatch = () => {
const { dn } = useDesignable();
const fieldSchema = useFieldSchema();
const field = useField();
const onUpdateComponentProps = useCallback((data) => {
_.set(fieldSchema, 'x-component-props', data);
field.componentProps = { ...field.componentProps, ...data };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
});
dn.refresh();
}, []);
return { onUpdateComponentProps };
};

View File

@ -0,0 +1,3 @@
export * from './initializers';
export * from './components';
export * from './scopes';

View File

@ -0,0 +1,81 @@
import { gridRowColWrap } from '@nocobase/client';
import { generateNTemplate } from '../../../locale';
// 页面里添加区块
export const MBlockInitializers = {
title: '{{t("Add block")}}',
icon: 'PlusOutlined',
wrap: gridRowColWrap,
items: [
{
key: 'dataBlocks',
type: 'itemGroup',
title: '{{t("Data blocks")}}',
children: [
{
key: 'GridCard',
type: 'item',
title: '{{t("Grid Card")}}',
component: 'GridCardBlockInitializer',
},
{
key: 'table',
type: 'item',
title: '{{t("Table")}}',
component: 'TableBlockInitializer',
},
{
key: 'form',
type: 'item',
title: '{{t("Form")}}',
component: 'FormBlockInitializer',
},
{
key: 'details',
type: 'item',
title: '{{t("Details")}}',
component: 'DetailsBlockInitializer',
},
{
key: 'calendar',
type: 'item',
title: '{{t("Calendar")}}',
component: 'CalendarBlockInitializer',
},
{
key: 'mapBlock',
type: 'item',
title: generateNTemplate('Map'),
component: 'MapBlockInitializer',
},
],
},
{
key: 'otherBlocks',
type: 'itemGroup',
title: '{{t("Other blocks")}}',
children: [
{
key: 'menu',
type: 'item',
title: generateNTemplate('Menu'),
component: 'MMenuBlockInitializer',
sort: 100,
},
{
key: 'markdown',
type: 'item',
title: '{{t("Markdown")}}',
component: 'MarkdownBlockInitializer',
},
{
key: 'settings',
type: 'item',
title: generateNTemplate('Settings'),
component: 'MSettingsBlockInitializer',
sort: 100,
},
],
},
],
};

View File

@ -0,0 +1 @@
export * from './BlockInitializers';

View File

@ -0,0 +1,35 @@
import { css } from '@emotion/css';
import { useInterfaceContext } from '../../../router/InterfaceProvider';
import { PaginationProps } from 'antd';
const listCss = css`
padding: 0 var(--nb-spacing);
& > .nb-action-bar {
padding: unset !important;
background: unset !important;
}
`;
export const useGridCardBlockItemProps = () => {
return {
className: listCss,
};
};
const columnCountConfig = {
xs: 1,
sm: 1,
md: 1,
lg: 1,
xl: 1,
xxl: 1,
};
export const useGridCardBlockProps = () => {
const isInterface = useInterfaceContext();
return {
columnCount: isInterface ? columnCountConfig : null,
pagination: {
simple: true,
} as PaginationProps,
};
};

View File

@ -0,0 +1 @@
export * from './grid-card';

View File

@ -0,0 +1,24 @@
import { css, cx } from '@emotion/css';
import React from 'react';
const iOS6: React.FC<{
className: string;
}> = (props) => {
return (
<div
className={cx(
'nb-mobile-device-ios6',
css(`
display: flex;
width: 375px;
height: 667px;
`),
props.className,
)}
>
{props.children}
</div>
);
};
export default iOS6;

View File

@ -0,0 +1,27 @@
import React from 'react';
import Device from './iOS6';
import { css, cx } from '@emotion/css';
export const MobileDevice: React.FC = (props) => {
return (
<div
className={cx(
'nb-mobile-device-wrapper',
css`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`,
)}
>
<Device
className={css`
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
`}
{...props}
></Device>
</div>
);
};

View File

@ -0,0 +1,33 @@
import { SettingsCenterProvider } from '@nocobase/client';
import React from 'react';
import { useTranslation } from './locale';
import { AppConfiguration, InterfaceConfiguration } from './configuration';
import { RouterSwitchProvider } from './router';
import { MobileCore } from './core';
export default React.memo((props) => {
const { t } = useTranslation();
return (
<SettingsCenterProvider
settings={{
['mobile-client']: {
title: t('Mobile Client-side'),
icon: 'MobileOutlined',
tabs: {
interface: {
title: t('Interface Configuration'),
component: InterfaceConfiguration,
},
app: {
title: t('App Configuration'),
component: AppConfiguration,
},
},
},
}}
>
<RouterSwitchProvider>{props.children}</RouterSwitchProvider>
</SettingsCenterProvider>
);
});

View File

@ -0,0 +1,5 @@
const locale = {
}
export default locale;

View File

@ -0,0 +1,23 @@
import { i18n } from '@nocobase/client';
import { useTranslation as useT } from 'react-i18next';
import enUS from './en-US';
import zhCN from './zh-CN';
export const NAMESPACE = 'mobile-client';
i18n.addResources('zh-CN', NAMESPACE, zhCN);
i18n.addResources('en-US', NAMESPACE, enUS);
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
}
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}' })}}`;
}
export function useTranslation() {
return useT([NAMESPACE, 'client'], {
nsMode: 'fallback',
});
}

View File

@ -0,0 +1,31 @@
const locale = {
'Mobile Client-side': '移动端',
'Interface Configuration': '界面配置',
'App Configuration': 'App配置',
'Enable TabBar': '启用底部标签栏',
Untitled: '未设置标题',
'Edit info': '编辑信息',
Title: '标题',
'Display back button': '展示退后按钮',
'Delete menu': '删除菜单',
'Edit menu info': '编辑菜单信息',
Menu: '菜单',
'Menu name': '菜单名',
Icon: '图标',
'Delete menu item?': '删除菜单项',
'Add menu item': '添加菜单项',
'Page template': '页面模板',
'Template mode': '模板模式',
'Enable Header': '启用头部栏',
'Enable Tabs': '启用标签栏',
'Delete tab item?': '是否删除标签项?',
'Add tab': '添加标签页',
'App level Configuration': '应用级别配置',
'Menu configuration': '菜单配置',
'Page configuration': '页面配置',
Settings: '设置',
'Delete settings block': '删除设置区块',
'Delete menu block': '删除菜单区块',
};
export default locale;

Some files were not shown because too many files have changed in this diff Show More