mirror of
https://github.com/supabase/supabase.git
synced 2026-06-08 18:47:20 +08:00
chore(studio): queue operations UI improvements (#42272)
## What kind of change does this PR introduce? UI improvements ## What is the current behavior? @awaseem added a schmick new “Queue table operations” feature in https://github.com/supabase/supabase/pull/42120 ## What is the new behavior? This adds some UI polish to that feature. ## Additional context https://github.com/user-attachments/assets/0b823bc9-44bd-42d1-8042-162084d058c7 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI/UX Improvements** * Repositioned and animated floating save/review bar for smoother entrance and centered alignment * Card-based redesign of operation items with clearer old/new value badges and improved action controls * Updated side panel text, button labels ("Review", "Save/Revert" with "all" when applicable), and focus behavior * Refined empty-state layout and amber background highlighting for modified cells * Improved spacing, padding, and visual polish across the operation queue UI <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
@@ -2,11 +2,10 @@ import { useOperationQueueActions } from 'components/grid/hooks/useOperationQueu
|
||||
import { useOperationQueueShortcuts } from 'components/grid/hooks/useOperationQueueShortcuts'
|
||||
import { useIsQueueOperationsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Eye } from 'lucide-react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getModKeyLabel } from '@/lib/helpers'
|
||||
|
||||
export const SaveQueueActionBar = () => {
|
||||
@@ -14,6 +13,7 @@ export const SaveQueueActionBar = () => {
|
||||
const snap = useTableEditorStateSnapshot()
|
||||
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
|
||||
const { handleSave } = useOperationQueueActions()
|
||||
const [leftPosition, setLeftPosition] = useState<string>('50%')
|
||||
|
||||
useOperationQueueShortcuts()
|
||||
|
||||
@@ -24,42 +24,80 @@ export const SaveQueueActionBar = () => {
|
||||
const isVisible =
|
||||
isQueueOperationsEnabled && snap.hasPendingOperations && !isOperationQueuePanelOpen
|
||||
|
||||
// Center position relative to grid container (viewport alignment)
|
||||
useEffect(() => {
|
||||
if (!isVisible) return
|
||||
|
||||
const gridContainer = document.querySelector('.sb-grid')
|
||||
const updatePosition = () => {
|
||||
if (!gridContainer) {
|
||||
setLeftPosition('50%')
|
||||
return
|
||||
}
|
||||
|
||||
const gridRect = gridContainer.getBoundingClientRect()
|
||||
const gridCenter = gridRect.left + gridRect.width / 2
|
||||
setLeftPosition(`${gridCenter}px`)
|
||||
}
|
||||
|
||||
updatePosition()
|
||||
|
||||
if (!gridContainer) return
|
||||
|
||||
const resizeObserver = new ResizeObserver(updatePosition)
|
||||
resizeObserver.observe(gridContainer)
|
||||
|
||||
window.addEventListener('resize', updatePosition)
|
||||
window.addEventListener('scroll', updatePosition, true)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', updatePosition)
|
||||
window.removeEventListener('scroll', updatePosition, true)
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
const content = (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50"
|
||||
<div
|
||||
className="fixed bottom-12 z-50 transform-gpu will-change-transform"
|
||||
style={{ left: leftPosition, transform: 'translateX(-50%)' }}
|
||||
>
|
||||
<div className="flex items-center gap-8 px-4 py-3 bg-surface-100 border rounded-lg shadow-lg">
|
||||
<span className="text-sm text-foreground">
|
||||
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => snap.toggleViewOperationQueue()}
|
||||
className="text-foreground-light hover:text-foreground transition-colors flex items-center"
|
||||
aria-label="View Details"
|
||||
>
|
||||
<Eye size={14} />
|
||||
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}.`}</span>
|
||||
</button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
Save
|
||||
<span className="text-foreground-lighter text-[10px] ml-1">{`${modKey}S`}</span>
|
||||
</Button>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 16 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 420,
|
||||
damping: 30,
|
||||
mass: 0.4,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-8 pl-4 pr-2 py-2 bg-surface-100 border rounded-lg shadow-lg">
|
||||
<span className="text-xs text-foreground-light max-w-40 truncate">
|
||||
{operationCount} pending change{operationCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" size="tiny" onClick={() => snap.toggleViewOperationQueue()}>
|
||||
Review{' '}
|
||||
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}.`}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
Save{operationCount > 1 && ' all'}
|
||||
<span className="text-[10px] text-foreground/40 ml-1.5">{`${modKey}S`}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Undo2 } from 'lucide-react'
|
||||
import { tableRowKeys } from 'data/table-rows/keys'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { AddRowPayload } from '@/state/table-editor-operation-queue.types'
|
||||
import { Card, CardContent, CardHeader } from 'ui'
|
||||
|
||||
interface AddRowOperationItemProps {
|
||||
operationId: string
|
||||
@@ -47,40 +48,52 @@ export const AddRowOperationItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-brand-500">
|
||||
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="pt-2.5 flex flex-row gap-2">
|
||||
<div className="min-w-0 flex-1 flex items-start gap-2">
|
||||
<Plus size={14} className="text-brand-500 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
|
||||
<div className="text-sm text-foreground-muted mt-0.5">
|
||||
<span className="font-medium text-foreground">New row</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
|
||||
{fullTableName}
|
||||
</code>
|
||||
<div className="text-xs text-foreground mt-1 ml-0.5">
|
||||
<span>New row</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<X size={14} />}
|
||||
aria-label="Revert change"
|
||||
className="px-1.5"
|
||||
icon={<Undo2 />}
|
||||
onClick={handleDelete}
|
||||
className="shrink-0"
|
||||
aria-label="Remove operation"
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
align: 'end',
|
||||
text: 'Revert change',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<div className="px-3 py-2 text-xs font-mono space-y-1 bg-brand-100/30">
|
||||
<CardContent className="font-mono text-xs space-y-1 text-brand-link">
|
||||
{previewColumns.map(([key, value]) => (
|
||||
<div key={key} className="flex items-start gap-2 text-foreground">
|
||||
<span className="text-foreground-light">{key}:</span>
|
||||
<span className="truncate" title={formatOperationItemValue(value)}>
|
||||
<div key={key} className="flex gap-2 py-0.5">
|
||||
<span className="text-brand-link select-none font-medium">+</span>
|
||||
<span className="shrink-0">{key}:</span>
|
||||
<span className="truncate min-w-0" title={formatOperationItemValue(value)}>
|
||||
{formatOperationItemValue(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<div className="text-foreground-light">+{remainingCount} more column(s)</div>
|
||||
<div className="flex gap-2 py-0.5">
|
||||
<span className="text-brand-link select-none font-medium">+</span>
|
||||
<span>+{remainingCount} more column(s)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Trash2, X } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import { Undo2 } from 'lucide-react'
|
||||
import { tableRowKeys } from 'data/table-rows/keys'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
|
||||
|
||||
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
|
||||
import { Card, CardContent, CardHeader } from 'ui'
|
||||
|
||||
interface DeleteRowOperationItemProps {
|
||||
operationId: string
|
||||
@@ -45,32 +46,40 @@ export const DeleteRowOperationItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-md overflow-hidden bg-surface-100 border-l-4 border-l-destructive-500">
|
||||
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
|
||||
<Card className="overflow-hidden border-destructive-500 bg-destructive-500/5">
|
||||
<CardHeader className="pt-2.5 flex flex-row gap-2 border-b border-destructive-500">
|
||||
<div className="min-w-0 flex-1 flex items-start gap-2">
|
||||
<Trash2 size={14} className="text-destructive-500 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
|
||||
<div className="text-sm text-foreground-muted mt-0.5">
|
||||
<span className="font-medium text-foreground">Delete row</span>
|
||||
<span className="text-foreground-muted mx-2">·</span>
|
||||
<span className="text-foreground text-xs">where {whereClause}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
|
||||
{fullTableName}
|
||||
</code>
|
||||
<div className="text-xs text-foreground mt-1 ml-0.5">
|
||||
<span>Delete row</span>
|
||||
<span className="text-foreground-muted mx-1.5">·</span>
|
||||
<span>where {whereClause}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<X size={14} />}
|
||||
aria-label="Revert change"
|
||||
className="px-1.5"
|
||||
icon={<Undo2 />}
|
||||
onClick={handleDelete}
|
||||
className="shrink-0"
|
||||
aria-label="Remove operation"
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
align: 'end',
|
||||
text: 'Revert change',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<div className="px-3 py-2 text-xs font-mono bg-destructive-100/30">
|
||||
<div className="text-destructive-500 line-through opacity-70">Row will be deleted</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="font-mono text-xs bg-destructive-100/30">
|
||||
<div className="text-destructive py-0.5">Row will be deleted</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Undo2 } from 'lucide-react'
|
||||
import { tableRowKeys } from 'data/table-rows/keys'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { X } from 'lucide-react'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
|
||||
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { EditCellContentPayload } from '@/state/table-editor-operation-queue.types'
|
||||
import { Card, CardContent, CardHeader } from 'ui'
|
||||
|
||||
interface OperationItemProps {
|
||||
operationId: string
|
||||
@@ -44,42 +45,50 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-md overflow-hidden bg-surface-100">
|
||||
<div className="px-3 py-2 border-b border-default bg-surface-200 flex items-start justify-between gap-2">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="pt-2.5 flex flex-row gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs text-foreground font-mono">{fullTableName}</div>
|
||||
<div className="text-sm text-foreground-muted mt-0.5">
|
||||
<span className="font-medium text-foreground">{columnName}</span>
|
||||
<span className="text-foreground-muted mx-2">•</span>
|
||||
<span className="text-foreground text-xs">where {whereClause}</span>
|
||||
<code className="text-code-inline dark:bg-surface-300 dark:border-foreground-muted/50">
|
||||
{fullTableName}
|
||||
</code>
|
||||
<div className="text-xs text-foreground mt-1 ml-0.5">
|
||||
<span>{columnName}</span>
|
||||
<span className="text-foreground-muted mx-1.5">·</span>
|
||||
<span>where {whereClause}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<X size={14} />}
|
||||
aria-label="Revert change"
|
||||
className="px-1.5"
|
||||
icon={<Undo2 />}
|
||||
onClick={handleDelete}
|
||||
className="shrink-0 w-7"
|
||||
aria-label="Remove operation"
|
||||
tooltip={{ content: { side: 'bottom', text: 'Remove operation' } }}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
align: 'end',
|
||||
text: 'Revert change',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<div className="font-mono text-xs">
|
||||
<div className="flex items-start gap-2 px-3 py-0.5 bg-red-400/20">
|
||||
<span className="text-red-900 select-none font-bold">-</span>
|
||||
<span className="text-red-900 truncate max-w-full" title={formattedOldValue}>
|
||||
<CardContent className="font-mono text-xs">
|
||||
<div className="flex gap-2 py-0.5">
|
||||
<span className="text-destructive select-none font-medium">-</span>
|
||||
<span className="text-destructive truncate max-w-full" title={formattedOldValue}>
|
||||
{formattedOldValue}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 px-3 py-0.5 bg-green-400/20">
|
||||
<span className="text-green-900 select-none font-bold">+</span>
|
||||
<span className="text-green-900 truncate max-w-full" title={formattedNewValue}>
|
||||
<div className="flex gap-2 py-0.5">
|
||||
<span className="text-brand-link select-none font-medium">+</span>
|
||||
<span className="text-brand-link truncate max-w-full" title={formattedNewValue}>
|
||||
{formattedNewValue}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ interface OperationListProps {
|
||||
|
||||
export const OperationList = ({ operations }: OperationListProps) => {
|
||||
if (operations.length === 0) {
|
||||
return <p className="text-sm text-foreground-light">No pending changes</p>
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-base text-foreground-muted">No pending changes</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const addOperations = operations.filter(isAddRowOperation)
|
||||
@@ -23,12 +27,13 @@ export const OperationList = ({ operations }: OperationListProps) => {
|
||||
const editOperations = operations.filter(isEditCellContentOperation)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{deleteOperations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-foreground-light">
|
||||
Rows to Delete ({deleteOperations.length})
|
||||
<h3 className="text-xs text-foreground-lighter">
|
||||
{deleteOperations.length} row deletion{deleteOperations.length !== 1 ? 's' : ''}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{deleteOperations.map((op) => (
|
||||
<DeleteRowOperationItem
|
||||
@@ -44,8 +49,8 @@ export const OperationList = ({ operations }: OperationListProps) => {
|
||||
|
||||
{addOperations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-foreground-light">
|
||||
Rows to Add ({addOperations.length})
|
||||
<h3 className="text-xs text-foreground-lighter">
|
||||
{addOperations.length} row addition{addOperations.length !== 1 ? 's' : ''}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{addOperations.map((op) => (
|
||||
@@ -62,9 +67,10 @@ export const OperationList = ({ operations }: OperationListProps) => {
|
||||
|
||||
{editOperations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-foreground-light">
|
||||
Cell Edits ({editOperations.length})
|
||||
<h3 className="text-xs text-foreground-lighter">
|
||||
{editOperations.length} cell edit{editOperations.length !== 1 ? 's' : ''}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{editOperations.map((op) => (
|
||||
<OperationItem
|
||||
|
||||
@@ -27,11 +27,12 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
|
||||
size="large"
|
||||
visible={visible}
|
||||
onCancel={closePanel}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()} // Prevent focus on first focussable element since it is the revert changes ButtonTooltip
|
||||
header={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>Pending changes</span>
|
||||
<span className="text-xs text-foreground-light">
|
||||
<span className="text-xs text-foreground-lighter">
|
||||
{operations.length} operation{operations.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -41,25 +42,29 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
|
||||
<div className="flex w-full justify-between border-t border-default px-3 py-4">
|
||||
<Button type="default" onClick={closePanel}>
|
||||
Close
|
||||
<span className="text-foreground-lighter text-xs ml-1.5">{modKey}.</span>
|
||||
<span className="text-foreground/40 text-[10px] ml-1.5">{modKey}.</span>
|
||||
</Button>
|
||||
<div className="flex space-x-3">
|
||||
<Button type="default" onClick={handleCancel} disabled={isSaving}>
|
||||
Cancel All
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving || operations.length === 0}
|
||||
>
|
||||
Revert{operations.length > 1 && ' all'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || operations.length === 0}
|
||||
loading={isSaving}
|
||||
>
|
||||
Save All
|
||||
<span className="text-foreground-lighter text-xs ml-1.5">{modKey}S</span>
|
||||
Save{operations.length > 1 && ' all'}
|
||||
<span className="text-foreground/40 text-[10px] ml-1.5">{modKey}S</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SidePanel.Content className="py-4">
|
||||
<SidePanel.Content className="py-4 h-full">
|
||||
<OperationList operations={operations} />
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
|
||||
@@ -36,17 +36,13 @@
|
||||
box-shadow: inset 0 0 0 1px #24b47e;
|
||||
}
|
||||
|
||||
// Cell with unsaved changes - yellow/amber border
|
||||
// Cell with unsaved changes - warning (amber) background/text color
|
||||
.rdg-cell.rdg-cell--dirty {
|
||||
box-shadow: inset 0 0 0 2px hsl(var(--warning-default));
|
||||
background-color: hsl(var(--warning-300) / 0.75);
|
||||
color: hsl(var(--warning-default));
|
||||
}
|
||||
|
||||
// When a dirty cell is also selected, keep the amber border
|
||||
.rdg-cell.rdg-cell--dirty[aria-selected='true'] {
|
||||
box-shadow: inset 0 0 0 2px hsl(var(--warning-default));
|
||||
}
|
||||
|
||||
// Row pending addition - green background
|
||||
// Row pending addition - green background, green text
|
||||
.rdg-row.rdg-row--added {
|
||||
background-color: hsl(var(--brand-200) / 0.3);
|
||||
|
||||
@@ -56,32 +52,33 @@
|
||||
|
||||
.rdg-cell {
|
||||
border-left-color: hsl(var(--brand-default));
|
||||
color: hsl(var(--brand-link));
|
||||
}
|
||||
|
||||
// First cell gets a stronger left border to indicate new row
|
||||
.rdg-cell:first-child {
|
||||
box-shadow: inset 3px 0 0 0 hsl(var(--brand-default));
|
||||
box-shadow: inset 2px 0 0 0 hsl(var(--brand-default));
|
||||
}
|
||||
}
|
||||
|
||||
// Row pending deletion - red background with strikethrough effect
|
||||
// Row pending deletion - red background with strikethrough effect, red text
|
||||
.rdg-row.rdg-row--deleted {
|
||||
background-color: hsl(var(--destructive-200) / 0.3);
|
||||
opacity: 0.6;
|
||||
background-color: hsl(var(--destructive-300) / 0.3);
|
||||
|
||||
&:hover {
|
||||
background-color: hsl(var(--destructive-200) / 0.5);
|
||||
background-color: hsl(var(--destructive-300) / 0.5);
|
||||
}
|
||||
|
||||
.rdg-cell {
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: hsl(var(--destructive-default));
|
||||
border-left-color: hsl(var(--destructive-default));
|
||||
color: hsl(var(--destructive-default));
|
||||
}
|
||||
|
||||
// First cell gets a stronger left border to indicate deleted row
|
||||
.rdg-cell:first-child {
|
||||
box-shadow: inset 3px 0 0 0 hsl(var(--destructive-default));
|
||||
box-shadow: inset 2px 0 0 0 hsl(var(--destructive-default));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +86,6 @@
|
||||
@apply box-border select-none overflow-x-auto overflow-y-scroll bg-dash-canvas;
|
||||
@apply border-t border-r-0 border-l-0;
|
||||
contain: strict;
|
||||
-webkit-user-select: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,15 +86,15 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('1 pending change')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
|
||||
await expect(sidePanel.getByTitle('original value')).toBeVisible()
|
||||
await expect(sidePanel.getByTitle('edited value')).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'edited value' })).toBeVisible()
|
||||
@@ -126,9 +126,9 @@ test.describe('Queue Table Operations', () => {
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText('1 pending change')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel All' }).click()
|
||||
await page.getByRole('button', { name: 'Revert', exact: true }).click()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible()
|
||||
await expect(page.getByRole('gridcell', { name: 'should be cancelled' })).not.toBeVisible()
|
||||
@@ -161,14 +161,14 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
|
||||
await expect(sidePanel.getByText('New row', { exact: true })).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
|
||||
@@ -202,13 +202,13 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('2 pending changes')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('2 operations')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Rows to Add (2)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('2 row additions')).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'row one' })).toBeVisible()
|
||||
@@ -243,15 +243,15 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('2 pending changes')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
const removeButtons = sidePanel.getByRole('button', { name: 'Remove operation' })
|
||||
const removeButtons = sidePanel.getByRole('button', { name: 'Revert change' })
|
||||
await removeButtons.last().click()
|
||||
|
||||
await expect(sidePanel.getByText('1 operation')).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'keep this row' })).toBeVisible()
|
||||
@@ -285,7 +285,7 @@ test.describe('Queue Table Operations', () => {
|
||||
await expect(page.getByRole('dialog').getByText('Pending changes')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('ControlOrMeta+.')
|
||||
await expect(page.getByRole('button', { name: 'View Details' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /Review/ })).toBeVisible()
|
||||
|
||||
await page.keyboard.press('ControlOrMeta+s')
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
@@ -318,14 +318,14 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('1 pending change')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Delete row', { exact: true })).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible()
|
||||
@@ -354,8 +354,8 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('1 pending change')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: 'Cancel All' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
await page.getByRole('button', { name: 'Revert', exact: true }).click()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'should not be deleted' })).toBeVisible()
|
||||
await expect(page.getByText('pending change')).not.toBeVisible()
|
||||
@@ -397,14 +397,14 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('3 pending changes')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('3 operations')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Rows to Delete (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Rows to Add (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('Cell Edits (1)')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
|
||||
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('gridcell', { name: 'edited row' })).toBeVisible()
|
||||
@@ -447,11 +447,11 @@ test.describe('Queue Table Operations', () => {
|
||||
|
||||
await expect(page.getByText('2 pending changes')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'View Details' }).click()
|
||||
await page.getByRole('button', { name: /Review/ }).click()
|
||||
const sidePanel = page.getByRole('dialog')
|
||||
await expect(sidePanel.getByText('2 operations')).toBeVisible()
|
||||
|
||||
await sidePanel.getByRole('button', { name: /Save All/ }).click()
|
||||
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
||||
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
|
||||
|
||||
Reference in New Issue
Block a user