mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-25 10:51:57 +01:00
Use TRee component for Environment dialog (#288)
This commit is contained in:
@@ -1,25 +1,28 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import type { Environment, Workspace } from '@yaakapp-internal/models';
|
||||
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import {
|
||||
environmentsBreakdownAtom,
|
||||
useEnvironmentsBreakdown,
|
||||
} from '../hooks/useEnvironmentsBreakdown';
|
||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { isBaseEnvironment } from '../lib/model_util';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { showColorPicker } from '../lib/showColorPicker';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { IconTooltip } from './core/IconTooltip';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import type { TreeNode } from './core/tree/common';
|
||||
import type { TreeHandle, TreeProps } from './core/tree/Tree';
|
||||
import { Tree } from './core/tree/Tree';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||
@@ -28,9 +31,10 @@ interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
}
|
||||
|
||||
type TreeModel = Environment | Workspace;
|
||||
|
||||
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const { baseEnvironment, otherBaseEnvironments, subEnvironments, allEnvironments } =
|
||||
useEnvironmentsBreakdown();
|
||||
const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
|
||||
initialEnvironment?.id ?? null,
|
||||
);
|
||||
@@ -40,23 +44,71 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
? allEnvironments.find((e) => e.id === selectedEnvironmentId)
|
||||
: baseEnvironment;
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
if (baseEnvironment == null) return;
|
||||
const id = await createSubEnvironmentAndActivate.mutateAsync(baseEnvironment);
|
||||
if (id != null) setSelectedEnvironmentId(id);
|
||||
};
|
||||
return (
|
||||
<SplitLayout
|
||||
name="env_editor"
|
||||
defaultRatio={0.75}
|
||||
layout="horizontal"
|
||||
className="gap-0"
|
||||
resizeHandleClassName="-translate-x-[1px]"
|
||||
firstSlot={() => (
|
||||
<EnvironmentEditDialogSidebar
|
||||
selectedEnvironmentId={selectedEnvironment?.id ?? null}
|
||||
setSelectedEnvironmentId={setSelectedEnvironmentId}
|
||||
/>
|
||||
)}
|
||||
secondSlot={() => (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
{baseEnvironments.length > 1 ? (
|
||||
<div className="p-3">
|
||||
<Banner color="notice">
|
||||
There are multiple base environments for this workspace. Please delete the
|
||||
environments you no longer need.
|
||||
</Banner>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{selectedEnvironment == null ? (
|
||||
<div className="p-3 mt-10">
|
||||
<Banner color="danger">
|
||||
Failed to find selected environment <InlineCode>{selectedEnvironmentId}</InlineCode>
|
||||
</Banner>
|
||||
</div>
|
||||
) : (
|
||||
<EnvironmentEditor className="pl-4 pt-3" environment={selectedEnvironment} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleDuplicateEnvironment = useCallback(async (environment: Environment) => {
|
||||
const name = await showPrompt({
|
||||
id: 'duplicate-environment',
|
||||
title: 'Duplicate Environment',
|
||||
label: 'Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
if (name) {
|
||||
const newId = await duplicateModel({ ...environment, name, public: false });
|
||||
setSelectedEnvironmentId(newId);
|
||||
}
|
||||
const sharableTooltip = (
|
||||
<IconTooltip
|
||||
icon="eye"
|
||||
iconSize="sm"
|
||||
content="This environment will be included in Directory Sync and data exports"
|
||||
/>
|
||||
);
|
||||
|
||||
function EnvironmentEditDialogSidebar({
|
||||
selectedEnvironmentId,
|
||||
setSelectedEnvironmentId,
|
||||
}: {
|
||||
selectedEnvironmentId: string | null;
|
||||
setSelectedEnvironmentId: (id: string | null) => void;
|
||||
}) {
|
||||
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom) ?? '';
|
||||
const treeId = `environment.${activeWorkspaceId}.sidebar`;
|
||||
const treeRef = useRef<TreeHandle>(null);
|
||||
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (selectedEnvironmentId == null) return;
|
||||
treeRef.current?.selectItem(selectedEnvironmentId);
|
||||
treeRef.current?.focus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDeleteEnvironment = useCallback(
|
||||
@@ -66,217 +118,283 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
setSelectedEnvironmentId(baseEnvironment?.id ?? null);
|
||||
}
|
||||
},
|
||||
[baseEnvironment?.id, selectedEnvironmentId],
|
||||
[baseEnvironment?.id, selectedEnvironmentId, setSelectedEnvironmentId],
|
||||
);
|
||||
|
||||
if (baseEnvironment == null) {
|
||||
return null;
|
||||
}
|
||||
const actions = useMemo(() => {
|
||||
const enable = () => treeRef.current?.hasFocus() ?? false;
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
name="env_editor"
|
||||
defaultRatio={0.75}
|
||||
layout="horizontal"
|
||||
className="gap-0"
|
||||
firstSlot={() => (
|
||||
<aside className="w-full min-w-0 pt-2">
|
||||
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
||||
{[baseEnvironment, ...otherBaseEnvironments].map((e) => (
|
||||
<EnvironmentDialogSidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id == e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
environment={e}
|
||||
duplicateEnvironment={handleDuplicateEnvironment}
|
||||
// Allow deleting the base environment if there are multiples
|
||||
deleteEnvironment={
|
||||
otherBaseEnvironments.length > 0 ? handleDeleteEnvironment : null
|
||||
}
|
||||
rightSlot={e.public && sharableTooltip}
|
||||
outerRightSlot={
|
||||
<IconButton
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
hotkeyAction="model.create"
|
||||
hotkeyPriority={999}
|
||||
title="Add sub environment"
|
||||
icon="plus_circle"
|
||||
iconClassName="text-text-subtlest group-hover:text-text-subtle"
|
||||
className="group mr-0.5"
|
||||
onClick={handleCreateEnvironment}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{resolvedModelName(e)}
|
||||
</EnvironmentDialogSidebarButton>
|
||||
))}
|
||||
{subEnvironments.length > 0 && (
|
||||
<div className="px-2">
|
||||
<Separator className="my-3" />
|
||||
</div>
|
||||
)}
|
||||
{subEnvironments.map((e) => (
|
||||
<EnvironmentDialogSidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
environment={e}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
rightSlot={e.public && sharableTooltip}
|
||||
duplicateEnvironment={handleDuplicateEnvironment}
|
||||
deleteEnvironment={handleDeleteEnvironment}
|
||||
>
|
||||
{e.name}
|
||||
</EnvironmentDialogSidebarButton>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
secondSlot={() =>
|
||||
selectedEnvironment == null ? (
|
||||
<div className="p-3 mt-10">
|
||||
<Banner color="danger">
|
||||
Failed to find selected environment <InlineCode>{selectedEnvironmentId}</InlineCode>
|
||||
</Banner>
|
||||
</div>
|
||||
) : (
|
||||
<EnvironmentEditor
|
||||
className="pl-4 pt-3 border-l border-border-subtle"
|
||||
environment={selectedEnvironment}
|
||||
/>
|
||||
)
|
||||
const actions = {
|
||||
'sidebar.selected.rename': {
|
||||
enable,
|
||||
allowDefault: true,
|
||||
priority: 100,
|
||||
cb: async function (items: TreeModel[]) {
|
||||
const item = items[0];
|
||||
if (items.length === 1 && item != null) {
|
||||
treeRef.current?.renameItem(item.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
'sidebar.selected.delete': {
|
||||
priority: 100,
|
||||
enable,
|
||||
cb: (items: TreeModel[]) => deleteModelWithConfirm(items),
|
||||
},
|
||||
'sidebar.selected.duplicate': {
|
||||
priority: 100,
|
||||
enable,
|
||||
cb: async function (items: TreeModel[]) {
|
||||
if (items.length === 1) {
|
||||
const item = items[0]!;
|
||||
const newId = await duplicateModel(item);
|
||||
setSelectedEnvironmentId(newId);
|
||||
} else {
|
||||
await Promise.all(items.map(duplicateModel));
|
||||
}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
return actions;
|
||||
}, [setSelectedEnvironmentId]);
|
||||
|
||||
const hotkeys = useMemo<TreeProps<TreeModel>['hotkeys']>(() => ({ actions }), [actions]);
|
||||
|
||||
const getContextMenu = useCallback(
|
||||
(items: TreeModel[]): ContextMenuProps['items'] => {
|
||||
const environment = items[0];
|
||||
if (environment == null || environment.model !== 'environment') return [];
|
||||
const singleEnvironment = items.length === 1;
|
||||
|
||||
const menuItems: DropdownItem[] = [
|
||||
{
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: isBaseEnvironment(environment) || !singleEnvironment,
|
||||
hotKeyAction: 'sidebar.selected.rename',
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: async () => {
|
||||
// Not sure why this is needed, but without it the
|
||||
// edit input blurs immediately after opening.
|
||||
requestAnimationFrame(() => {
|
||||
actions['sidebar.selected.rename'].cb(items);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
hotKeyAction: 'sidebar.selected.duplicate',
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
|
||||
},
|
||||
{
|
||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||
leftSlot: <Icon icon="palette" />,
|
||||
hidden: isBaseEnvironment(environment) || !singleEnvironment,
|
||||
onSelect: async () => showColorPicker(environment),
|
||||
},
|
||||
{
|
||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||
rightSlot: <EnvironmentSharableTooltip />,
|
||||
onSelect: async () => {
|
||||
await patchModel(environment, { public: !environment.public });
|
||||
},
|
||||
},
|
||||
{
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
hotKeyAction: 'sidebar.selected.delete',
|
||||
hotKeyLabelOnly: true,
|
||||
hidden: !(isBaseEnvironment(environment) && baseEnvironments.length > 1),
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => handleDeleteEnvironment(environment),
|
||||
},
|
||||
];
|
||||
|
||||
if (isBaseEnvironment(environment) && singleEnvironment) {
|
||||
menuItems.push({ type: 'separator' });
|
||||
menuItems.push({
|
||||
label: 'Create Sub Environment',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
hidden: !isBaseEnvironment(environment),
|
||||
onSelect: async () => {
|
||||
await createSubEnvironment();
|
||||
},
|
||||
});
|
||||
}
|
||||
/>
|
||||
|
||||
return menuItems;
|
||||
},
|
||||
[actions, baseEnvironments.length, handleDeleteEnvironment],
|
||||
);
|
||||
};
|
||||
|
||||
function EnvironmentDialogSidebarButton({
|
||||
children,
|
||||
className,
|
||||
active,
|
||||
onClick,
|
||||
deleteEnvironment,
|
||||
rightSlot,
|
||||
outerRightSlot,
|
||||
duplicateEnvironment,
|
||||
environment,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
rightSlot?: ReactNode;
|
||||
outerRightSlot?: ReactNode;
|
||||
environment: Environment;
|
||||
deleteEnvironment: ((environment: Environment) => void) | null;
|
||||
duplicateEnvironment: ((environment: Environment) => void) | null;
|
||||
}) {
|
||||
const [showContextMenu, setShowContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const handleDragEnd = useCallback(async function handleDragEnd({
|
||||
items,
|
||||
children,
|
||||
insertAt,
|
||||
}: {
|
||||
items: TreeModel[];
|
||||
children: TreeModel[];
|
||||
insertAt: number;
|
||||
}) {
|
||||
const prev = children[insertAt - 1] as Exclude<TreeModel, Workspace>;
|
||||
const next = children[insertAt] as Exclude<TreeModel, Workspace>;
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowContextMenu({ x: e.clientX, y: e.clientY });
|
||||
const beforePriority = prev?.sortPriority ?? 0;
|
||||
const afterPriority = next?.sortPriority ?? 0;
|
||||
const shouldUpdateAll = afterPriority - beforePriority < 1;
|
||||
|
||||
try {
|
||||
if (shouldUpdateAll) {
|
||||
// Add items to children at insertAt
|
||||
children.splice(insertAt, 0, ...items);
|
||||
await Promise.all(children.map((m, i) => patchModel(m, { sortPriority: i * 1000 })));
|
||||
} else {
|
||||
const range = afterPriority - beforePriority;
|
||||
const increment = range / (items.length + 2);
|
||||
await Promise.all(
|
||||
items.map((m, i) => {
|
||||
const sortPriority = beforePriority + (i + 1) * increment;
|
||||
// Spread item sortPriority out over before/after range
|
||||
return patchModel(m, { sortPriority });
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleActivate = useCallback(
|
||||
(item: TreeModel) => {
|
||||
setSelectedEnvironmentId(item.id);
|
||||
},
|
||||
[setSelectedEnvironmentId],
|
||||
);
|
||||
|
||||
const tree = useAtomValue(treeAtom);
|
||||
return (
|
||||
<aside className="x-theme-sidebar h-full w-full min-w-0 grid overflow-y-auto border-r border-border-subtle ">
|
||||
{tree != null && (
|
||||
<div className="pt-2">
|
||||
<Tree
|
||||
ref={treeRef}
|
||||
treeId={treeId}
|
||||
className="px-2 pb-10"
|
||||
hotkeys={hotkeys}
|
||||
root={tree}
|
||||
getContextMenu={getContextMenu}
|
||||
onDragEnd={handleDragEnd}
|
||||
getItemKey={(i) => `${i.id}::${i.name}`}
|
||||
ItemLeftSlotInner={ItemLeftSlotInner}
|
||||
ItemRightSlot={ItemRightSlot}
|
||||
ItemInner={ItemInner}
|
||||
onActivate={handleActivate}
|
||||
getEditOptions={getEditOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
const treeAtom = atom<TreeNode<TreeModel> | null>((get) => {
|
||||
const activeWorkspace = get(activeWorkspaceAtom);
|
||||
const { baseEnvironment, baseEnvironments, subEnvironments } = get(environmentsBreakdownAtom);
|
||||
if (activeWorkspace == null || baseEnvironment == null) return null;
|
||||
|
||||
const root: TreeNode<TreeModel> = {
|
||||
item: activeWorkspace,
|
||||
parent: null,
|
||||
children: [],
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
for (const item of baseEnvironments) {
|
||||
root.children?.push({
|
||||
item,
|
||||
parent: root,
|
||||
depth: 0,
|
||||
draggable: false,
|
||||
});
|
||||
}
|
||||
|
||||
const parent = root.children?.[0];
|
||||
if (baseEnvironments.length <= 1 && parent != null) {
|
||||
const sortedEnvironments = [...subEnvironments].sort((a, b) => {
|
||||
if (a.sortPriority === b.sortPriority) return a.updatedAt > b.updatedAt ? 1 : -1;
|
||||
else return a.sortPriority - b.sortPriority;
|
||||
});
|
||||
|
||||
parent.children = sortedEnvironments.map((item) => ({
|
||||
item,
|
||||
parent,
|
||||
depth: 1,
|
||||
localDrag: true,
|
||||
}));
|
||||
}
|
||||
|
||||
return root;
|
||||
});
|
||||
|
||||
function ItemLeftSlotInner({ item }: { item: TreeModel }) {
|
||||
const { baseEnvironments } = useEnvironmentsBreakdown();
|
||||
return baseEnvironments.length > 1 ? (
|
||||
<Icon icon="alert_triangle" color="notice" />
|
||||
) : (
|
||||
item.model === 'environment' && item.color && <EnvironmentColorIndicator environment={item} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemRightSlot({ item }: { item: TreeModel }) {
|
||||
const { baseEnvironments } = useEnvironmentsBreakdown();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center gap-0.5',
|
||||
'px-2', // Padding to show the focus border
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
{item.model === 'environment' && baseEnvironments.length <= 1 && isBaseEnvironment(item) && (
|
||||
<IconButton
|
||||
size="sm"
|
||||
color="custom"
|
||||
size="xs"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
active ? 'text bg-surface-active' : 'text-text-subtle hover:text',
|
||||
)}
|
||||
justify="start"
|
||||
onClick={onClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
rightSlot={rightSlot}
|
||||
>
|
||||
<EnvironmentColorIndicator environment={environment} />
|
||||
{children}
|
||||
</Button>
|
||||
{outerRightSlot}
|
||||
</div>
|
||||
<ContextMenu
|
||||
triggerPosition={showContextMenu}
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
items={[
|
||||
{
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
onSelect: async () => {
|
||||
const name = await showPrompt({
|
||||
id: 'rename-environment',
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
label: 'Name',
|
||||
confirmText: 'Save',
|
||||
placeholder: 'New Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
if (name == null) return;
|
||||
await patchModel(environment, { name });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
onSelect: () => {
|
||||
duplicateEnvironment?.(environment);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||
leftSlot: <Icon icon="palette" />,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
onSelect: async () => showColorPicker(environment),
|
||||
},
|
||||
{
|
||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||
rightSlot: <EnvironmentSharableTooltip />,
|
||||
onSelect: async () => {
|
||||
await patchModel(environment, { public: !environment.public });
|
||||
},
|
||||
},
|
||||
...((deleteEnvironment
|
||||
? [
|
||||
{
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => {
|
||||
deleteEnvironment(environment);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
]}
|
||||
/>
|
||||
iconSize="sm"
|
||||
icon="plus_circle"
|
||||
className="opacity-50 hover:opacity-100"
|
||||
title="Add Sub-Environment"
|
||||
onClick={createSubEnvironment}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const sharableTooltip = (
|
||||
<IconTooltip
|
||||
icon="eye"
|
||||
content="This environment will be included in Directory Sync and data exports"
|
||||
/>
|
||||
);
|
||||
function ItemInner({ item }: { item: TreeModel }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)] w-full items-center">
|
||||
{item.model === 'environment' && item.public ? (
|
||||
<div className="mr-2 flex items-center">{sharableTooltip}</div>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
<div className="truncate min-w-0 text-left">{resolvedModelName(item)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function createSubEnvironment() {
|
||||
const { baseEnvironment } = jotaiStore.get(environmentsBreakdownAtom);
|
||||
if (baseEnvironment == null) return;
|
||||
const id = await createSubEnvironmentAndActivate.mutateAsync(baseEnvironment);
|
||||
return id;
|
||||
}
|
||||
|
||||
function getEditOptions(item: TreeModel) {
|
||||
const options: ReturnType<NonNullable<TreeProps<TreeModel>['getEditOptions']>> = {
|
||||
defaultValue: item.name,
|
||||
placeholder: 'Name',
|
||||
async onChange(item, name) {
|
||||
await patchModel(item, { name });
|
||||
},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user