Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: more context menu actions #1135

Merged
merged 4 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface ISpaceActionTrigger {
onRename?: () => void;
onDelete?: () => void;
onSpaceSetting?: () => void;
open?: boolean;
setOpen?: (open: boolean) => void;
}

export const SpaceActionTrigger: React.FC<React.PropsWithChildren<ISpaceActionTrigger>> = (
Expand All @@ -34,6 +36,8 @@ export const SpaceActionTrigger: React.FC<React.PropsWithChildren<ISpaceActionTr
onDelete,
onRename,
onSpaceSetting,
open,
setOpen,
} = props;
const { t } = useTranslation(spaceConfig.i18nNamespaces);
const [deleteConfirm, setDeleteConfirm] = React.useState(false);
Expand All @@ -42,7 +46,7 @@ export const SpaceActionTrigger: React.FC<React.PropsWithChildren<ISpaceActionTr
}
return (
<>
<DropdownMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{showRename && (
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,26 +19,86 @@ interface IProps {
export const SpaceItem: React.FC<IProps> = ({ space, isActive }) => {
const { id, name } = space;
const ref = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const queryClient = useQueryClient();
const inputRef = useRef<HTMLInputElement>(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 (
<ItemButton className="group" isActive={isActive} ref={ref}>
<Link
href={{
pathname: '/space/[spaceId]',
query: {
spaceId: id,
},
}}
title={name}
>
<Component className="size-4 shrink-0" />
<p className="grow truncate">{' ' + name}</p>
<StarButton id={id} type={PinType.Space} />
</Link>
</ItemButton>
<div className="relative overflow-y-auto">
<ItemButton className="group" isActive={isActive} ref={ref}>
<Link
href={{
pathname: '/space/[spaceId]',
query: {
spaceId: id,
},
}}
title={name}
onContextMenu={() => setOpen(true)}
onDoubleClick={() => hasPermission(space.role, 'space|update') && setIsEditing(true)}
>
<Component className="size-4 shrink-0" />
<p className="grow truncate">{' ' + name}</p>
<StarButton id={id} type={PinType.Space} />

<SpaceOperation
space={space}
onRename={() => setIsEditing(true)}
open={open}
setOpen={setOpen}
className="size-4 shrink-0 sm:opacity-0 sm:group-hover:opacity-100"
/>
</Link>
</ItemButton>
{isEditing && (
<Input
ref={inputRef}
type="text"
placeholder="name"
defaultValue={space.name}
style={{
boxShadow: 'none',
}}
className="round-none absolute left-0 top-0 size-full cursor-text bg-background px-4 outline-none"
onKeyDown={async (e) => {
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);
}
}}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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
<div
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<SpaceActionTrigger
space={space}
showRename={menuPermission.spaceUpdate}
showDelete={menuPermission.spaceDelete}
showSpaceSetting={menuPermission.spaceUpdate}
onDelete={() => deleteSpaceMutator(space.id)}
onRename={onRename}
onSpaceSetting={onSpaceSetting}
open={open}
setOpen={setOpen}
>
<div>
<MoreHorizontal className={className} />
</div>
</SpaceActionTrigger>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -13,10 +14,12 @@ interface IProps {
isActive: boolean;
isDragging?: boolean;
className?: string;
open?: boolean;
}

export const TableListItem: React.FC<IProps> = ({ table, isActive, className, isDragging }) => {
const [isEditing, setIsEditing] = useState(false);
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
const { baseId } = router.query;
Expand All @@ -43,6 +46,13 @@ export const TableListItem: React.FC<IProps> = ({ 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 (
<>
<Button
Expand All @@ -57,6 +67,7 @@ export const TableListItem: React.FC<IProps> = ({ table, isActive, className, is
}
)}
onClick={navigateHandler}
onContextMenu={() => setOpen(true)}
>
<div>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
Expand Down Expand Up @@ -86,6 +97,8 @@ export const TableListItem: React.FC<IProps> = ({ table, isActive, className, is
table={table}
className="size-4 shrink-0 sm:opacity-0 sm:group-hover:opacity-100"
onRename={() => setIsEditing(true)}
open={open}
setOpen={setOpen}
/>
)}
</div>
Expand All @@ -100,12 +113,6 @@ export const TableListItem: React.FC<IProps> = ({ table, isActive, className, is
boxShadow: 'none',
}}
className="round-none absolute left-0 top-0 size-full cursor-text bg-background px-4 outline-none"
onBlur={(e) => {
if (e.target.value && e.target.value !== table.name) {
table.updateName(e.target.value);
}
setIsEditing(false);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.currentTarget.value && e.currentTarget.value !== table.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ interface ITableOperationProps {
className?: string;
table: Table;
onRename?: () => void;
open?: boolean;
setOpen?: (open: boolean) => void;
}

export const TableOperation = (props: ITableOperationProps) => {
const { table, className, onRename } = props;
const { table, className, onRename, open, setOpen } = props;
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [importVisible, setImportVisible] = useState(false);
const [importType, setImportType] = useState(SUPPORTEDTYPE.CSV);
Expand Down Expand Up @@ -91,7 +93,7 @@ export const TableOperation = (props: ITableOperationProps) => {
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onMouseDown={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<div>
<MoreHorizontal className={className} />
Expand Down
Loading
Loading