mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-29 20:17:05 +02:00
Dir sync filesystem watching
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
|
||||
import {useWatchWorkspace} from "@yaakapp-internal/sync";
|
||||
import {
|
||||
useEnsureActiveCookieJar,
|
||||
useSubscribeActiveCookieJarId,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspace, useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
||||
@@ -22,6 +23,7 @@ import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useSubscribeRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
|
||||
import { useSyncModelStores } from '../hooks/useSyncModelStores';
|
||||
import { useSyncWorkspace } from '../hooks/useSyncWorkspace';
|
||||
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
|
||||
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
|
||||
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
|
||||
@@ -53,6 +55,12 @@ export function GlobalHooks() {
|
||||
useActiveWorkspaceChangedToast();
|
||||
useEnsureActiveCookieJar();
|
||||
|
||||
// Trigger workspace sync operation when workspace files change
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const { debouncedSync } = useSyncWorkspace(activeWorkspace, { debounceMillis: 1000 });
|
||||
useListenToTauriEvent('upserted_model', debouncedSync);
|
||||
useWatchWorkspace(activeWorkspace, debouncedSync);
|
||||
|
||||
const activeRequest = useActiveRequest();
|
||||
const duplicateHttpRequest = useDuplicateHttpRequest({
|
||||
id: activeRequest?.id ?? null,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SplitLayout } from './core/SplitLayout';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { Prose } from './Prose';
|
||||
|
||||
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey'> {
|
||||
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey' | 'forceUpdateKey'> {
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
defaultValue: string;
|
||||
@@ -19,15 +19,7 @@ interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey'> {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function MarkdownEditor({
|
||||
className,
|
||||
defaultValue,
|
||||
onChange,
|
||||
name,
|
||||
placeholder,
|
||||
heightMode,
|
||||
stateKey,
|
||||
}: Props) {
|
||||
export function MarkdownEditor({ className, defaultValue, onChange, name, ...editorProps }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [width] = useSize(containerRef.current);
|
||||
@@ -54,9 +46,7 @@ export function MarkdownEditor({
|
||||
language="markdown"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
heightMode={heightMode}
|
||||
stateKey={stateKey}
|
||||
{...editorProps}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -488,6 +488,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
<PlainInput
|
||||
label="Request Name"
|
||||
hideLabel
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
defaultValue={activeRequest.name}
|
||||
className="font-sans !text-xl !px-0"
|
||||
containerClassName="border-0"
|
||||
@@ -499,6 +500,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
placeholder="Request description"
|
||||
defaultValue={activeRequest.description}
|
||||
stateKey={`description.${activeRequest.id}`}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
onChange={(description) =>
|
||||
updateRequest({ id: activeRequestId, update: { description } })
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
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 { useSyncWorkspace } from '../hooks/useSyncWorkspace';
|
||||
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';
|
||||
|
||||
@@ -35,11 +29,10 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
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 { sync } = useSyncWorkspace(activeWorkspace);
|
||||
|
||||
const orderedWorkspaces = useMemo(
|
||||
() => [...workspaces].sort((a, b) => (a.name.localeCompare(b.name) > 0 ? 1 : -1)),
|
||||
@@ -79,92 +72,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
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)}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
onSelect: sync,
|
||||
},
|
||||
{
|
||||
key: 'delete-responses',
|
||||
@@ -184,12 +92,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
return { workspaceItems, extraItems };
|
||||
}, [
|
||||
orderedWorkspaces,
|
||||
activeWorkspace,
|
||||
activeWorkspace?.settingSyncDir,
|
||||
activeWorkspace?.id,
|
||||
sync,
|
||||
deleteSendHistory,
|
||||
createWorkspace,
|
||||
dialog,
|
||||
confirm,
|
||||
toast,
|
||||
]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
|
||||
@@ -117,7 +117,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
|
||||
// Add empty last pair if there is none
|
||||
const lastPair = newPairs[newPairs.length - 1];
|
||||
if (lastPair != null && !isPairEmpty(lastPair)) {
|
||||
if (lastPair == null || !isPairEmpty(lastPair)) {
|
||||
newPairs.push(emptyPair());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { DialogProps } from '../components/core/Dialog';
|
||||
import type { ConfirmProps } from './Confirm';
|
||||
import { Confirm } from './Confirm';
|
||||
@@ -5,27 +6,30 @@ import { useDialog } from './useDialog';
|
||||
|
||||
export function useConfirm() {
|
||||
const dialog = useDialog();
|
||||
return ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
variant,
|
||||
confirmText,
|
||||
}: {
|
||||
id: string;
|
||||
title: DialogProps['title'];
|
||||
description?: DialogProps['description'];
|
||||
variant?: ConfirmProps['variant'];
|
||||
confirmText?: ConfirmProps['confirmText'];
|
||||
}) =>
|
||||
new Promise((onResult: ConfirmProps['onResult']) => {
|
||||
dialog.show({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
|
||||
});
|
||||
});
|
||||
return useCallback(
|
||||
({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
variant,
|
||||
confirmText,
|
||||
}: {
|
||||
id: string;
|
||||
title: DialogProps['title'];
|
||||
description?: DialogProps['description'];
|
||||
variant?: ConfirmProps['variant'];
|
||||
confirmText?: ConfirmProps['confirmText'];
|
||||
}) =>
|
||||
new Promise((onResult: ConfirmProps['onResult']) => {
|
||||
dialog.show({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
|
||||
});
|
||||
}),
|
||||
[dialog],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { debounce } from '../lib/debounce';
|
||||
|
||||
export function useDebouncedState<T>(
|
||||
defaultValue: T,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OsType } from '@tauri-apps/plugin-os';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { capitalize } from '../lib/capitalize';
|
||||
import { debounce } from '../lib/debounce';
|
||||
import { useOsInfo } from './useOsInfo';
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
|
||||
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
|
||||
|
||||
|
||||
102
src-web/hooks/useSyncWorkspace.tsx
Normal file
102
src-web/hooks/useSyncWorkspace.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { VStack } from '../components/core/Stacks';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
import { useConfirm } from './useConfirm';
|
||||
|
||||
export function useSyncWorkspace(
|
||||
workspace: Workspace | null,
|
||||
{
|
||||
debounceMillis = 1000,
|
||||
}: {
|
||||
debounceMillis?: number;
|
||||
} = {},
|
||||
) {
|
||||
const confirm = useConfirm();
|
||||
|
||||
const sync = useCallback(async () => {
|
||||
if (workspace == null) return;
|
||||
|
||||
const ops = await calculateSync(workspace);
|
||||
if (ops.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dbChanges = ops.filter((o) => o.type.startsWith('db'));
|
||||
|
||||
if (dbChanges.length === 0) {
|
||||
await applySync(workspace, ops);
|
||||
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(workspace, ops);
|
||||
}
|
||||
}, [confirm, workspace]);
|
||||
|
||||
const debouncedSync = useMemo(() => {
|
||||
return debounce(sync, debounceMillis);
|
||||
}, [debounceMillis, sync]);
|
||||
|
||||
return { sync, debouncedSync };
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function debounce(fn: (...args: any[]) => void, delay = 500) {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = function (...args: any[]) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
result.cancel = function () {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
return result;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --force --debug hmr",
|
||||
"dev": "vite dev --force",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user