mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-17 14:37:05 +01:00
238 lines
8.1 KiB
TypeScript
238 lines
8.1 KiB
TypeScript
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
|
import classNames from 'classnames';
|
|
import { memo, useCallback, useMemo } from 'react';
|
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
|
import { useConfirm } from '../hooks/useConfirm';
|
|
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
|
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
|
|
import { useDialog } from '../hooks/useDialog';
|
|
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
|
import { useSettings } from '../hooks/useSettings';
|
|
import { useToast } from '../hooks/useToast';
|
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
|
import { pluralizeCount } from '../lib/pluralize';
|
|
import { getWorkspace } from '../lib/store';
|
|
import type { ButtonProps } from './core/Button';
|
|
import { Button } from './core/Button';
|
|
import type { DropdownItem } from './core/Dropdown';
|
|
import { Icon } from './core/Icon';
|
|
import { InlineCode } from './core/InlineCode';
|
|
import type { RadioDropdownItem } from './core/RadioDropdown';
|
|
import { RadioDropdown } from './core/RadioDropdown';
|
|
import { VStack } from './core/Stacks';
|
|
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
|
|
import { WorkspaceSettingsDialog } from './WorkspaceSettingsDialog';
|
|
|
|
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
|
|
|
export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|
className,
|
|
...buttonProps
|
|
}: Props) {
|
|
const workspaces = useWorkspaces();
|
|
const activeWorkspace = useActiveWorkspace();
|
|
const createWorkspace = useCreateWorkspace();
|
|
const { mutate: deleteSendHistory } = useDeleteSendHistory();
|
|
const dialog = useDialog();
|
|
const confirm = useConfirm();
|
|
const toast = useToast();
|
|
const settings = useSettings();
|
|
const openWorkspace = useOpenWorkspace();
|
|
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
|
|
|
|
const orderedWorkspaces = useMemo(
|
|
() => [...workspaces].sort((a, b) => (a.name.localeCompare(b.name) > 0 ? 1 : -1)),
|
|
[workspaces],
|
|
);
|
|
|
|
const { workspaceItems, extraItems } = useMemo<{
|
|
workspaceItems: RadioDropdownItem[];
|
|
extraItems: DropdownItem[];
|
|
}>(() => {
|
|
const workspaceItems: RadioDropdownItem[] = orderedWorkspaces.map((w) => ({
|
|
key: w.id,
|
|
label: w.name,
|
|
value: w.id,
|
|
leftSlot: w.id === activeWorkspace?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
|
}));
|
|
|
|
const extraItems: DropdownItem[] = [
|
|
{
|
|
key: 'workspace-settings',
|
|
label: 'Workspace Settings',
|
|
leftSlot: <Icon icon="settings" />,
|
|
hotKeyAction: 'workspace_settings.show',
|
|
onSelect: async () => {
|
|
dialog.show({
|
|
id: 'workspace-settings',
|
|
title: 'Workspace Settings',
|
|
size: 'md',
|
|
render: ({ hide }) => (
|
|
<WorkspaceSettingsDialog workspaceId={activeWorkspace?.id ?? null} hide={hide} />
|
|
),
|
|
});
|
|
},
|
|
},
|
|
{
|
|
key: 'sync',
|
|
label: 'Sync Workspace',
|
|
leftSlot: <Icon icon="folder_sync" />,
|
|
hidden: !activeWorkspace?.settingSyncDir,
|
|
onSelect: async () => {
|
|
if (activeWorkspace == null) return;
|
|
|
|
const ops = await calculateSync(activeWorkspace);
|
|
if (ops.length === 0) {
|
|
toast.show({
|
|
id: 'no-sync-changes',
|
|
message: 'No changes to sync',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const dbChanges = ops.filter((o) => o.type.startsWith('db'));
|
|
|
|
if (dbChanges.length === 0) {
|
|
await applySync(activeWorkspace, ops);
|
|
toast.show({
|
|
id: 'applied-sync-changes',
|
|
message: `Wrote ${pluralizeCount('change', ops.length)}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const confirmed = await confirm({
|
|
id: 'commit-sync',
|
|
title: 'Filesystem Changes Detected',
|
|
confirmText: 'Apply Changes',
|
|
description: (
|
|
<VStack space={3}>
|
|
<p>
|
|
{pluralizeCount('file', dbChanges.length)} in the directory have changed. Do you want to apply the updates to your
|
|
workspace?
|
|
</p>
|
|
<div className="overflow-y-auto max-h-[10rem]">
|
|
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
|
<thead>
|
|
<tr>
|
|
<th className="py-1 text-left">Name</th>
|
|
<th className="py-1 text-right pl-4">Operation</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-surface-highlight">
|
|
{dbChanges.map((op, i) => {
|
|
let name = '';
|
|
let label = '';
|
|
let color = '';
|
|
|
|
if (op.type === 'dbCreate') {
|
|
label = 'create';
|
|
name = fallbackRequestName(op.fs.model);
|
|
color = 'text-success';
|
|
} else if (op.type === 'dbUpdate') {
|
|
label = 'update';
|
|
name = fallbackRequestName(op.fs.model);
|
|
color = 'text-info';
|
|
} else if (op.type === 'dbDelete') {
|
|
label = 'delete';
|
|
name = fallbackRequestName(op.model);
|
|
color = 'text-danger';
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<tr key={i} className="text-text">
|
|
<td className="py-1">{name}</td>
|
|
<td className="py-1 pl-4 text-right">
|
|
<InlineCode className={color}>{label}</InlineCode>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</VStack>
|
|
),
|
|
});
|
|
if (confirmed) {
|
|
await applySync(activeWorkspace, ops);
|
|
toast.show({
|
|
id: 'applied-confirmed-sync-changes',
|
|
message: `Wrote ${pluralizeCount('change', ops.length)}`,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
{
|
|
key: 'delete-responses',
|
|
label: 'Clear Send History',
|
|
leftSlot: <Icon icon="history" />,
|
|
onSelect: deleteSendHistory,
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
key: 'create-workspace',
|
|
label: 'New Workspace',
|
|
leftSlot: <Icon icon="plus" />,
|
|
onSelect: createWorkspace,
|
|
},
|
|
];
|
|
|
|
return { workspaceItems, extraItems };
|
|
}, [
|
|
orderedWorkspaces,
|
|
activeWorkspace,
|
|
deleteSendHistory,
|
|
createWorkspace,
|
|
dialog,
|
|
confirm,
|
|
toast,
|
|
]);
|
|
|
|
const handleChange = useCallback(
|
|
async (workspaceId: string | null) => {
|
|
if (workspaceId == null) return;
|
|
|
|
if (typeof openWorkspaceNewWindow === 'boolean') {
|
|
openWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow });
|
|
return;
|
|
}
|
|
|
|
const workspace = await getWorkspace(workspaceId);
|
|
if (workspace == null) return;
|
|
|
|
dialog.show({
|
|
id: 'open-workspace',
|
|
size: 'sm',
|
|
title: 'Open Workspace',
|
|
render: ({ hide }) => <OpenWorkspaceDialog workspace={workspace} hide={hide} />,
|
|
});
|
|
},
|
|
[dialog, openWorkspace, openWorkspaceNewWindow],
|
|
);
|
|
|
|
return (
|
|
<RadioDropdown
|
|
items={workspaceItems}
|
|
extraItems={extraItems}
|
|
onChange={handleChange}
|
|
value={activeWorkspace?.id ?? null}
|
|
>
|
|
<Button
|
|
size="sm"
|
|
className={classNames(
|
|
className,
|
|
'text !px-2 truncate',
|
|
activeWorkspace === null && 'italic opacity-disabled',
|
|
)}
|
|
{...buttonProps}
|
|
>
|
|
{activeWorkspace?.name ?? 'Workspace'}
|
|
</Button>
|
|
</RadioDropdown>
|
|
);
|
|
});
|