mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-10-26 13:12:25 +03:00
project settings page
This commit is contained in:
parent
374978cc1e
commit
de0dd25c90
@ -43,7 +43,7 @@ dependencies:
|
||||
idb-keyval: 6.2.0
|
||||
mm-jsr: 3.0.2
|
||||
svelte-icons: 2.1.0
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/33d9b712e9058ed82c110cb186345215f82b88e2
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/921afb3366b14ac43e3d8041a7def4b85d4d7192
|
||||
yjs: 13.5.45
|
||||
|
||||
devDependencies:
|
||||
@ -2574,8 +2574,8 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-log/33d9b712e9058ed82c110cb186345215f82b88e2:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/33d9b712e9058ed82c110cb186345215f82b88e2}
|
||||
github.com/tauri-apps/tauri-plugin-log/921afb3366b14ac43e3d8041a7def4b85d4d7192:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/921afb3366b14ac43e3d8041a7def4b85d4d7192}
|
||||
name: tauri-plugin-log-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -1,11 +1,24 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ApiProject {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub repository_id: String,
|
||||
pub git_url: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
|
||||
pub sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Project {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub path: String,
|
||||
pub api: Option<ApiProject>,
|
||||
}
|
||||
|
||||
impl AsRef<Project> for Project {
|
||||
@ -40,6 +53,7 @@ impl Project {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title,
|
||||
path: path.to_str().unwrap().to_string(),
|
||||
api: None,
|
||||
})
|
||||
.ok_or_else(|| anyhow!("failed to get title from path"))
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ pub struct Storage {
|
||||
pub struct UpdateRequest {
|
||||
id: String,
|
||||
title: Option<String>,
|
||||
api: Option<project::ApiProject>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
@ -38,9 +39,15 @@ impl Storage {
|
||||
.iter_mut()
|
||||
.find(|p| p.id == update_request.id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Project not found"))?;
|
||||
|
||||
if let Some(title) = &update_request.title {
|
||||
project.title = title.clone();
|
||||
}
|
||||
|
||||
if let Some(api) = &update_request.api {
|
||||
project.api = Some(api.clone());
|
||||
}
|
||||
|
||||
let projects = serde_json::to_string(&projects)?;
|
||||
self.storage.write(PROJECTS_FILE, &projects)?;
|
||||
Ok(self.get_project(&update_request.id)?.unwrap())
|
||||
|
112
src/lib/api.ts
Normal file
112
src/lib/api.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { dev } from "$app/environment";
|
||||
|
||||
const apiUrl = dev
|
||||
? new URL("https://test.app.gitbutler.com/api/")
|
||||
: new URL("https://app.gitbutler.com/api/");
|
||||
|
||||
const getUrl = (path: string) => new URL(path, apiUrl).toString();
|
||||
|
||||
export type LoginToken = {
|
||||
token: string;
|
||||
expires: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
locale: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
name: string;
|
||||
description: string | null;
|
||||
repository_id: string;
|
||||
git_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
const parseResponseJSON = async (response: Response) => {
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(
|
||||
`HTTP Error ${response.statusText}: ${await response.text()}`
|
||||
);
|
||||
} else {
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
|
||||
export default (
|
||||
{ fetch }: { fetch: typeof window.fetch } = { fetch: window.fetch }
|
||||
) => ({
|
||||
login: {
|
||||
token: {
|
||||
create: (params: {} = {}): Promise<LoginToken> =>
|
||||
fetch(getUrl("login/token.json"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then(parseResponseJSON)
|
||||
.then((token) => {
|
||||
const url = new URL(token.url);
|
||||
url.host = apiUrl.host;
|
||||
return {
|
||||
...token,
|
||||
url: url.toString(),
|
||||
};
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
get: (token: string): Promise<User> =>
|
||||
fetch(getUrl(`login/user/${token}.json`), {
|
||||
method: "GET",
|
||||
}).then(parseResponseJSON),
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
create: (
|
||||
token: string,
|
||||
params: { name: string; uid?: string }
|
||||
): Promise<Project> =>
|
||||
fetch(getUrl("projects.json"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}).then(parseResponseJSON),
|
||||
list: (token: string): Promise<Project[]> =>
|
||||
fetch(getUrl("projects.json"), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
get: (token: string, repositoryId: string): Promise<Project> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
delete: (token: string, repositoryId: string): Promise<void> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
},
|
||||
});
|
@ -1,68 +0,0 @@
|
||||
import { dev } from "$app/environment";
|
||||
|
||||
const apiUrl = dev
|
||||
? new URL("https://test.app.gitbutler.com/api/")
|
||||
: new URL("https://app.gitbutler.com/api/");
|
||||
|
||||
const getUrl = (path: string) => new URL(path, apiUrl).toString();
|
||||
|
||||
export type LoginToken = {
|
||||
token: string;
|
||||
expires: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
locale: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
const parseJSON = async (response: Response) => {
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
}
|
||||
if (response.status >= 400) {
|
||||
throw new Error(
|
||||
`HTTP Error ${response.statusText}: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export default (
|
||||
{ fetch }: { fetch: typeof window.fetch } = { fetch: window.fetch }
|
||||
) => ({
|
||||
login: {
|
||||
token: {
|
||||
create: (params: {} = {}): Promise<LoginToken> =>
|
||||
fetch(getUrl("login/token.json"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then(parseJSON)
|
||||
.then((token) => {
|
||||
const url = new URL(token.url);
|
||||
url.host = apiUrl.host;
|
||||
return {
|
||||
...token,
|
||||
url: url.toString(),
|
||||
};
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
get: (token: string): Promise<User> =>
|
||||
fetch(getUrl(`login/user/${token}.json`), {
|
||||
method: "GET",
|
||||
}).then(parseJSON),
|
||||
},
|
||||
},
|
||||
});
|
46
src/lib/components/Login.svelte
Normal file
46
src/lib/components/Login.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type Users from "$lib/users";
|
||||
import type Api from "$lib/api";
|
||||
import type { LoginToken } 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>>;
|
||||
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await api.login.user.get(token).catch(() => null);
|
||||
if (apiUser) {
|
||||
user.set(apiUser);
|
||||
return apiUser;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await pollForUser(token));
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const token = writable<LoginToken | null>(null);
|
||||
const authUrl = derived(token, ($token) => $token?.url as string);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $user}
|
||||
<button on:click={() => user.delete()}>Log out</button>
|
||||
{:else if $token !== null}
|
||||
{#await Promise.all([open($token.url), pollForUser($token.token)])}
|
||||
<div>Log in in your system browser</div>
|
||||
{/await}
|
||||
<p>
|
||||
<button class="underline" on:click={() => open($authUrl)}
|
||||
>Click here</button
|
||||
>
|
||||
if you are not redirected automatically, you can
|
||||
</p>
|
||||
{:else}
|
||||
<button on:click={() => api.login.token.create().then(token.set)}
|
||||
>Log in</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
@ -1,3 +1,4 @@
|
||||
export { default as Timeline } from "./Timeline.svelte";
|
||||
export { default as CodeViewer } from "./CodeViewer.svelte";
|
||||
export { default as BackForwardButtons } from "./BackForwardButtons.svelte";
|
||||
export { default as Login } from "./Login.svelte";
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { writable } from "svelte/store";
|
||||
import { derived, writable } from "svelte/store";
|
||||
import type { Project as ApiProject } from "$lib/api";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
api: ApiProject & { sync: boolean };
|
||||
};
|
||||
|
||||
const list = () => invoke<Project[]>("list_projects");
|
||||
|
||||
const update = (params: { project: { id: string; title?: string } }) =>
|
||||
invoke<Project>("update_project", params);
|
||||
const update = (params: {
|
||||
project: {
|
||||
id: string;
|
||||
title?: string;
|
||||
api?: ApiProject & { sync: boolean };
|
||||
};
|
||||
}) => invoke<Project>("update_project", params);
|
||||
|
||||
const add = (params: { path: string }) =>
|
||||
invoke<Project>("add_project", params);
|
||||
@ -21,19 +28,32 @@ export default async () => {
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
get: (id: string) => {
|
||||
const project = derived(store, (store) =>
|
||||
store.find((p) => p.id === id)
|
||||
);
|
||||
return {
|
||||
subscribe: project.subscribe,
|
||||
update: (params: { title?: string; api?: Project["api"] }) =>
|
||||
update({
|
||||
project: {
|
||||
id,
|
||||
...params,
|
||||
},
|
||||
}).then((project) => {
|
||||
store.update((projects) =>
|
||||
projects.map((p) =>
|
||||
p.id === project.id ? project : p
|
||||
)
|
||||
);
|
||||
return project;
|
||||
}),
|
||||
};
|
||||
},
|
||||
add: (params: { path: string }) =>
|
||||
add(params).then((project) => {
|
||||
store.update((projects) => [...projects, project]);
|
||||
return project;
|
||||
}),
|
||||
|
||||
update: (params: { project: { id: string; title?: string } }) =>
|
||||
update(params).then((project) => {
|
||||
console.log(project);
|
||||
store.update((projects) =>
|
||||
projects.map((p) => (p.id === project.id ? project : p))
|
||||
);
|
||||
return project;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { User } from "$lib/authentication";
|
||||
import type { User } from "$lib/api";
|
||||
import { writable } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
|
||||
|
@ -2,33 +2,34 @@ 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";
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
export const csr = true;
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
export const load: LayoutLoad = async ({ fetch }) => {
|
||||
const projects = building
|
||||
? {
|
||||
...readable<Project[]>([]),
|
||||
add: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
update: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
...readable<Project[]>([]),
|
||||
add: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
get: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
: await (await import("$lib/projects")).default();
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
: await (await import("$lib/users")).default();
|
||||
return { projects, user };
|
||||
return { projects, user, api: Api({ fetch }) };
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData, LayoutLoad } from "./$types";
|
||||
import type { LayoutData } from "./$types";
|
||||
export let data: LayoutData;
|
||||
|
||||
$: project = data.project;
|
||||
@ -7,16 +7,37 @@
|
||||
$: lastSessionId = $sessions[$sessions.length - 1]?.id;
|
||||
</script>
|
||||
|
||||
<div class="h-12 p-3 flex flex-row space-x-3 text-zinc-500 text-lg select-none border-b border-zinc-700">
|
||||
<div>Week</div>
|
||||
<a href="/projects/{$project?.id}" class="hover:text-zinc-300">Day</a>
|
||||
{#if lastSessionId}
|
||||
<a
|
||||
href="/projects/{$project?.id}/sessions/{lastSessionId}"
|
||||
class="hover:text-zinc-300"
|
||||
title="go to current session">Session</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<nav
|
||||
class="h-12 p-3 flex justify-between space-x-3 text-zinc-500 text-lg select-none border-b border-zinc-700"
|
||||
>
|
||||
<ul class="flex gap-2">
|
||||
<li>
|
||||
<div>Week</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/projects/{$project?.id}" class="hover:text-zinc-300"
|
||||
>Day</a
|
||||
>
|
||||
</li>
|
||||
{#if lastSessionId}
|
||||
<li>
|
||||
<a
|
||||
href="/projects/{$project?.id}/sessions/{lastSessionId}"
|
||||
class="hover:text-zinc-300"
|
||||
title="go to current session">Session</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/projects/{$project?.id}/settings"
|
||||
class="hover:text-zinc-300">Settings</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<slot />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { derived, readable } from "svelte/store";
|
||||
import { readable } from "svelte/store";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { building } from "$app/environment";
|
||||
import type { Session } from "$lib/sessions";
|
||||
@ -13,9 +13,7 @@ export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
await import("$lib/sessions")
|
||||
).default({ projectId: params.projectId });
|
||||
return {
|
||||
project: derived(projects, (projects) =>
|
||||
projects.find((project) => project.id === params.projectId)
|
||||
),
|
||||
project: projects.get(params.projectId),
|
||||
sessions,
|
||||
};
|
||||
};
|
||||
|
52
src/routes/projects/[projectId]/settings/+page.svelte
Normal file
52
src/routes/projects/[projectId]/settings/+page.svelte
Normal file
@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { derived } from "svelte/store";
|
||||
import { Login } from "$lib/components";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
const { project, user, api } = data;
|
||||
|
||||
const isSyncing = derived(project, (project) => project?.api?.sync);
|
||||
|
||||
const onSyncChange = async (event: Event) => {
|
||||
if ($project === undefined) return;
|
||||
if ($user === undefined) return;
|
||||
|
||||
const target = event.target as HTMLInputElement;
|
||||
const sync = target.checked;
|
||||
|
||||
if (!$project.api) {
|
||||
const apiProject = await api.projects.create($user.access_token, {
|
||||
name: $project.title,
|
||||
uid: $project.id,
|
||||
});
|
||||
await project.update({ api: { ...apiProject, sync } });
|
||||
} else {
|
||||
await project.update({ api: { ...$project.api, sync } });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<article class="">
|
||||
<header>
|
||||
<h2>{$project?.title}</h2>
|
||||
</header>
|
||||
|
||||
{#if $user}
|
||||
<form disabled={$user === undefined}>
|
||||
<label for="sync">Sync</label>
|
||||
<input
|
||||
disabled={$user === undefined}
|
||||
type="checkbox"
|
||||
checked={$isSyncing}
|
||||
on:change={onSyncChange}
|
||||
/>
|
||||
</form>
|
||||
{:else}
|
||||
<div><Login {user} {api} /> to sync</div>
|
||||
{/if}
|
||||
|
||||
<code class="whitespace-pre">
|
||||
{JSON.stringify($project, null, 2)}
|
||||
</code>
|
||||
</article>
|
@ -1,41 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { open } from "@tauri-apps/api/shell";
|
||||
import Authentication from "$lib/authentication";
|
||||
import { Login } from "$lib/components";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
const { user } = data;
|
||||
|
||||
const authApi = Authentication();
|
||||
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await authApi.login.user.get(token).catch(() => null);
|
||||
if (apiUser) {
|
||||
user.set(apiUser);
|
||||
return apiUser;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await pollForUser(token));
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
const { user, api } = data;
|
||||
</script>
|
||||
|
||||
{#if $user}
|
||||
<div>
|
||||
Welcome, {$user.name}!
|
||||
</div>
|
||||
{:else}
|
||||
{#await authApi.login.token.create()}
|
||||
<div>loading...</div>
|
||||
{:then { url, token }}
|
||||
{#await Promise.all([open(url), pollForUser(token)])}
|
||||
<div>Log in in your system browser</div>
|
||||
<p>
|
||||
If you are not redirected automatically, you can
|
||||
<button on:click={() => open(url)}>Try again</button>
|
||||
</p>
|
||||
{/await}
|
||||
{/await}
|
||||
{/if}
|
||||
<div>
|
||||
{#if $user}
|
||||
<div>Welcome, {$user.name}!</div>
|
||||
{/if}
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
const config = {
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
darkMode: 'class',
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
darkMode: "class",
|
||||
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
plugins: [],
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
Loading…
Reference in New Issue
Block a user