diff --git a/.github/PULL_REQUEST_TEMPLATE/rc.md b/.github/PULL_REQUEST_TEMPLATE/rc.md index 7547b7757..0df1d0112 100644 --- a/.github/PULL_REQUEST_TEMPLATE/rc.md +++ b/.github/PULL_REQUEST_TEMPLATE/rc.md @@ -109,57 +109,35 @@ I can successfully: I can successfully: -- [ ] Create Range Modal - - [ ] Create a new local range. - - [ ] Create a new persisted range. - - [ ] Set parent range - - [ ] Add labels - - [ ] Rename existing range - - [ ] Change times on existing range -- [ ] Range Layout - - [ ] Rename range. - - [ ] Rename range from tab. - - [ ] Change start and end times. - - [ ] Add labels. - - [ ] Set metadata. - - [ ] Delete metadata. - - [ ] Add child ranges. - - [ ] Open snapshots. - - [ ] Navigate to and from child ranges -- [ ] Search and Command Palette - - [ ] Open an existing range window - - [ ] Open create range dialog -- [ ] Range Toolbar - - [ ] Open create range modal from toolbar link when no range exists - - [ ] Switch the active range by clicking - - [ ] Context Menu - - [ ] Open create range modal - - [ ] Rename range - - [ ] Set active range - - [ ] Open create range modal with child range - - [ ] Add to active line plot - - [ ] Add to new line plot - - [ ] Remove from range toolbar - - [ ] Delete persisted range - - [ ] Copy link of persisted range - - [ ] Save local range to Synnax -- [ ] Resources Toolbar - - [ ] Open the range overview dialog by clicking on a range - - [ ] Context Menu - - [ ] Set active range - - [ ] Rename range - - [ ] Open create range modal with child range - - [ ] Group ranges - - [ ] Add to active line plot - - [ ] Add multiple ranges to active line plot - - [ ] Add to new line plot - - [ ] Add multiple ranges to new line plot - - [ ] Delete range - - [ ] Delete multiple ranges - - [ ] Copy link to range -- [ ] Open a range from its url -- [ ] Make changes to a range with resources toolbar, overview, and ranges toolbar open - and see changes propagate to all three. +- [ ] Open create range dialog from command search bar. +- [ ] Open create range dialog from range toolbar. +- [ ] Open create range dialog from context menu in range toolbar. +- [ ] Create a new local range. +- [ ] Create a new persisted range. +- [ ] Set a parent range for a range. +- [ ] Attach labels to a range. +- [ ] Open the range overview dialog from the resources view. +- [ ] Edit range properties from the overview dialog. +- [ ] Edit meta-data properties on the range. +- [ ] Add child ranges to a range. +- [ ] Navigate to and from child ranges on a range. +- [ ] Make a change to the range in the edit dialog and see the changes propagate to the + overview dialog. +- [ ] Save a local range to Synnax in the range toolbar. +- [ ] Switch the active range in the range toolbar. +- [ ] Load a local range from the search bar. +- [ ] Load a persisted range from the search bar. +- [ ] Rename a range from the range toolbar. +- [ ] Edit a range from the range toolbar. +- [ ] Remove a range from the range toolbar. +- [ ] Delete a persisted range from the range toolbar. +- [ ] Delete a range in the resources view. +- [ ] Delete multiple ranges in the resources view. +- [ ] Set a range as an active range from the resources view. +- [ ] Edit a range from the resources view. +- [ ] Add a range to a plot from the resources view. +- [ ] Copy a link to a range and open it from the resources view. +- [ ] Rename a range from the range toolbar. ### Channels diff --git a/console/src/lineplot/LinePlot.tsx b/console/src/lineplot/LinePlot.tsx index 2146416f9..dd2753e93 100644 --- a/console/src/lineplot/LinePlot.tsx +++ b/console/src/lineplot/LinePlot.tsx @@ -328,7 +328,7 @@ const Loaded = ({ layoutKey }: { layoutKey: string }): ReactElement => { break; case "range": placer( - Range.createLayout({ + Range.createEditLayout({ initial: { timeRange: { start: Number(timeRange.start.valueOf()), diff --git a/console/src/range/CreateLayout.css b/console/src/range/EditLayout.css similarity index 87% rename from console/src/range/CreateLayout.css rename to console/src/range/EditLayout.css index 8e5898e78..95970e620 100644 --- a/console/src/range/CreateLayout.css +++ b/console/src/range/EditLayout.css @@ -9,7 +9,7 @@ * included in the file licenses/APL.txt. */ -.console-range-create-layout { - padding-top: 2rem; +.console-range-edit-layout { + padding-top: 2rem; height: 100%; -} +} \ No newline at end of file diff --git a/console/src/range/CreateLayout.tsx b/console/src/range/EditLayout.tsx similarity index 66% rename from console/src/range/CreateLayout.tsx rename to console/src/range/EditLayout.tsx index 4bb5639f0..7d576bd5b 100644 --- a/console/src/range/CreateLayout.tsx +++ b/console/src/range/EditLayout.tsx @@ -7,16 +7,21 @@ // License, use of this software will be governed by the Apache License, Version 2.0, // included in the file licenses/APL.txt. -import "@/range/CreateLayout.css"; +import "@/range/EditLayout.css"; -import { ontology, type ranger, TimeRange, TimeStamp } from "@synnaxlabs/client"; -import { Icon } from "@synnaxlabs/media"; +import { + ontology, + ranger, + TimeRange, + TimeStamp, + UnexpectedError, +} from "@synnaxlabs/client"; +import { Icon, Logo } from "@synnaxlabs/media"; import { Align, Button, Form, Icon as PIcon, - Input, Nav, Ranger, Status, @@ -24,8 +29,9 @@ import { Text, Triggers, } from "@synnaxlabs/pluto"; +import { Input } from "@synnaxlabs/pluto/input"; import { deep, primitiveIsZero } from "@synnaxlabs/x"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { type ReactElement, useCallback, useRef } from "react"; import { useDispatch } from "react-redux"; import { v4 as uuidv4 } from "uuid"; @@ -34,6 +40,7 @@ import { z } from "zod"; import { CSS } from "@/css"; import { Label } from "@/label"; import { Layout } from "@/layout"; +import { useSelect } from "@/range/selectors"; import { add } from "@/range/slice"; const formSchema = z.object({ @@ -44,31 +51,29 @@ const formSchema = z.object({ parent: z.string().optional(), }); -type FormProps = z.infer; - -type Args = Partial; +type Args = Partial>; -export const CREATE_LAYOUT_TYPE = "editRange"; +export const EDIT_LAYOUT_TYPE = "editRange"; const SAVE_TRIGGER: Triggers.Trigger = ["Control", "Enter"]; -interface CreateLayoutProps extends Partial { +interface CreateEditLayoutProps extends Partial { initial?: Partial; } -export const createLayout = ({ +export const createEditLayout = ({ name, initial = {}, window, ...rest -}: CreateLayoutProps): Layout.State => ({ +}: CreateEditLayoutProps): Layout.State => ({ ...rest, - key: CREATE_LAYOUT_TYPE, - type: CREATE_LAYOUT_TYPE, - windowKey: CREATE_LAYOUT_TYPE, + key: EDIT_LAYOUT_TYPE, + type: EDIT_LAYOUT_TYPE, + windowKey: EDIT_LAYOUT_TYPE, icon: "Range", location: "modal", - name: name ?? "Range.Create", + name: name ?? (initial.key != null ? "Range.Edit" : "Range.Create"), window: { resizable: false, size: { height: 370, width: 700 }, @@ -78,55 +83,99 @@ export const createLayout = ({ args: initial, }); +type DefineRangeFormProps = z.infer; + const parentRangeIcon = ( }> ); -export const Create = (props: Layout.RendererProps): ReactElement => { +export const Edit = (props: Layout.RendererProps): ReactElement => { const { layoutKey } = props; const now = useRef(Number(TimeStamp.now().valueOf())).current; const args = Layout.useSelectArgs(layoutKey); - const initialValues: FormProps = { - name: "", - labels: [], - timeRange: { start: now, end: now }, - parent: "", - ...args, - }; - - return ; + const range = useSelect(args.key); + const client = Synnax.use(); + const isCreate = args.key == null; + const isEdit = !isCreate && (range == null || range.persisted); + const initialValues = useQuery({ + queryKey: ["range-edit", args], + queryFn: async () => { + if (isCreate) + return { + name: "", + labels: [], + timeRange: { start: now, end: now }, + parent: "", + ...args, + }; + if (range == null || range.persisted) { + const key = args.key as string; + if (client == null) throw new UnexpectedError("Client is not available"); + const rng = await client.ranges.retrieve(key); + const parent = await rng.retrieveParent(); + const labels = await rng.labels(); + return { + key: rng.key, + name: rng.name, + timeRange: rng.timeRange.numeric, + labels: labels.map((l) => l.key), + parent: parent?.key ?? "", + }; + } + if (range.variant !== "static") throw new UnexpectedError("Range is not static"); + return { + key: range.key, + name: range.name, + timeRange: range.timeRange, + labels: [], + parent: "", + }; + }, + }); + if (initialValues.isPending) return ; + if (initialValues.isError) throw initialValues.error; + return ( + + ); }; -interface CreateLayoutFormProps extends Layout.RendererProps { - initialValues: FormProps; +interface EditLayoutFormProps extends Layout.RendererProps { + initialValues: DefineRangeFormProps; + isRemoteEdit: boolean; onClose: () => void; } -const CreateLayoutForm = ({ +const EditLayoutForm = ({ initialValues, + isRemoteEdit, onClose, -}: CreateLayoutFormProps): ReactElement => { +}: EditLayoutFormProps): ReactElement => { const methods = Form.use({ values: deep.copy(initialValues), schema: formSchema }); const dispatch = useDispatch(); const client = Synnax.use(); - const clientExists = client != null; const addStatus = Status.useAggregator(); + const isCreate = initialValues.key == null; const { mutate, isPending } = useMutation({ - mutationFn: async (persisted: boolean) => { + mutationFn: async (persist: boolean) => { if (!methods.validate()) return; const values = methods.value(); - const { timeRange: tr, parent } = values; + const { timeRange: tr, parent } = methods.value(); const timeRange = new TimeRange(tr); const name = values.name.trim(); - const key = initialValues.key ?? uuidv4(); + const key = isCreate ? uuidv4() : (initialValues.key as string); + const persisted = persist || isRemoteEdit; const parentID = primitiveIsZero(parent) ? undefined : new ontology.ID({ key: parent as string, type: "range" }); const otgID = new ontology.ID({ key, type: "range" }); - if (persisted && clientExists) { + if (persisted && client != null) { await client.ranges.create({ key, name, timeRange }, { parent: parentID }); await client.labels.label(otgID, values.labels, { replace: true }); } @@ -149,7 +198,7 @@ const CreateLayoutForm = ({ ); return ( - + - path="parent" visible padHelpText={false}> + + path="parent" + visible={isCreate || isRemoteEdit} + padHelpText={false} + > {({ onChange, ...p }) => ( - To Save to Synnax + To Save - mutate(false)} - disabled={isPending} - > - Save Locally - mutate(true)} - disabled={!clientExists || isPending} - tooltip={clientExists ? "Save to Cluster" : "No Cluster Connected"} - tooltipLocation="bottom" - loading={isPending} + disabled={isPending} triggers={[SAVE_TRIGGER]} > - Save to Synnax + Save diff --git a/console/src/range/Select.tsx b/console/src/range/Select.tsx index 82283be28..6e6e8ab09 100644 --- a/console/src/range/Select.tsx +++ b/console/src/range/Select.tsx @@ -24,16 +24,17 @@ import { import { type ReactElement } from "react"; import { Layout } from "@/layout"; -import { createLayout } from "@/range/CreateLayout"; +import { createEditLayout } from "@/range/EditLayout"; import { type Range } from "@/range/slice"; -interface SelectMultipleRangesProps extends Select.MultipleProps {} +export interface SelectMultipleRangesProps + extends Select.MultipleProps {} const dynamicIcon = ( ); -const listColumns: Array> = [ +export const listColumns: Array> = [ { key: "name", name: "Name", @@ -71,7 +72,9 @@ const RenderTag = ({ const renderTag = componentRenderProp(RenderTag); -const SelectMultipleRanges = (props: SelectMultipleRangesProps): ReactElement => ( +export const SelectMultipleRanges = ( + props: SelectMultipleRangesProps, +): ReactElement => ( /> ); -interface SelectSingleRangeProps extends Select.SingleProps {} +export interface SelectSingleRangeProps extends Select.SingleProps {} -const SelectRange = (props: SelectSingleRangeProps): ReactElement => ( +export const SelectRange = (props: SelectSingleRangeProps): ReactElement => ( ); -interface SelectMultipleInputItemProps +export interface SelectMultipleInputItemProps extends Omit, Pick { value: string[]; @@ -94,13 +97,18 @@ interface SelectMultipleInputItemProps } const SelectEmptyContent = (): ReactElement => { - const placer = Layout.usePlacer(); + const newLayout = Layout.usePlacer(); return ( No Ranges: - placer(createLayout({}))}> + { + newLayout(createEditLayout({})); + }} + > Define a Range @@ -123,7 +131,7 @@ export const SelectMultipleInputItem = ({ ); -interface SelectInputItemProps +export interface SelectInputItemProps extends Omit, Input.Control, Pick {} diff --git a/console/src/range/Toolbar.tsx b/console/src/range/Toolbar.tsx index 1c86313b6..6912271e1 100644 --- a/console/src/range/Toolbar.tsx +++ b/console/src/range/Toolbar.tsx @@ -10,7 +10,7 @@ import "@/range/Toolbar.css"; import { Store } from "@reduxjs/toolkit"; -import { type label, ranger, Synnax as Client } from "@synnaxlabs/client"; +import { type label, ranger, Synnax as Client, TimeRange } from "@synnaxlabs/client"; import { Icon } from "@synnaxlabs/media"; import { Align, @@ -40,7 +40,7 @@ import { Layout } from "@/layout"; import { create as createLinePlot } from "@/lineplot/LinePlot"; import { setRanges as setLinePlotRanges } from "@/lineplot/slice"; import { Link } from "@/link"; -import { createLayout } from "@/range/CreateLayout"; +import { createEditLayout } from "@/range/EditLayout"; import { overviewLayout } from "@/range/external"; import { select, useSelect, useSelectMultiple } from "@/range/selectors"; import { @@ -89,13 +89,7 @@ export const addChildRangeMenuItem = ( } > - Create Child Range - -); - -export const deleteMenuItem = ( - } itemKey="delete"> - Delete + Add Child Range ); @@ -105,22 +99,19 @@ export const setAsActiveMenuItem = ( ); -export const viewDetailsMenuItem = ( - } itemKey="details"> - View Details - -); - export const fromClientRange = (ranges: ranger.Range | ranger.Range[]): Range[] => toArray(ranges).map((range) => ({ variant: "static", key: range.key, name: range.name, - timeRange: range.timeRange.numeric, + timeRange: { + start: Number(range.timeRange.start.valueOf()), + end: Number(range.timeRange.end.valueOf()), + }, persisted: true, })); -const fetchIfNotInState = async ( +export const fetchIfNotInState = async ( store: Store, client: Client, key: string, @@ -134,7 +125,7 @@ const fetchIfNotInState = async ( return existing; }; -const useAddToActivePlot = (): ((key: string) => void) => { +export const useAddToActivePlot = (): ((key: string) => void) => { const store = useStore(); const client = Synnax.use(); const addStatus = Status.useAggregator(); @@ -153,12 +144,13 @@ const useAddToActivePlot = (): ((key: string) => void) => { }), ); }, - onError: (e) => + onError: (e) => { addStatus({ variant: "error", message: `Failed to add range to plot`, description: e.message, - }), + }); + }, }).mutate; }; @@ -186,65 +178,49 @@ const useViewDetails = (): ((key: string) => void) => { const useAddToNewPlot = (): ((key: string) => void) => { const store = useStore(); const client = Synnax.use(); - const placer = Layout.usePlacer(); + const placeLayout = Layout.usePlacer(); const addStatus = Status.useAggregator(); return useMutation({ mutationFn: async (key: string) => { if (client == null) return; const res = await fetchIfNotInState(store, client, key); - placer( + placeLayout( createLinePlot({ name: `Plot for ${res.name}`, ranges: { x1: [key], x2: [] }, }), ); }, - onError: (e) => + onError: (e) => { addStatus({ variant: "error", message: `Failed to add range to plot`, description: e.message, - }), + }); + }, }).mutate; }; -interface NoRangesProps { - onLinkClick: (key?: string) => void; -} - -const NoRanges = ({ onLinkClick }: NoRangesProps): ReactElement => { - const handleLinkClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - onLinkClick(); - }; - - return ( - - - No ranges added. - - Add a range - - - - ); -}; - -const List = (): ReactElement => { +export const List = (): ReactElement => { const menuProps = PMenu.useContextMenu(); const client = Synnax.use(); - const placer = Layout.usePlacer(); + const placeLayout = Layout.usePlacer(); const removeLayout = Layout.useRemover(); const dispatch = useDispatch(); const ranges = useSelectMultiple(); const activeRange = useSelect(); - const handleCreate = (key?: string): void => - void placer(createLayout({ initial: { key } })); + const handleAddOrEdit = (key?: string): void => { + placeLayout(createEditLayout({ initial: { key } })); + }; - const handleRemove = (keys: string[]): void => void dispatch(remove({ keys })); + const handleRemove = (keys: string[]): void => { + dispatch(remove({ keys })); + }; - const handleSelect = (key: string): void => void dispatch(setActive(key)); + const handleSelect = (key: string): void => { + dispatch(setActive(key)); + }; const addStatus = Status.useAggregator(); @@ -278,27 +254,46 @@ const List = (): ReactElement => { }); const save = useMutation({ - onMutate: async (key: string) => { - const range = ranges.find((r) => r.key === key); - if (range == null || range.variant === "dynamic") return; - dispatch(add({ ranges: [{ ...range, persisted: true }] })); - return range; - }, mutationFn: async (key: string) => { const range = ranges.find((r) => r.key === key); if (range == null || range.variant === "dynamic") return; - await client?.ranges.create({ ...range }); + await client?.ranges.create({ + key: range.key, + timeRange: new TimeRange(range.timeRange.start, range.timeRange.end), + name: range.name, + }); + dispatch(add({ ranges: [{ ...range, persisted: true }] })); }, - onError: (e) => + onError: (e, _, range) => { addStatus({ variant: "error", message: "Failed to save range", description: e.message, - }), + }); + dispatch(add({ ranges: [range as Range] })); + }, }); const handleLink = Link.useCopyToClipboard(); + const NoRanges = (): ReactElement => { + const handleLinkClick: React.MouseEventHandler = (e) => { + e.stopPropagation(); + handleAddOrEdit(); + }; + + return ( + + + No ranges. + + Create a range + + + + ); + }; + const ContextMenu = ({ keys: [key], }: PMenu.ContextMenuMenuProps): ReactElement | null => { @@ -306,28 +301,31 @@ const List = (): ReactElement => { const activeLayout = Layout.useSelectActiveMosaicLayout(); const addToActivePlot = useAddToActivePlot(); const addToNewPlot = useAddToNewPlot(); - const placer = Layout.usePlacer(); - const handleSetActive = () => void dispatch(setActive(key)); + const placeLayout = Layout.usePlacer(); + const handleSetActive = () => { + dispatch(setActive(key)); + }; const handleViewDetails = useViewDetails(); - const handleAddChildRange = () => - void placer(createLayout({ initial: { parent: key } })); - const rangeExists = rng != null; + const handleAddChildRange = () => { + placeLayout(createEditLayout({ initial: { parent: key } })); + }; - const handleSelect: Record void> = { - rename: () => Text.edit(`text-${key}`), - create: () => handleCreate(), - remove: () => rangeExists && handleRemove([rng.key]), - delete: () => rangeExists && del.mutate(rng.key), - details: () => rangeExists && handleViewDetails(rng.key), - save: () => rangeExists && save.mutate(rng.key), + const handleSelect = { + rename: (): void => Text.edit(`text-${key}`), + create: () => handleAddOrEdit(), + edit: () => handleAddOrEdit(rng?.key), + remove: () => rng != null && handleRemove([rng.key]), + delete: () => rng != null && del.mutate(rng.key), + details: () => rng != null && handleViewDetails(rng.key), + save: () => rng != null && save.mutate(rng.key), link: () => - rangeExists && + rng != null && handleLink({ name: rng.name, ontologyID: { key: rng.key, - type: ranger.RangeOntologyType, + type: "range", }, }), addToActivePlot: () => addToActivePlot(key), @@ -338,18 +336,22 @@ const List = (): ReactElement => { return ( + } itemKey="details"> + View Details + } itemKey="create"> Create New - {rangeExists && ( + + {rng != null && ( <> - - {rng.key !== activeRange?.key && setAsActiveMenuItem} - {viewDetailsMenuItem} - + } itemKey="edit"> + Edit + {addChildRangeMenuItem} + {rng.key !== activeRange?.key && setAsActiveMenuItem} {activeLayout?.type === "lineplot" && addToActivePlotMenuItem} {addToNewPlotMenuItem} @@ -357,11 +359,9 @@ const List = (): ReactElement => { Remove from List {rng.persisted ? ( - <> - {deleteMenuItem} - - - + } itemKey="delete"> + Delete + ) : ( client != null && ( <> @@ -372,9 +372,15 @@ const List = (): ReactElement => { ) )} + {rng.persisted && ( + <> + + + + )} + )} - ); @@ -384,7 +390,7 @@ const List = (): ReactElement => { } {...menuProps}> data={ranges.filter((r) => r.variant === "static") as StaticRange[]} - emptyContent={} + emptyContent={} > { }; const Content = (): ReactElement => { - const placer = Layout.usePlacer(); + const p = Layout.usePlacer(); return ( @@ -477,7 +483,7 @@ const Content = (): ReactElement => { {[ { children: , - onClick: () => placer(createLayout({})), + onClick: () => p(createEditLayout({})), }, ]} diff --git a/console/src/range/external.ts b/console/src/range/external.ts index 164bf15fc..c660c3620 100644 --- a/console/src/range/external.ts +++ b/console/src/range/external.ts @@ -8,11 +8,11 @@ // included in the file licenses/APL.txt. import { Layout } from "@/layout"; -import { Create, CREATE_LAYOUT_TYPE } from "@/range/CreateLayout"; +import { Edit, EDIT_LAYOUT_TYPE } from "@/range/EditLayout"; import { Overview, overviewLayout } from "@/range/overview/Overview"; export * from "@/range/ContextMenu"; -export * from "@/range/CreateLayout"; +export * from "@/range/EditLayout"; export * from "@/range/overview/Overview"; export * from "@/range/Select"; export * from "@/range/selectors"; @@ -21,6 +21,6 @@ export * from "@/range/slice"; export * from "@/range/Toolbar"; export const LAYOUTS: Record = { - [CREATE_LAYOUT_TYPE]: Create, + [EDIT_LAYOUT_TYPE]: Edit, [overviewLayout.type]: Overview, }; diff --git a/console/src/range/overview/ChildRanges.tsx b/console/src/range/overview/ChildRanges.tsx index 2cafd9723..be826c5a5 100644 --- a/console/src/range/overview/ChildRanges.tsx +++ b/console/src/range/overview/ChildRanges.tsx @@ -23,7 +23,7 @@ import { import { FC, useState } from "react"; import { Layout } from "@/layout"; -import { createLayout, overviewLayout } from "@/range/external"; +import { createEditLayout, overviewLayout } from "@/range/external"; export const ChildRangeListItem = (props: List.ItemProps) => { const { entry } = props; @@ -99,7 +99,7 @@ export const ChildRanges: FC = ({ rangeKey }) => { style={{ width: "fit-content" }} iconSpacing="small" variant="text" - onClick={() => placer(createLayout({ initial: { parent: rangeKey } }))} + onClick={() => placer(createEditLayout({ initial: { parent: rangeKey } }))} > Add Child Range diff --git a/console/src/range/services/ontology.tsx b/console/src/range/services/ontology.tsx index ec6aa4702..1573785cd 100644 --- a/console/src/range/services/ontology.tsx +++ b/console/src/range/services/ontology.tsx @@ -8,7 +8,7 @@ // included in the file licenses/APL.txt. import { type Store } from "@reduxjs/toolkit"; -import { ontology, ranger, type Synnax } from "@synnaxlabs/client"; +import { ontology, type ranger, type Synnax } from "@synnaxlabs/client"; import { Icon } from "@synnaxlabs/media"; import { type Haul, List, Menu as PMenu, Ranger, Text, Tree } from "@synnaxlabs/pluto"; import { CrudeTimeRange, errors } from "@synnaxlabs/x"; @@ -21,7 +21,7 @@ import { LinePlot } from "@/lineplot"; import { Link } from "@/link"; import { Ontology } from "@/ontology"; import { useConfirmDelete } from "@/ontology/hooks"; -import { createLayout } from "@/range/CreateLayout"; +import { createEditLayout } from "@/range/EditLayout"; import { overviewLayout } from "@/range/overview/Overview"; import { select, useSelect } from "@/range/selectors"; import { add, remove, rename, setActive, type StoreState } from "@/range/slice"; @@ -29,10 +29,8 @@ import { addChildRangeMenuItem, addToActivePlotMenuItem, addToNewPlotMenuItem, - deleteMenuItem, fromClientRange, setAsActiveMenuItem, - viewDetailsMenuItem, } from "@/range/Toolbar"; const handleSelect: Ontology.HandleSelect = async ({ @@ -59,7 +57,7 @@ const handleRename: Ontology.HandleTreeRename = { }, }; -const fetchIfNotInState = async ( +export const fetchIfNotInState = async ( store: Store, client: Synnax, key: string, @@ -78,12 +76,13 @@ const useActivate = (): ((props: Ontology.TreeContextMenuProps) => void) => await fetchIfNotInState(store, client, res.id.key); store.dispatch(setActive(res.id.key)); }, - onError: (e, { addStatus }) => + onError: (e, { addStatus }) => { addStatus({ variant: "error", message: `Failed to activate range`, description: e.message, - }), + }); + }, }).mutate; const useAddToActivePlot = (): ((props: Ontology.TreeContextMenuProps) => void) => @@ -102,12 +101,13 @@ const useAddToActivePlot = (): ((props: Ontology.TreeContextMenuProps) => void) }), ); }, - onError: (e, { addStatus }) => + onError: (e, { addStatus }) => { addStatus({ variant: "error", message: `Failed to add range to plot`, description: e.message, - }), + }); + }, }).mutate; const useAddToNewPlot = (): ((props: Ontology.TreeContextMenuProps) => void) => @@ -125,22 +125,24 @@ const useAddToNewPlot = (): ((props: Ontology.TreeContextMenuProps) => void) => }), ); }, - onError: (e, { addStatus }) => + onError: (e, { addStatus }) => { addStatus({ variant: "error", message: `Failed to add range to plot`, description: e.message, - }), + }); + }, }).mutate; const useViewDetails = (): ((props: Ontology.TreeContextMenuProps) => void) => { - const placer = Layout.usePlacer(); - return ({ selection: { resources } }) => - placer({ + const placeLayout = Layout.usePlacer(); + return ({ selection: { resources } }) => { + placeLayout({ ...overviewLayout, name: resources[0].name, key: resources[0].id.key, }); + }; }; const useDelete = (): ((props: Ontology.TreeContextMenuProps) => void) => { @@ -191,7 +193,8 @@ const useDelete = (): ((props: Ontology.TreeContextMenuProps) => void) => { store.dispatch(add({ ranges })); } let message = "Failed to delete ranges"; - if (resources.length === 1) message = `Failed to delete ${resources[0].name}`; + if (resources.length === 1) + message = `Failed to delete range ${resources[0].name}`; addStatus({ variant: "error", message, @@ -201,6 +204,13 @@ const useDelete = (): ((props: Ontology.TreeContextMenuProps) => void) => { }).mutate; }; +const handleEdit = ({ + selection: { resources }, + placeLayout, +}: Ontology.TreeContextMenuProps): void => { + placeLayout(createEditLayout({ initial: { key: resources[0].id.key } })); +}; + const TreeContextMenu: Ontology.TreeContextMenu = (props) => { const { selection, @@ -208,22 +218,24 @@ const TreeContextMenu: Ontology.TreeContextMenu = (props) => { } = props; const activeRange = useSelect(); const layout = Layout.useSelectActiveMosaicLayout(); - const handleDelete = useDelete(); + const del = useDelete(); const addToActivePlot = useAddToActivePlot(); const addToNewPlot = useAddToNewPlot(); const activate = useActivate(); const groupFromSelection = Group.useCreateFromSelection(); const handleLink = Link.useCopyToClipboard(); - const placer = Layout.usePlacer(); - const handleAddChildRange = () => - void placer(createLayout({ initial: { parent: resources[0].id.key } })); + const placeLayout = Layout.usePlacer(); + const handleAddChildRange = () => { + placeLayout(createEditLayout({ initial: { parent: resources[0].id.key } })); + }; const viewDetails = useViewDetails(); const handleSelect = { - delete: () => handleDelete(props), + delete: () => del(props), rename: () => Tree.startRenaming(nodes[0].key), setAsActive: () => activate(props), addToActivePlot: () => addToActivePlot(props), addToNewPlot: () => addToNewPlot(props), + edit: () => handleEdit(props), group: () => groupFromSelection(props), viewDetails: () => viewDetails(props), link: () => @@ -239,25 +251,27 @@ const TreeContextMenu: Ontology.TreeContextMenu = (props) => { {isSingle && ( <> {resources[0].id.key !== activeRange?.key && setAsActiveMenuItem} - {viewDetailsMenuItem} + }> + View Details + + }> + Edit + {addChildRangeMenuItem} - )} + {layout?.type === "lineplot" && addToActivePlotMenuItem} {addToNewPlotMenuItem} - {deleteMenuItem} + }> + Delete + + {isSingle && } - {isSingle && ( - <> - - - - )} ); @@ -265,7 +279,7 @@ const TreeContextMenu: Ontology.TreeContextMenu = (props) => { const haulItems = ({ id }: ontology.Resource): Haul.Item[] => [ { - type: ranger.RangeOntologyType, + type: "range", key: id.key, }, ]; diff --git a/console/src/range/services/palette.tsx b/console/src/range/services/palette.tsx index e0cfffeb5..c8979e550 100644 --- a/console/src/range/services/palette.tsx +++ b/console/src/range/services/palette.tsx @@ -10,13 +10,13 @@ import { Icon } from "@synnaxlabs/media"; import { type Command } from "@/palette/Palette"; -import { createLayout } from "@/range/CreateLayout"; +import { createEditLayout } from "@/range/EditLayout"; export const defineCommand: Command = { key: "define-range", name: "Create a Range", icon: , - onSelect: ({ placeLayout }) => placeLayout(createLayout({})), + onSelect: ({ placeLayout }) => placeLayout(createEditLayout({})), }; export const COMMANDS = [defineCommand];