From c5c9b86c69a7cd006fcd4e79be619821492511c6 Mon Sep 17 00:00:00 2001 From: kpodp0ra Date: Wed, 4 Dec 2024 08:38:05 +0100 Subject: [PATCH 1/4] feat: more context menu actions --- .../space/component/SpaceActionTrigger.tsx | 6 +- .../blocks/space/space-side-bar/SpaceItem.tsx | 100 ++++++++++++++---- .../space/space-side-bar/SpaceOperation.tsx | 72 +++++++++++++ .../app/blocks/table-list/TableListItem.tsx | 18 ++-- .../app/blocks/table-list/TableOperation.tsx | 6 +- .../app/blocks/view/list/ViewListItem.tsx | 27 +++-- 6 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceOperation.tsx diff --git a/apps/nextjs-app/src/features/app/blocks/space/component/SpaceActionTrigger.tsx b/apps/nextjs-app/src/features/app/blocks/space/component/SpaceActionTrigger.tsx index cbae152da5..000cc5c1ff 100644 --- a/apps/nextjs-app/src/features/app/blocks/space/component/SpaceActionTrigger.tsx +++ b/apps/nextjs-app/src/features/app/blocks/space/component/SpaceActionTrigger.tsx @@ -20,6 +20,8 @@ interface ISpaceActionTrigger { onRename?: () => void; onDelete?: () => void; onSpaceSetting?: () => void; + open?: boolean; + setOpen?: (open: boolean) => void; } export const SpaceActionTrigger: React.FC> = ( @@ -34,6 +36,8 @@ export const SpaceActionTrigger: React.FC - + {children} {showRename && ( diff --git a/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceItem.tsx b/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceItem.tsx index 21de77cce9..5304afbad8 100644 --- a/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceItem.tsx +++ b/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceItem.tsx @@ -1,8 +1,13 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { hasPermission } from '@teable/core'; import { Component } from '@teable/icons'; -import { PinType, type IGetSpaceVo } from '@teable/openapi'; +import { PinType, type IGetSpaceVo, updateSpace } from '@teable/openapi'; +import { ReactQueryKeys } from '@teable/sdk'; +import { Input } from '@teable/ui-lib'; import Link from 'next/link'; -import { useRef } from 'react'; -import { useMount } from 'react-use'; +import { useEffect, useRef, useState } from 'react'; +import { useClickAway, useMount } from 'react-use'; +import { SpaceOperation } from '@/features/app/blocks/space/space-side-bar/SpaceOperation'; import { ItemButton } from './ItemButton'; import { StarButton } from './StarButton'; @@ -14,26 +19,85 @@ interface IProps { export const SpaceItem: React.FC = ({ space, isActive }) => { const { id, name } = space; const ref = useRef(null); + const [open, setOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const queryClient = useQueryClient(); + const inputRef = useRef(null); + + const { mutateAsync: updateSpaceMutator } = useMutation({ + mutationFn: updateSpace, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ReactQueryKeys.spaceList() }); + queryClient.invalidateQueries({ queryKey: ReactQueryKeys.space(space.id) }); + }, + }); + + useEffect(() => { + if (isEditing) setTimeout(() => inputRef.current?.focus()); + }, [isEditing]); + + useClickAway(inputRef, async () => { + if (isEditing && inputRef.current?.value && inputRef.current.value !== space.name) + await updateSpaceMutator({ + spaceId: space.id, + updateSpaceRo: { name: inputRef.current.value }, + }); + setIsEditing(false); + }); useMount(() => { isActive && ref.current?.scrollIntoView({ block: 'center' }); }); return ( - - - -

{' ' + name}

- - -
+
+ + setOpen(true)} + onDoubleClick={() => hasPermission(space.role, 'space|update') && setIsEditing(true)} + > + +

{' ' + name}

+ + + setIsEditing(true)} + open={open} + setOpen={setOpen} + className="size-4 shrink-0 sm:opacity-0 sm:group-hover:opacity-100" + /> + +
+ {isEditing && ( + { + if (e.key === 'Enter') { + if (e.currentTarget.value && e.currentTarget.value !== space.name) + await updateSpaceMutator({ + spaceId: space.id, + updateSpaceRo: { name: e.currentTarget.value }, + }); + setIsEditing(false); + } + }} + /> + )} +
); }; diff --git a/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceOperation.tsx b/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceOperation.tsx new file mode 100644 index 0000000000..99f5372d6d --- /dev/null +++ b/apps/nextjs-app/src/features/app/blocks/space/space-side-bar/SpaceOperation.tsx @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { hasPermission } from '@teable/core'; +import { MoreHorizontal } from '@teable/icons'; +import { deleteSpace, type IGetSpaceVo } from '@teable/openapi'; +import { ReactQueryKeys } from '@teable/sdk/config'; +import { useRouter } from 'next/router'; +import React, { useMemo } from 'react'; +import { SpaceActionTrigger } from '@/features/app/blocks/space/component/SpaceActionTrigger'; + +interface ISpaceOperationProps { + className?: string; + space: IGetSpaceVo; + onRename?: () => void; + open?: boolean; + setOpen?: (open: boolean) => void; +} + +export const SpaceOperation = (props: ISpaceOperationProps) => { + const { space, className, onRename, open, setOpen } = props; + const queryClient = useQueryClient(); + const router = useRouter(); + const menuPermission = useMemo(() => { + return { + spaceUpdate: hasPermission(space.role, 'space|update'), + spaceDelete: hasPermission(space.role, 'space|delete'), + }; + }, [space.role]); + + const { mutate: deleteSpaceMutator } = useMutation({ + mutationFn: deleteSpace, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ReactQueryKeys.spaceList() }); + }, + }); + + const onSpaceSetting = () => { + router.push({ + pathname: '/space/[spaceId]/setting/general', + query: { spaceId: space.id }, + }); + }; + + if (!Object.values(menuPermission).some(Boolean)) { + return null; + } + + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions +
{ + e.stopPropagation(); + e.preventDefault(); + }} + > + deleteSpaceMutator(space.id)} + onRename={onRename} + onSpaceSetting={onSpaceSetting} + open={open} + setOpen={setOpen} + > +
+ +
+
+
+ ); +}; diff --git a/apps/nextjs-app/src/features/app/blocks/table-list/TableListItem.tsx b/apps/nextjs-app/src/features/app/blocks/table-list/TableListItem.tsx index 848124c737..9f29cc8bb6 100644 --- a/apps/nextjs-app/src/features/app/blocks/table-list/TableListItem.tsx +++ b/apps/nextjs-app/src/features/app/blocks/table-list/TableListItem.tsx @@ -4,6 +4,7 @@ import { Button, cn } from '@teable/ui-lib/shadcn'; import { Input } from '@teable/ui-lib/shadcn/ui/input'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { useClickAway } from 'react-use'; import { Emoji } from '../../components/emoji/Emoji'; import { EmojiPicker } from '../../components/emoji/EmojiPicker'; import { TableOperation } from './TableOperation'; @@ -13,10 +14,12 @@ interface IProps { isActive: boolean; isDragging?: boolean; className?: string; + open?: boolean; } export const TableListItem: React.FC = ({ table, isActive, className, isDragging }) => { const [isEditing, setIsEditing] = useState(false); + const [open, setOpen] = useState(false); const inputRef = useRef(null); const router = useRouter(); const { baseId } = router.query; @@ -43,6 +46,12 @@ export const TableListItem: React.FC = ({ table, isActive, className, is } }, [isEditing]); + useClickAway(inputRef, () => { + if (isEditing && inputRef.current?.value && inputRef.current.value !== table.name) + table.updateName(inputRef.current.value); + setIsEditing(false); + }); + return ( <> -
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
ev.stopPropagation()}> {permission['view|update'] && ( )} @@ -172,7 +172,7 @@ export const ViewListItem: React.FC = ({ view, removable, isActive }) => }} className="flex justify-start" > - + {t('import.menu.downAsCsv')} )} @@ -205,7 +205,7 @@ export const ViewListItem: React.FC = ({ view, removable, isActive }) => deleteView(); }} > - + {t('view.action.delete')} From a6a78a764eef484053ec5a8c0e007e08554e9f9c Mon Sep 17 00:00:00 2001 From: caoxing Date: Thu, 19 Dec 2024 11:35:07 +0800 Subject: [PATCH 4/4] fix: popover flash left top corner --- .../app/blocks/view/list/ViewListItem.tsx | 106 +++++++++--------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx b/apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx index f1f7352945..7d841a83c5 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx @@ -147,71 +147,73 @@ export const ViewListItem: React.FC = ({ view, removable, isActive }) => {commonPart} )} - - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} -
ev.stopPropagation()}> - {permission['view|update'] && ( - - )} - {view.type === 'grid' && permission['view|read'] && ( - - )} - {permission['view|create'] && ( - <> + {open && ( + + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
ev.stopPropagation()}> + {permission['view|update'] && ( - - )} - {permission['view|delete'] && ( - <> - + )} + {view.type === 'grid' && permission['view|read'] && ( - - )} -
-
+ )} + {permission['view|create'] && ( + <> + + + )} + {permission['view|delete'] && ( + <> + + + + )} +
+
+ )}