mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-27 03:41:26 +01:00
Refactor desktop app into separate client and proxy apps
This commit is contained in:
@@ -1,60 +1,75 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
|
||||
import { queryClient } from '@yaakapp/app/lib/queryClient';
|
||||
import { useMemo } from 'react';
|
||||
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
||||
import { showToast } from '@yaakapp/app/lib/toast';
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { createFastMutation } from "@yaakapp/yaak-client/hooks/useFastMutation";
|
||||
import { queryClient } from "@yaakapp/yaak-client/lib/queryClient";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
BranchDeleteResult,
|
||||
CloneResult,
|
||||
GitCommit,
|
||||
GitRemote,
|
||||
GitStatusSummary,
|
||||
PullResult,
|
||||
PushResult,
|
||||
} from "./bindings/gen_git";
|
||||
import { showToast } from "@yaakapp/yaak-client/lib/toast";
|
||||
|
||||
export * from './bindings/gen_git';
|
||||
export * from './bindings/gen_models';
|
||||
export * from "./bindings/gen_git";
|
||||
export * from "./bindings/gen_models";
|
||||
|
||||
export interface GitCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
|
||||
export type DivergedStrategy = "force_reset" | "merge" | "cancel";
|
||||
|
||||
export type UncommittedChangesStrategy = 'reset' | 'cancel';
|
||||
export type UncommittedChangesStrategy = "reset" | "cancel";
|
||||
|
||||
export interface GitCallbacks {
|
||||
addRemote: () => Promise<GitRemote | null>;
|
||||
promptCredentials: (
|
||||
result: Extract<PushResult, { type: 'needs_credentials' }>,
|
||||
result: Extract<PushResult, { type: "needs_credentials" }>,
|
||||
) => Promise<GitCredentials | null>;
|
||||
promptDiverged: (
|
||||
result: Extract<PullResult, { type: 'diverged' }>,
|
||||
result: Extract<PullResult, { type: "diverged" }>,
|
||||
) => Promise<DivergedStrategy>;
|
||||
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
|
||||
forceSync: () => Promise<void>;
|
||||
}
|
||||
|
||||
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
|
||||
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ["git"] });
|
||||
|
||||
export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) {
|
||||
const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]);
|
||||
export function useGit(
|
||||
dir: string,
|
||||
callbacks: GitCallbacks,
|
||||
refreshKey?: string,
|
||||
) {
|
||||
const mutations = useMemo(
|
||||
() => gitMutations(dir, callbacks),
|
||||
[dir, callbacks],
|
||||
);
|
||||
const fetchAll = useQuery<void, string>({
|
||||
queryKey: ['git', 'fetch_all', dir, refreshKey],
|
||||
queryFn: () => invoke('cmd_git_fetch_all', { dir }),
|
||||
queryKey: ["git", "fetch_all", dir, refreshKey],
|
||||
queryFn: () => invoke("cmd_git_fetch_all", { dir }),
|
||||
refetchInterval: 10 * 60_000,
|
||||
});
|
||||
return [
|
||||
{
|
||||
remotes: useQuery<GitRemote[], string>({
|
||||
queryKey: ['git', 'remotes', dir, refreshKey],
|
||||
queryKey: ["git", "remotes", dir, refreshKey],
|
||||
queryFn: () => getRemotes(dir),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
log: useQuery<GitCommit[], string>({
|
||||
queryKey: ['git', 'log', dir, refreshKey],
|
||||
queryFn: () => invoke('cmd_git_log', { dir }),
|
||||
queryKey: ["git", "log", dir, refreshKey],
|
||||
queryFn: () => invoke("cmd_git_log", { dir }),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
status: useQuery<GitStatusSummary, string>({
|
||||
refetchOnMount: true,
|
||||
queryKey: ['git', 'status', dir, refreshKey, fetchAll.dataUpdatedAt],
|
||||
queryFn: () => invoke('cmd_git_status', { dir }),
|
||||
queryKey: ["git", "status", dir, refreshKey, fetchAll.dataUpdatedAt],
|
||||
queryFn: () => invoke("cmd_git_status", { dir }),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
},
|
||||
@@ -67,151 +82,180 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
const remotes = await getRemotes(dir);
|
||||
if (remotes.length === 0) {
|
||||
const remote = await callbacks.addRemote();
|
||||
if (remote == null) throw new Error('No remote found');
|
||||
if (remote == null) throw new Error("No remote found");
|
||||
}
|
||||
|
||||
const result = await invoke<PushResult>('cmd_git_push', { dir });
|
||||
if (result.type !== 'needs_credentials') return result;
|
||||
const result = await invoke<PushResult>("cmd_git_push", { dir });
|
||||
if (result.type !== "needs_credentials") return result;
|
||||
|
||||
// Needs credentials, prompt for them
|
||||
const creds = await callbacks.promptCredentials(result);
|
||||
if (creds == null) throw new Error('Canceled');
|
||||
if (creds == null) throw new Error("Canceled");
|
||||
|
||||
await invoke('cmd_git_add_credential', {
|
||||
await invoke("cmd_git_add_credential", {
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
// Push again
|
||||
return invoke<PushResult>('cmd_git_push', { dir });
|
||||
return invoke<PushResult>("cmd_git_push", { dir });
|
||||
};
|
||||
|
||||
const handleError = (err: unknown) => {
|
||||
showToast({
|
||||
id: `${err}`,
|
||||
message: `${err}`,
|
||||
color: 'danger',
|
||||
color: "danger",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: createFastMutation<void, string, void>({
|
||||
mutationKey: ['git', 'init'],
|
||||
mutationFn: () => invoke('cmd_git_initialize', { dir }),
|
||||
mutationKey: ["git", "init"],
|
||||
mutationFn: () => invoke("cmd_git_initialize", { dir }),
|
||||
onSuccess,
|
||||
}),
|
||||
add: createFastMutation<void, string, { relaPaths: string[] }>({
|
||||
mutationKey: ['git', 'add', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_add', { dir, ...args }),
|
||||
mutationKey: ["git", "add", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_add", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
addRemote: createFastMutation<GitRemote, string, GitRemote>({
|
||||
mutationKey: ['git', 'add-remote'],
|
||||
mutationFn: (args) => invoke('cmd_git_add_remote', { dir, ...args }),
|
||||
mutationKey: ["git", "add-remote"],
|
||||
mutationFn: (args) => invoke("cmd_git_add_remote", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
rmRemote: createFastMutation<void, string, { name: string }>({
|
||||
mutationKey: ['git', 'rm-remote', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
|
||||
mutationKey: ["git", "rm-remote", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_rm_remote", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
|
||||
mutationKey: ['git', 'branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
|
||||
createBranch: createFastMutation<
|
||||
void,
|
||||
string,
|
||||
{ branch: string; base?: string }
|
||||
>({
|
||||
mutationKey: ["git", "branch", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_branch", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
||||
mutationKey: ['git', 'merge', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
|
||||
mutationKey: ["git", "merge", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_merge_branch", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
|
||||
mutationKey: ['git', 'delete-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
|
||||
deleteBranch: createFastMutation<
|
||||
BranchDeleteResult,
|
||||
string,
|
||||
{ branch: string; force?: boolean }
|
||||
>({
|
||||
mutationKey: ["git", "delete-branch", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_delete_branch", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
|
||||
mutationKey: ['git', 'delete-remote-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_delete_remote_branch', { dir, ...args }),
|
||||
mutationKey: ["git", "delete-remote-branch", dir],
|
||||
mutationFn: (args) =>
|
||||
invoke("cmd_git_delete_remote_branch", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
renameBranch: createFastMutation<void, string, { oldName: string, newName: string }>({
|
||||
mutationKey: ['git', 'rename-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_rename_branch', { dir, ...args }),
|
||||
renameBranch: createFastMutation<
|
||||
void,
|
||||
string,
|
||||
{ oldName: string; newName: string }
|
||||
>({
|
||||
mutationKey: ["git", "rename-branch", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_rename_branch", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
||||
mutationKey: ['git', 'checkout', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
|
||||
checkout: createFastMutation<
|
||||
string,
|
||||
string,
|
||||
{ branch: string; force: boolean }
|
||||
>({
|
||||
mutationKey: ["git", "checkout", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_checkout", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
commit: createFastMutation<void, string, { message: string }>({
|
||||
mutationKey: ['git', 'commit', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_commit', { dir, ...args }),
|
||||
mutationKey: ["git", "commit", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_commit", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
commitAndPush: createFastMutation<PushResult, string, { message: string }>({
|
||||
mutationKey: ['git', 'commit_push', dir],
|
||||
mutationKey: ["git", "commit_push", dir],
|
||||
mutationFn: async (args) => {
|
||||
await invoke('cmd_git_commit', { dir, ...args });
|
||||
await invoke("cmd_git_commit", { dir, ...args });
|
||||
return push();
|
||||
},
|
||||
onSuccess,
|
||||
}),
|
||||
|
||||
push: createFastMutation<PushResult, string, void>({
|
||||
mutationKey: ['git', 'push', dir],
|
||||
mutationKey: ["git", "push", dir],
|
||||
mutationFn: push,
|
||||
onSuccess,
|
||||
}),
|
||||
pull: createFastMutation<PullResult, string, void>({
|
||||
mutationKey: ['git', 'pull', dir],
|
||||
mutationKey: ["git", "pull", dir],
|
||||
async mutationFn() {
|
||||
const result = await invoke<PullResult>('cmd_git_pull', { dir });
|
||||
const result = await invoke<PullResult>("cmd_git_pull", { dir });
|
||||
|
||||
if (result.type === 'needs_credentials') {
|
||||
if (result.type === "needs_credentials") {
|
||||
const creds = await callbacks.promptCredentials(result);
|
||||
if (creds == null) throw new Error('Canceled');
|
||||
if (creds == null) throw new Error("Canceled");
|
||||
|
||||
await invoke('cmd_git_add_credential', {
|
||||
await invoke("cmd_git_add_credential", {
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
// Pull again after credentials
|
||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||
return invoke<PullResult>("cmd_git_pull", { dir });
|
||||
}
|
||||
|
||||
if (result.type === 'uncommitted_changes') {
|
||||
callbacks.promptUncommittedChanges().then(async (strategy) => {
|
||||
if (strategy === 'cancel') return;
|
||||
if (result.type === "uncommitted_changes") {
|
||||
callbacks
|
||||
.promptUncommittedChanges()
|
||||
.then(async (strategy) => {
|
||||
if (strategy === "cancel") return;
|
||||
|
||||
await invoke('cmd_git_reset_changes', { dir });
|
||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
||||
await invoke("cmd_git_reset_changes", { dir });
|
||||
return invoke<PullResult>("cmd_git_pull", { dir });
|
||||
})
|
||||
.then(async () => {
|
||||
onSuccess();
|
||||
await callbacks.forceSync();
|
||||
}, handleError);
|
||||
}
|
||||
|
||||
if (result.type === 'diverged') {
|
||||
callbacks.promptDiverged(result).then((strategy) => {
|
||||
if (strategy === 'cancel') return;
|
||||
if (result.type === "diverged") {
|
||||
callbacks
|
||||
.promptDiverged(result)
|
||||
.then((strategy) => {
|
||||
if (strategy === "cancel") return;
|
||||
|
||||
if (strategy === 'force_reset') {
|
||||
return invoke<PullResult>('cmd_git_pull_force_reset', {
|
||||
if (strategy === "force_reset") {
|
||||
return invoke<PullResult>("cmd_git_pull_force_reset", {
|
||||
dir,
|
||||
remote: result.remote,
|
||||
branch: result.branch,
|
||||
});
|
||||
}
|
||||
|
||||
return invoke<PullResult>("cmd_git_pull_merge", {
|
||||
dir,
|
||||
remote: result.remote,
|
||||
branch: result.branch,
|
||||
});
|
||||
}
|
||||
|
||||
return invoke<PullResult>('cmd_git_pull_merge', {
|
||||
dir,
|
||||
remote: result.remote,
|
||||
branch: result.branch,
|
||||
});
|
||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
||||
})
|
||||
.then(async () => {
|
||||
onSuccess();
|
||||
await callbacks.forceSync();
|
||||
}, handleError);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -219,20 +263,20 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
onSuccess,
|
||||
}),
|
||||
unstage: createFastMutation<void, string, { relaPaths: string[] }>({
|
||||
mutationKey: ['git', 'unstage', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
|
||||
mutationKey: ["git", "unstage", dir],
|
||||
mutationFn: (args) => invoke("cmd_git_unstage", { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
resetChanges: createFastMutation<void, string, void>({
|
||||
mutationKey: ['git', 'reset-changes', dir],
|
||||
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
|
||||
mutationKey: ["git", "reset-changes", dir],
|
||||
mutationFn: () => invoke("cmd_git_reset_changes", { dir }),
|
||||
onSuccess,
|
||||
}),
|
||||
} as const;
|
||||
};
|
||||
|
||||
async function getRemotes(dir: string) {
|
||||
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
|
||||
return invoke<GitRemote[]>("cmd_git_remotes", { dir });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,21 +285,27 @@ async function getRemotes(dir: string) {
|
||||
export async function gitClone(
|
||||
url: string,
|
||||
dir: string,
|
||||
promptCredentials: (args: { url: string; error: string | null }) => Promise<GitCredentials | null>,
|
||||
promptCredentials: (args: {
|
||||
url: string;
|
||||
error: string | null;
|
||||
}) => Promise<GitCredentials | null>,
|
||||
): Promise<CloneResult> {
|
||||
const result = await invoke<CloneResult>('cmd_git_clone', { url, dir });
|
||||
if (result.type !== 'needs_credentials') return result;
|
||||
const result = await invoke<CloneResult>("cmd_git_clone", { url, dir });
|
||||
if (result.type !== "needs_credentials") return result;
|
||||
|
||||
// Prompt for credentials
|
||||
const creds = await promptCredentials({ url: result.url, error: result.error });
|
||||
if (creds == null) return {type: 'cancelled'};
|
||||
const creds = await promptCredentials({
|
||||
url: result.url,
|
||||
error: result.error,
|
||||
});
|
||||
if (creds == null) return { type: "cancelled" };
|
||||
|
||||
// Store credentials and retry
|
||||
await invoke('cmd_git_add_credential', {
|
||||
await invoke("cmd_git_add_credential", {
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
return invoke<CloneResult>('cmd_git_clone', { url, dir });
|
||||
return invoke<CloneResult>("cmd_git_clone", { url, dir });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { resolvedModelName } from '@yaakapp/app/lib/resolvedModelName';
|
||||
import { AnyModel, ModelPayload } from '../bindings/gen_models';
|
||||
import { modelStoreDataAtom } from './atoms';
|
||||
import { ExtractModel, JotaiStore, ModelStoreData } from './types';
|
||||
import { newStoreData } from './util';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { resolvedModelName } from "@yaakapp/yaak-client/lib/resolvedModelName";
|
||||
import { AnyModel, ModelPayload } from "../bindings/gen_models";
|
||||
import { modelStoreDataAtom } from "./atoms";
|
||||
import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
|
||||
import { newStoreData } from "./util";
|
||||
|
||||
let _store: JotaiStore | null = null;
|
||||
|
||||
@@ -12,11 +12,11 @@ export function initModelStore(store: JotaiStore) {
|
||||
_store = store;
|
||||
|
||||
getCurrentWebviewWindow()
|
||||
.listen<ModelPayload>('model_write', ({ payload }) => {
|
||||
.listen<ModelPayload>("model_write", ({ payload }) => {
|
||||
if (shouldIgnoreModel(payload)) return;
|
||||
|
||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||
if (payload.change.type === 'upsert') {
|
||||
if (payload.change.type === "upsert") {
|
||||
return {
|
||||
...prev,
|
||||
[payload.model.model]: {
|
||||
@@ -36,7 +36,7 @@ export function initModelStore(store: JotaiStore) {
|
||||
|
||||
function mustStore(): JotaiStore {
|
||||
if (_store == null) {
|
||||
throw new Error('Model store was not initialized');
|
||||
throw new Error("Model store was not initialized");
|
||||
}
|
||||
|
||||
return _store;
|
||||
@@ -45,8 +45,8 @@ function mustStore(): JotaiStore {
|
||||
let _activeWorkspaceId: string | null = null;
|
||||
|
||||
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
||||
console.log('Syncing models with new workspace', workspaceId);
|
||||
const workspaceModelsStr = await invoke<string>('models_workspace_models', {
|
||||
console.log("Syncing models with new workspace", workspaceId);
|
||||
const workspaceModelsStr = await invoke<string>("models_workspace_models", {
|
||||
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
|
||||
});
|
||||
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
|
||||
@@ -57,25 +57,30 @@ export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
||||
|
||||
mustStore().set(modelStoreDataAtom, data);
|
||||
|
||||
console.log('Synced model store with workspace', workspaceId, data);
|
||||
console.log("Synced model store with workspace", workspaceId, data);
|
||||
|
||||
_activeWorkspaceId = workspaceId;
|
||||
}
|
||||
|
||||
export function listModels<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
modelType: M | ReadonlyArray<M>,
|
||||
): T[] {
|
||||
export function listModels<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(modelType: M | ReadonlyArray<M>): T[] {
|
||||
let data = mustStore().get(modelStoreDataAtom);
|
||||
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
|
||||
const types: ReadonlyArray<M> = Array.isArray(modelType)
|
||||
? modelType
|
||||
: [modelType];
|
||||
return types.flatMap((t) => Object.values(data[t]) as T[]);
|
||||
}
|
||||
|
||||
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
modelType: M | ReadonlyArray<M>,
|
||||
id: string,
|
||||
): T | null {
|
||||
export function getModel<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(modelType: M | ReadonlyArray<M>, id: string): T | null {
|
||||
let data = mustStore().get(modelStoreDataAtom);
|
||||
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
|
||||
const types: ReadonlyArray<M> = Array.isArray(modelType)
|
||||
? modelType
|
||||
: [modelType];
|
||||
for (const t of types) {
|
||||
let v = data[t][id];
|
||||
if (v?.model === t) return v as T;
|
||||
@@ -83,9 +88,7 @@ export function getModel<M extends AnyModel['model'], T extends ExtractModel<Any
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAnyModel(
|
||||
id: string,
|
||||
): AnyModel | null {
|
||||
export function getAnyModel(id: string): AnyModel | null {
|
||||
let data = mustStore().get(modelStoreDataAtom);
|
||||
for (const t of Object.keys(data)) {
|
||||
let v = (data as any)[t]?.[id];
|
||||
@@ -94,64 +97,71 @@ export function getAnyModel(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patchModelById<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
model: M,
|
||||
id: string,
|
||||
patch: Partial<T> | ((prev: T) => T),
|
||||
): Promise<string> {
|
||||
export function patchModelById<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(model: M, id: string, patch: Partial<T> | ((prev: T) => T)): Promise<string> {
|
||||
let prev = getModel<M, T>(model, id);
|
||||
if (prev == null) {
|
||||
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
|
||||
}
|
||||
|
||||
const newModel = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
|
||||
const newModel =
|
||||
typeof patch === "function" ? patch(prev) : { ...prev, ...patch };
|
||||
return updateModel(newModel);
|
||||
}
|
||||
|
||||
export async function patchModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
base: Pick<T, 'id' | 'model'>,
|
||||
patch: Partial<T>,
|
||||
): Promise<string> {
|
||||
export async function patchModel<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(base: Pick<T, "id" | "model">, patch: Partial<T>): Promise<string> {
|
||||
return patchModelById<M, T>(base.model, base.id, patch);
|
||||
}
|
||||
|
||||
export async function updateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
model: T,
|
||||
): Promise<string> {
|
||||
return invoke<string>('models_upsert', { model });
|
||||
export async function updateModel<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(model: T): Promise<string> {
|
||||
return invoke<string>("models_upsert", { model });
|
||||
}
|
||||
|
||||
export async function deleteModelById<
|
||||
M extends AnyModel['model'],
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(modelType: M | M[], id: string) {
|
||||
let model = getModel<M, T>(modelType, id);
|
||||
await deleteModel(model);
|
||||
}
|
||||
|
||||
export async function deleteModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
model: T | null,
|
||||
) {
|
||||
export async function deleteModel<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(model: T | null) {
|
||||
if (model == null) {
|
||||
throw new Error('Failed to delete null model');
|
||||
throw new Error("Failed to delete null model");
|
||||
}
|
||||
await invoke<string>('models_delete', { model });
|
||||
await invoke<string>("models_delete", { model });
|
||||
}
|
||||
|
||||
export function duplicateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
model: T | null,
|
||||
) {
|
||||
export function duplicateModel<
|
||||
M extends AnyModel["model"],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(model: T | null) {
|
||||
if (model == null) {
|
||||
throw new Error('Failed to duplicate null model');
|
||||
throw new Error("Failed to duplicate null model");
|
||||
}
|
||||
|
||||
// If the model has a name, try to duplicate it with a name that doesn't conflict
|
||||
let name = 'name' in model ? resolvedModelName(model) : undefined;
|
||||
let name = "name" in model ? resolvedModelName(model) : undefined;
|
||||
if (name != null) {
|
||||
const existingModels = listModels(model.model);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const hasConflict = existingModels.some((m) => {
|
||||
if ('folderId' in m && 'folderId' in model && model.folderId !== m.folderId) {
|
||||
if (
|
||||
"folderId" in m &&
|
||||
"folderId" in model &&
|
||||
model.folderId !== m.folderId
|
||||
) {
|
||||
return false;
|
||||
} else if (resolvedModelName(m) !== name) {
|
||||
return false;
|
||||
@@ -165,7 +175,7 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
|
||||
// Name conflict. Try another one
|
||||
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
|
||||
if (m != null && m.groups?.n == null) {
|
||||
name = name.substring(0, m.index) + ' Copy 2';
|
||||
name = name.substring(0, m.index) + " Copy 2";
|
||||
} else if (m != null && m.groups?.n != null) {
|
||||
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
|
||||
} else {
|
||||
@@ -174,23 +184,23 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
|
||||
}
|
||||
}
|
||||
|
||||
return invoke<string>('models_duplicate', { model: { ...model, name } });
|
||||
return invoke<string>("models_duplicate", { model: { ...model, name } });
|
||||
}
|
||||
|
||||
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
||||
patch: Partial<T> & Pick<T, 'model'>,
|
||||
): Promise<string> {
|
||||
return invoke<string>('models_upsert', { model: patch });
|
||||
export async function createGlobalModel<
|
||||
T extends Exclude<AnyModel, { workspaceId: string }>,
|
||||
>(patch: Partial<T> & Pick<T, "model">): Promise<string> {
|
||||
return invoke<string>("models_upsert", { model: patch });
|
||||
}
|
||||
|
||||
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
||||
patch: Partial<T> & Pick<T, 'model' | 'workspaceId'>,
|
||||
): Promise<string> {
|
||||
return invoke<string>('models_upsert', { model: patch });
|
||||
export async function createWorkspaceModel<
|
||||
T extends Extract<AnyModel, { workspaceId: string }>,
|
||||
>(patch: Partial<T> & Pick<T, "model" | "workspaceId">): Promise<string> {
|
||||
return invoke<string>("models_upsert", { model: patch });
|
||||
}
|
||||
|
||||
export function replaceModelsInStore<
|
||||
M extends AnyModel['model'],
|
||||
M extends AnyModel["model"],
|
||||
T extends Extract<AnyModel, { model: M }>,
|
||||
>(model: M, models: T[]) {
|
||||
const newModels: Record<string, T> = {};
|
||||
@@ -207,7 +217,7 @@ export function replaceModelsInStore<
|
||||
}
|
||||
|
||||
export function mergeModelsInStore<
|
||||
M extends AnyModel['model'],
|
||||
M extends AnyModel["model"],
|
||||
T extends Extract<AnyModel, { model: M }>,
|
||||
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||
@@ -236,7 +246,7 @@ export function mergeModelsInStore<
|
||||
|
||||
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
||||
// Never ignore updates from non-user sources
|
||||
if (updateSource.type !== 'window') {
|
||||
if (updateSource.type !== "window") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -246,11 +256,11 @@ function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
||||
}
|
||||
|
||||
// Only sync models that belong to this workspace, if a workspace ID is present
|
||||
if ('workspaceId' in model && model.workspaceId !== _activeWorkspaceId) {
|
||||
if ("workspaceId" in model && model.workspaceId !== _activeWorkspaceId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model.model === 'key_value' && model.namespace === 'no_sync') {
|
||||
if (model.model === "key_value" && model.namespace === "no_sync") {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ impl PluginManager {
|
||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
||||
if self.dev_mode {
|
||||
// Use plugins directly for easy development
|
||||
// Tauri runs from crates-tauri/yaak-app/, so go up two levels to reach project root
|
||||
// Tauri runs from crates-tauri/yaak-app-client/, so go up two levels to reach project root
|
||||
env::current_dir()
|
||||
.map(|cwd| cwd.join("../../plugins").canonicalize().unwrap())
|
||||
.unwrap_or_else(|_| self.vendored_plugin_dir.clone())
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { Channel, invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import type { WatchResult } from '@yaakapp-internal/tauri';
|
||||
import { SyncOp } from './bindings/gen_sync';
|
||||
import { WatchEvent } from './bindings/gen_watch';
|
||||
import { Channel, invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import type { WatchResult } from "@yaakapp-internal/tauri-client";
|
||||
import { SyncOp } from "./bindings/gen_sync";
|
||||
import { WatchEvent } from "./bindings/gen_watch";
|
||||
|
||||
export * from './bindings/gen_models';
|
||||
export * from "./bindings/gen_models";
|
||||
|
||||
export async function calculateSync(workspaceId: string, syncDir: string) {
|
||||
return invoke<SyncOp[]>('cmd_sync_calculate', {
|
||||
return invoke<SyncOp[]>("cmd_sync_calculate", {
|
||||
workspaceId,
|
||||
syncDir,
|
||||
});
|
||||
}
|
||||
|
||||
export async function calculateSyncFsOnly(dir: string) {
|
||||
return invoke<SyncOp[]>('cmd_sync_calculate_fs', { dir });
|
||||
return invoke<SyncOp[]>("cmd_sync_calculate_fs", { dir });
|
||||
}
|
||||
|
||||
export async function applySync(workspaceId: string, syncDir: string, syncOps: SyncOp[]) {
|
||||
return invoke<void>('cmd_sync_apply', {
|
||||
export async function applySync(
|
||||
workspaceId: string,
|
||||
syncDir: string,
|
||||
syncOps: SyncOp[],
|
||||
) {
|
||||
return invoke<void>("cmd_sync_apply", {
|
||||
workspaceId,
|
||||
syncDir,
|
||||
syncOps: syncOps,
|
||||
@@ -30,10 +34,10 @@ export function watchWorkspaceFiles(
|
||||
syncDir: string,
|
||||
callback: (e: WatchEvent) => void,
|
||||
) {
|
||||
console.log('Watching workspace files', workspaceId, syncDir);
|
||||
console.log("Watching workspace files", workspaceId, syncDir);
|
||||
const channel = new Channel<WatchEvent>();
|
||||
channel.onmessage = callback;
|
||||
const unlistenPromise = invoke<WatchResult>('cmd_sync_watch', {
|
||||
const unlistenPromise = invoke<WatchResult>("cmd_sync_watch", {
|
||||
workspaceId,
|
||||
syncDir,
|
||||
channel,
|
||||
@@ -46,7 +50,7 @@ export function watchWorkspaceFiles(
|
||||
return () =>
|
||||
unlistenPromise
|
||||
.then(async ({ unlistenEvent }) => {
|
||||
console.log('Unwatching workspace files', workspaceId, syncDir);
|
||||
console.log("Unwatching workspace files", workspaceId, syncDir);
|
||||
unlistenToWatcher(unlistenEvent);
|
||||
})
|
||||
.catch(console.error);
|
||||
@@ -59,11 +63,16 @@ function unlistenToWatcher(unlistenEvent: string) {
|
||||
}
|
||||
|
||||
function getWatchKeys() {
|
||||
return sessionStorage.getItem('workspace-file-watchers')?.split(',').filter(Boolean) ?? [];
|
||||
return (
|
||||
sessionStorage
|
||||
.getItem("workspace-file-watchers")
|
||||
?.split(",")
|
||||
.filter(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function setWatchKeys(keys: string[]) {
|
||||
sessionStorage.setItem('workspace-file-watchers', keys.join(','));
|
||||
sessionStorage.setItem("workspace-file-watchers", keys.join(","));
|
||||
}
|
||||
|
||||
function addWatchKey(key: string) {
|
||||
@@ -79,6 +88,6 @@ function removeWatchKey(key: string) {
|
||||
// On page load, unlisten to all zombie watchers
|
||||
const keys = getWatchKeys();
|
||||
if (keys.length > 0) {
|
||||
console.log('Unsubscribing to zombie file watchers', keys);
|
||||
console.log("Unsubscribing to zombie file watchers", keys);
|
||||
keys.forEach(unlistenToWatcher);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user