mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 11:45:06 +03:00
frontend: refactor api structure
This commit is contained in:
parent
96bccad65e
commit
a2c332302c
2
src/lib/api/cloud/index.ts
Normal file
2
src/lib/api/cloud/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Api } from './api';
|
||||
export type { User, LoginToken, Project } from './api';
|
2
src/lib/api/index.ts
Normal file
2
src/lib/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ipc';
|
||||
export { Api as CloudApi, type User, type LoginToken } from './cloud';
|
@ -1,8 +1,7 @@
|
||||
import { log } from '$lib';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { clone } from './utils';
|
||||
import { clone } from '$lib/utils';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type OperationDelete = { delete: [number, number] };
|
||||
export type OperationInsert = { insert: [number, string] };
|
||||
@ -19,11 +18,6 @@ export namespace Operation {
|
||||
|
||||
export type Delta = { timestampMs: number; operations: Operation[] };
|
||||
|
||||
export type DeltasEvent = {
|
||||
deltas: Delta[];
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const cache: Record<string, Record<string, Promise<Record<string, Delta[]>>>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
|
||||
@ -53,22 +47,27 @@ export const list = async (params: { projectId: string; sessionId: string; paths
|
||||
);
|
||||
};
|
||||
|
||||
export default async (params: { projectId: string; sessionId: string }) => {
|
||||
const init = await list(params);
|
||||
|
||||
const store = writable<Record<string, Delta[]>>(init);
|
||||
appWindow.listen<DeltasEvent>(
|
||||
export const subscribe = (
|
||||
params: { projectId: string; sessionId: string },
|
||||
callback: (params: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
filePath: string;
|
||||
deltas: Delta[];
|
||||
}) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<{ deltas: Delta[]; filePath: string }>(
|
||||
`project://${params.projectId}/sessions/${params.sessionId}/deltas`,
|
||||
(event) => {
|
||||
log.info(
|
||||
`Received deltas for ${params.projectId}, ${params.sessionId}, ${event.payload.filePath}`
|
||||
);
|
||||
store.update((deltas) => ({
|
||||
...deltas,
|
||||
[event.payload.filePath]: event.payload.deltas
|
||||
}));
|
||||
}
|
||||
(event) => callback({ ...params, ...event.payload })
|
||||
);
|
||||
|
||||
return store as Readable<Record<string, Delta[]>>;
|
||||
export const Deltas = async (params: { projectId: string; sessionId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
subscribe(params, ({ filePath, deltas }) => {
|
||||
store.update((deltasCache) => {
|
||||
deltasCache[filePath] = deltas;
|
||||
return deltasCache;
|
||||
});
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
31
src/lib/api/ipc/files.ts
Normal file
31
src/lib/api/ipc/files.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { clone } from '$lib/utils';
|
||||
|
||||
const cache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
|
||||
const sessionFilesCache = cache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
cache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
28
src/lib/api/ipc/git/activities.ts
Normal file
28
src/lib/api/ipc/git/activities.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const list = (params: { projectId: string; startTimeMs?: number }) =>
|
||||
invoke<Activity[]>('git_activity', params);
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string }) => Promise<void> | void
|
||||
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback(params));
|
||||
|
||||
export const Activities = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
subscribe(params, async () => {
|
||||
const activity = get(store);
|
||||
const startTimeMs = activity.at(-1)?.timestampMs;
|
||||
const newActivities = await list({ projectId: params.projectId, startTimeMs });
|
||||
store.update((activities) => [...activities, ...newActivities]);
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
13
src/lib/api/ipc/git/diffs.ts
Normal file
13
src/lib/api/ipc/git/diffs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sessions, git } from '$lib/api';
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
|
||||
export const Diffs = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
git.activities.subscribe(params, ({ projectId }) => list({ projectId }).then(store.set));
|
||||
sessions.subscribe(params, () => list(params).then(store.set));
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
19
src/lib/api/ipc/git/heads.ts
Normal file
19
src/lib/api/ipc/git/heads.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const get = (params: { projectId: string }) => invoke<string>('git_head', params);
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string; head: string }) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, (event) =>
|
||||
callback({ ...params, ...event.payload })
|
||||
);
|
||||
|
||||
export const Head = async (params: { projectId: string }) => {
|
||||
const store = writable(await get(params));
|
||||
subscribe(params, ({ head }) => store.set(head));
|
||||
return derived(store, (head) => head.replace('refs/heads/', ''));
|
||||
};
|
@ -1,7 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
export * as statuses from './statuses';
|
||||
export { Status } from './statuses';
|
||||
export * as activities from './activities';
|
||||
export type { Activity } from './activities';
|
||||
export * as heads from './heads';
|
||||
export * as diffs from './diffs';
|
||||
export * as indexes from './indexes';
|
||||
|
||||
export { default as statuses } from './statuses';
|
||||
export { default as activity } from './activity';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export const commit = (params: { projectId: string; message: string; push: boolean }) =>
|
||||
invoke<boolean>('git_commit', params);
|
6
src/lib/api/ipc/git/indexes.ts
Normal file
6
src/lib/api/ipc/git/indexes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string }) => Promise<void>
|
||||
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback({ ...params }));
|
@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sessions, git } from '$lib/api';
|
||||
|
||||
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'typeChange' | 'other';
|
||||
|
||||
@ -16,22 +16,13 @@ export namespace Status {
|
||||
'unstaged' in status && status.unstaged !== null;
|
||||
}
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
export const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, Status>>('git_status', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const statuses = await list(params);
|
||||
const store = writable(statuses);
|
||||
|
||||
[
|
||||
`project://${params.projectId}/git/index`,
|
||||
`project://${params.projectId}/git/activity`,
|
||||
`project://${params.projectId}/sessions`
|
||||
].forEach((eventName) => {
|
||||
appWindow.listen(eventName, async () => {
|
||||
store.set(await list(params));
|
||||
});
|
||||
});
|
||||
|
||||
return store as Readable<Record<string, Status>>;
|
||||
export const Statuses = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
sessions.subscribe(params, () => list(params).then(store.set));
|
||||
git.activities.subscribe(params, () => list(params).then(store.set));
|
||||
git.indexes.subscribe(params, () => list(params).then(store.set));
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
12
src/lib/api/ipc/index.ts
Normal file
12
src/lib/api/ipc/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * as git from './git';
|
||||
export { Status, type Activity } from './git';
|
||||
export * as deltas from './deltas';
|
||||
export { type Delta, Operation } from './deltas';
|
||||
export * as sessions from './sessions';
|
||||
export { Session } from './sessions';
|
||||
export * as users from './users';
|
||||
export * as projects from './projects';
|
||||
export type { Project } from './projects';
|
||||
export * as searchResults from './search';
|
||||
export { type SearchResult } from './search';
|
||||
export * as files from './files';
|
@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Project as ApiProject } from '$lib/api';
|
||||
import type { Project as ApiProject } from '$lib/api/cloud';
|
||||
import { derived, readable, writable } from 'svelte/store';
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@ -9,9 +9,9 @@ export type Project = {
|
||||
api: ApiProject & { sync: boolean };
|
||||
};
|
||||
|
||||
const list = () => invoke<Project[]>('list_projects');
|
||||
export const list = () => invoke<Project[]>('list_projects');
|
||||
|
||||
const update = (params: {
|
||||
export const update = (params: {
|
||||
project: {
|
||||
id: string;
|
||||
title?: string;
|
||||
@ -19,14 +19,12 @@ const update = (params: {
|
||||
};
|
||||
}) => invoke<Project>('update_project', params);
|
||||
|
||||
const add = (params: { path: string }) => invoke<Project>('add_project', params);
|
||||
export const add = (params: { path: string }) => invoke<Project>('add_project', params);
|
||||
|
||||
const del = (params: { id: string }) => invoke('delete_project', params);
|
||||
|
||||
export default async () => {
|
||||
const init = await list();
|
||||
const store = writable<Project[]>(init);
|
||||
export const del = (params: { id: string }) => invoke('delete_project', params);
|
||||
|
||||
export const Projects = async () => {
|
||||
const store = writable(await list());
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
get: (id: string) => {
|
@ -10,7 +10,7 @@ export type SearchResult = {
|
||||
highlighted: string[];
|
||||
};
|
||||
|
||||
export const search = (params: {
|
||||
export const list = (params: {
|
||||
projectId: string;
|
||||
query: string;
|
||||
limit?: number;
|
63
src/lib/api/ipc/sessions.ts
Normal file
63
src/lib/api/ipc/sessions.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { clone } from '$lib/utils';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export namespace Session {
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
};
|
||||
const cache: Record<string, Promise<Session[]>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
if (params.projectId in cache) {
|
||||
return cache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
cache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return cache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string; session: Session }) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) =>
|
||||
callback({ ...params, session: event.payload })
|
||||
);
|
||||
|
||||
export const Sessions = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
subscribe(params, ({ session }) => {
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((s) => s.id === session.id);
|
||||
if (index === -1) return [...sessions, session];
|
||||
sessions[index] = session;
|
||||
return sessions;
|
||||
});
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
@ -1,18 +1,15 @@
|
||||
import type { User } from '$lib/api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const get = () => invoke<User | undefined>('get_user');
|
||||
export const get = () => invoke<User | undefined>('get_user');
|
||||
|
||||
const set = (params: { user: User }) => invoke<void>('set_user', params);
|
||||
export const set = (params: { user: User }) => invoke<void>('set_user', params);
|
||||
|
||||
const del = () => invoke<void>('delete_user');
|
||||
export const del = () => invoke<void>('delete_user');
|
||||
|
||||
export default async () => {
|
||||
const store = writable<User | undefined>(undefined);
|
||||
|
||||
const init = await get();
|
||||
store.set(init);
|
||||
export const CurrentUser = async () => {
|
||||
const store = writable<User | undefined>(await get());
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: async (user: User) => {
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { IconHome } from './icons';
|
||||
import { Tooltip } from '$lib/components';
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type Delta, Operation } from '$lib/deltas';
|
||||
import { type Delta, Operation } from '$lib/api';
|
||||
import { lineDiff } from './diff';
|
||||
import { create } from './CodeHighlighter';
|
||||
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import tinykeys from 'tinykeys';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { derived, readable, writable, type Readable } from 'svelte/store';
|
||||
import { Modal } from '$lib/components';
|
||||
import listAvailableCommands, { Action, type Group } from './commands';
|
||||
@ -8,10 +8,12 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { IconExternalLink } from '../icons';
|
||||
import type { User } from '$lib/api';
|
||||
|
||||
export let projects: Readable<Project[]>;
|
||||
export let addProject: (params: { path: string }) => Promise<Project>;
|
||||
export let project = readable<Project | undefined>(undefined);
|
||||
export let user = readable<User | undefined>(undefined);
|
||||
|
||||
const input = writable('');
|
||||
const scopeToProject = writable(!!$project);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Project } from '$lib/projects';
|
||||
import { type Project, git } from '$lib/api';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts } from '$lib';
|
||||
import {
|
||||
@ -12,7 +12,6 @@ import {
|
||||
IconAdjustmentsHorizontal,
|
||||
IconDiscord
|
||||
} from '../icons';
|
||||
import { matchFiles } from '$lib/git';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { format, startOfISOWeek, startOfMonth, subDays, subMonths, subWeeks } from 'date-fns';
|
||||
|
||||
@ -216,7 +215,7 @@ const fileGroup = ({
|
||||
description: 'type part of a file name',
|
||||
commands: []
|
||||
}
|
||||
: matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
|
||||
: git.matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
|
||||
title: 'Files',
|
||||
description: files.length === 0 ? `no files containing '${input}'` : '',
|
||||
commands: files.map((file) => ({
|
||||
|
@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type Users from '$lib/users';
|
||||
import type Api from '$lib/api';
|
||||
import type { LoginToken } from '$lib/api';
|
||||
import type { LoginToken, CloudApi, users } from '$lib/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
export let user: Awaited<ReturnType<typeof Users>>;
|
||||
export let api: Awaited<ReturnType<typeof Api>>;
|
||||
export let user: Awaited<ReturnType<typeof users.CurrentUser>>;
|
||||
export let api: Awaited<ReturnType<typeof CloudApi>>;
|
||||
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await api.login.user.get(token).catch(() => null);
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { collapsable } from '$lib/paths';
|
||||
import { Status } from '$lib/git/statuses';
|
||||
import { Status } from '$lib/api';
|
||||
|
||||
export let statuses: Record<string, Status>;
|
||||
</script>
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const list = (params: { projectId: string; startTimeMs?: number }) =>
|
||||
invoke<Activity[]>('git_activity', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const activity = await list(params);
|
||||
const store = writable(activity);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/activity`, async () => {
|
||||
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
|
||||
const startTimeMs = activity.at(-1)?.timestampMs;
|
||||
const newActivities = await list({ projectId: params.projectId, startTimeMs });
|
||||
store.update((activities) => [...activities, ...newActivities]);
|
||||
});
|
||||
|
||||
return store as Readable<Activity[]>;
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
const getDiffs = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const diffs = await getDiffs(params);
|
||||
const store = writable(diffs);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/sessions`, async () => {
|
||||
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
|
||||
store.set(await getDiffs(params));
|
||||
});
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/index`, async () => {
|
||||
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
|
||||
store.set(await getDiffs(params));
|
||||
});
|
||||
|
||||
return store as Readable<Record<string, string>>;
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
const list = (params: { projectId: string }) => invoke<string>('git_head', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const head = await list(params);
|
||||
const store = writable(head);
|
||||
|
||||
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, async (payload) => {
|
||||
log.info(`Status: Received git head event, projectId: ${params.projectId}`);
|
||||
store.set(payload.payload.head);
|
||||
});
|
||||
|
||||
return derived(store, (head) => head.replace('refs/heads/', ''));
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
export * as deltas from './deltas';
|
||||
export * as projects from './projects';
|
||||
export * as api from './api';
|
||||
export * as log from './log';
|
||||
export * as toasts from './toasts';
|
||||
export * as sessions from './sessions';
|
||||
export { Toaster } from './toasts';
|
||||
export * as week from './week';
|
||||
export * as uisessions from './uisessions';
|
||||
export * from './search';
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
import type { Activity } from './git/activity';
|
||||
import { clone } from './utils';
|
||||
|
||||
export namespace Session {
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
};
|
||||
|
||||
const filesCache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
|
||||
|
||||
export const listFiles = async (params: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
paths?: string[];
|
||||
}) => {
|
||||
const sessionFilesCache = filesCache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
filesCache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const sessionsCache: Record<string, Promise<Session[]>> = {};
|
||||
|
||||
const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
if (params.projectId in sessionsCache) {
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
const store = writable([] as Session[]);
|
||||
list(params).then((sessions) => {
|
||||
store.set(sessions);
|
||||
});
|
||||
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
|
||||
log.info(`Received sessions event, projectId: ${params.projectId}`);
|
||||
const session = event.payload;
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((session) => session.id === event.payload.id);
|
||||
if (index === -1) {
|
||||
return [...sessions, session];
|
||||
} else {
|
||||
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return store as Readable<Session[]>;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
|
||||
export { Toaster } from 'svelte-french-toast';
|
||||
|
||||
const defaultOptions = {
|
||||
position: 'bottom-center' as ToastPosition,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Session, Delta } from '$lib/api';
|
||||
|
||||
export type UISession = {
|
||||
session: Session;
|
||||
|
@ -2,14 +2,11 @@
|
||||
import '../app.postcss';
|
||||
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts } from '$lib';
|
||||
import { toasts, Toaster } from '$lib';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
import type { LayoutData } from './$types';
|
||||
import { BackForwardButtons, Button } from '$lib/components';
|
||||
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
|
||||
import { BackForwardButtons, Button, CommandPalette, Breadcrumbs } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import CommandPalette from '$lib/components/CommandPalette/CommandPalette.svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -79,5 +76,11 @@
|
||||
<slot />
|
||||
</div>
|
||||
<Toaster />
|
||||
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
|
||||
<CommandPalette
|
||||
bind:this={commandPalette}
|
||||
{projects}
|
||||
{project}
|
||||
{user}
|
||||
addProject={projects.add}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { building } from '$app/environment';
|
||||
import type { Project } from '$lib/projects';
|
||||
import Api from '$lib/api';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Api } from '$lib/api/cloud';
|
||||
import Posthog from '$lib/posthog';
|
||||
import Sentry from '$lib/sentry';
|
||||
import * as log from '$lib/log';
|
||||
import { setup as setupLogger } from '$lib/log';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const ssr = false;
|
||||
@ -23,7 +23,7 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/projects')).default();
|
||||
: await (await import('$lib/api')).projects.Projects();
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
@ -34,8 +34,8 @@ export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/users')).default();
|
||||
await log.setup();
|
||||
: await (await import('$lib/api')).users.CurrentUser();
|
||||
setupLogger();
|
||||
return {
|
||||
projects,
|
||||
user,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Button, Tooltip } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { building } from '$app/environment';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { Status } from '$lib/git/statuses';
|
||||
import type { Session, Delta, Status } from '$lib/api';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export const prerender = false;
|
||||
@ -12,17 +10,19 @@ export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
const sessions = building
|
||||
? readable<Session[]>([])
|
||||
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.sessions.Sessions({ projectId: params.projectId }));
|
||||
const statuses = building
|
||||
? readable<Record<string, Status>>({})
|
||||
: await import('$lib/git/statuses').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) =>
|
||||
m.git.statuses.Statuses({ projectId: params.projectId })
|
||||
);
|
||||
const head = building
|
||||
? readable<string>('')
|
||||
: await import('$lib/git/head').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.git.heads.Head({ projectId: params.projectId }));
|
||||
const deltas = building
|
||||
? () => Promise.resolve(readable<Record<string, Delta[]>>({}))
|
||||
: (sessionId: string) =>
|
||||
import('$lib/deltas').then((m) => m.default({ projectId: params.projectId, sessionId }));
|
||||
import('$lib/api').then((m) => m.deltas.Deltas({ projectId: params.projectId, sessionId }));
|
||||
|
||||
const cache: Record<string, Promise<Readable<Record<string, Delta[]>>>> = {};
|
||||
const cachedDeltas = (sessionId: string) => {
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { IconGitBranch, IconLoading } from '$lib/components/icons';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import FileSummaries from './FileSummaries.svelte';
|
||||
import { Button, Statuses, Tooltip } from '$lib/components';
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { building } from '$app/environment';
|
||||
import type { PageLoad } from './$types';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { Activity } from '$lib/git/activity';
|
||||
import type { Activity } from '$lib/api';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const load: PageLoad = wrapLoadWithSentry(async ({ params }) => {
|
||||
const activity = building
|
||||
? readable<Activity[]>([])
|
||||
: await import('$lib/git/activity').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) =>
|
||||
m.git.activities.Activities({ projectId: params.projectId })
|
||||
);
|
||||
return {
|
||||
activity
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import { bucketByTimestamp } from './histogram';
|
||||
|
||||
export let deltas: Delta[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { format, startOfDay } from 'date-fns';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
import { collapsable } from '$lib/paths';
|
||||
import FileActivity from './FileActivity.svelte';
|
||||
|
@ -284,7 +284,7 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
.automated-message {
|
||||
@apply max-w-[500px] rounded-[18px] rounded-tl-md bg-zinc-200 text-[14px] font-medium text-zinc-800;
|
||||
}
|
||||
|
@ -3,8 +3,7 @@
|
||||
import { Button } from '$lib/components';
|
||||
import { collapsable } from '$lib/paths';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import * as git from '$lib/git';
|
||||
import { Status } from '$lib/git/statuses';
|
||||
import { git, Status } from '$lib/api';
|
||||
import DiffViewer from '$lib/components/DiffViewer.svelte';
|
||||
import { error, success } from '$lib/toasts';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
@ -7,7 +7,7 @@ export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params }) => {
|
||||
const { project } = await parent();
|
||||
const diffs = building
|
||||
? readable<Record<string, string>>({})
|
||||
: await import('$lib/git/diffs').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.git.diffs.Diffs({ projectId: params.projectId }));
|
||||
return {
|
||||
diffs,
|
||||
project
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import { deltas } from '$lib/api';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { format } from 'date-fns';
|
||||
import { derived } from 'svelte/store';
|
||||
@ -14,7 +14,7 @@
|
||||
const dates = asyncDerived([sessions, fileFilter], async ([sessions, fileFilter]) => {
|
||||
const sessionDeltas = await Promise.all(
|
||||
sessions.map((session) =>
|
||||
listDeltas({
|
||||
deltas.list({
|
||||
projectId,
|
||||
sessionId: session.id,
|
||||
paths: fileFilter ? [fileFilter] : undefined
|
||||
|
@ -1,20 +1,21 @@
|
||||
<script lang="ts" context="module">
|
||||
import { listFiles, type Session } from '$lib/sessions';
|
||||
import { list as listDeltas, type Delta } from '$lib/deltas';
|
||||
import { deltas, files, type Session, type Delta } from '$lib/api';
|
||||
const enrichSession = async (projectId: string, session: Session, paths?: string[]) => {
|
||||
const files = await listFiles({ projectId, sessionId: session.id, paths });
|
||||
const deltas = await listDeltas({ projectId, sessionId: session.id, paths }).then((deltas) =>
|
||||
Object.entries(deltas)
|
||||
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
|
||||
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
||||
);
|
||||
const deltasFiles = new Set(deltas.map(([path]) => path));
|
||||
const sessionFiles = await files.list({ projectId, sessionId: session.id, paths });
|
||||
const sessionDeltas = await deltas
|
||||
.list({ projectId, sessionId: session.id, paths })
|
||||
.then((deltas) =>
|
||||
Object.entries(deltas)
|
||||
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
|
||||
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
||||
);
|
||||
const deltasFiles = new Set(sessionDeltas.map(([path]) => path));
|
||||
return {
|
||||
...session,
|
||||
files: Object.fromEntries(
|
||||
Object.entries(files).filter(([filepath]) => deltasFiles.has(filepath))
|
||||
Object.entries(sessionFiles).filter(([filepath]) => deltasFiles.has(filepath))
|
||||
),
|
||||
deltas
|
||||
deltas: sessionDeltas
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import slider from '$lib/slider';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
type RichSession = { id: string; deltas: [string, Delta][] };
|
||||
export let sessions: RichSession[];
|
||||
|
@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { search, type SearchResult } from '$lib';
|
||||
import { IconChevronLeft, IconChevronRight } from '$lib/components/icons';
|
||||
import { listFiles } from '$lib/sessions';
|
||||
import { files, deltas, searchResults, type SearchResult } from '$lib/api';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { IconLoading } from '$lib/components/icons';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import { CodeViewer } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
@ -29,16 +27,17 @@
|
||||
index,
|
||||
highlighted
|
||||
}: SearchResult) => {
|
||||
const [doc, deltas] = await Promise.all([
|
||||
listFiles({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
|
||||
listDeltas({ projectId, sessionId, paths: [filePath] })
|
||||
const [doc, dd] = await Promise.all([
|
||||
files.list({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
|
||||
deltas
|
||||
.list({ projectId, sessionId, paths: [filePath] })
|
||||
.then((r) => r[filePath] ?? [])
|
||||
.then((d) => d.slice(0, index + 1))
|
||||
]);
|
||||
const date = format(deltas[deltas.length - 1].timestampMs, 'yyyy-MM-dd');
|
||||
const date = format(dd[dd.length - 1].timestampMs, 'yyyy-MM-dd');
|
||||
return {
|
||||
doc,
|
||||
deltas,
|
||||
deltas: dd,
|
||||
filepath: filePath,
|
||||
highlight: highlighted,
|
||||
sessionId,
|
||||
@ -47,11 +46,11 @@
|
||||
};
|
||||
};
|
||||
|
||||
const { store: searchResults, state: searchState } = asyncDerived(
|
||||
const { store: results, state: searchState } = asyncDerived(
|
||||
[query, project, offset],
|
||||
async ([query, project, offset]) => {
|
||||
if (!query || !project) return { page: [], total: 0, haveNext: false, havePrev: false };
|
||||
const results = await search({ projectId: project.id, query, limit, offset });
|
||||
const results = await searchResults.list({ projectId: project.id, query, limit, offset });
|
||||
return {
|
||||
page: await Promise.all(results.page.map(fetchResultData)),
|
||||
haveNext: offset + limit < results.total,
|
||||
@ -78,16 +77,16 @@
|
||||
</figcaption>
|
||||
{:else if $searchState?.isLoaded}
|
||||
<figcaption class="mt-14">
|
||||
{#if $searchResults.total > 0}
|
||||
{#if $results.total > 0}
|
||||
<p class="mb-2 text-xl text-[#D4D4D8]">Results for "{$query}"</p>
|
||||
<p class="text-lg text-[#717179]">{$searchResults.total} change instances</p>
|
||||
<p class="text-lg text-[#717179]">{$results.total} change instances</p>
|
||||
{:else}
|
||||
<p class="mb-2 text-xl text-[#D4D4D8]">No results for "{$query}"</p>
|
||||
{/if}
|
||||
</figcaption>
|
||||
|
||||
<ul class="search-result-list -mr-14 flex flex-auto flex-col gap-6 overflow-auto pb-6">
|
||||
{#each $searchResults.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
|
||||
{#each $results.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
|
||||
{@const timestamp = deltas[deltas.length - 1].timestampMs}
|
||||
<li class="search-result mr-14">
|
||||
<a
|
||||
@ -111,18 +110,18 @@
|
||||
<nav class="mx-auto flex text-zinc-400">
|
||||
<button
|
||||
on:click={openPrevPage}
|
||||
disabled={!$searchResults.havePrev}
|
||||
disabled={!$results.havePrev}
|
||||
title="Back"
|
||||
class:text-zinc-50={$searchResults.havePrev}
|
||||
class:text-zinc-50={$results.havePrev}
|
||||
class="h-9 w-9 rounded-tl-md rounded-bl-md border border-r-0 border-zinc-700 hover:bg-zinc-700"
|
||||
>
|
||||
<IconChevronLeft class="ml-1 h-5 w-6" />
|
||||
</button>
|
||||
<button
|
||||
on:click={openNextPage}
|
||||
disabled={!$searchResults.haveNext}
|
||||
disabled={!$results.haveNext}
|
||||
title="Next"
|
||||
class:text-zinc-50={$searchResults.haveNext}
|
||||
class:text-zinc-50={$results.haveNext}
|
||||
class="h-9 w-9 rounded-tr-md rounded-br-md border border-l border-zinc-700 hover:bg-zinc-700"
|
||||
>
|
||||
<IconChevronRight class="ml-1 h-5 w-6" />
|
||||
|
@ -3,7 +3,7 @@
|
||||
import ResizeObserver from 'svelte-resize-observer';
|
||||
import setupTerminal from './terminal';
|
||||
import 'xterm/css/xterm.css';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { debounce } from '$lib/utils';
|
||||
import { Button, Statuses } from '$lib/components';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Terminal } from 'xterm';
|
||||
import { CanvasAddon } from 'xterm-addon-canvas';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
||||
ignoreEmpty: true
|
||||
},
|
||||
telemetry: false,
|
||||
uploadSourceMaps: true
|
||||
uploadSourceMaps: process.env.SENTRY_RELEASE !== undefined
|
||||
}
|
||||
}),
|
||||
sveltekit()
|
||||
|
Loading…
Reference in New Issue
Block a user