diff --git a/.changeset/neat-symbols-count.md b/.changeset/neat-symbols-count.md new file mode 100644 index 00000000000..340592e0169 --- /dev/null +++ b/.changeset/neat-symbols-count.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/laboratory': patch +'@graphql-hive/render-laboratory': patch +--- + +Enhanced behavior when no collection exists and the user attempts to save an operation, along with +the ability to edit the collection name. diff --git a/packages/libraries/laboratory/src/components/laboratory/collections.tsx b/packages/libraries/laboratory/src/components/laboratory/collections.tsx index 9b831d1bfe3..728aa9ab7f5 100644 --- a/packages/libraries/laboratory/src/components/laboratory/collections.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/collections.tsx @@ -1,12 +1,20 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { + CheckIcon, FolderIcon, FolderOpenIcon, FolderPlusIcon, + PencilIcon, SearchIcon, TrashIcon, XIcon, } from 'lucide-react'; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from '@/components/ui/input-group'; import { TooltipTrigger } from '@radix-ui/react-tooltip'; import type { LaboratoryCollection, LaboratoryCollectionOperation } from '../../lib/collections'; import { cn } from '../../lib/utils'; @@ -44,6 +52,7 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => { addOperation, setActiveOperation, deleteCollection, + updateCollection, deleteOperationFromCollection, addTab, setActiveTab, @@ -51,67 +60,155 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => { } = useLaboratory(); const [isOpen, setIsOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editedName, setEditedName] = useState(props.collection.name); + + const hasActiveOperation = useMemo(() => { + return props.collection.operations.some(operation => operation.id === activeOperation?.id); + }, [props.collection.operations, activeOperation]); + + useEffect(() => { + if (hasActiveOperation) { + setIsOpen(true); + } + }, [hasActiveOperation]); return ( - - - - - - Are you sure you want to delete collection? - - - {props.collection.name} will be permanently deleted. All operations in this - collection will be deleted as well. - - - - Cancel - + + Edit collection + + )} + {checkPermissions?.('collections:delete') && ( + + + + - - - - - - Delete collection - - )} - + + + + + Are you sure you want to delete collection? + + + {props.collection.name} will be permanently deleted. All operations in + this collection will be deleted as well. + + + + Cancel + + + + + + + + Delete collection + + )} + + + )} {isOpen && diff --git a/packages/libraries/laboratory/src/components/laboratory/laboratory.tsx b/packages/libraries/laboratory/src/components/laboratory/laboratory.tsx index 5170d8fab75..44c887be381 100644 --- a/packages/libraries/laboratory/src/components/laboratory/laboratory.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/laboratory.tsx @@ -648,157 +648,6 @@ export const Laboratory = ( ref={setContainer} > - - - - Update endpoint - Update the endpoint of your laboratory. - -
-
{ - e.preventDefault(); - void updateEndpointForm.handleSubmit(); - }} - > - - - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - - return ( - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Enter endpoint" - autoComplete="off" - /> - ); - }} - - -
-
- - - - - - -
-
- - - - - Add collection - - Add a new collection of operations to your laboratory. - - -
-
{ - e.preventDefault(); - void addCollectionForm.handleSubmit(); - }} - > - - - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - Name - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Enter name of the collection" - autoComplete="off" - /> - {isInvalid && } - - ); - }} - - -
-
- - - - - - -
-
- - - - Add test - Add a new test to your laboratory. - -
-
{ - e.preventDefault(); - void addTestForm.handleSubmit(); - }} - > - - - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - Name - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Enter name of the test" - autoComplete="off" - /> - {isInvalid && } - - ); - }} - - -
-
- - - - - - -
-
+ + + + Update endpoint + Update the endpoint of your laboratory. + +
+
{ + e.preventDefault(); + void updateEndpointForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter endpoint" + autoComplete="off" + /> + ); + }} + + +
+
+ + + + + + +
+
+ + + + + Add collection + + Add a new collection of operations to your laboratory. + + +
+
{ + e.preventDefault(); + void addCollectionForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + Name + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter name of the collection" + autoComplete="off" + /> + {isInvalid && } + + ); + }} + + +
+
+ + + + + + +
+
+ + + + Add test + Add a new test to your laboratory. + +
+
{ + e.preventDefault(); + void addTestForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + Name + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter name of the test" + autoComplete="off" + /> + {isInvalid && } + + ); + }} + + +
+
+ + + + + + +
+
diff --git a/packages/libraries/laboratory/src/components/laboratory/operation.tsx b/packages/libraries/laboratory/src/components/laboratory/operation.tsx index 1cbf408c776..e207d960d00 100644 --- a/packages/libraries/laboratory/src/components/laboratory/operation.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/operation.tsx @@ -20,6 +20,7 @@ import { compressToEncodedURIComponent } from 'lz-string'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import { toast } from 'sonner'; import { z } from 'zod'; +import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { QueryPlanSchema } from '@/lib/query-plan/schema'; import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; @@ -45,7 +46,7 @@ import { } from '../ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from '../ui/dropdown-menu'; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty'; -import { Field, FieldGroup, FieldLabel } from '../ui/field'; +import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable'; import { ScrollArea, ScrollBar } from '../ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; @@ -461,6 +462,7 @@ export const Query = (props: { updateActiveOperation, collections, addOperationToCollection, + addCollection, addHistory, stopActiveOperation, addResponseToHistory, @@ -616,15 +618,34 @@ export const Query = (props: { return; } - addOperationToCollection(value.collectionId, { - id: operation.id ?? '', - name: operation.name ?? '', - query: operation.query ?? '', - variables: operation.variables ?? '', - headers: operation.headers ?? '', - extensions: operation.extensions ?? '', - description: '', - }); + const collection = collections.find(c => c.id === value.collectionId); + + if (!collection) { + addCollection({ + name: value.collectionId, + operations: [ + { + id: operation.id ?? '', + name: operation.name ?? '', + query: operation.query ?? '', + variables: operation.variables ?? '', + headers: operation.headers ?? '', + extensions: operation.extensions ?? '', + description: '', + }, + ], + }); + } else { + addOperationToCollection(value.collectionId, { + id: operation.id ?? '', + name: operation.name ?? '', + query: operation.query ?? '', + variables: operation.variables ?? '', + headers: operation.headers ?? '', + extensions: operation.extensions ?? '', + description: '', + }); + } setIsSaveToCollectionDialogOpen(false); }, @@ -668,10 +689,8 @@ export const Query = (props: { - Add collection - - Add a new collection of operations to your laboratory. - + Save operation to collection + Save the current operation to a collection.
- - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - - return ( - - Collection - - - ); - }} - + {collections.length > 0 ? ( + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + Collection + + + ); + }} + + ) : ( + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + New collection name + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter name of the collection" + autoComplete="off" + /> + {isInvalid && } + + ); + }} + + )}
diff --git a/packages/libraries/laboratory/src/components/ui/alert-dialog.tsx b/packages/libraries/laboratory/src/components/ui/alert-dialog.tsx index 2acb5d7fad1..85b60829cdb 100644 --- a/packages/libraries/laboratory/src/components/ui/alert-dialog.tsx +++ b/packages/libraries/laboratory/src/components/ui/alert-dialog.tsx @@ -1,5 +1,6 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import { cn } from '../../lib/utils'; +import { useLaboratory } from '../laboratory/context'; import { buttonVariants } from './button'; function AlertDialog({ ...props }: React.ComponentProps) { @@ -13,7 +14,11 @@ function AlertDialogTrigger({ } function AlertDialogPortal({ ...props }: React.ComponentProps) { - return ; + const { container } = useLaboratory(); + + return ( + + ); } function AlertDialogOverlay({ diff --git a/packages/libraries/laboratory/src/components/ui/button.tsx b/packages/libraries/laboratory/src/components/ui/button.tsx index 845a83693f5..910b9a8641c 100644 --- a/packages/libraries/laboratory/src/components/ui/button.tsx +++ b/packages/libraries/laboratory/src/components/ui/button.tsx @@ -9,7 +9,7 @@ const buttonVariants = cva( variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - '!text-white hover:bg-destructive/90 focus-visible:ring-destructive/40 bg-destructive/60', + '!text-white !hover:bg-destructive/90 !focus-visible:ring-destructive/40 !bg-destructive/60', outline: 'border shadow-sm hover:text-accent-foreground bg-input/30 border-input hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', diff --git a/packages/libraries/laboratory/src/components/ui/combobox.tsx b/packages/libraries/laboratory/src/components/ui/combobox.tsx deleted file mode 100644 index d915eb34ac6..00000000000 --- a/packages/libraries/laboratory/src/components/ui/combobox.tsx +++ /dev/null @@ -1,275 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { CheckIcon, XIcon } from 'lucide-react'; -import { Combobox as ComboboxPrimitive } from '@base-ui/react'; -import { cn } from '../../lib/utils'; -import { useLaboratory } from '../laboratory/context'; -import { Button } from './button'; -import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './input-group'; - -const Combobox = ComboboxPrimitive.Root; - -function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { - return ; -} - -function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) { - return ( - - {children} - - ); -} - -function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { - return ( - } - className={cn(className)} - {...props} - > - - - ); -} - -function ComboboxInput({ - className, - children, - disabled = false, - showTrigger = true, - showClear = false, - ...props -}: ComboboxPrimitive.Input.Props & { - showTrigger?: boolean; - showClear?: boolean; -}) { - return ( - - } {...props} /> - - {showTrigger && ( - - - - )} - {showClear && } - - {children} - - ); -} - -function ComboboxContent({ - className, - side = 'bottom', - sideOffset = 6, - align = 'start', - alignOffset = 0, - anchor, - ...props -}: ComboboxPrimitive.Popup.Props & - Pick< - ComboboxPrimitive.Positioner.Props, - 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor' - >) { - const { container } = useLaboratory(); - - return ( - - - - - - ); -} - -function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { - return ( - - ); -} - -function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) { - return ( - - {children} - - } - > - - - - ); -} - -function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { - return ( - - ); -} - -function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { - return ( - - ); -} - -function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { - return ; -} - -function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { - return ( -