import React, { Children, createContext, useContext, useEffect, useRef, useState, forwardRef, } from 'react'; import { connect, observer, mapProps, mapReadPretty, useField, useFieldSchema, RecursionField, Schema, SchemaOptionsContext, FormProvider, useForm, } from '@formily/react'; import { Menu as AntdMenu, MenuProps, MenuItemProps, SubMenuProps, DividerProps, Dropdown, Modal, Button, Space, } from 'antd'; import { uid } from '@formily/shared'; import cls from 'classnames'; import { SchemaField, SchemaRenderer, useDesignable, } from '../../components/schema-renderer'; import { MenuOutlined, PlusOutlined, GroupOutlined, LinkOutlined, DeleteOutlined, EditOutlined, ArrowUpOutlined, ArrowDownOutlined, ArrowRightOutlined, DragOutlined, LockOutlined, } from '@ant-design/icons'; import { IconPicker } from '../../components/icon-picker'; import { useDefaultAction } from '..'; import { useMount } from 'ahooks'; import './style.less'; import { findPropertyByPath, getSchemaPath, useSchemaPath } from '../../'; import { generateDefaultSchema } from './defaultSchemas'; import _, { cloneDeep, get, isNull } from 'lodash'; import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import deepmerge from 'deepmerge'; import { onFieldChange } from '@formily/core'; import { VisibleContext } from '../../context'; import { DragHandle, SortableItem, SortableItemContext, } from '../../components/Sortable'; import { DndContext, DragOverlay } from '@dnd-kit/core'; import { createPortal } from 'react-dom'; import { Resource } from '../../resource'; import { useClient } from '../../constate'; import { useTranslation } from 'react-i18next'; import { useCompile } from '../../hooks/useCompile'; export interface MenuContextProps { schema?: Schema; onRemove?: any; } export const MenuModeContext = createContext(null); export const MenuContext = createContext(null); const SideMenu = (props: any) => { const { selectedKey, defaultSelectedKeys, onSelect, path } = props; const { t } = useTranslation(); const { schema } = useDesignable(); const [selectedKeys, setSelectedKeys] = useState(defaultSelectedKeys); useEffect(() => { setSelectedKeys(defaultSelectedKeys); }, [defaultSelectedKeys]); if (!selectedKey) { return null; } const child = schema.properties && schema.properties[selectedKey]; if (!child || child['x-component'] !== 'Menu.SubMenu') { return null; } return ( { setSelectedKeys(openKeys); }} openKeys={selectedKeys} selectedKeys={selectedKeys} > ); }; export const MenuSelectedKeysContext = createContext([]); export const Menu: any = observer((props: any) => { const { mode, onSelect, sideMenuRef, defaultSelectedKeys: keys, getSelectedKeys, onRemove, ...others } = props; const defaultSelectedKeys = useContext(MenuSelectedKeysContext); const { root, schema, insertAfter, remove } = useDesignable(); const moveToAfter = (path1, path2) => { if (!path1 || !path2) { return; } if (path1.join('.') === path2.join('.')) { return; } const data = findPropertyByPath(root, path1); if (!data) { return; } remove(path1); return insertAfter(data.toJSON(), path2); }; const fieldSchema = useFieldSchema(); console.log('Menu.schema', schema, fieldSchema); const [selectedKey, setSelectedKey] = useState( defaultSelectedKeys[0] || null, ); const ref = useRef(); const path = useSchemaPath(); const child = schema.properties && schema.properties[selectedKey]; const isSubMenu = child && child['x-component'] === 'Menu.SubMenu'; const { updateSchema } = useClient(); const { t } = useTranslation(); useMount(() => { if (mode !== 'mix') { return; } const sideMenuElement = sideMenuRef && (sideMenuRef.current as HTMLElement); if (!sideMenuElement) { return; } if (sideMenuElement && ref.current) { sideMenuElement.querySelector(':scope > div').appendChild(ref.current); } sideMenuElement.style.display = isSubMenu ? 'block' : 'none'; }); useEffect(() => { const sideMenuElement = sideMenuRef && (sideMenuRef.current as HTMLElement); if (!sideMenuElement) { return; } sideMenuElement.style.display = isSubMenu ? 'block' : 'none'; }, [selectedKey]); const [dragOverlayContent, setDragOverlayContent] = useState(''); // console.log('defaultSelectedKeys', defaultSelectedKeys, getSelectedKeys); return ( { console.log({ event }); setDragOverlayContent(event.active.data?.current?.title || ''); }} onDragEnd={async (event) => { console.log({ event }); const path1 = event.active?.data?.current?.path; const path2 = event.over?.data?.current?.path; const data = moveToAfter(path1, path2); await updateSchema(data); }} > {createPortal( {dragOverlayContent} , document.body, )} { if (mode === 'mix') { setSelectedKey(info.key); } const selectedSchema = schema.properties[info.key]; console.log({ selectedSchema }); onSelect && onSelect({ ...info, schema: selectedSchema }); }} > {/* */} {mode === 'mix' && (
{ const keyPath = [selectedKey, ...[...info.keyPath].reverse()]; const selectedSchema = findPropertyByPath(schema, keyPath); console.log('keyPath', keyPath, selectedSchema); onSelect && onSelect({ ...info, keyPath, schema: selectedSchema }); }} defaultSelectedKeys={defaultSelectedKeys || []} selectedKey={selectedKey} sideMenuRef={sideMenuRef} />
)}
); }); Menu.Divider = observer(AntdMenu.Divider); Menu.Item = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); const compile = useCompile(); const title = compile(schema.title); return ( {icon && ( )} {title} ); }); Menu.Link = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); const compile = useCompile(); const title = compile(schema.title); return ( {icon && ( )} {title} {/* {schema.title} */} ); }); Menu.URL = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); const compile = useCompile(); const title = compile(schema.title); return ( { window.open(props.href); }} > {icon && ( )} {title} ); }); Menu.Action = observer((props: any) => { const { icon, useAction = useDefaultAction, ...others } = props; const { schema, DesignableBar } = useDesignable(); const field = useField(); const [visible, setVisible] = useState(false); const { run } = useAction(); const compile = useCompile(); const title = compile(schema.title); return ( } onClick={async () => { await run(); setVisible(true); }} > {title} {props.children} {/* */} ); }); Menu.SubMenu = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); const mode = useContext(MenuModeContext); const compile = useCompile(); const title = compile(schema.title); return mode === 'mix' ? ( ) : ( } title={ {icon && ( )} {title} // <> // {schema.title} // } eventKey={schema.name} key={schema.name} > ); }); Menu.AddNew = observer((props: any) => { const { designable, appendChild } = useDesignable(props.path); const { createSchema, removeSchema, updateSchema } = useClient(); const { t } = useTranslation(); if (!designable) { return null; } const schemas = { 'Menu.Link': { icon: , title: t('Add page'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.SubMenu': { icon: , title: t('Add group'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.URL': { icon: , title: t('Add link'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, 'x-component-props.href': { type: 'string', title: t('Link'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }; return ( { console.log({ info }); const values = await FormDialog(schemas[info.key].title, () => { return ( ); }).open(); const defaults = generateDefaultSchema(info.key); const data = appendChild(deepmerge(defaults, values)); await createSchema(data); }} > }> {t('Group')} }> {t('Page')} }> {t('Link')} } > {props.children} } /> ); }); Menu.DesignableBar = (props) => { const { t } = useTranslation(); const schemas = { 'Menu.Action': { icon: , title: t('Page'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.Item': { icon: , title: t('Page'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.Link': { icon: , title: t('Page'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.SubMenu': { icon: , title: t('Group'), schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.URL': { icon: , title: 'Link', schema: { type: 'object', properties: { title: { type: 'string', title: t('Name'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: t('Icon'), 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, 'x-component-props.href': { type: 'string', title: t('Link'), required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }; const field = useField(); const [visible, setVisible] = useState(false); const { designable, schema, remove, refresh, insertAfter, insertBefore, appendChild, } = useDesignable(); const formConfig = schemas[schema['x-component']]; const isSubMenu = schema['x-component'] === 'Menu.SubMenu'; const ctx = useContext(MenuContext); const mode = useContext(MenuModeContext); const { createSchema, removeSchema, updateSchema } = useClient(); return (
{ e.stopPropagation(); e.preventDefault(); }} className={'designable-bar-actions'} > { if (['update', 'move', 'delete'].includes(info.key)) { return; } const methodLabels = { insertBefore: 'before', insertAfter: 'after', appendChild: 'in', }; const keys = info.key.split('.'); const method = keys.shift(); const type = keys.join('.'); const config = schemas[type]; if (!config) { return; } const values = await FormDialog( t(`Add {{type}} ${methodLabels[method]} "{{title}}"`, { type: (config.title as string).toLowerCase(), title: schema.title }), // `在「${schema.title}」${methodLabels[method]}插入${config.title}`, () => { return ( ); }, ).open(); const methods = { insertAfter, insertBefore, appendChild, }; const defaults = generateDefaultSchema(type); const data = methods[method](deepmerge(defaults, values)); await createSchema(data); }} > { const initialValues = {}; Object.keys(formConfig.schema.properties).forEach( (name) => { _.set(initialValues, name, get(schema, name)); }, ); const values = await FormDialog(t('Edit menu item'), () => { return ( ); }).open({ initialValues, }); if (values.title) { schema.title = values.title; } const icon = _.get(values, 'x-component-props.icon') || null; schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['icon'] = icon; field.componentProps['icon'] = icon; refresh(); await updateSchema(schema); }} > {t('Edit')} { let menuSchema: Schema; let parent = schema.parent; while (parent) { if (parent['x-component'] === 'Menu') { menuSchema = parent; break; } parent = parent.parent; } console.log({ menuSchema }); const toTreeData = (s: Schema) => { const items = []; Object.keys(s.properties || {}).forEach((name) => { const current = s.properties[name]; if ( !(current['x-component'] as string).startsWith( 'Menu.', ) ) { return; } // if (current.name === schema.name) { // return; // } items.push({ key: current['key'] || current.name, label: current.title, title: current.title, value: getSchemaPath(current).join('.'), children: toTreeData(current), }); }); return items; }; const dataSource = toTreeData(menuSchema); const values = await FormDialog( t(`Move {{title}} to`, { title: schema.title }), () => { return ( ); }, ).open({ effects(form) { onFieldChange('path', (field) => { const target = findPropertyByPath( schema.root, // @ts-ignore field.value, ); console.log({ field }); field.query('method').take((f) => { // @ts-ignore // f.value = 'insertAfter'; // @ts-ignore f.dataSource = target['x-component'] === 'Menu.SubMenu' ? [ { label: t('After'), value: 'insertAfter' }, { label: t('Before'), value: 'insertBefore' }, { label: t('Inner'), value: 'appendChild' }, ] : [ { label: t('After'), value: 'insertAfter' }, { label: t('Before'), value: 'insertBefore' }, ]; }); }); }, }); const methods = { insertAfter, insertBefore, appendChild, }; const data = schema.toJSON(); remove(); const source = methods[values.method](data, values.path); await updateSchema(source); }} > {t('Move to')} } title={t(`Insert ${mode == 'inline' ? 'above' : 'left'}`)} > } > {t('Group')} } > {t('Page')} } > {t('Link')} } title={t(`Insert ${mode == 'inline' ? 'below' : 'right'}`)} > } > {t('Group')} } > {t('Page')} } > {t('Link')} {isSubMenu && ( } title={t('Insert inner')} > } > {t('Group')} } > {t('Page')} } > {t('Link')} )} {/* } onClick={async () => { const loadRoles = async () => { const resource = Resource.make('roles'); const data = await resource.list(); console.log('loadRoles', data); return data?.data.map((item) => ({ label: item.title, value: item.name, })); }; const resource = Resource.make({ associatedName: 'ui_schemas', associatedKey: schema['key'], resourceName: 'roles', }); const uiSchemasRoles = await resource.list(); console.log({ uiSchemasRoles }); const values = await FormDialog(`设置权限`, () => { return ( ); }).open({ initialValues: { roles: uiSchemasRoles?.data?.map((role) => role.name), }, }); await Resource.make({ resourceName: 'ui_schemas', resourceKey: schema['key'], }).save(values); }} > 设置权限 */} { Modal.confirm({ title: t(`Delete menu item`), content: t('Are you sure you want to delete it?'), onOk: async () => { const target = remove(); await removeSchema(target); ctx.onRemove && ctx.onRemove(target); }, }); }} > {t('Delete')} } >
); }; export default Menu;