diff --git a/client/packages/design/components/WebMetaForm/index.tsx b/client/packages/design/components/WebMetaForm/index.tsx index 6248014e351..6d4e562872e 100644 --- a/client/packages/design/components/WebMetaForm/index.tsx +++ b/client/packages/design/components/WebMetaForm/index.tsx @@ -35,6 +35,8 @@ export function setWebFastifyFormConfig(config: typeof webFastifyFormConfig) { const WebFastifyFormContainer: FastifyFormContainerComponent = React.memo( (props) => { const layout = props.layout; + const suffixElement = props.extraProps?.suffixElement; + const submitButtonRender = useMemo(() => { return ( {props.children} + {suffixElement} {submitButtonRender} ); diff --git a/client/shared/hooks/useEditValue.ts b/client/shared/hooks/useEditValue.ts new file mode 100644 index 00000000000..6363ba3b2ec --- /dev/null +++ b/client/shared/hooks/useEditValue.ts @@ -0,0 +1,15 @@ +import { useCallback, useLayoutEffect, useState } from 'react'; + +export function useEditValue(value: T, onChange: (val: T) => void) { + const [inner, setInner] = useState(value); + + useLayoutEffect(() => { + setInner(value); + }, [value]); + + const onSave = useCallback(() => { + onChange(inner); + }, [inner, onChange]); + + return [inner, setInner, onSave] as const; +} diff --git a/client/shared/hooks/useLazyValue.ts b/client/shared/hooks/useLazyValue.ts new file mode 100644 index 00000000000..d21686469df --- /dev/null +++ b/client/shared/hooks/useLazyValue.ts @@ -0,0 +1,17 @@ +import { useLayoutEffect, useState } from 'react'; +import { useEvent } from './useEvent'; + +export function useLazyValue(value: T, onChange: (val: T) => void) { + const [inner, setInner] = useState(value); + + useLayoutEffect(() => { + setInner(value); + }, [value]); + + const handleChange = useEvent((val: T) => { + setInner(val); + onChange(val); + }); + + return [inner, handleChange] as const; +} diff --git a/client/shared/index.tsx b/client/shared/index.tsx index 18bd97f59f3..37fafbfa72c 100644 --- a/client/shared/index.tsx +++ b/client/shared/index.tsx @@ -74,8 +74,10 @@ export { useAsyncFn } from './hooks/useAsyncFn'; export { useAsyncRefresh } from './hooks/useAsyncRefresh'; export { useAsyncRequest } from './hooks/useAsyncRequest'; export { useDebounce } from './hooks/useDebounce'; +export { useEditValue } from './hooks/useEditValue'; export { useEvent } from './hooks/useEvent'; export { useInterval } from './hooks/useInterval'; +export { useLazyValue } from './hooks/useLazyValue'; export { useMemoizedFn } from './hooks/useMemoizedFn'; export { useMountedState } from './hooks/useMountedState'; export { usePrevious } from './hooks/usePrevious'; diff --git a/client/web/src/components/CollapseView.tsx b/client/web/src/components/CollapseView.tsx index 5f33e85ea12..c7d59c7308f 100644 --- a/client/web/src/components/CollapseView.tsx +++ b/client/web/src/components/CollapseView.tsx @@ -4,10 +4,11 @@ import React from 'react'; interface CollapseViewProps extends React.PropsWithChildren { title: string; className?: string; + style?: React.CSSProperties; } export const CollapseView: React.FC = React.memo((props) => { return ( - + {props.children} diff --git a/client/web/src/components/modals/GroupPanel/AdvanceGroupPanelPermission.tsx b/client/web/src/components/modals/GroupPanel/AdvanceGroupPanelPermission.tsx new file mode 100644 index 00000000000..3f01cd64f4b --- /dev/null +++ b/client/web/src/components/modals/GroupPanel/AdvanceGroupPanelPermission.tsx @@ -0,0 +1,120 @@ +import { PermissionList } from '@/components/PermissionList'; +import { Button } from 'antd'; +import clsx from 'clsx'; +import React, { PropsWithChildren, useState } from 'react'; +import { + ALL_PERMISSION, + getDefaultPermissionList, + t, + useAppSelector, + useEvent, + useLazyValue, +} from 'tailchat-shared'; +import _isEqual from 'lodash/isEqual'; + +interface AdvanceGroupPanelPermissionProps { + height?: number; + groupId: string; + panelId: string; + onChange: ( + permissionMap: Record | undefined + ) => void; +} + +export const AdvanceGroupPanelPermission: React.FC = + React.memo((props) => { + const [selectedRoleId, setSelectedRoleId] = useState< + typeof ALL_PERMISSION | string + >(ALL_PERMISSION); + + const roles = useAppSelector((state) => { + const groupInfo = state.group.groups[props.groupId]; + return groupInfo.roles; + }); + + const permissionMap: Record = + useAppSelector((state) => { + const groupInfo = state.group.groups[props.groupId]; + const panelInfo = groupInfo.panels.find((p) => p.id === props.panelId); + if (!panelInfo) { + return { [ALL_PERMISSION]: getDefaultPermissionList() }; + } else { + return { + [ALL_PERMISSION]: + panelInfo.fallbackPermissions ?? getDefaultPermissionList(), + ...panelInfo.permissionMap, + }; + } + }, _isEqual); + + const [editPermissionMap, setEditPermissionMap] = useLazyValue( + permissionMap, + props.onChange + ); + + const handleUpdatePermissionMap = useEvent((permissions: string[]) => { + const newMap = { ...editPermissionMap, [selectedRoleId]: permissions }; + setEditPermissionMap(newMap); + }); + + const handleSyncWithGroup = useEvent(() => { + setEditPermissionMap({ + [ALL_PERMISSION]: getDefaultPermissionList(), + }); + props.onChange(undefined); + }); + + return ( +
+
+ setSelectedRoleId(ALL_PERMISSION)} + > + {t('所有人')} + + {roles.map((r) => ( + setSelectedRoleId(r._id)} + > + {r.name} + + ))} +
+
+
+ +
+ +
+
+ ); + }); +AdvanceGroupPanelPermission.displayName = 'AdvanceGroupPanelPermission'; + +const RoleItem: React.FC< + PropsWithChildren<{ + active: boolean; + onClick?: () => void; + }> +> = React.memo((props) => { + return ( +
+ {props.children} +
+ ); +}); +RoleItem.displayName = 'RoleItem'; diff --git a/client/web/src/components/modals/GroupPanel/ModifyGroupPanel.tsx b/client/web/src/components/modals/GroupPanel/ModifyGroupPanel.tsx index 8d65e9731e6..c80c834351e 100644 --- a/client/web/src/components/modals/GroupPanel/ModifyGroupPanel.tsx +++ b/client/web/src/components/modals/GroupPanel/ModifyGroupPanel.tsx @@ -6,12 +6,18 @@ import { modifyGroupPanel, showToasts, useGroupPanelInfo, + useEvent, + ALL_PERMISSION, + DevContainer, } from 'tailchat-shared'; import { ModalWrapper } from '../../Modal'; import { WebMetaForm } from 'tailchat-design'; import { buildDataFromValues, pickValuesFromGroupPanelInfo } from './helper'; import type { GroupPanelValues } from './types'; import { useGroupPanelFields } from './useGroupPanelFields'; +import { AdvanceGroupPanelPermission } from './AdvanceGroupPanelPermission'; +import { CollapseView } from '@/components/CollapseView'; +import _omit from 'lodash/omit'; /** * 修改群组面板 @@ -22,35 +28,70 @@ export const ModalModifyGroupPanel: React.FC<{ onSuccess?: () => void; }> = React.memo((props) => { const groupPanelInfo = useGroupPanelInfo(props.groupId, props.groupPanelId); - const [currentValues, setValues] = useState>({}); - - const [, handleSubmit] = useAsyncRequest( - async (values: GroupPanelValues) => { - await modifyGroupPanel( - props.groupId, - props.groupPanelId, - buildDataFromValues(values) - ); - showToasts(t('修改成功'), 'success'); - typeof props.onSuccess === 'function' && props.onSuccess(); - }, - [props.groupId, props.groupPanelId, props.onSuccess] + const [currentValues, setValues] = useState( + pickValuesFromGroupPanelInfo(groupPanelInfo) ); + const [, handleSubmit] = useAsyncRequest(async () => { + await modifyGroupPanel( + props.groupId, + props.groupPanelId, + buildDataFromValues(currentValues) + ); + showToasts(t('修改成功'), 'success'); + typeof props.onSuccess === 'function' && props.onSuccess(); + }, [props.groupId, props.groupPanelId, props.onSuccess, currentValues]); + const { fields, schema } = useGroupPanelFields(props.groupId, currentValues); + const handleUpdateValues = useEvent((values: Partial) => { + setValues((state) => ({ + ...state, + ...values, + })); + }); + if (!groupPanelInfo) { return ; } return ( - + f.type !== 'type')} // 变更时不显示类型 initialValues={pickValuesFromGroupPanelInfo(groupPanelInfo)} - onChange={setValues} + onChange={handleUpdateValues} onSubmit={handleSubmit} + extraProps={{ + suffixElement: ( + + + { + if (permissionMap) { + const fallbackPermissions = permissionMap[ALL_PERMISSION]; + const others = { ...permissionMap }; + + handleUpdateValues({ + fallbackPermissions, + permissionMap: _omit(others, [ALL_PERMISSION]), + }); + } else { + handleUpdateValues({ + fallbackPermissions: undefined, + permissionMap: undefined, + }); + } + }} + /> + + + ), + }} /> ); diff --git a/client/web/src/components/modals/GroupPanel/helper.ts b/client/web/src/components/modals/GroupPanel/helper.ts index 4da6389374c..52303104c4a 100644 --- a/client/web/src/components/modals/GroupPanel/helper.ts +++ b/client/web/src/components/modals/GroupPanel/helper.ts @@ -6,7 +6,7 @@ import type { GroupPanelValues } from './types'; * 根据表单数据生成需要提交的内容 */ export function buildDataFromValues(values: GroupPanelValues) { - const { name, type, ...meta } = values; + const { name, type, permissionMap, fallbackPermissions, ...meta } = values; let panelType: number; let provider: string | undefined = undefined; let pluginPanelName: string | undefined = undefined; @@ -30,6 +30,8 @@ export function buildDataFromValues(values: GroupPanelValues) { provider, pluginPanelName, meta, + permissionMap, + fallbackPermissions, }; } @@ -37,8 +39,15 @@ export function buildDataFromValues(values: GroupPanelValues) { * 从群组面板信息中获取面板属性修改相关信息 */ export function pickValuesFromGroupPanelInfo( - groupPanelInfo: GroupPanel + groupPanelInfo: GroupPanel | null ): GroupPanelValues { + if (groupPanelInfo === null) { + return { + name: '', + type: GroupPanelType.TEXT, + }; + } + return { ...groupPanelInfo.meta, name: groupPanelInfo.name, @@ -46,5 +55,7 @@ export function pickValuesFromGroupPanelInfo( groupPanelInfo.type === GroupPanelType.PLUGIN ? String(groupPanelInfo.pluginPanelName) : groupPanelInfo.type, + permissionMap: groupPanelInfo.permissionMap, + fallbackPermissions: groupPanelInfo.fallbackPermissions, }; }