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:
parent
b401c54442
commit
9c165db0f7
@ -2,12 +2,17 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, shrink-to-fit=no">
|
||||||
<title>Loading...</title>
|
<title>Loading...</title>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
<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="32x32" href="/favicon/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/favicon/site.webmanifest">
|
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||||
<style>
|
<style>
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
/* width */
|
/* width */
|
||||||
.win ::-webkit-scrollbar {
|
.win ::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -41,4 +46,4 @@
|
|||||||
window.routerBase = match ? match[0] : "/";
|
window.routerBase = match ? match[0] : "/";
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
1
packages/app/client/src/plugins/mobile-client.ts
Normal file
1
packages/app/client/src/plugins/mobile-client.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@nocobase/plugin-mobile-client/client';
|
@ -53,9 +53,7 @@ const App = React.memo((props: any) => {
|
|||||||
const C = compose(...props.providers)(() => {
|
const C = compose(...props.providers)(() => {
|
||||||
const routes = useRoutes();
|
const routes = useRoutes();
|
||||||
return (
|
return (
|
||||||
<div>
|
<RouteSwitch routes={routes} />
|
||||||
<RouteSwitch routes={routes} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return <C />;
|
return <C />;
|
||||||
|
@ -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 { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||||
@ -262,8 +262,21 @@ const SettingsCenter = (props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
<Layout>
|
className={cx(
|
||||||
|
'nb-setting-center',
|
||||||
|
css`
|
||||||
|
&.nb-setting-center {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Layout
|
||||||
|
className={css`
|
||||||
|
height: 100%;
|
||||||
|
`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@ -302,7 +315,12 @@ const SettingsCenter = (props) => {
|
|||||||
items={menuItems as any}
|
items={menuItems as any}
|
||||||
/>
|
/>
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
<Layout.Content>
|
<Layout.Content
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`}
|
||||||
|
>
|
||||||
{aclPluginTabCheck && (
|
{aclPluginTabCheck && (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
ghost={false}
|
ghost={false}
|
||||||
@ -321,7 +339,7 @@ const SettingsCenter = (props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={'m24'} style={{ margin: 24 }}>
|
<div className={'m24'} style={{ margin: 24, flex: 1 }}>
|
||||||
{aclPluginTabCheck ? (
|
{aclPluginTabCheck ? (
|
||||||
component && React.createElement(component)
|
component && React.createElement(component)
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom';
|
||||||
import { RouteContext } from './context';
|
import { RouteContext } from './context';
|
||||||
import { useRouteComponent } from './hooks';
|
import { useRoute, useRouteComponent } from './hooks';
|
||||||
import { RouteSwitchProps } from './types';
|
import { RouteSwitchProps } from './types';
|
||||||
|
|
||||||
export function RouteSwitch(props: RouteSwitchProps) {
|
export function RouteSwitch(props: RouteSwitchProps) {
|
||||||
const { routes = [] } = props;
|
const { routes = [] } = props;
|
||||||
|
const { url: base, path } = useRouteMatch();
|
||||||
|
|
||||||
if (!routes.length) {
|
if (!routes.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -28,21 +30,20 @@ export function RouteSwitch(props: RouteSwitchProps) {
|
|||||||
if (!route.path && Array.isArray(route.routes)) {
|
if (!route.path && Array.isArray(route.routes)) {
|
||||||
route.path = route.routes.map((r) => r.path) as any;
|
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 (
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={index}
|
key={[nextPath].flat().join(',') as string}
|
||||||
path={route.path}
|
path={nextPath}
|
||||||
exact={route.exact}
|
exact={route.exact}
|
||||||
strict={route.strict}
|
strict={route.strict}
|
||||||
sensitive={route.sensitive}
|
sensitive={route.sensitive}
|
||||||
render={(props) => {
|
>
|
||||||
return (
|
<ComponentRenderer route={route} />
|
||||||
<RouteContext.Provider value={route}>
|
</Route>
|
||||||
<ComponentRenderer {...props} route={route} />
|
|
||||||
</RouteContext.Provider>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -50,10 +51,16 @@ export function RouteSwitch(props: RouteSwitchProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ComponentRenderer(props) {
|
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 (
|
return (
|
||||||
<Component {...props}>
|
<RouteContext.Provider value={route}>
|
||||||
<RouteSwitch routes={props.route.routes} />
|
<Component {...props}>
|
||||||
</Component>
|
<RouteSwitch routes={route?.routes} />
|
||||||
|
</Component>
|
||||||
|
</RouteContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
export const AdminLayout = (props) => {
|
||||||
return (
|
return (
|
||||||
<CurrentAppInfoProvider>
|
<AdminProvider>
|
||||||
<CurrentUserProvider>
|
<InternalAdminLayout {...props} />
|
||||||
<RemoteSchemaTemplateManagerProvider>
|
</AdminProvider>
|
||||||
<RemoteCollectionManagerProvider>
|
|
||||||
<ACLRolesCheckProvider>
|
|
||||||
<InternalAdminLayout {...props} />
|
|
||||||
</ACLRolesCheckProvider>
|
|
||||||
</RemoteCollectionManagerProvider>
|
|
||||||
</RemoteSchemaTemplateManagerProvider>
|
|
||||||
</CurrentUserProvider>
|
|
||||||
</CurrentAppInfoProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { RouteProps } from './types';
|
import { RouteProps } from './types';
|
||||||
|
|
||||||
export const RouteSwitchContext = createContext({
|
export const RouteSwitchContext = createContext({
|
||||||
@ -7,3 +7,5 @@ export const RouteSwitchContext = createContext({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const RouteContext = createContext<RouteProps>(null);
|
export const RouteContext = createContext<RouteProps>(null);
|
||||||
|
|
||||||
|
export const useRouteSwitchContext = () => useContext(RouteSwitchContext);
|
||||||
|
@ -31,4 +31,5 @@ export type RouteRedirectProps = RedirectProps | RouteProps;
|
|||||||
export interface RouteSwitchProps {
|
export interface RouteSwitchProps {
|
||||||
routes?: RouteRedirectProps[];
|
routes?: RouteRedirectProps[];
|
||||||
components?: any;
|
components?: any;
|
||||||
|
base?: string;
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
|||||||
import { Drawer } from 'antd';
|
import { Drawer } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { OpenSize } from './';
|
||||||
import { useActionContext } from './hooks';
|
import { useActionContext } from './hooks';
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
import { OpenSize } from './';
|
|
||||||
|
|
||||||
const openSizeWidthMap = new Map<OpenSize, string>([
|
const openSizeWidthMap = new Map<OpenSize, string>([
|
||||||
['small', '30%'],
|
['small', '30%'],
|
||||||
@ -18,7 +17,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
|||||||
(props) => {
|
(props) => {
|
||||||
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
|
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { visible, setVisible, openSize = 'middle' } = useActionContext();
|
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
|
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
|
||||||
@ -29,90 +28,86 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
|||||||
}
|
}
|
||||||
return buf;
|
return buf;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Drawer
|
||||||
{createPortal(
|
width={openSizeWidthMap.get(openSize)}
|
||||||
<div
|
title={field.title}
|
||||||
onClick={(e) => {
|
{...others}
|
||||||
e.stopPropagation();
|
{...drawerProps}
|
||||||
}}
|
style={{
|
||||||
>
|
...drawerProps?.style,
|
||||||
<Drawer
|
...others?.style,
|
||||||
width={openSizeWidthMap.get(finalOpenSize)}
|
}}
|
||||||
title={field.title}
|
destroyOnClose
|
||||||
{...others}
|
open={visible}
|
||||||
destroyOnClose
|
onClose={() => setVisible(false, true)}
|
||||||
open={visible}
|
className={classNames(
|
||||||
onClose={() => setVisible(false, true)}
|
drawerProps?.className,
|
||||||
className={classNames(
|
others.className,
|
||||||
others.className,
|
css`
|
||||||
css`
|
&.nb-action-popup {
|
||||||
&.nb-action-popup {
|
.ant-drawer-header {
|
||||||
.ant-drawer-header {
|
display: none;
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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
|
<RecursionField
|
||||||
basePath={field.address}
|
basePath={field.address}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onlyRenderProperties
|
onlyRenderProperties
|
||||||
filterProperties={(s) => {
|
filterProperties={(s) => {
|
||||||
return s['x-component'] !== footerNodeName;
|
return s['x-component'] === footerNodeName;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</div>
|
||||||
</div>,
|
)
|
||||||
document.body,
|
}
|
||||||
)}
|
>
|
||||||
</>
|
<RecursionField
|
||||||
|
basePath={field.address}
|
||||||
|
schema={schema}
|
||||||
|
onlyRenderProperties
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] !== footerNodeName;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'ActionDrawer' },
|
{ displayName: 'ActionDrawer' },
|
||||||
|
@ -3,7 +3,6 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
|||||||
import { Modal, ModalProps } from 'antd';
|
import { Modal, ModalProps } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { OpenSize, useActionContext } from '.';
|
import { OpenSize, useActionContext } from '.';
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ const openSizeWidthMap = new Map<OpenSize, string>([
|
|||||||
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = 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 actualWidth = width ?? openSizeWidthMap.get(openSize);
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -26,84 +25,79 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
|||||||
return buf;
|
return buf;
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal
|
||||||
{createPortal(
|
width={actualWidth}
|
||||||
<div
|
title={field.title}
|
||||||
onClick={(e) => {
|
{...(others as ModalProps)}
|
||||||
e.stopPropagation();
|
{...modalProps}
|
||||||
}}
|
style={{
|
||||||
>
|
...modalProps?.style,
|
||||||
<Modal
|
...others?.style,
|
||||||
width={actualWidth}
|
}}
|
||||||
title={field.title}
|
destroyOnClose
|
||||||
{...(others as ModalProps)}
|
open={visible}
|
||||||
destroyOnClose
|
onCancel={() => setVisible(false, true)}
|
||||||
visible={visible}
|
className={classNames(
|
||||||
onCancel={() => setVisible(false, true)}
|
others.className,
|
||||||
className={classNames(
|
modalProps?.className,
|
||||||
others.className,
|
css`
|
||||||
css`
|
&.nb-action-popup {
|
||||||
&.nb-action-popup {
|
.ant-modal-header {
|
||||||
.ant-modal-header {
|
display: none;
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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
|
<RecursionField
|
||||||
basePath={field.address}
|
basePath={field.address}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onlyRenderProperties
|
onlyRenderProperties
|
||||||
filterProperties={(s) => {
|
filterProperties={(s) => {
|
||||||
return s['x-component'] !== footerNodeName;
|
return s['x-component'] === footerNodeName;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</div>
|
||||||
</div>,
|
) : (
|
||||||
document.body,
|
false
|
||||||
)}
|
)
|
||||||
</>
|
}
|
||||||
|
>
|
||||||
|
<RecursionField
|
||||||
|
basePath={field.address}
|
||||||
|
schema={schema}
|
||||||
|
onlyRenderProperties
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] !== footerNodeName;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'ActionModal' },
|
{ displayName: 'ActionModal' },
|
||||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { Button, Modal, Popover } from 'antd';
|
import { Button, Modal, Popover } from 'antd';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useActionContext } from '../..';
|
import { useActionContext } from '../..';
|
||||||
import { useDesignable } from '../../';
|
import { useDesignable } from '../../';
|
||||||
import { Icon } from '../../../icon';
|
import { Icon } from '../../../icon';
|
||||||
@ -16,7 +16,7 @@ import { ActionDrawer } from './Action.Drawer';
|
|||||||
import { ActionLink } from './Action.Link';
|
import { ActionLink } from './Action.Link';
|
||||||
import { ActionModal } from './Action.Modal';
|
import { ActionModal } from './Action.Modal';
|
||||||
import { ActionPage } from './Action.Page';
|
import { ActionPage } from './Action.Page';
|
||||||
import { ActionContext } from './context';
|
import { ActionContextProvider } from './context';
|
||||||
import { useA } from './hooks';
|
import { useA } from './hooks';
|
||||||
import { ComposedAction } from './types';
|
import { ComposedAction } from './types';
|
||||||
import { linkageAction } from './utils';
|
import { linkageAction } from './utils';
|
||||||
@ -95,16 +95,6 @@ export const Action: ComposedAction = observer(
|
|||||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const tarComponent = useComponent(component) || component;
|
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(() => {
|
useEffect(() => {
|
||||||
field.linkageProperty = {};
|
field.linkageProperty = {};
|
||||||
linkageRules
|
linkageRules
|
||||||
@ -127,6 +117,7 @@ export const Action: ComposedAction = observer(
|
|||||||
icon={<Icon type={icon} />}
|
icon={<Icon type={icon} />}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
|
...others.style,
|
||||||
opacity: designable && field?.data?.hidden && 0.1,
|
opacity: designable && field?.data?.hidden && 0.1,
|
||||||
}}
|
}}
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
@ -151,30 +142,28 @@ export const Action: ComposedAction = observer(
|
|||||||
component={tarComponent || Button}
|
component={tarComponent || Button}
|
||||||
className={classnames(className, actionDesignerCss)}
|
className={classnames(className, actionDesignerCss)}
|
||||||
>
|
>
|
||||||
{renderTitle}
|
{title || compile(fieldSchema.title)}
|
||||||
<Designer {...designerProps} />
|
<Designer {...designerProps} />
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider
|
<ActionContextProvider
|
||||||
value={{
|
button={renderButton()}
|
||||||
button: renderButton(),
|
visible={visible}
|
||||||
visible,
|
setVisible={setVisible}
|
||||||
setVisible,
|
formValueChanged={formValueChanged}
|
||||||
formValueChanged,
|
setFormValueChanged={setFormValueChanged}
|
||||||
setFormValueChanged,
|
openMode={openMode}
|
||||||
openMode,
|
openSize={openSize}
|
||||||
openSize,
|
containerRefKey={containerRefKey}
|
||||||
containerRefKey,
|
fieldSchema={fieldSchema}
|
||||||
fieldSchema,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||||
{!popover && renderButton()}
|
{!popover && renderButton()}
|
||||||
{!popover && props.children}
|
{!popover && props.children}
|
||||||
</ActionContext.Provider>
|
</ActionContextProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'Action' },
|
{ displayName: 'Action' },
|
||||||
|
@ -1,37 +1,81 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||||
import { Space } from 'antd';
|
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 { useSchemaInitializer } from '../../../schema-initializer';
|
||||||
import { DndContext } from '../../common';
|
import { DndContext } from '../../common';
|
||||||
import { useDesignable, useProps } from '../../hooks';
|
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(
|
export const ActionBar = observer(
|
||||||
(props: any) => {
|
(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 fieldSchema = useFieldSchema();
|
||||||
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
|
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
|
|
||||||
if (layout === 'one-column') {
|
if (layout === 'one-column') {
|
||||||
return (
|
return (
|
||||||
<DndContext>
|
<Portal>
|
||||||
<div
|
<DndContext>
|
||||||
style={{ display: 'flex', alignItems: 'center', ...style }}
|
<div
|
||||||
{...others}
|
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style }}
|
||||||
className={cx(others.className, 'nb-action-bar')}
|
{...others}
|
||||||
>
|
className={cx(others.className, 'nb-action-bar')}
|
||||||
{props.children && (
|
>
|
||||||
<div style={{ marginRight: 8 }}>
|
{props.children && (
|
||||||
<Space {...spaceProps}>
|
<div>
|
||||||
{fieldSchema.mapProperties((schema, key) => {
|
<Space {...spaceProps}>
|
||||||
return <RecursionField key={key} name={key} schema={schema} />;
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
})}
|
return <RecursionField key={key} name={key} schema={schema} />;
|
||||||
</Space>
|
})}
|
||||||
</div>
|
</Space>
|
||||||
)}
|
</div>
|
||||||
<InitializerComponent />
|
)}
|
||||||
</div>
|
<InitializerComponent style={{ margin: '0 !important' }} />
|
||||||
</DndContext>
|
</div>
|
||||||
|
</DndContext>
|
||||||
|
</Portal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
|
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
|
||||||
@ -41,25 +85,18 @@ export const ActionBar = observer(
|
|||||||
!designable && !hasActions
|
!designable && !hasActions
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
...style,
|
...style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{...others}
|
{...others}
|
||||||
className={cx(others.className, 'nb-action-bar')}
|
className={cx(others.className, 'nb-action-bar')}
|
||||||
>
|
>
|
||||||
<div
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||||
className={css`
|
|
||||||
.ant-space:last-child {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
|
|
||||||
>
|
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<Space {...spaceProps}>
|
<Space {...spaceProps}>
|
||||||
{fieldSchema.mapProperties((schema, key) => {
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
import { Schema } from '@formily/react';
|
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 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 type OpenSize = 'small' | 'middle' | 'large';
|
||||||
export interface ActionContextProps {
|
export interface ActionContextProps {
|
||||||
button?: any;
|
button?: any;
|
||||||
@ -15,4 +26,6 @@ export interface ActionContextProps {
|
|||||||
formValueChanged?: boolean;
|
formValueChanged?: boolean;
|
||||||
setFormValueChanged?: (v: boolean) => void;
|
setFormValueChanged?: (v: boolean) => void;
|
||||||
fieldSchema?: Schema;
|
fieldSchema?: Schema;
|
||||||
|
drawerProps?: DrawerProps;
|
||||||
|
modalProps?: ModalProps;
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,16 @@ import { css } from '@emotion/css';
|
|||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SortableItem } from '../../common';
|
import { SortableItem } from '../../common';
|
||||||
import { useDesigner } from '../../hooks';
|
import { useDesigner, useProps } from '../../hooks';
|
||||||
|
|
||||||
export const BlockItem: React.FC<any> = (props) => {
|
export const BlockItem: React.FC<any> = (props) => {
|
||||||
|
const { className, children } = useProps(props);
|
||||||
const Designer = useDesigner();
|
const Designer = useDesigner();
|
||||||
return (
|
return (
|
||||||
<SortableItem
|
<SortableItem
|
||||||
className={cls(
|
className={cls(
|
||||||
'nb-block-item',
|
'nb-block-item',
|
||||||
props.className,
|
className,
|
||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -57,7 +58,7 @@ export const BlockItem: React.FC<any> = (props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Designer />
|
<Designer />
|
||||||
{props.children}
|
{children}
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export const CardItem: React.FC = (props) => {
|
|||||||
|
|
||||||
return templateKey && !template ? null : (
|
return templateKey && !template ? null : (
|
||||||
<BlockItem className={'noco-card-item'}>
|
<BlockItem className={'noco-card-item'}>
|
||||||
<Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
|
<Card style={{ marginBottom: 'var(--nb-spacing)' }} bordered={false} {...restProps}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card>
|
</Card>
|
||||||
</BlockItem>
|
</BlockItem>
|
||||||
|
@ -74,7 +74,6 @@ export const GridCardDesigner = () => {
|
|||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
<SchemaComponentOptions components={{ Slider }}>
|
<SchemaComponentOptions components={{ Slider }}>
|
||||||
<SchemaSettings.BlockTitleItem />
|
|
||||||
<SchemaSettings.ModalItem
|
<SchemaSettings.ModalItem
|
||||||
title={t('Set the count of columns displayed in a row')}
|
title={t('Set the count of columns displayed in a row')}
|
||||||
initialValues={columnCount}
|
initialValues={columnCount}
|
||||||
@ -155,7 +154,7 @@ export const GridCardDesigner = () => {
|
|||||||
field: {
|
field: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: sortFields,
|
enum: sortFields,
|
||||||
required:true,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
|
@ -5,7 +5,7 @@ import { List as AntdList, PaginationProps, Col } from 'antd';
|
|||||||
import { useGridCardActionBarProps } from './hooks';
|
import { useGridCardActionBarProps } from './hooks';
|
||||||
import { SortableItem } from '../../common';
|
import { SortableItem } from '../../common';
|
||||||
import { SchemaComponentOptions } from '../../core';
|
import { SchemaComponentOptions } from '../../core';
|
||||||
import { useDesigner } from '../../hooks';
|
import { useDesigner, useProps } from '../../hooks';
|
||||||
import { GridCardItem } from './GridCard.Item';
|
import { GridCardItem } from './GridCard.Item';
|
||||||
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
|
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
|
||||||
import { GridCardDesigner } from './GridCard.Designer';
|
import { GridCardDesigner } from './GridCard.Designer';
|
||||||
@ -55,7 +55,9 @@ const designerCss = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const InternalGridCard = (props) => {
|
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 { run, params } = service;
|
||||||
const meta = service?.data?.meta;
|
const meta = service?.data?.meta;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -70,7 +72,9 @@ const InternalGridCard = (props) => {
|
|||||||
new Schema({
|
new Schema({
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
[key]: fieldSchema.properties['item'],
|
[key]: {
|
||||||
|
...fieldSchema.properties['item'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -104,6 +108,7 @@ const InternalGridCard = (props) => {
|
|||||||
!meta || meta.count <= meta.pageSize
|
!meta || meta.count <= meta.pageSize
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
|
...pagination,
|
||||||
onChange: onPaginationChange,
|
onChange: onPaginationChange,
|
||||||
total: meta?.count || 0,
|
total: meta?.count || 0,
|
||||||
pageSize: meta?.pageSize || 10,
|
pageSize: meta?.pageSize || 10,
|
||||||
|
@ -332,6 +332,7 @@ export const useGridRowContext = () => {
|
|||||||
|
|
||||||
export const Grid: any = observer(
|
export const Grid: any = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
|
const { showDivider = true } = props;
|
||||||
const gridRef = useRef(null);
|
const gridRef = useRef(null);
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -346,36 +347,40 @@ export const Grid: any = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GridContext.Provider
|
<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}>
|
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
|
||||||
<DndWrapper dndContext={props.dndContext}>
|
<DndWrapper dndContext={props.dndContext}>
|
||||||
<RowDivider
|
{showDivider ? (
|
||||||
rows={rows}
|
<RowDivider
|
||||||
first
|
rows={rows}
|
||||||
id={`${addr}_0`}
|
first
|
||||||
data={{
|
id={`${addr}_0`}
|
||||||
breakRemoveOn: breakRemoveOnGrid,
|
data={{
|
||||||
wrapSchema: wrapRowSchema,
|
breakRemoveOn: breakRemoveOnGrid,
|
||||||
insertAdjacent: 'afterBegin',
|
wrapSchema: wrapRowSchema,
|
||||||
schema: fieldSchema,
|
insertAdjacent: 'afterBegin',
|
||||||
}}
|
schema: fieldSchema,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{rows.map((schema, index) => {
|
{rows.map((schema, index) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<RecursionField name={schema.name} schema={schema} />
|
<RecursionField name={schema.name} schema={schema} />
|
||||||
<RowDivider
|
{showDivider ? (
|
||||||
rows={rows}
|
<RowDivider
|
||||||
index={index}
|
rows={rows}
|
||||||
id={`${addr}_${index + 1}`}
|
index={index}
|
||||||
data={{
|
id={`${addr}_${index + 1}`}
|
||||||
breakRemoveOn: breakRemoveOnGrid,
|
data={{
|
||||||
wrapSchema: wrapRowSchema,
|
breakRemoveOn: breakRemoveOnGrid,
|
||||||
insertAdjacent: 'afterEnd',
|
wrapSchema: wrapRowSchema,
|
||||||
schema,
|
insertAdjacent: 'afterEnd',
|
||||||
}}
|
schema,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -394,6 +399,7 @@ Grid.Row = observer(
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const addr = field.address.toString();
|
const addr = field.address.toString();
|
||||||
const cols = useColProperties();
|
const cols = useColProperties();
|
||||||
|
const { showDivider } = useGridContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
|
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
|
||||||
@ -402,40 +408,46 @@ Grid.Row = observer(
|
|||||||
'nb-grid-row',
|
'nb-grid-row',
|
||||||
css`
|
css`
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin: 0 calc(-1 * var(--nb-spacing));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
/* z-index: 0; */
|
/* z-index: 0; */
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
margin: showDivider ? '0 calc(-1 * var(--nb-spacing))' : null,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ColDivider
|
{showDivider && (
|
||||||
cols={cols}
|
<ColDivider
|
||||||
first
|
cols={cols}
|
||||||
id={`${addr}_0`}
|
first
|
||||||
data={{
|
id={`${addr}_0`}
|
||||||
breakRemoveOn: breakRemoveOnRow,
|
data={{
|
||||||
wrapSchema: wrapColSchema,
|
breakRemoveOn: breakRemoveOnRow,
|
||||||
insertAdjacent: 'afterBegin',
|
wrapSchema: wrapColSchema,
|
||||||
schema: fieldSchema,
|
insertAdjacent: 'afterBegin',
|
||||||
}}
|
schema: fieldSchema,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{cols.map((schema, index) => {
|
{cols.map((schema, index) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<RecursionField name={schema.name} schema={schema} />
|
<RecursionField name={schema.name} schema={schema} />
|
||||||
<ColDivider
|
{showDivider && (
|
||||||
cols={cols}
|
<ColDivider
|
||||||
index={index}
|
cols={cols}
|
||||||
last={index === cols.length - 1}
|
index={index}
|
||||||
id={`${addr}_${index + 1}`}
|
last={index === cols.length - 1}
|
||||||
data={{
|
id={`${addr}_${index + 1}`}
|
||||||
breakRemoveOn: breakRemoveOnRow,
|
data={{
|
||||||
wrapSchema: wrapColSchema,
|
breakRemoveOn: breakRemoveOnRow,
|
||||||
insertAdjacent: 'afterEnd',
|
wrapSchema: wrapColSchema,
|
||||||
schema,
|
insertAdjacent: 'afterEnd',
|
||||||
}}
|
schema,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -449,6 +461,7 @@ Grid.Row = observer(
|
|||||||
Grid.Col = observer(
|
Grid.Col = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { cols = [] } = useContext(GridRowContext);
|
const { cols = [] } = useContext(GridRowContext);
|
||||||
|
const { showDivider } = useGridContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
|
|
||||||
@ -456,7 +469,7 @@ Grid.Col = observer(
|
|||||||
let width = '';
|
let width = '';
|
||||||
if (cols?.length) {
|
if (cols?.length) {
|
||||||
const w = schema?.['x-component-props']?.['width'] || 100 / 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;
|
return width;
|
||||||
}, [cols?.length, schema?.['x-component-props']?.['width']]);
|
}, [cols?.length, schema?.['x-component-props']?.['width']]);
|
||||||
@ -477,5 +490,5 @@ Grid.Col = observer(
|
|||||||
</GridColContext.Provider>
|
</GridColContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'Grid.Col' },
|
{ displayName: 'Grid.Row' },
|
||||||
);
|
);
|
||||||
|
@ -21,12 +21,22 @@
|
|||||||
--nb-designer-offset: -10px;
|
--nb-designer-offset: -10px;
|
||||||
}
|
}
|
||||||
.theme-compact {
|
.theme-compact {
|
||||||
--nb-spacing: 16px;
|
--nb-spacing: 12px;
|
||||||
--nb-designer-offset: -6px;
|
--nb-designer-offset: -6px;
|
||||||
.ant-formily-item {
|
.ant-formily-item {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.ant-card {
|
.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;
|
||||||
|
}
|
||||||
|
@ -11,11 +11,13 @@ const FixedBlockContext = React.createContext<{
|
|||||||
height: number | string;
|
height: number | string;
|
||||||
fixedBlockUID: boolean | string;
|
fixedBlockUID: boolean | string;
|
||||||
fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
|
fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
|
||||||
|
inFixedBlock: boolean;
|
||||||
}>({
|
}>({
|
||||||
setFixedBlock: () => {},
|
setFixedBlock: () => {},
|
||||||
height: 0,
|
height: 0,
|
||||||
fixedBlockUID: false,
|
fixedBlockUID: false,
|
||||||
fixedBlockUIDRef: { current: false },
|
fixedBlockUIDRef: { current: false },
|
||||||
|
inFixedBlock: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFixedSchema = () => {
|
export const useFixedSchema = () => {
|
||||||
@ -69,8 +71,9 @@ export const FixedBlockDesignerItem = () => {
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { inFixedBlock } = useFixedBlock();
|
||||||
|
|
||||||
if (Object.keys(record).length) {
|
if (Object.keys(record).length || !inFixedBlock) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -127,7 +130,7 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
|
|||||||
_setFixedBlock(v);
|
_setFixedBlock(v);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<FixedBlockContext.Provider value={{ height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
|
<FixedBlockContext.Provider value={{ inFixedBlock: true, height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
|
||||||
<div
|
<div
|
||||||
className={fixedBlockUID ? fixedBlockCss : ''}
|
className={fixedBlockUID ? fixedBlockCss : ''}
|
||||||
style={{
|
style={{
|
||||||
|
@ -245,7 +245,7 @@ export const Page = (props) => {
|
|||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
tab={
|
tab={
|
||||||
<SortableItem
|
<SortableItem
|
||||||
id={schema.name}
|
id={schema.name as string}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
className={classNames('nb-action-link', designerCss, props.className)}
|
className={classNames('nb-action-link', designerCss, props.className)}
|
||||||
>
|
>
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
||||||
import { FormProvider, SchemaComponentOptions } from '../../core';
|
import { FormProvider, SchemaComponentOptions } from '../../core';
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContext, useActionContext } from '../action';
|
import { ActionContextProvider, useActionContext } from '../action';
|
||||||
import { FileSelector } from '../preview';
|
import { FileSelector } from '../preview';
|
||||||
import { useFieldNames } from './useFieldNames';
|
import { useFieldNames } from './useFieldNames';
|
||||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
@ -269,7 +269,7 @@ const Drawer: React.FunctionComponent<{
|
|||||||
return (
|
return (
|
||||||
<RecordPickerProvider {...recordPickerProps}>
|
<RecordPickerProvider {...recordPickerProps}>
|
||||||
<CollectionProvider allowNull name={collectionField?.target}>
|
<CollectionProvider allowNull name={collectionField?.target}>
|
||||||
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
|
<ActionContextProvider openMode="drawer" visible={visible} setVisible={setVisible}>
|
||||||
<FormProvider>
|
<FormProvider>
|
||||||
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
||||||
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
|
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
|
||||||
@ -283,7 +283,7 @@ const Drawer: React.FunctionComponent<{
|
|||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
</TableSelectorParamsProvider>
|
</TableSelectorParamsProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</ActionContext.Provider>
|
</ActionContextProvider>
|
||||||
</CollectionProvider>
|
</CollectionProvider>
|
||||||
</RecordPickerProvider>
|
</RecordPickerProvider>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||||
import { toArr } from '@formily/shared';
|
import { toArr } from '@formily/shared';
|
||||||
import React, { Fragment, useRef, useState } from 'react';
|
import React, { Fragment, useRef, useState } from 'react';
|
||||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||||
import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
|
|
||||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
|
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||||
import { FormProvider } from '../../core';
|
import { FormProvider } from '../../core';
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContext, useActionContext } from '../action';
|
import { ActionContextProvider, useActionContext } from '../action';
|
||||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||||
import { Preview } from '../preview';
|
import { Preview } from '../preview';
|
||||||
import { isShowFilePicker } from './InputRecordPicker';
|
import { isShowFilePicker } from './InputRecordPicker';
|
||||||
@ -56,8 +55,7 @@ export const ReadPrettyRecordPicker: React.FC = observer(
|
|||||||
const text = getLabelFormatValue(labelUiSchema, val, true);
|
const text = getLabelFormatValue(labelUiSchema, val, true);
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${record.id}_${index}`}>
|
<Fragment key={`${record.id}_${index}`}>
|
||||||
{/* test-record-picker-read-pretty-item 用于在单元测试中方便选中元素 */}
|
<span>
|
||||||
<span className="test-record-picker-read-pretty-item">
|
|
||||||
{snapshot || isTagsMode ? (
|
{snapshot || isTagsMode ? (
|
||||||
text
|
text
|
||||||
) : (
|
) : (
|
||||||
@ -112,11 +110,11 @@ export const ReadPrettyRecordPicker: React.FC = observer(
|
|||||||
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
|
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
|
||||||
{renderRecords()}
|
{renderRecords()}
|
||||||
</EllipsisWithTooltip>
|
</EllipsisWithTooltip>
|
||||||
<ActionContext.Provider
|
<ActionContextProvider
|
||||||
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
|
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
|
||||||
>
|
>
|
||||||
{renderRecordProvider()}
|
{renderRecordProvider()}
|
||||||
</ActionContext.Provider>
|
</ActionContextProvider>
|
||||||
</CollectionProvider>
|
</CollectionProvider>
|
||||||
</BlockAssociationContext.Provider>
|
</BlockAssociationContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import { TableSelector } from './TableSelector';
|
|||||||
export * from './TableBlockDesigner';
|
export * from './TableBlockDesigner';
|
||||||
export * from './TableField';
|
export * from './TableField';
|
||||||
export * from './TableSelectorDesigner';
|
export * from './TableSelectorDesigner';
|
||||||
|
export * from './FilterDynamicComponent';
|
||||||
|
|
||||||
export const TableV2 = Table;
|
export const TableV2 = Table;
|
||||||
|
|
||||||
|
@ -7,15 +7,18 @@ import { Icon } from '../../../icon';
|
|||||||
import { useSchemaInitializer } from '../../../schema-initializer';
|
import { useSchemaInitializer } from '../../../schema-initializer';
|
||||||
import { DndContext, SortableItem } from '../../common';
|
import { DndContext, SortableItem } from '../../common';
|
||||||
import { useDesigner } from '../../hooks/useDesigner';
|
import { useDesigner } from '../../hooks/useDesigner';
|
||||||
|
import { useTabsContext } from './context';
|
||||||
import { TabsDesigner } from './Tabs.Designer';
|
import { TabsDesigner } from './Tabs.Designer';
|
||||||
|
|
||||||
export const Tabs: any = observer(
|
export const Tabs: any = observer(
|
||||||
(props: TabsProps) => {
|
(props: TabsProps) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||||
|
const contextProps = useTabsContext();
|
||||||
return (
|
return (
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<AntdTabs
|
<AntdTabs
|
||||||
|
{...contextProps}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
tabBarExtraContent={{
|
tabBarExtraContent={{
|
||||||
right: render(),
|
right: render(),
|
||||||
@ -93,7 +96,7 @@ Tabs.TabPane = observer(
|
|||||||
</SortableItem>
|
</SortableItem>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'TabPane' },
|
{ displayName: 'Tabs.TabPane' },
|
||||||
);
|
);
|
||||||
|
|
||||||
Tabs.Designer = TabsDesigner;
|
Tabs.Designer = TabsDesigner;
|
||||||
|
@ -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);
|
||||||
|
};
|
@ -1 +1,2 @@
|
|||||||
export * from './Tabs';
|
export * from './Tabs';
|
||||||
|
export * from './context';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
import { cx } from '@emotion/css';
|
||||||
import React, { createContext, useContext } from 'react';
|
import { observer, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React, { createContext, HTMLAttributes, useContext } from 'react';
|
||||||
|
|
||||||
export const DraggableContext = createContext(null);
|
export const DraggableContext = createContext(null);
|
||||||
export const SortableContext = createContext(null);
|
export const SortableContext = createContext(null);
|
||||||
@ -19,18 +20,21 @@ export const SortableProvider = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Sortable = (props: any) => {
|
export const Sortable = (props: any) => {
|
||||||
const { component, style, children, openMode, ...others } = props;
|
const { component, overStyle, style, children, openMode, ...others } = props;
|
||||||
const { droppable } = useContext(SortableContext);
|
const { draggable, droppable } = useContext(SortableContext);
|
||||||
const { isOver, setNodeRef } = droppable;
|
const { isOver, setNodeRef } = droppable;
|
||||||
const droppableStyle = { ...style };
|
const droppableStyle = { ...style };
|
||||||
|
|
||||||
if (isOver) {
|
if (isOver && draggable?.active?.id !== droppable?.over?.id) {
|
||||||
droppableStyle['color'] = 'rgba(241, 139, 98, .1)';
|
droppableStyle[component === 'a' ? 'color' : 'background'] = 'rgba(241, 139, 98, .15)';
|
||||||
|
Object.assign(droppableStyle, overStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
component || 'div',
|
component || 'div',
|
||||||
{
|
{
|
||||||
...others,
|
...others,
|
||||||
|
className: cx('nb-sortable-designer', props.className),
|
||||||
ref: setNodeRef,
|
ref: setNodeRef,
|
||||||
style: droppableStyle,
|
style: droppableStyle,
|
||||||
},
|
},
|
||||||
@ -55,9 +59,15 @@ const useSortableItemId = (props) => {
|
|||||||
return field.address.toString();
|
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) => {
|
(props) => {
|
||||||
const { schema, id, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
|
const { schema, id, eid, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
|
||||||
return (
|
return (
|
||||||
<SortableProvider
|
<SortableProvider
|
||||||
id={id}
|
id={id}
|
||||||
@ -67,7 +77,9 @@ export const SortableItem: React.FC<any> = observer(
|
|||||||
removeParentsIfNoChildren: removeParentsIfNoChildren ?? true,
|
removeParentsIfNoChildren: removeParentsIfNoChildren ?? true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sortable {...others}>{props.children}</Sortable>
|
<Sortable id={eid} {...others}>
|
||||||
|
{props.children}
|
||||||
|
</Sortable>
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -144,7 +144,7 @@ export class Designable {
|
|||||||
if (!current['x-uid']) {
|
if (!current['x-uid']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await api.request({
|
const res = await api.request({
|
||||||
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
|
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
@ -165,7 +165,7 @@ export class Designable {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onSuccess?.();
|
onSuccess?.(res?.data?.data);
|
||||||
message.success(t('Saved successfully'), 0.2);
|
message.success(t('Saved successfully'), 0.2);
|
||||||
});
|
});
|
||||||
this.on('patch', async ({ schema }) => {
|
this.on('patch', async ({ schema }) => {
|
||||||
@ -321,7 +321,7 @@ export class Designable {
|
|||||||
removed = parent;
|
removed = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.emit('remove', { removed });
|
return this.emit('remove', { removed });
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
|
removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
|
||||||
@ -501,7 +501,7 @@ export class Designable {
|
|||||||
const s = this.current.addProperty(wrapped.name || uid(), wrapped);
|
const s = this.current.addProperty(wrapped.name || uid(), wrapped);
|
||||||
s.parent = this.current;
|
s.parent = this.current;
|
||||||
const [schema1, schema2] = splitWrapSchema(s, schema);
|
const [schema1, schema2] = splitWrapSchema(s, schema);
|
||||||
this.emit('insertAdjacent', {
|
return this.emit('insertAdjacent', {
|
||||||
position: 'beforeEnd',
|
position: 'beforeEnd',
|
||||||
schema: schema2,
|
schema: schema2,
|
||||||
wrap: schema1,
|
wrap: schema1,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { css } from '@emotion/css';
|
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 { Button, Dropdown, Menu, Switch } from 'antd';
|
||||||
import classNames from 'classnames';
|
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 { Icon } from '../icon';
|
||||||
|
import { SchemaComponent, useActionContext } from '../schema-component';
|
||||||
import { useCompile, useDesignable } from '../schema-component/hooks';
|
import { useCompile, useDesignable } from '../schema-component/hooks';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
import {
|
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>;
|
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
||||||
if (!designable && props.designable !== true) {
|
if (!designable && props.designable !== true) {
|
||||||
return null;
|
return null;
|
||||||
@ -131,22 +150,7 @@ SchemaInitializer.Button = observer(
|
|||||||
{...dropdown}
|
{...dropdown}
|
||||||
overlay={menu}
|
overlay={menu}
|
||||||
>
|
>
|
||||||
{component ? (
|
{component ? component : buttonDom}
|
||||||
component
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type={'dashed'}
|
|
||||||
style={{
|
|
||||||
borderColor: '#f18b62',
|
|
||||||
color: '#f18b62',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...others}
|
|
||||||
icon={<Icon type={icon as string} />}
|
|
||||||
>
|
|
||||||
{compile(props.children || props.title)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</SchemaInitializerButtonContext.Provider>
|
</SchemaInitializerButtonContext.Provider>
|
||||||
);
|
);
|
||||||
@ -242,6 +246,102 @@ SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
|
|||||||
return component;
|
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) => {
|
SchemaInitializer.SwitchItem = (props) => {
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Item onClick={props.onClick}>
|
<SchemaInitializer.Item onClick={props.onClick}>
|
||||||
|
@ -4,9 +4,7 @@ import { SchemaComponent, useActionContext, useDesignable, useRecordIndex } from
|
|||||||
|
|
||||||
export const TabPaneInitializers = (props?: any) => {
|
export const TabPaneInitializers = (props?: any) => {
|
||||||
const { designable, insertBeforeEnd } = useDesignable();
|
const { designable, insertBeforeEnd } = useDesignable();
|
||||||
if (!designable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const useSubmitAction = () => {
|
const useSubmitAction = () => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
@ -118,6 +116,11 @@ export const TabPaneInitializers = (props?: any) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!designable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <SchemaComponent schema={schema} />;
|
return <SchemaComponent schema={schema} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1064,6 +1064,9 @@ export const createGridCardBlockSchema = (options) => {
|
|||||||
...others,
|
...others,
|
||||||
},
|
},
|
||||||
'x-component': 'BlockItem',
|
'x-component': 'BlockItem',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useGridCardBlockItemProps }}',
|
||||||
|
},
|
||||||
'x-designer': 'GridCard.Designer',
|
'x-designer': 'GridCard.Designer',
|
||||||
properties: {
|
properties: {
|
||||||
actionBar: {
|
actionBar: {
|
||||||
@ -1081,7 +1084,7 @@ export const createGridCardBlockSchema = (options) => {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'GridCard',
|
'x-component': 'GridCard',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
props: '{{ useGridCardBlockProps }}',
|
useProps: '{{ useGridCardBlockProps }}',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
item: {
|
item: {
|
||||||
|
@ -56,7 +56,7 @@ export const GeneralSchemaItems: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SchemaSettings.SwitchItem
|
<SchemaSettings.SwitchItem
|
||||||
checked={field.decoratorProps.showTitle ?? true}
|
checked={fieldSchema['x-decorator-props']?.['showTitle'] ?? true}
|
||||||
title={t('Display title')}
|
title={t('Display title')}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||||
|
@ -31,6 +31,11 @@ const titleCss = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const overrideAntdCSS = css`
|
||||||
|
& .ant-space-item .anticon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const GeneralSchemaDesigner = (props: any) => {
|
export const GeneralSchemaDesigner = (props: any) => {
|
||||||
const { disableInitializer, title, template, draggable = true } = props;
|
const { disableInitializer, title, template, draggable = true } = props;
|
||||||
const { dn, designable } = useDesignable();
|
const { dn, designable } = useDesignable();
|
||||||
@ -62,7 +67,7 @@ export const GeneralSchemaDesigner = (props: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'general-schema-designer'}>
|
<div className={classNames('general-schema-designer', overrideAntdCSS)}>
|
||||||
{title && (
|
{title && (
|
||||||
<div className={classNames('general-schema-designer-title', titleCss)}>
|
<div className={classNames('general-schema-designer-title', titleCss)}>
|
||||||
<Space size={2}>
|
<Space size={2}>
|
||||||
|
@ -148,7 +148,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
|||||||
};
|
};
|
||||||
|
|
||||||
SchemaSettings.Template = function Template(props) {
|
SchemaSettings.Template = function Template(props) {
|
||||||
const { componentName, collectionName, resourceName } = props;
|
const { componentName, collectionName, resourceName, needRender } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getCollection } = useCollectionManager();
|
const { getCollection } = useCollectionManager();
|
||||||
const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
|
const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
|
||||||
@ -156,7 +156,7 @@ SchemaSettings.Template = function Template(props) {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { dn: tdn } = useBlockTemplateContext();
|
const { dn: tdn } = useBlockTemplateContext();
|
||||||
const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
|
const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
|
||||||
if (!collectionName) {
|
if (!collectionName && !needRender) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (template) {
|
if (template) {
|
||||||
@ -182,7 +182,7 @@ SchemaSettings.Template = function Template(props) {
|
|||||||
<SchemaSettings.Item
|
<SchemaSettings.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
const { title } = getCollection(collectionName);
|
const { title } = collectionName ? getCollection(collectionName) : { title: '' };
|
||||||
const values = await FormDialog(t('Save as template'), () => {
|
const values = await FormDialog(t('Save as template'), () => {
|
||||||
return (
|
return (
|
||||||
<FormLayout layout={'vertical'}>
|
<FormLayout layout={'vertical'}>
|
||||||
@ -194,7 +194,7 @@ SchemaSettings.Template = function Template(props) {
|
|||||||
name: {
|
name: {
|
||||||
title: t('Template name'),
|
title: t('Template name'),
|
||||||
required: true,
|
required: true,
|
||||||
default: `${compile(title)}_${t(componentName)}`,
|
default: title ? `${compile(title)}_${t(componentName)}` : t(componentName),
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
},
|
},
|
||||||
|
@ -123,6 +123,10 @@ export const useSchemaTemplateManager = () => {
|
|||||||
const items = templates?.filter?.((template) => template.collectionName === collectionName);
|
const items = templates?.filter?.((template) => template.collectionName === collectionName);
|
||||||
return items || [];
|
return items || [];
|
||||||
},
|
},
|
||||||
|
getTemplatesByComponentName(componentName: string): Array<any> {
|
||||||
|
const items = templates?.filter?.((template) => template.componentName === componentName);
|
||||||
|
return items || [];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { uid } from '@formily/shared';
|
|||||||
import { Menu } from 'antd';
|
import { Menu } from 'antd';
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActionContext, SchemaComponent, useActionContext } from '../';
|
import { ActionContextProvider, SchemaComponent, useActionContext } from '../';
|
||||||
import { useAPIClient } from '../api-client';
|
import { useAPIClient } from '../api-client';
|
||||||
import { DropdownVisibleContext } from './CurrentUser';
|
import { DropdownVisibleContext } from './CurrentUser';
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export const ChangePassword = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const ctx = useContext(DropdownVisibleContext);
|
const ctx = useContext(DropdownVisibleContext);
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="password"
|
key="password"
|
||||||
eventKey={'ChangePassword'}
|
eventKey={'ChangePassword'}
|
||||||
@ -131,6 +131,6 @@ export const ChangePassword = () => {
|
|||||||
{t('Change password')}
|
{t('Change password')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||||
</ActionContext.Provider>
|
</ActionContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,6 @@ import { EditProfile } from './EditProfile';
|
|||||||
import { LanguageSettings } from './LanguageSettings';
|
import { LanguageSettings } from './LanguageSettings';
|
||||||
import { SwitchRole } from './SwitchRole';
|
import { SwitchRole } from './SwitchRole';
|
||||||
import { ThemeSettings } from './ThemeSettings';
|
import { ThemeSettings } from './ThemeSettings';
|
||||||
|
|
||||||
const ApplicationVersion = () => {
|
const ApplicationVersion = () => {
|
||||||
const data = useCurrentAppInfo();
|
const data = useCurrentAppInfo();
|
||||||
return (
|
return (
|
||||||
@ -20,16 +19,18 @@ const ApplicationVersion = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownVisibleContext = createContext(null);
|
/**
|
||||||
|
* @note If you want to change here, Note the Setting block on the mobile side
|
||||||
export const CurrentUser = () => {
|
*/
|
||||||
|
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 history = useHistory();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { t } = useTranslation();
|
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 silenceApi = useAPIClient();
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
@ -52,6 +53,69 @@ export const CurrentUser = () => {
|
|||||||
}, 3000);
|
}, 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 (
|
return (
|
||||||
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
|
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
|
||||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||||
@ -60,64 +124,7 @@ export const CurrentUser = () => {
|
|||||||
onOpenChange={(visible) => {
|
onOpenChange={(visible) => {
|
||||||
setVisible(visible);
|
setVisible(visible);
|
||||||
}}
|
}}
|
||||||
overlay={
|
overlay={<SettingsMenu />}
|
||||||
<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>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={css`
|
className={css`
|
||||||
|
@ -4,7 +4,7 @@ import { Menu } from 'antd';
|
|||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ActionContext,
|
ActionContextProvider,
|
||||||
DropdownVisibleContext,
|
DropdownVisibleContext,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
@ -119,18 +119,18 @@ export const EditProfile = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const ctx = useContext(DropdownVisibleContext);
|
const ctx = useContext(DropdownVisibleContext);
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="profile"
|
key="profile"
|
||||||
eventKey={'EditProfile'}
|
eventKey={'EditProfile'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
ctx.setVisible(false);
|
ctx?.setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Edit profile')}
|
{t('Edit profile')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||||
</ActionContext.Provider>
|
</ActionContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -54,7 +54,7 @@ export function useRedirect(next = '/admin') {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const redirect = location?.['query']?.redirect;
|
const redirect = location?.['query']?.redirect;
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
history.push(redirect || '/admin');
|
history.replace(redirect || '/admin');
|
||||||
}, [redirect]);
|
}, [redirect]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ export const SwitchRole = () => {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const roles = useCurrentRoles();
|
const roles = useCurrentRoles();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
|
||||||
if (roles.length <= 1) {
|
if (roles.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -57,7 +56,7 @@ export const SwitchRole = () => {
|
|||||||
onChange={async (roleName) => {
|
onChange={async (roleName) => {
|
||||||
api.auth.setRole(roleName);
|
api.auth.setRole(roleName);
|
||||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||||
history.push('/');
|
location.reload();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,7 @@ import { css } from '@emotion/css';
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useCollection } from '@nocobase/client';
|
import { useCollection } from '@nocobase/client';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
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 React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useMapConfiguration } from '../hooks';
|
import { useMapConfiguration } from '../hooks';
|
||||||
@ -102,7 +102,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
|||||||
const overlay = useRef<AMap.Polygon>();
|
const overlay = useRef<AMap.Polygon>();
|
||||||
const editor = useRef(null);
|
const editor = useRef(null);
|
||||||
const history = useHistory();
|
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>({
|
const [commonOptions] = useState<AMap.PolylineOptions & AMap.PolygonOptions>({
|
||||||
strokeWeight: 5,
|
strokeWeight: 5,
|
||||||
@ -316,7 +316,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
|||||||
plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
|
plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
|
||||||
})
|
})
|
||||||
.then((amap) => {
|
.then((amap) => {
|
||||||
requestIdleCallback(() => {
|
return requestIdleCallback(() => {
|
||||||
map.current = new amap.Map(id.current, {
|
map.current = new amap.Map(id.current, {
|
||||||
resizeEnable: true,
|
resizeEnable: true,
|
||||||
zoom,
|
zoom,
|
||||||
@ -327,10 +327,14 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.includes('多个不一致的 key')) {
|
if (typeof err === 'string') {
|
||||||
setErrMessage(t('The AccessKey is incorrect, please check it'));
|
if (err.includes('多个不一致的 key')) {
|
||||||
} else {
|
setErrMessage(t('The AccessKey is incorrect, please check it'));
|
||||||
setErrMessage(err);
|
} 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}
|
id={id.current}
|
||||||
style={props?.style}
|
style={props?.style}
|
||||||
>
|
>
|
||||||
|
{!aMap.current && (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!disabled ? (
|
{!disabled ? (
|
||||||
<>
|
<>
|
||||||
<Search toCenter={toCenter} aMap={aMap.current} />
|
<Search toCenter={toCenter} aMap={aMap.current} />
|
||||||
|
4
packages/plugins/mobile-client/client.d.ts
vendored
Executable file
4
packages/plugins/mobile-client/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/client';
|
||||||
|
export { default } from './lib/client';
|
||||||
|
|
30
packages/plugins/mobile-client/client.js
Executable file
30
packages/plugins/mobile-client/client.js
Executable 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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
7
packages/plugins/mobile-client/docs/en-US/index.md
Normal file
7
packages/plugins/mobile-client/docs/en-US/index.md
Normal 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.
|
13
packages/plugins/mobile-client/docs/en-US/installation.md
Normal file
13
packages/plugins/mobile-client/docs/en-US/installation.md
Normal 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
|
||||||
|
```
|
14
packages/plugins/mobile-client/docs/en-US/tabs.json
Normal file
14
packages/plugins/mobile-client/docs/en-US/tabs.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Introduction",
|
||||||
|
"path": "index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Installation",
|
||||||
|
"path": "installation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Usage",
|
||||||
|
"path": "usage"
|
||||||
|
}
|
||||||
|
]
|
1
packages/plugins/mobile-client/docs/en-US/usage.md
Normal file
1
packages/plugins/mobile-client/docs/en-US/usage.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Mobile-client Usage
|
7
packages/plugins/mobile-client/docs/zh-CN/index.md
Normal file
7
packages/plugins/mobile-client/docs/zh-CN/index.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 移动端
|
||||||
|
|
||||||
|
提供移动端应用,独立于桌面端。
|
||||||
|
|
||||||
|
## 如何访问?
|
||||||
|
|
||||||
|
当你的桌面端启动后,可以在浏览器中输入 `http://localhost:3000/mobile` 访问移动端。注意,`/mobile` 就是我们移动端的路由。
|
13
packages/plugins/mobile-client/docs/zh-CN/installation.md
Normal file
13
packages/plugins/mobile-client/docs/zh-CN/installation.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 移动端安装方法
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @nocobase/plugin-mobile-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## 激活插件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn pm enable mobile-client
|
||||||
|
```
|
14
packages/plugins/mobile-client/docs/zh-CN/tabs.json
Normal file
14
packages/plugins/mobile-client/docs/zh-CN/tabs.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "介绍",
|
||||||
|
"path": "index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "安装",
|
||||||
|
"path": "installation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "用法",
|
||||||
|
"path": "usage"
|
||||||
|
}
|
||||||
|
]
|
2
packages/plugins/mobile-client/docs/zh-CN/usage.md
Normal file
2
packages/plugins/mobile-client/docs/zh-CN/usage.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 移动端用法
|
||||||
|
|
27
packages/plugins/mobile-client/package.json
Normal file
27
packages/plugins/mobile-client/package.json
Normal 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
4
packages/plugins/mobile-client/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/server';
|
||||||
|
export { default } from './lib/server';
|
||||||
|
|
30
packages/plugins/mobile-client/server.js
Executable file
30
packages/plugins/mobile-client/server.js
Executable 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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const AppConfiguration = () => {
|
||||||
|
return <>App Configuration</>;
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { InterfaceRouter } from '../router';
|
||||||
|
import { MobileDevice } from '../devices';
|
||||||
|
|
||||||
|
export const InterfaceConfiguration = () => {
|
||||||
|
return (
|
||||||
|
<MobileDevice>
|
||||||
|
<InterfaceRouter></InterfaceRouter>
|
||||||
|
</MobileDevice>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './App';
|
||||||
|
export * from './Interface';
|
@ -0,0 +1 @@
|
|||||||
|
import './native-call';
|
@ -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);
|
||||||
|
};
|
@ -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];
|
||||||
|
});
|
45
packages/plugins/mobile-client/src/client/core/index.tsx
Normal file
45
packages/plugins/mobile-client/src/client/core/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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';
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Container';
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Header';
|
@ -0,0 +1,6 @@
|
|||||||
|
export * from './menu';
|
||||||
|
export * from './container';
|
||||||
|
export * from './tab-bar';
|
||||||
|
export * from './page';
|
||||||
|
export * from './header';
|
||||||
|
export * from './settings';
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
@ -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;
|
@ -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': {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './MenuBlockInitializer';
|
||||||
|
export * from './Menu';
|
@ -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': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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';
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Page';
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
@ -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': {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Settings';
|
||||||
|
export * from './SettingsBlockInitializer';
|
@ -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;
|
@ -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';
|
@ -0,0 +1 @@
|
|||||||
|
export * from './TabBar';
|
@ -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': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './useSchemaPatch';
|
@ -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 };
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './initializers';
|
||||||
|
export * from './components';
|
||||||
|
export * from './scopes';
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './BlockInitializers';
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './grid-card';
|
24
packages/plugins/mobile-client/src/client/devices/iOS6.tsx
Normal file
24
packages/plugins/mobile-client/src/client/devices/iOS6.tsx
Normal 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;
|
27
packages/plugins/mobile-client/src/client/devices/index.tsx
Normal file
27
packages/plugins/mobile-client/src/client/devices/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
33
packages/plugins/mobile-client/src/client/index.tsx
Normal file
33
packages/plugins/mobile-client/src/client/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
const locale = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default locale;
|
23
packages/plugins/mobile-client/src/client/locale/index.ts
Normal file
23
packages/plugins/mobile-client/src/client/locale/index.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
31
packages/plugins/mobile-client/src/client/locale/zh-CN.ts
Normal file
31
packages/plugins/mobile-client/src/client/locale/zh-CN.ts
Normal 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
Loading…
Reference in New Issue
Block a user