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>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8" />
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, shrink-to-fit=no">
 | 
			
		||||
  <title>Loading...</title>
 | 
			
		||||
  <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
 | 
			
		||||
  <link rel="manifest" href="/favicon/site.webmanifest">
 | 
			
		||||
  <style>
 | 
			
		||||
  #root {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  /* width */
 | 
			
		||||
  .win ::-webkit-scrollbar {
 | 
			
		||||
    width: 8px;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 routes = useRoutes();
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
      <RouteSwitch routes={routes} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  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 { sortBy } from 'lodash';
 | 
			
		||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
 | 
			
		||||
@ -262,8 +262,21 @@ const SettingsCenter = (props) => {
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
    <div
 | 
			
		||||
      className={cx(
 | 
			
		||||
        'nb-setting-center',
 | 
			
		||||
        css`
 | 
			
		||||
          &.nb-setting-center {
 | 
			
		||||
            flex: 1;
 | 
			
		||||
          }
 | 
			
		||||
        `,
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      <Layout
 | 
			
		||||
        className={css`
 | 
			
		||||
          height: 100%;
 | 
			
		||||
        `}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          style={
 | 
			
		||||
            {
 | 
			
		||||
@ -302,7 +315,12 @@ const SettingsCenter = (props) => {
 | 
			
		||||
            items={menuItems as any}
 | 
			
		||||
          />
 | 
			
		||||
        </Layout.Sider>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
        <Layout.Content
 | 
			
		||||
          className={css`
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
          `}
 | 
			
		||||
        >
 | 
			
		||||
          {aclPluginTabCheck && (
 | 
			
		||||
            <PageHeader
 | 
			
		||||
              ghost={false}
 | 
			
		||||
@ -321,7 +339,7 @@ const SettingsCenter = (props) => {
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          <div className={'m24'} style={{ margin: 24 }}>
 | 
			
		||||
          <div className={'m24'} style={{ margin: 24, flex: 1 }}>
 | 
			
		||||
            {aclPluginTabCheck ? (
 | 
			
		||||
              component && React.createElement(component)
 | 
			
		||||
            ) : (
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,13 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Redirect, Route, Switch } from 'react-router-dom';
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom';
 | 
			
		||||
import { RouteContext } from './context';
 | 
			
		||||
import { useRouteComponent } from './hooks';
 | 
			
		||||
import { useRoute, useRouteComponent } from './hooks';
 | 
			
		||||
import { RouteSwitchProps } from './types';
 | 
			
		||||
 | 
			
		||||
export function RouteSwitch(props: RouteSwitchProps) {
 | 
			
		||||
  const { routes = [] } = props;
 | 
			
		||||
  const { url: base, path } = useRouteMatch();
 | 
			
		||||
 | 
			
		||||
  if (!routes.length) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
@ -28,21 +30,20 @@ export function RouteSwitch(props: RouteSwitchProps) {
 | 
			
		||||
        if (!route.path && Array.isArray(route.routes)) {
 | 
			
		||||
          route.path = route.routes.map((r) => r.path) as any;
 | 
			
		||||
        }
 | 
			
		||||
        let nextPath = route.path;
 | 
			
		||||
        if (route.path && typeof route.path === 'string' && !route.path.startsWith('/')) {
 | 
			
		||||
          nextPath = `${base.endsWith('/') ? base : base + '/'}${route.path}`;
 | 
			
		||||
        }
 | 
			
		||||
        return (
 | 
			
		||||
          <Route
 | 
			
		||||
            key={index}
 | 
			
		||||
            path={route.path}
 | 
			
		||||
            key={[nextPath].flat().join(',') as string}
 | 
			
		||||
            path={nextPath}
 | 
			
		||||
            exact={route.exact}
 | 
			
		||||
            strict={route.strict}
 | 
			
		||||
            sensitive={route.sensitive}
 | 
			
		||||
            render={(props) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <RouteContext.Provider value={route}>
 | 
			
		||||
                  <ComponentRenderer {...props} route={route} />
 | 
			
		||||
                </RouteContext.Provider>
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          >
 | 
			
		||||
            <ComponentRenderer route={route} />
 | 
			
		||||
          </Route>
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </Switch>
 | 
			
		||||
@ -50,10 +51,16 @@ export function RouteSwitch(props: RouteSwitchProps) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ComponentRenderer(props) {
 | 
			
		||||
  const Component = useRouteComponent(props?.route?.component);
 | 
			
		||||
  const { route } = props;
 | 
			
		||||
  const Component = useRouteComponent(route?.component);
 | 
			
		||||
  if (React.isValidElement(Component)) {
 | 
			
		||||
    return React.cloneElement(Component, props, <RouteSwitch routes={route.routes} />);
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <RouteContext.Provider value={route}>
 | 
			
		||||
      <Component {...props}>
 | 
			
		||||
      <RouteSwitch routes={props.route.routes} />
 | 
			
		||||
        <RouteSwitch routes={route?.routes} />
 | 
			
		||||
      </Component>
 | 
			
		||||
    </RouteContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -286,19 +286,25 @@ export const InternalAdminLayout = (props: any) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const AdminLayout = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <CurrentAppInfoProvider>
 | 
			
		||||
export const AdminProvider = (props) => {
 | 
			
		||||
  return <CurrentAppInfoProvider>
 | 
			
		||||
    <CurrentUserProvider>
 | 
			
		||||
      <RemoteSchemaTemplateManagerProvider>
 | 
			
		||||
        <RemoteCollectionManagerProvider>
 | 
			
		||||
          <ACLRolesCheckProvider>
 | 
			
		||||
              <InternalAdminLayout {...props} />
 | 
			
		||||
            {props.children}
 | 
			
		||||
          </ACLRolesCheckProvider>
 | 
			
		||||
        </RemoteCollectionManagerProvider>
 | 
			
		||||
      </RemoteSchemaTemplateManagerProvider>
 | 
			
		||||
    </CurrentUserProvider>
 | 
			
		||||
  </CurrentAppInfoProvider>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AdminLayout = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <AdminProvider>
 | 
			
		||||
      <InternalAdminLayout {...props} />
 | 
			
		||||
    </AdminProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { createContext, useContext } from 'react';
 | 
			
		||||
import { RouteProps } from './types';
 | 
			
		||||
 | 
			
		||||
export const RouteSwitchContext = createContext({
 | 
			
		||||
@ -7,3 +7,5 @@ export const RouteSwitchContext = createContext({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const RouteContext = createContext<RouteProps>(null);
 | 
			
		||||
 | 
			
		||||
export const useRouteSwitchContext = () => useContext(RouteSwitchContext);
 | 
			
		||||
 | 
			
		||||
@ -31,4 +31,5 @@ export type RouteRedirectProps = RedirectProps | RouteProps;
 | 
			
		||||
export interface RouteSwitchProps {
 | 
			
		||||
  routes?: RouteRedirectProps[];
 | 
			
		||||
  components?: any;
 | 
			
		||||
  base?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,10 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
 | 
			
		||||
import { Drawer } from 'antd';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { createPortal } from 'react-dom';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { OpenSize } from './';
 | 
			
		||||
import { useActionContext } from './hooks';
 | 
			
		||||
import { ComposedActionDrawer } from './types';
 | 
			
		||||
import { OpenSize } from './';
 | 
			
		||||
 | 
			
		||||
const openSizeWidthMap = new Map<OpenSize, string>([
 | 
			
		||||
  ['small', '30%'],
 | 
			
		||||
@ -18,7 +17,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
 | 
			
		||||
  (props) => {
 | 
			
		||||
    const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { visible, setVisible, openSize = 'middle' } = useActionContext();
 | 
			
		||||
    const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
 | 
			
		||||
    const schema = useFieldSchema();
 | 
			
		||||
    const field = useField();
 | 
			
		||||
    const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
 | 
			
		||||
@ -29,22 +28,22 @@ export const ActionDrawer: ComposedActionDrawer = observer(
 | 
			
		||||
      }
 | 
			
		||||
      return buf;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {createPortal(
 | 
			
		||||
          <div
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              e.stopPropagation();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
      <Drawer
 | 
			
		||||
              width={openSizeWidthMap.get(finalOpenSize)}
 | 
			
		||||
        width={openSizeWidthMap.get(openSize)}
 | 
			
		||||
        title={field.title}
 | 
			
		||||
        {...others}
 | 
			
		||||
        {...drawerProps}
 | 
			
		||||
        style={{
 | 
			
		||||
          ...drawerProps?.style,
 | 
			
		||||
          ...others?.style,
 | 
			
		||||
        }}
 | 
			
		||||
        destroyOnClose
 | 
			
		||||
        open={visible}
 | 
			
		||||
        onClose={() => setVisible(false, true)}
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          drawerProps?.className,
 | 
			
		||||
          others.className,
 | 
			
		||||
          css`
 | 
			
		||||
            &.nb-action-popup {
 | 
			
		||||
@ -109,10 +108,6 @@ export const ActionDrawer: ComposedActionDrawer = observer(
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </Drawer>
 | 
			
		||||
          </div>,
 | 
			
		||||
          document.body,
 | 
			
		||||
        )}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'ActionDrawer' },
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
 | 
			
		||||
import { Modal, ModalProps } from 'antd';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { createPortal } from 'react-dom';
 | 
			
		||||
import { OpenSize, useActionContext } from '.';
 | 
			
		||||
import { ComposedActionDrawer } from './types';
 | 
			
		||||
 | 
			
		||||
@ -15,7 +14,7 @@ const openSizeWidthMap = new Map<OpenSize, string>([
 | 
			
		||||
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
 | 
			
		||||
  (props) => {
 | 
			
		||||
    const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
 | 
			
		||||
    const { visible, setVisible, openSize = 'large' } = useActionContext();
 | 
			
		||||
    const { visible, setVisible, openSize = 'large', modalProps } = useActionContext();
 | 
			
		||||
    const actualWidth = width ?? openSizeWidthMap.get(openSize);
 | 
			
		||||
    const schema = useFieldSchema();
 | 
			
		||||
    const field = useField();
 | 
			
		||||
@ -26,22 +25,21 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
 | 
			
		||||
      return buf;
 | 
			
		||||
    });
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {createPortal(
 | 
			
		||||
          <div
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
              e.stopPropagation();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
      <Modal
 | 
			
		||||
        width={actualWidth}
 | 
			
		||||
        title={field.title}
 | 
			
		||||
        {...(others as ModalProps)}
 | 
			
		||||
        {...modalProps}
 | 
			
		||||
        style={{
 | 
			
		||||
          ...modalProps?.style,
 | 
			
		||||
          ...others?.style,
 | 
			
		||||
        }}
 | 
			
		||||
        destroyOnClose
 | 
			
		||||
              visible={visible}
 | 
			
		||||
        open={visible}
 | 
			
		||||
        onCancel={() => setVisible(false, true)}
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          others.className,
 | 
			
		||||
          modalProps?.className,
 | 
			
		||||
          css`
 | 
			
		||||
            &.nb-action-popup {
 | 
			
		||||
              .ant-modal-header {
 | 
			
		||||
@ -100,10 +98,6 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </Modal>
 | 
			
		||||
          </div>,
 | 
			
		||||
          document.body,
 | 
			
		||||
        )}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'ActionModal' },
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
 | 
			
		||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
 | 
			
		||||
import { Button, Modal, Popover } from 'antd';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import React, { useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useActionContext } from '../..';
 | 
			
		||||
import { useDesignable } from '../../';
 | 
			
		||||
import { Icon } from '../../../icon';
 | 
			
		||||
@ -16,7 +16,7 @@ import { ActionDrawer } from './Action.Drawer';
 | 
			
		||||
import { ActionLink } from './Action.Link';
 | 
			
		||||
import { ActionModal } from './Action.Modal';
 | 
			
		||||
import { ActionPage } from './Action.Page';
 | 
			
		||||
import { ActionContext } from './context';
 | 
			
		||||
import { ActionContextProvider } from './context';
 | 
			
		||||
import { useA } from './hooks';
 | 
			
		||||
import { ComposedAction } from './types';
 | 
			
		||||
import { linkageAction } from './utils';
 | 
			
		||||
@ -95,16 +95,6 @@ export const Action: ComposedAction = observer(
 | 
			
		||||
    const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
 | 
			
		||||
    const { designable } = useDesignable();
 | 
			
		||||
    const tarComponent = useComponent(component) || component;
 | 
			
		||||
    const renderTitle = useMemo(() => {
 | 
			
		||||
      if (title) {
 | 
			
		||||
        return title;
 | 
			
		||||
      }
 | 
			
		||||
      if (fieldSchema.title) {
 | 
			
		||||
        return compile(fieldSchema.title);
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }, [compile, fieldSchema.title, title]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      field.linkageProperty = {};
 | 
			
		||||
      linkageRules
 | 
			
		||||
@ -127,6 +117,7 @@ export const Action: ComposedAction = observer(
 | 
			
		||||
          icon={<Icon type={icon} />}
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
          style={{
 | 
			
		||||
            ...others.style,
 | 
			
		||||
            opacity: designable && field?.data?.hidden && 0.1,
 | 
			
		||||
          }}
 | 
			
		||||
          onClick={(e: React.MouseEvent) => {
 | 
			
		||||
@ -151,30 +142,28 @@ export const Action: ComposedAction = observer(
 | 
			
		||||
          component={tarComponent || Button}
 | 
			
		||||
          className={classnames(className, actionDesignerCss)}
 | 
			
		||||
        >
 | 
			
		||||
          {renderTitle}
 | 
			
		||||
          {title || compile(fieldSchema.title)}
 | 
			
		||||
          <Designer {...designerProps} />
 | 
			
		||||
        </SortableItem>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <ActionContext.Provider
 | 
			
		||||
        value={{
 | 
			
		||||
          button: renderButton(),
 | 
			
		||||
          visible,
 | 
			
		||||
          setVisible,
 | 
			
		||||
          formValueChanged,
 | 
			
		||||
          setFormValueChanged,
 | 
			
		||||
          openMode,
 | 
			
		||||
          openSize,
 | 
			
		||||
          containerRefKey,
 | 
			
		||||
          fieldSchema,
 | 
			
		||||
        }}
 | 
			
		||||
      <ActionContextProvider
 | 
			
		||||
        button={renderButton()}
 | 
			
		||||
        visible={visible}
 | 
			
		||||
        setVisible={setVisible}
 | 
			
		||||
        formValueChanged={formValueChanged}
 | 
			
		||||
        setFormValueChanged={setFormValueChanged}
 | 
			
		||||
        openMode={openMode}
 | 
			
		||||
        openSize={openSize}
 | 
			
		||||
        containerRefKey={containerRefKey}
 | 
			
		||||
        fieldSchema={fieldSchema}
 | 
			
		||||
      >
 | 
			
		||||
        {popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
 | 
			
		||||
        {!popover && renderButton()}
 | 
			
		||||
        {!popover && props.children}
 | 
			
		||||
      </ActionContext.Provider>
 | 
			
		||||
      </ActionContextProvider>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'Action' },
 | 
			
		||||
 | 
			
		||||
@ -1,27 +1,70 @@
 | 
			
		||||
import { css, cx } from '@emotion/css';
 | 
			
		||||
import { cx } from '@emotion/css';
 | 
			
		||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Space } from 'antd';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, { CSSProperties, useContext } from 'react';
 | 
			
		||||
import { createPortal } from 'react-dom';
 | 
			
		||||
import { useSchemaInitializer } from '../../../schema-initializer';
 | 
			
		||||
import { DndContext } from '../../common';
 | 
			
		||||
import { useDesignable, useProps } from '../../hooks';
 | 
			
		||||
 | 
			
		||||
interface ActionBarContextForceProps {
 | 
			
		||||
  layout?: 'one-column' | 'tow-columns';
 | 
			
		||||
  style?: CSSProperties;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ActionBarContextValue {
 | 
			
		||||
  container?: Element | DocumentFragment;
 | 
			
		||||
  /**
 | 
			
		||||
   * override props
 | 
			
		||||
   */
 | 
			
		||||
  forceProps?: ActionBarContextForceProps;
 | 
			
		||||
  parentComponents?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ActionBarContext = React.createContext<ActionBarContextValue>({
 | 
			
		||||
  container: null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const ActionBarProvider: React.FC<ActionBarContextValue> = ({ children, ...props }) => {
 | 
			
		||||
  return <ActionBarContext.Provider value={props}>{children}</ActionBarContext.Provider>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useActionBarContext = () => {
 | 
			
		||||
  return useContext(ActionBarContext);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Portal: React.FC = (props) => {
 | 
			
		||||
  const filedSchema = useFieldSchema();
 | 
			
		||||
  const { container, parentComponents = ['BlockItem', 'CardItem'] } = useActionBarContext();
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {container && parentComponents.includes(filedSchema.parent['x-component'])
 | 
			
		||||
        ? createPortal(props.children, container)
 | 
			
		||||
        : props.children}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ActionBar = observer(
 | 
			
		||||
  (props: any) => {
 | 
			
		||||
    const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
 | 
			
		||||
    const { forceProps = {} } = useActionBarContext();
 | 
			
		||||
    const { layout = 'tow-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
    const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
 | 
			
		||||
    const { designable } = useDesignable();
 | 
			
		||||
 | 
			
		||||
    if (layout === 'one-column') {
 | 
			
		||||
      return (
 | 
			
		||||
        <Portal>
 | 
			
		||||
          <DndContext>
 | 
			
		||||
            <div
 | 
			
		||||
            style={{ display: 'flex', alignItems: 'center', ...style }}
 | 
			
		||||
              style={{ display: 'flex', alignItems: 'center', gap: 8, ...style }}
 | 
			
		||||
              {...others}
 | 
			
		||||
              className={cx(others.className, 'nb-action-bar')}
 | 
			
		||||
            >
 | 
			
		||||
              {props.children && (
 | 
			
		||||
              <div style={{ marginRight: 8 }}>
 | 
			
		||||
                <div>
 | 
			
		||||
                  <Space {...spaceProps}>
 | 
			
		||||
                    {fieldSchema.mapProperties((schema, key) => {
 | 
			
		||||
                      return <RecursionField key={key} name={key} schema={schema} />;
 | 
			
		||||
@ -29,9 +72,10 @@ export const ActionBar = observer(
 | 
			
		||||
                  </Space>
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
            <InitializerComponent />
 | 
			
		||||
              <InitializerComponent style={{ margin: '0 !important' }} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </DndContext>
 | 
			
		||||
        </Portal>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
 | 
			
		||||
@ -52,14 +96,7 @@ export const ActionBar = observer(
 | 
			
		||||
        {...others}
 | 
			
		||||
        className={cx(others.className, 'nb-action-bar')}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          className={css`
 | 
			
		||||
            .ant-space:last-child {
 | 
			
		||||
              margin-left: 8px;
 | 
			
		||||
            }
 | 
			
		||||
          `}
 | 
			
		||||
          style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
 | 
			
		||||
        >
 | 
			
		||||
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
 | 
			
		||||
          <DndContext>
 | 
			
		||||
            <Space {...spaceProps}>
 | 
			
		||||
              {fieldSchema.mapProperties((schema, key) => {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,19 @@
 | 
			
		||||
import { Schema } from '@formily/react';
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { DrawerProps, ModalProps } from 'antd';
 | 
			
		||||
import React, { createContext } from 'react';
 | 
			
		||||
import { useActionContext } from './hooks';
 | 
			
		||||
 | 
			
		||||
export const ActionContext = createContext<ActionContextProps>({});
 | 
			
		||||
 | 
			
		||||
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
 | 
			
		||||
  const contextProps = useActionContext();
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </ActionContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type OpenSize = 'small' | 'middle' | 'large';
 | 
			
		||||
export interface ActionContextProps {
 | 
			
		||||
  button?: any;
 | 
			
		||||
@ -15,4 +26,6 @@ export interface ActionContextProps {
 | 
			
		||||
  formValueChanged?: boolean;
 | 
			
		||||
  setFormValueChanged?: (v: boolean) => void;
 | 
			
		||||
  fieldSchema?: Schema;
 | 
			
		||||
  drawerProps?: DrawerProps;
 | 
			
		||||
  modalProps?: ModalProps;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,15 +2,16 @@ import { css } from '@emotion/css';
 | 
			
		||||
import cls from 'classnames';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { SortableItem } from '../../common';
 | 
			
		||||
import { useDesigner } from '../../hooks';
 | 
			
		||||
import { useDesigner, useProps } from '../../hooks';
 | 
			
		||||
 | 
			
		||||
export const BlockItem: React.FC<any> = (props) => {
 | 
			
		||||
  const { className, children } = useProps(props);
 | 
			
		||||
  const Designer = useDesigner();
 | 
			
		||||
  return (
 | 
			
		||||
    <SortableItem
 | 
			
		||||
      className={cls(
 | 
			
		||||
        'nb-block-item',
 | 
			
		||||
        props.className,
 | 
			
		||||
        className,
 | 
			
		||||
        css`
 | 
			
		||||
          position: relative;
 | 
			
		||||
          &:hover {
 | 
			
		||||
@ -57,7 +58,7 @@ export const BlockItem: React.FC<any> = (props) => {
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      <Designer />
 | 
			
		||||
      {props.children}
 | 
			
		||||
      {children}
 | 
			
		||||
    </SortableItem>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ export const CardItem: React.FC = (props) => {
 | 
			
		||||
 | 
			
		||||
  return templateKey && !template ? null : (
 | 
			
		||||
    <BlockItem className={'noco-card-item'}>
 | 
			
		||||
      <Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
 | 
			
		||||
      <Card style={{ marginBottom: 'var(--nb-spacing)' }} bordered={false} {...restProps}>
 | 
			
		||||
        {props.children}
 | 
			
		||||
      </Card>
 | 
			
		||||
    </BlockItem>
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,6 @@ export const GridCardDesigner = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <GeneralSchemaDesigner template={template} title={title || name}>
 | 
			
		||||
      <SchemaComponentOptions components={{ Slider }}>
 | 
			
		||||
        <SchemaSettings.BlockTitleItem />
 | 
			
		||||
        <SchemaSettings.ModalItem
 | 
			
		||||
          title={t('Set the count of columns displayed in a row')}
 | 
			
		||||
          initialValues={columnCount}
 | 
			
		||||
@ -155,7 +154,7 @@ export const GridCardDesigner = () => {
 | 
			
		||||
                          field: {
 | 
			
		||||
                            type: 'string',
 | 
			
		||||
                            enum: sortFields,
 | 
			
		||||
                            required:true,
 | 
			
		||||
                            required: true,
 | 
			
		||||
                            'x-decorator': 'FormItem',
 | 
			
		||||
                            'x-component': 'Select',
 | 
			
		||||
                            'x-component-props': {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { List as AntdList, PaginationProps, Col } from 'antd';
 | 
			
		||||
import { useGridCardActionBarProps } from './hooks';
 | 
			
		||||
import { SortableItem } from '../../common';
 | 
			
		||||
import { SchemaComponentOptions } from '../../core';
 | 
			
		||||
import { useDesigner } from '../../hooks';
 | 
			
		||||
import { useDesigner, useProps } from '../../hooks';
 | 
			
		||||
import { GridCardItem } from './GridCard.Item';
 | 
			
		||||
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
 | 
			
		||||
import { GridCardDesigner } from './GridCard.Designer';
 | 
			
		||||
@ -55,7 +55,9 @@ const designerCss = css`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const InternalGridCard = (props) => {
 | 
			
		||||
  const { service, columnCount = defaultColumnCount } = useGridCardBlockContext();
 | 
			
		||||
  const { columnCount: columnCountProp, pagination } = useProps(props);
 | 
			
		||||
  const { service, _columnCount = defaultColumnCount } = useGridCardBlockContext();
 | 
			
		||||
  const columnCount = columnCountProp || _columnCount;
 | 
			
		||||
  const { run, params } = service;
 | 
			
		||||
  const meta = service?.data?.meta;
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
@ -70,7 +72,9 @@ const InternalGridCard = (props) => {
 | 
			
		||||
          new Schema({
 | 
			
		||||
            type: 'object',
 | 
			
		||||
            properties: {
 | 
			
		||||
              [key]: fieldSchema.properties['item'],
 | 
			
		||||
              [key]: {
 | 
			
		||||
                ...fieldSchema.properties['item'],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
@ -104,6 +108,7 @@ const InternalGridCard = (props) => {
 | 
			
		||||
            !meta || meta.count <= meta.pageSize
 | 
			
		||||
              ? false
 | 
			
		||||
              : {
 | 
			
		||||
                  ...pagination,
 | 
			
		||||
                  onChange: onPaginationChange,
 | 
			
		||||
                  total: meta?.count || 0,
 | 
			
		||||
                  pageSize: meta?.pageSize || 10,
 | 
			
		||||
 | 
			
		||||
@ -332,6 +332,7 @@ export const useGridRowContext = () => {
 | 
			
		||||
 | 
			
		||||
export const Grid: any = observer(
 | 
			
		||||
  (props: any) => {
 | 
			
		||||
    const { showDivider = true } = props;
 | 
			
		||||
    const gridRef = useRef(null);
 | 
			
		||||
    const field = useField();
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
@ -346,10 +347,11 @@ export const Grid: any = observer(
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <GridContext.Provider
 | 
			
		||||
        value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}
 | 
			
		||||
        value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent, showDivider }}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
 | 
			
		||||
          <DndWrapper dndContext={props.dndContext}>
 | 
			
		||||
            {showDivider ? (
 | 
			
		||||
              <RowDivider
 | 
			
		||||
                rows={rows}
 | 
			
		||||
                first
 | 
			
		||||
@ -361,10 +363,12 @@ export const Grid: any = observer(
 | 
			
		||||
                  schema: fieldSchema,
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : null}
 | 
			
		||||
            {rows.map((schema, index) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <React.Fragment key={index}>
 | 
			
		||||
                  <RecursionField name={schema.name} schema={schema} />
 | 
			
		||||
                  {showDivider ? (
 | 
			
		||||
                    <RowDivider
 | 
			
		||||
                      rows={rows}
 | 
			
		||||
                      index={index}
 | 
			
		||||
@ -376,6 +380,7 @@ export const Grid: any = observer(
 | 
			
		||||
                        schema,
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  ) : null}
 | 
			
		||||
                </React.Fragment>
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
@ -394,6 +399,7 @@ Grid.Row = observer(
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
    const addr = field.address.toString();
 | 
			
		||||
    const cols = useColProperties();
 | 
			
		||||
    const { showDivider } = useGridContext();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
 | 
			
		||||
@ -402,13 +408,16 @@ Grid.Row = observer(
 | 
			
		||||
            'nb-grid-row',
 | 
			
		||||
            css`
 | 
			
		||||
              overflow-x: hidden;
 | 
			
		||||
              margin: 0 calc(-1 * var(--nb-spacing));
 | 
			
		||||
              display: flex;
 | 
			
		||||
              position: relative;
 | 
			
		||||
              /* z-index: 0; */
 | 
			
		||||
            `,
 | 
			
		||||
          )}
 | 
			
		||||
          style={{
 | 
			
		||||
            margin: showDivider ? '0 calc(-1 * var(--nb-spacing))' : null,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {showDivider && (
 | 
			
		||||
            <ColDivider
 | 
			
		||||
              cols={cols}
 | 
			
		||||
              first
 | 
			
		||||
@ -420,10 +429,12 @@ Grid.Row = observer(
 | 
			
		||||
                schema: fieldSchema,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {cols.map((schema, index) => {
 | 
			
		||||
            return (
 | 
			
		||||
              <React.Fragment key={index}>
 | 
			
		||||
                <RecursionField name={schema.name} schema={schema} />
 | 
			
		||||
                {showDivider && (
 | 
			
		||||
                  <ColDivider
 | 
			
		||||
                    cols={cols}
 | 
			
		||||
                    index={index}
 | 
			
		||||
@ -436,6 +447,7 @@ Grid.Row = observer(
 | 
			
		||||
                      schema,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
              </React.Fragment>
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
@ -449,6 +461,7 @@ Grid.Row = observer(
 | 
			
		||||
Grid.Col = observer(
 | 
			
		||||
  (props: any) => {
 | 
			
		||||
    const { cols = [] } = useContext(GridRowContext);
 | 
			
		||||
    const { showDivider } = useGridContext();
 | 
			
		||||
    const schema = useFieldSchema();
 | 
			
		||||
    const field = useField();
 | 
			
		||||
 | 
			
		||||
@ -456,7 +469,7 @@ Grid.Col = observer(
 | 
			
		||||
      let width = '';
 | 
			
		||||
      if (cols?.length) {
 | 
			
		||||
        const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
 | 
			
		||||
        width = `calc(${w}% - var(--nb-spacing) *  ${(cols.length + 1) / cols.length})`;
 | 
			
		||||
        width = `calc(${w}% - var(--nb-spacing) *  ${(showDivider ? cols.length + 1 : 0) / cols.length})`;
 | 
			
		||||
      }
 | 
			
		||||
      return width;
 | 
			
		||||
    }, [cols?.length, schema?.['x-component-props']?.['width']]);
 | 
			
		||||
@ -477,5 +490,5 @@ Grid.Col = observer(
 | 
			
		||||
      </GridColContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'Grid.Col' },
 | 
			
		||||
  { displayName: 'Grid.Row' },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -21,12 +21,22 @@
 | 
			
		||||
  --nb-designer-offset: -10px;
 | 
			
		||||
}
 | 
			
		||||
.theme-compact {
 | 
			
		||||
  --nb-spacing: 16px;
 | 
			
		||||
  --nb-spacing: 12px;
 | 
			
		||||
  --nb-designer-offset: -6px;
 | 
			
		||||
  .ant-formily-item {
 | 
			
		||||
    margin-bottom: 16px;
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
  }
 | 
			
		||||
  .ant-card {
 | 
			
		||||
    margin-bottom: 16px !important;
 | 
			
		||||
    margin-bottom: 12px !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ant-formily-item-control-content-component .ant-tag {
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html body {
 | 
			
		||||
  --adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB,
 | 
			
		||||
  Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
 | 
			
		||||
  Segoe UI Symbol;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,11 +11,13 @@ const FixedBlockContext = React.createContext<{
 | 
			
		||||
  height: number | string;
 | 
			
		||||
  fixedBlockUID: boolean | string;
 | 
			
		||||
  fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
 | 
			
		||||
  inFixedBlock: boolean;
 | 
			
		||||
}>({
 | 
			
		||||
  setFixedBlock: () => {},
 | 
			
		||||
  height: 0,
 | 
			
		||||
  fixedBlockUID: false,
 | 
			
		||||
  fixedBlockUIDRef: { current: false },
 | 
			
		||||
  inFixedBlock: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const useFixedSchema = () => {
 | 
			
		||||
@ -69,8 +71,9 @@ export const FixedBlockDesignerItem = () => {
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const { dn } = useDesignable();
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
  const { inFixedBlock } = useFixedBlock();
 | 
			
		||||
 | 
			
		||||
  if (Object.keys(record).length) {
 | 
			
		||||
  if (Object.keys(record).length || !inFixedBlock) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
@ -127,7 +130,7 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
 | 
			
		||||
    _setFixedBlock(v);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <FixedBlockContext.Provider value={{ height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
 | 
			
		||||
    <FixedBlockContext.Provider value={{ inFixedBlock: true, height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={fixedBlockUID ? fixedBlockCss : ''}
 | 
			
		||||
        style={{
 | 
			
		||||
 | 
			
		||||
@ -245,7 +245,7 @@ export const Page = (props) => {
 | 
			
		||||
                          <Tabs.TabPane
 | 
			
		||||
                            tab={
 | 
			
		||||
                              <SortableItem
 | 
			
		||||
                                id={schema.name}
 | 
			
		||||
                                id={schema.name as string}
 | 
			
		||||
                                schema={schema}
 | 
			
		||||
                                className={classNames('nb-action-link', designerCss, props.className)}
 | 
			
		||||
                              >
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import {
 | 
			
		||||
import { CollectionProvider, useCollection } from '../../../collection-manager';
 | 
			
		||||
import { FormProvider, SchemaComponentOptions } from '../../core';
 | 
			
		||||
import { useCompile } from '../../hooks';
 | 
			
		||||
import { ActionContext, useActionContext } from '../action';
 | 
			
		||||
import { ActionContextProvider, useActionContext } from '../action';
 | 
			
		||||
import { FileSelector } from '../preview';
 | 
			
		||||
import { useFieldNames } from './useFieldNames';
 | 
			
		||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
 | 
			
		||||
@ -269,7 +269,7 @@ const Drawer: React.FunctionComponent<{
 | 
			
		||||
  return (
 | 
			
		||||
    <RecordPickerProvider {...recordPickerProps}>
 | 
			
		||||
      <CollectionProvider allowNull name={collectionField?.target}>
 | 
			
		||||
        <ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
 | 
			
		||||
        <ActionContextProvider openMode="drawer" visible={visible} setVisible={setVisible}>
 | 
			
		||||
          <FormProvider>
 | 
			
		||||
            <TableSelectorParamsProvider params={{ filter: getFilter() }}>
 | 
			
		||||
              <SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
 | 
			
		||||
@ -283,7 +283,7 @@ const Drawer: React.FunctionComponent<{
 | 
			
		||||
              </SchemaComponentOptions>
 | 
			
		||||
            </TableSelectorParamsProvider>
 | 
			
		||||
          </FormProvider>
 | 
			
		||||
        </ActionContext.Provider>
 | 
			
		||||
        </ActionContextProvider>
 | 
			
		||||
      </CollectionProvider>
 | 
			
		||||
    </RecordPickerProvider>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,12 @@
 | 
			
		||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { toArr } from '@formily/shared';
 | 
			
		||||
import React, { Fragment, useRef, useState } from 'react';
 | 
			
		||||
import { WithoutTableFieldResource } from '../../../block-provider';
 | 
			
		||||
import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
 | 
			
		||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
 | 
			
		||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
 | 
			
		||||
import { RecordProvider, useRecord } from '../../../record-provider';
 | 
			
		||||
import { FormProvider } from '../../core';
 | 
			
		||||
import { useCompile } from '../../hooks';
 | 
			
		||||
import { ActionContext, useActionContext } from '../action';
 | 
			
		||||
import { ActionContextProvider, useActionContext } from '../action';
 | 
			
		||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
 | 
			
		||||
import { Preview } from '../preview';
 | 
			
		||||
import { isShowFilePicker } from './InputRecordPicker';
 | 
			
		||||
@ -56,8 +55,7 @@ export const ReadPrettyRecordPicker: React.FC = observer(
 | 
			
		||||
        const text = getLabelFormatValue(labelUiSchema, val, true);
 | 
			
		||||
        return (
 | 
			
		||||
          <Fragment key={`${record.id}_${index}`}>
 | 
			
		||||
            {/* test-record-picker-read-pretty-item 用于在单元测试中方便选中元素 */}
 | 
			
		||||
            <span className="test-record-picker-read-pretty-item">
 | 
			
		||||
            <span>
 | 
			
		||||
              {snapshot || isTagsMode ? (
 | 
			
		||||
                text
 | 
			
		||||
              ) : (
 | 
			
		||||
@ -112,11 +110,11 @@ export const ReadPrettyRecordPicker: React.FC = observer(
 | 
			
		||||
            <EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
 | 
			
		||||
              {renderRecords()}
 | 
			
		||||
            </EllipsisWithTooltip>
 | 
			
		||||
            <ActionContext.Provider
 | 
			
		||||
            <ActionContextProvider
 | 
			
		||||
              value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
 | 
			
		||||
            >
 | 
			
		||||
              {renderRecordProvider()}
 | 
			
		||||
            </ActionContext.Provider>
 | 
			
		||||
            </ActionContextProvider>
 | 
			
		||||
          </CollectionProvider>
 | 
			
		||||
        </BlockAssociationContext.Provider>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import { TableSelector } from './TableSelector';
 | 
			
		||||
export * from './TableBlockDesigner';
 | 
			
		||||
export * from './TableField';
 | 
			
		||||
export * from './TableSelectorDesigner';
 | 
			
		||||
export * from './FilterDynamicComponent';
 | 
			
		||||
 | 
			
		||||
export const TableV2 = Table;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,15 +7,18 @@ import { Icon } from '../../../icon';
 | 
			
		||||
import { useSchemaInitializer } from '../../../schema-initializer';
 | 
			
		||||
import { DndContext, SortableItem } from '../../common';
 | 
			
		||||
import { useDesigner } from '../../hooks/useDesigner';
 | 
			
		||||
import { useTabsContext } from './context';
 | 
			
		||||
import { TabsDesigner } from './Tabs.Designer';
 | 
			
		||||
 | 
			
		||||
export const Tabs: any = observer(
 | 
			
		||||
  (props: TabsProps) => {
 | 
			
		||||
    const fieldSchema = useFieldSchema();
 | 
			
		||||
    const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
 | 
			
		||||
    const contextProps = useTabsContext();
 | 
			
		||||
    return (
 | 
			
		||||
      <DndContext>
 | 
			
		||||
        <AntdTabs
 | 
			
		||||
          {...contextProps}
 | 
			
		||||
          style={props.style}
 | 
			
		||||
          tabBarExtraContent={{
 | 
			
		||||
            right: render(),
 | 
			
		||||
@ -93,7 +96,7 @@ Tabs.TabPane = observer(
 | 
			
		||||
      </SortableItem>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { displayName: 'TabPane' },
 | 
			
		||||
  { displayName: 'Tabs.TabPane' },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
Tabs.Designer = TabsDesigner;
 | 
			
		||||
 | 
			
		||||
@ -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 './context';
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
 | 
			
		||||
import { observer, useField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import React, { createContext, useContext } from 'react';
 | 
			
		||||
import { cx } from '@emotion/css';
 | 
			
		||||
import { observer, Schema, useField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import React, { createContext, HTMLAttributes, useContext } from 'react';
 | 
			
		||||
 | 
			
		||||
export const DraggableContext = createContext(null);
 | 
			
		||||
export const SortableContext = createContext(null);
 | 
			
		||||
@ -19,18 +20,21 @@ export const SortableProvider = (props) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Sortable = (props: any) => {
 | 
			
		||||
  const { component, style, children, openMode, ...others } = props;
 | 
			
		||||
  const { droppable } = useContext(SortableContext);
 | 
			
		||||
  const { component, overStyle, style, children, openMode, ...others } = props;
 | 
			
		||||
  const { draggable, droppable } = useContext(SortableContext);
 | 
			
		||||
  const { isOver, setNodeRef } = droppable;
 | 
			
		||||
  const droppableStyle = { ...style };
 | 
			
		||||
 | 
			
		||||
  if (isOver) {
 | 
			
		||||
    droppableStyle['color'] = 'rgba(241, 139, 98, .1)';
 | 
			
		||||
  if (isOver && draggable?.active?.id !== droppable?.over?.id) {
 | 
			
		||||
    droppableStyle[component === 'a' ? 'color' : 'background'] = 'rgba(241, 139, 98, .15)';
 | 
			
		||||
    Object.assign(droppableStyle, overStyle);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return React.createElement(
 | 
			
		||||
    component || 'div',
 | 
			
		||||
    {
 | 
			
		||||
      ...others,
 | 
			
		||||
      className: cx('nb-sortable-designer', props.className),
 | 
			
		||||
      ref: setNodeRef,
 | 
			
		||||
      style: droppableStyle,
 | 
			
		||||
    },
 | 
			
		||||
@ -55,9 +59,15 @@ const useSortableItemId = (props) => {
 | 
			
		||||
  return field.address.toString();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const SortableItem: React.FC<any> = observer(
 | 
			
		||||
interface SortableItemProps extends HTMLAttributes<HTMLDivElement> {
 | 
			
		||||
  eid?: string;
 | 
			
		||||
  schema?: Schema;
 | 
			
		||||
  removeParentsIfNoChildren?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SortableItem: React.FC<SortableItemProps> = observer(
 | 
			
		||||
  (props) => {
 | 
			
		||||
    const { schema, id, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
 | 
			
		||||
    const { schema, id, eid, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
 | 
			
		||||
    return (
 | 
			
		||||
      <SortableProvider
 | 
			
		||||
        id={id}
 | 
			
		||||
@ -67,7 +77,9 @@ export const SortableItem: React.FC<any> = observer(
 | 
			
		||||
          removeParentsIfNoChildren: removeParentsIfNoChildren ?? true,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Sortable {...others}>{props.children}</Sortable>
 | 
			
		||||
        <Sortable id={eid} {...others}>
 | 
			
		||||
          {props.children}
 | 
			
		||||
        </Sortable>
 | 
			
		||||
      </SortableProvider>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,7 @@ export class Designable {
 | 
			
		||||
      if (!current['x-uid']) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      await api.request({
 | 
			
		||||
      const res = await api.request({
 | 
			
		||||
        url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        data: {
 | 
			
		||||
@ -165,7 +165,7 @@ export class Designable {
 | 
			
		||||
          method: 'post',
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      onSuccess?.();
 | 
			
		||||
      onSuccess?.(res?.data?.data);
 | 
			
		||||
      message.success(t('Saved successfully'), 0.2);
 | 
			
		||||
    });
 | 
			
		||||
    this.on('patch', async ({ schema }) => {
 | 
			
		||||
@ -321,7 +321,7 @@ export class Designable {
 | 
			
		||||
        removed = parent;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.emit('remove', { removed });
 | 
			
		||||
    return this.emit('remove', { removed });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
 | 
			
		||||
@ -501,7 +501,7 @@ export class Designable {
 | 
			
		||||
    const s = this.current.addProperty(wrapped.name || uid(), wrapped);
 | 
			
		||||
    s.parent = this.current;
 | 
			
		||||
    const [schema1, schema2] = splitWrapSchema(s, schema);
 | 
			
		||||
    this.emit('insertAdjacent', {
 | 
			
		||||
    return this.emit('insertAdjacent', {
 | 
			
		||||
      position: 'beforeEnd',
 | 
			
		||||
      schema: schema2,
 | 
			
		||||
      wrap: schema1,
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { ISchema, observer } from '@formily/react';
 | 
			
		||||
import { ISchema, observer, useForm } from '@formily/react';
 | 
			
		||||
import { Button, Dropdown, Menu, Switch } from 'antd';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import React, { createContext, useContext, useState } from 'react';
 | 
			
		||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
 | 
			
		||||
import { Icon } from '../icon';
 | 
			
		||||
import { SchemaComponent, useActionContext } from '../schema-component';
 | 
			
		||||
import { useCompile, useDesignable } from '../schema-component/hooks';
 | 
			
		||||
import './style.less';
 | 
			
		||||
import {
 | 
			
		||||
@ -106,6 +107,24 @@ SchemaInitializer.Button = observer(
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const buttonDom = (
 | 
			
		||||
      <Button
 | 
			
		||||
        type={'dashed'}
 | 
			
		||||
        style={{
 | 
			
		||||
          borderColor: '#f18b62',
 | 
			
		||||
          color: '#f18b62',
 | 
			
		||||
          ...style,
 | 
			
		||||
        }}
 | 
			
		||||
        {...others}
 | 
			
		||||
        icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
 | 
			
		||||
      >
 | 
			
		||||
        {compile(props.children || props.title)}
 | 
			
		||||
      </Button>
 | 
			
		||||
    );
 | 
			
		||||
    if (!items.length) {
 | 
			
		||||
      return buttonDom;
 | 
			
		||||
    }
 | 
			
		||||
    const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
 | 
			
		||||
    if (!designable && props.designable !== true) {
 | 
			
		||||
      return null;
 | 
			
		||||
@ -131,22 +150,7 @@ SchemaInitializer.Button = observer(
 | 
			
		||||
          {...dropdown}
 | 
			
		||||
          overlay={menu}
 | 
			
		||||
        >
 | 
			
		||||
          {component ? (
 | 
			
		||||
            component
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Button
 | 
			
		||||
              type={'dashed'}
 | 
			
		||||
              style={{
 | 
			
		||||
                borderColor: '#f18b62',
 | 
			
		||||
                color: '#f18b62',
 | 
			
		||||
                ...style,
 | 
			
		||||
              }}
 | 
			
		||||
              {...others}
 | 
			
		||||
              icon={<Icon type={icon as string} />}
 | 
			
		||||
            >
 | 
			
		||||
              {compile(props.children || props.title)}
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          {component ? component : buttonDom}
 | 
			
		||||
        </Dropdown>
 | 
			
		||||
      </SchemaInitializerButtonContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
@ -242,6 +246,102 @@ SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
 | 
			
		||||
  return component;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface SchemaInitializerActionModalProps {
 | 
			
		||||
  title: string;
 | 
			
		||||
  schema: any;
 | 
			
		||||
  onCancel?: () => void;
 | 
			
		||||
  onSubmit?: (values: any) => void;
 | 
			
		||||
  buttonText?: any;
 | 
			
		||||
}
 | 
			
		||||
SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
 | 
			
		||||
  const { title, schema, buttonText, onCancel, onSubmit } = props;
 | 
			
		||||
 | 
			
		||||
  const useCancelAction = useCallback(() => {
 | 
			
		||||
    const form = useForm();
 | 
			
		||||
    const ctx = useActionContext();
 | 
			
		||||
    return {
 | 
			
		||||
      async run() {
 | 
			
		||||
        await onCancel?.();
 | 
			
		||||
        ctx.setVisible(false);
 | 
			
		||||
        form.reset();
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }, [onCancel]);
 | 
			
		||||
 | 
			
		||||
  const useSubmitAction = useCallback(() => {
 | 
			
		||||
    const form = useForm();
 | 
			
		||||
    const ctx = useActionContext();
 | 
			
		||||
    return {
 | 
			
		||||
      async run() {
 | 
			
		||||
        await form.validate();
 | 
			
		||||
        await onSubmit?.(form.values);
 | 
			
		||||
        ctx.setVisible(false);
 | 
			
		||||
        form.reset();
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }, [onSubmit]);
 | 
			
		||||
  const defaultSchema = useMemo(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      type: 'void',
 | 
			
		||||
      properties: {
 | 
			
		||||
        action1: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          'x-component': 'Action',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            icon: 'PlusOutlined',
 | 
			
		||||
            style: {
 | 
			
		||||
              borderColor: 'rgb(241, 139, 98)',
 | 
			
		||||
              color: 'rgb(241, 139, 98)',
 | 
			
		||||
            },
 | 
			
		||||
            title: buttonText,
 | 
			
		||||
            type: 'dashed',
 | 
			
		||||
          },
 | 
			
		||||
          properties: {
 | 
			
		||||
            drawer1: {
 | 
			
		||||
              'x-decorator': 'Form',
 | 
			
		||||
              'x-component': 'Action.Modal',
 | 
			
		||||
              'x-component-props': {
 | 
			
		||||
                style: {
 | 
			
		||||
                  maxWidth: '520px',
 | 
			
		||||
                  width: '100%',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
              type: 'void',
 | 
			
		||||
              title,
 | 
			
		||||
              properties: {
 | 
			
		||||
                ...(schema?.properties || schema),
 | 
			
		||||
                footer: {
 | 
			
		||||
                  'x-component': 'Action.Modal.Footer',
 | 
			
		||||
                  type: 'void',
 | 
			
		||||
                  properties: {
 | 
			
		||||
                    cancel: {
 | 
			
		||||
                      title: '{{t("Cancel")}}',
 | 
			
		||||
                      'x-component': 'Action',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        useAction: useCancelAction,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                    submit: {
 | 
			
		||||
                      title: '{{t("Submit")}}',
 | 
			
		||||
                      'x-component': 'Action',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        type: 'primary',
 | 
			
		||||
                        useAction: useSubmitAction,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }, [buttonText, schema, title, useCancelAction, useSubmitAction]);
 | 
			
		||||
 | 
			
		||||
  return <SchemaComponent schema={defaultSchema as any} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SchemaInitializer.SwitchItem = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Item onClick={props.onClick}>
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,7 @@ import { SchemaComponent, useActionContext, useDesignable, useRecordIndex } from
 | 
			
		||||
 | 
			
		||||
export const TabPaneInitializers = (props?: any) => {
 | 
			
		||||
  const { designable, insertBeforeEnd } = useDesignable();
 | 
			
		||||
  if (!designable) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const useSubmitAction = () => {
 | 
			
		||||
    const form = useForm();
 | 
			
		||||
    const ctx = useActionContext();
 | 
			
		||||
@ -118,6 +116,11 @@ export const TabPaneInitializers = (props?: any) => {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (!designable) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <SchemaComponent schema={schema} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1064,6 +1064,9 @@ export const createGridCardBlockSchema = (options) => {
 | 
			
		||||
      ...others,
 | 
			
		||||
    },
 | 
			
		||||
    'x-component': 'BlockItem',
 | 
			
		||||
    'x-component-props': {
 | 
			
		||||
      useProps: '{{ useGridCardBlockItemProps }}',
 | 
			
		||||
    },
 | 
			
		||||
    'x-designer': 'GridCard.Designer',
 | 
			
		||||
    properties: {
 | 
			
		||||
      actionBar: {
 | 
			
		||||
@ -1081,7 +1084,7 @@ export const createGridCardBlockSchema = (options) => {
 | 
			
		||||
        type: 'array',
 | 
			
		||||
        'x-component': 'GridCard',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          props: '{{ useGridCardBlockProps }}',
 | 
			
		||||
          useProps: '{{ useGridCardBlockProps }}',
 | 
			
		||||
        },
 | 
			
		||||
        properties: {
 | 
			
		||||
          item: {
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ export const GeneralSchemaItems: React.FC<{
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <SchemaSettings.SwitchItem
 | 
			
		||||
          checked={field.decoratorProps.showTitle ?? true}
 | 
			
		||||
          checked={fieldSchema['x-decorator-props']?.['showTitle'] ?? true}
 | 
			
		||||
          title={t('Display title')}
 | 
			
		||||
          onChange={(checked) => {
 | 
			
		||||
            fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,11 @@ const titleCss = css`
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const overrideAntdCSS = css`
 | 
			
		||||
  & .ant-space-item .anticon {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
export const GeneralSchemaDesigner = (props: any) => {
 | 
			
		||||
  const { disableInitializer, title, template, draggable = true } = props;
 | 
			
		||||
  const { dn, designable } = useDesignable();
 | 
			
		||||
@ -62,7 +67,7 @@ export const GeneralSchemaDesigner = (props: any) => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={'general-schema-designer'}>
 | 
			
		||||
    <div className={classNames('general-schema-designer', overrideAntdCSS)}>
 | 
			
		||||
      {title && (
 | 
			
		||||
        <div className={classNames('general-schema-designer-title', titleCss)}>
 | 
			
		||||
          <Space size={2}>
 | 
			
		||||
 | 
			
		||||
@ -148,7 +148,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SchemaSettings.Template = function Template(props) {
 | 
			
		||||
  const { componentName, collectionName, resourceName } = props;
 | 
			
		||||
  const { componentName, collectionName, resourceName, needRender } = props;
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { getCollection } = useCollectionManager();
 | 
			
		||||
  const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
 | 
			
		||||
@ -156,7 +156,7 @@ SchemaSettings.Template = function Template(props) {
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const { dn: tdn } = useBlockTemplateContext();
 | 
			
		||||
  const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
 | 
			
		||||
  if (!collectionName) {
 | 
			
		||||
  if (!collectionName && !needRender) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  if (template) {
 | 
			
		||||
@ -182,7 +182,7 @@ SchemaSettings.Template = function Template(props) {
 | 
			
		||||
    <SchemaSettings.Item
 | 
			
		||||
      onClick={async () => {
 | 
			
		||||
        setVisible(false);
 | 
			
		||||
        const { title } = getCollection(collectionName);
 | 
			
		||||
        const { title } = collectionName ? getCollection(collectionName) : { title: '' };
 | 
			
		||||
        const values = await FormDialog(t('Save as template'), () => {
 | 
			
		||||
          return (
 | 
			
		||||
            <FormLayout layout={'vertical'}>
 | 
			
		||||
@ -194,7 +194,7 @@ SchemaSettings.Template = function Template(props) {
 | 
			
		||||
                    name: {
 | 
			
		||||
                      title: t('Template name'),
 | 
			
		||||
                      required: true,
 | 
			
		||||
                      default: `${compile(title)}_${t(componentName)}`,
 | 
			
		||||
                      default: title ? `${compile(title)}_${t(componentName)}` : t(componentName),
 | 
			
		||||
                      'x-decorator': 'FormItem',
 | 
			
		||||
                      'x-component': 'Input',
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
@ -123,6 +123,10 @@ export const useSchemaTemplateManager = () => {
 | 
			
		||||
      const items = templates?.filter?.((template) => template.collectionName === collectionName);
 | 
			
		||||
      return items || [];
 | 
			
		||||
    },
 | 
			
		||||
    getTemplatesByComponentName(componentName: string): Array<any> {
 | 
			
		||||
      const items = templates?.filter?.((template) => template.componentName === componentName);
 | 
			
		||||
      return items || [];
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { uid } from '@formily/shared';
 | 
			
		||||
import { Menu } from 'antd';
 | 
			
		||||
import React, { useContext, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { ActionContext, SchemaComponent, useActionContext } from '../';
 | 
			
		||||
import { ActionContextProvider, SchemaComponent, useActionContext } from '../';
 | 
			
		||||
import { useAPIClient } from '../api-client';
 | 
			
		||||
import { DropdownVisibleContext } from './CurrentUser';
 | 
			
		||||
 | 
			
		||||
@ -119,7 +119,7 @@ export const ChangePassword = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const ctx = useContext(DropdownVisibleContext);
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
    <ActionContextProvider value={{ visible, setVisible }}>
 | 
			
		||||
      <Menu.Item
 | 
			
		||||
        key="password"
 | 
			
		||||
        eventKey={'ChangePassword'}
 | 
			
		||||
@ -131,6 +131,6 @@ export const ChangePassword = () => {
 | 
			
		||||
        {t('Change password')}
 | 
			
		||||
      </Menu.Item>
 | 
			
		||||
      <SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
 | 
			
		||||
    </ActionContext.Provider>
 | 
			
		||||
    </ActionContextProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import { EditProfile } from './EditProfile';
 | 
			
		||||
import { LanguageSettings } from './LanguageSettings';
 | 
			
		||||
import { SwitchRole } from './SwitchRole';
 | 
			
		||||
import { ThemeSettings } from './ThemeSettings';
 | 
			
		||||
 | 
			
		||||
const ApplicationVersion = () => {
 | 
			
		||||
  const data = useCurrentAppInfo();
 | 
			
		||||
  return (
 | 
			
		||||
@ -20,16 +19,18 @@ const ApplicationVersion = () => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DropdownVisibleContext = createContext(null);
 | 
			
		||||
 | 
			
		||||
export const CurrentUser = () => {
 | 
			
		||||
/**
 | 
			
		||||
 * @note If you want to change here, Note the Setting block on the mobile side
 | 
			
		||||
 */
 | 
			
		||||
export const SettingsMenu: React.FC<{
 | 
			
		||||
  redirectUrl?: string;
 | 
			
		||||
}> = (props) => {
 | 
			
		||||
  const { redirectUrl = '' } = props;
 | 
			
		||||
  const { allowAll, snippets } = useACLRoleContext();
 | 
			
		||||
  const appAllowed = allowAll || snippets?.includes('app');
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
  const { data } = useCurrentUserContext();
 | 
			
		||||
  const { allowAll, snippets } = useACLRoleContext();
 | 
			
		||||
  const appAllowed = allowAll || snippets?.includes('app');
 | 
			
		||||
  const silenceApi = useAPIClient();
 | 
			
		||||
  const check = async () => {
 | 
			
		||||
    return await new Promise((resolve) => {
 | 
			
		||||
@ -53,14 +54,6 @@ export const CurrentUser = () => {
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
 | 
			
		||||
      <DropdownVisibleContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
        <Dropdown
 | 
			
		||||
          open={visible}
 | 
			
		||||
          onOpenChange={(visible) => {
 | 
			
		||||
            setVisible(visible);
 | 
			
		||||
          }}
 | 
			
		||||
          overlay={
 | 
			
		||||
    <Menu>
 | 
			
		||||
      <ApplicationVersion />
 | 
			
		||||
      <Menu.Divider />
 | 
			
		||||
@ -111,13 +104,27 @@ export const CurrentUser = () => {
 | 
			
		||||
        key="signout"
 | 
			
		||||
        onClick={async () => {
 | 
			
		||||
          await api.auth.signOut();
 | 
			
		||||
                  history.push('/signin');
 | 
			
		||||
          history.push(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {t('Sign out')}
 | 
			
		||||
      </Menu.Item>
 | 
			
		||||
    </Menu>
 | 
			
		||||
          }
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
export const DropdownVisibleContext = createContext(null);
 | 
			
		||||
export const CurrentUser = () => {
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
  const { data } = useCurrentUserContext();
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
 | 
			
		||||
      <DropdownVisibleContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
        <Dropdown
 | 
			
		||||
          open={visible}
 | 
			
		||||
          onOpenChange={(visible) => {
 | 
			
		||||
            setVisible(visible);
 | 
			
		||||
          }}
 | 
			
		||||
          overlay={<SettingsMenu />}
 | 
			
		||||
        >
 | 
			
		||||
          <span
 | 
			
		||||
            className={css`
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { Menu } from 'antd';
 | 
			
		||||
import React, { useContext, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import {
 | 
			
		||||
  ActionContext,
 | 
			
		||||
  ActionContextProvider,
 | 
			
		||||
  DropdownVisibleContext,
 | 
			
		||||
  SchemaComponent,
 | 
			
		||||
  useActionContext,
 | 
			
		||||
@ -119,18 +119,18 @@ export const EditProfile = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const ctx = useContext(DropdownVisibleContext);
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
    <ActionContextProvider value={{ visible, setVisible }}>
 | 
			
		||||
      <Menu.Item
 | 
			
		||||
        key="profile"
 | 
			
		||||
        eventKey={'EditProfile'}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setVisible(true);
 | 
			
		||||
          ctx.setVisible(false);
 | 
			
		||||
          ctx?.setVisible(false);
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {t('Edit profile')}
 | 
			
		||||
      </Menu.Item>
 | 
			
		||||
      <SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
 | 
			
		||||
    </ActionContext.Provider>
 | 
			
		||||
    </ActionContextProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ export function useRedirect(next = '/admin') {
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const redirect = location?.['query']?.redirect;
 | 
			
		||||
  return useCallback(() => {
 | 
			
		||||
    history.push(redirect || '/admin');
 | 
			
		||||
    history.replace(redirect || '/admin');
 | 
			
		||||
  }, [redirect]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,6 @@ export const SwitchRole = () => {
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const roles = useCurrentRoles();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  if (roles.length <= 1) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
@ -57,7 +56,7 @@ export const SwitchRole = () => {
 | 
			
		||||
          onChange={async (roleName) => {
 | 
			
		||||
            api.auth.setRole(roleName);
 | 
			
		||||
            await api.resource('users').setDefaultRole({ values: { roleName } });
 | 
			
		||||
            history.push('/');
 | 
			
		||||
            location.reload();
 | 
			
		||||
            window.location.reload();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { css } from '@emotion/css';
 | 
			
		||||
import { useFieldSchema } from '@formily/react';
 | 
			
		||||
import { useCollection } from '@nocobase/client';
 | 
			
		||||
import { useMemoizedFn } from 'ahooks';
 | 
			
		||||
import { Alert, Button, Modal } from 'antd';
 | 
			
		||||
import { Alert, Button, Modal, Spin } from 'antd';
 | 
			
		||||
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
 | 
			
		||||
import { useHistory } from 'react-router';
 | 
			
		||||
import { useMapConfiguration } from '../hooks';
 | 
			
		||||
@ -102,7 +102,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
  const overlay = useRef<AMap.Polygon>();
 | 
			
		||||
  const editor = useRef(null);
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
 | 
			
		||||
  const id = useRef(`nocobase-map-${type || ''}-${Date.now().toString(32)}`);
 | 
			
		||||
 | 
			
		||||
  const [commonOptions] = useState<AMap.PolylineOptions & AMap.PolygonOptions>({
 | 
			
		||||
    strokeWeight: 5,
 | 
			
		||||
@ -316,7 +316,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
      plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
 | 
			
		||||
    })
 | 
			
		||||
      .then((amap) => {
 | 
			
		||||
        requestIdleCallback(() => {
 | 
			
		||||
        return requestIdleCallback(() => {
 | 
			
		||||
          map.current = new amap.Map(id.current, {
 | 
			
		||||
            resizeEnable: true,
 | 
			
		||||
            zoom,
 | 
			
		||||
@ -327,11 +327,15 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        if (typeof err === 'string') {
 | 
			
		||||
          if (err.includes('多个不一致的 key')) {
 | 
			
		||||
            setErrMessage(t('The AccessKey is incorrect, please check it'));
 | 
			
		||||
          } else {
 | 
			
		||||
            setErrMessage(err);
 | 
			
		||||
          }
 | 
			
		||||
        } else if (err?.type === 'error') {
 | 
			
		||||
          setErrMessage('Something went wrong, please refresh the page and try again');
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
@ -379,6 +383,19 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
 | 
			
		||||
      id={id.current}
 | 
			
		||||
      style={props?.style}
 | 
			
		||||
    >
 | 
			
		||||
      {!aMap.current && (
 | 
			
		||||
        <div
 | 
			
		||||
          className={css`
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            inset: 0;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
          `}
 | 
			
		||||
        >
 | 
			
		||||
          <Spin />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {!disabled ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Search toCenter={toCenter} aMap={aMap.current} />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								packages/plugins/mobile-client/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										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