mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
330 lines
9.9 KiB
TypeScript
330 lines
9.9 KiB
TypeScript
import { Kbd } from 'components/ui/DataTable/primitives/Kbd'
|
|
import { GripVertical, Pencil, PlusIcon, Star, X } from 'lucide-react'
|
|
import {
|
|
Button,
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { type QuickActionOption, useQuickActionOptions } from './quick-actions.utils'
|
|
import { DndContext, type DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
verticalListSortingStrategy,
|
|
useSortable,
|
|
} from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import { useEffect, useState } from 'react'
|
|
import { useSendEventMutation } from '../../../../data/telemetry/send-event-mutation'
|
|
import { useParams } from 'common'
|
|
import { useSelectedOrganizationQuery } from '../../../../hooks/misc/useSelectedOrganization'
|
|
|
|
const KbdSlot = ({ option }: { option: QuickActionOption }) => (
|
|
<div>
|
|
{option.kbd?.map((k) => (
|
|
<Kbd key={`kbd-${k}`} className="text-foreground-lighter">
|
|
{k}
|
|
</Kbd>
|
|
))}
|
|
</div>
|
|
)
|
|
|
|
interface SortableActionItemProps {
|
|
action: QuickActionOption
|
|
onRemove: (action: QuickActionOption) => void
|
|
}
|
|
|
|
const SortableActionItem = ({ action, onRemove }: SortableActionItemProps) => {
|
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
id: action.id,
|
|
})
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
}
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
className={cn(
|
|
'group/grab cursor-grab active:cursor-grabbing flex items-center gap-2 text-foreground-light hover:text-foreground',
|
|
isDragging && 'opacity-50'
|
|
)}
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
<div className="text-foreground-muted group-hover/grab:text-foreground-lighter hover:bg-muted/50 rounded">
|
|
<GripVertical size={14} />
|
|
</div>
|
|
<action.icon size={14} className="text-foreground-lighter flex-shrink-0" />
|
|
<span className="flex-1">{action.label}</span>
|
|
<Button
|
|
type="text"
|
|
size="tiny"
|
|
onClick={() => onRemove(action)}
|
|
className="w-4 h-4 !p-0 text-foreground-lighter hover:text-foreground"
|
|
>
|
|
<X size={12} />
|
|
</Button>
|
|
</DropdownMenuItem>
|
|
)
|
|
}
|
|
|
|
interface ReorderableListProps {
|
|
selectedActions: QuickActionOption[]
|
|
allActions: QuickActionOption[]
|
|
onReorder: (newOrder: QuickActionOption[]) => void
|
|
onToggleAction: (action: QuickActionOption, isSelected: boolean) => void
|
|
}
|
|
|
|
const ReorderableList = ({
|
|
selectedActions,
|
|
allActions,
|
|
onReorder,
|
|
onToggleAction,
|
|
}: ReorderableListProps) => {
|
|
const unselectedActions = allActions.filter(
|
|
(action) => !selectedActions.some((selected) => selected.id === action.id)
|
|
)
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
})
|
|
)
|
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event
|
|
|
|
if (over && active.id !== over.id) {
|
|
const oldIndex = selectedActions.findIndex((item) => item.id === active.id)
|
|
const newIndex = selectedActions.findIndex((item) => item.id === over.id)
|
|
|
|
if (oldIndex !== -1 && newIndex !== -1) {
|
|
onReorder(arrayMove(selectedActions, oldIndex, newIndex))
|
|
}
|
|
}
|
|
}
|
|
|
|
const removeAction = (action: QuickActionOption) => {
|
|
onToggleAction(action, false)
|
|
}
|
|
|
|
const addAction = (action: QuickActionOption) => {
|
|
onToggleAction(action, true)
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-y-auto">
|
|
{/* Selected Actions */}
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel>Your Quick Actions</DropdownMenuLabel>
|
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
|
<SortableContext
|
|
items={selectedActions.map((action) => action.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{selectedActions.map((action) => (
|
|
<SortableActionItem key={action.id} action={action} onRemove={removeAction} />
|
|
))}
|
|
</SortableContext>
|
|
</DndContext>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
|
|
{/* Unselected Actions */}
|
|
{unselectedActions.length > 0 && (
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel>Other Actions</DropdownMenuLabel>
|
|
<div>
|
|
{unselectedActions.map((action) => (
|
|
<DropdownMenuItem
|
|
key={`unselected-action-${action.id}`}
|
|
className="flex items-center gap-2 cursor-pointer w-full text-left text-foreground-light hover:text-foreground"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
addAction(action)
|
|
}}
|
|
>
|
|
<action.icon size={14} className="text-foreground-lighter flex-shrink-0" />
|
|
<span className="flex-1">{action.label}</span>
|
|
<div className="flex items-center justify-center text-foreground-lighter hover:text-foreground">
|
|
<Star size={12} />
|
|
</div>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</div>
|
|
</DropdownMenuGroup>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const QuickActions = () => {
|
|
const { ref } = useParams()
|
|
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
|
const [open, setOpen] = useState(false)
|
|
const {
|
|
allActions,
|
|
selectedActions,
|
|
setSelectedActions,
|
|
editQuickActions,
|
|
setEditQuickActions,
|
|
saveSelectedActions,
|
|
resetSelectedActions,
|
|
keyboardShortcuts,
|
|
} = useQuickActionOptions({
|
|
organization: selectedOrg?.slug,
|
|
})
|
|
|
|
const { mutate: sendEvent } = useSendEventMutation()
|
|
|
|
const handleReorder = (newOrder: QuickActionOption[]) => setSelectedActions(newOrder)
|
|
|
|
const handleToggleAction = (action: QuickActionOption, isSelected: boolean) => {
|
|
if (isSelected) {
|
|
setSelectedActions([...selectedActions, action])
|
|
} else {
|
|
setSelectedActions(selectedActions.filter((a) => a.id !== action.id))
|
|
}
|
|
}
|
|
|
|
const handleSave = () => {
|
|
saveSelectedActions()
|
|
setEditQuickActions(false)
|
|
}
|
|
|
|
const reset = () => {
|
|
setEditQuickActions(false)
|
|
resetSelectedActions()
|
|
keyboardShortcuts.reset()
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
reset()
|
|
}
|
|
}, [open])
|
|
|
|
// Clear keyboard shortcuts when editing
|
|
useEffect(() => {
|
|
if (editQuickActions) {
|
|
keyboardShortcuts.reset()
|
|
}
|
|
}, [editQuickActions, keyboardShortcuts])
|
|
|
|
return (
|
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
<DropdownMenuTrigger
|
|
onClick={() =>
|
|
// Track quick actions button click
|
|
sendEvent({
|
|
action: 'quick_actions_button_clicked',
|
|
groups: {
|
|
project: ref ?? 'Unknown',
|
|
organization: selectedOrg?.slug ?? 'Unknown',
|
|
},
|
|
})
|
|
}
|
|
className="w-7 h-7 rounded-full hover:!cursor-pointer"
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button type="primary" className="!p-0 rounded-full w-7 h-7">
|
|
<PlusIcon size={14} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
{!open && <TooltipContent>Create</TooltipContent>}
|
|
</Tooltip>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
side="bottom"
|
|
align="end"
|
|
className="flex flex-col w-60 max-h-[calc(100vh-50px)]"
|
|
>
|
|
{editQuickActions ? (
|
|
<ReorderableList
|
|
selectedActions={selectedActions}
|
|
allActions={allActions}
|
|
onReorder={handleReorder}
|
|
onToggleAction={handleToggleAction}
|
|
/>
|
|
) : (
|
|
<div className="overflow-y-auto">
|
|
<DropdownMenuLabel className="cursor-default flex items-center gap-2 w-full">
|
|
<span className="flex-1">Create...</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="tiny"
|
|
type="text"
|
|
className="!p-0 !w-4 !h-4 text-foreground-lighter hover:text-foreground-light"
|
|
onClick={() => setEditQuickActions(true)}
|
|
>
|
|
<Pencil size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Manage quick actions</TooltipContent>
|
|
</Tooltip>
|
|
</DropdownMenuLabel>
|
|
{selectedActions.map((option) => (
|
|
<DropdownMenuItem asChild key={option.label}>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
option.onClick?.({
|
|
organization: selectedOrg?.slug,
|
|
triggerMethod: 'click',
|
|
})
|
|
}
|
|
className="group w-full cursor-pointer flex items-center gap-2 text-foreground-light hover:text-foreground"
|
|
>
|
|
<option.icon size={14} className="text-foreground-lighter" />
|
|
<span className="text-left flex-1">{option.label}</span>
|
|
<KbdSlot option={option} />
|
|
</button>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</div>
|
|
)}
|
|
{editQuickActions && (
|
|
<div className="flex items-center gap-1 w-full border-t pt-1 -mx-1 px-1">
|
|
<Button type="default" onClick={reset} className="w-1/2">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
if (editQuickActions) {
|
|
handleSave()
|
|
} else {
|
|
setEditQuickActions(true)
|
|
}
|
|
}}
|
|
className="w-full"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
|
|
export default QuickActions
|