Skip to content

Commit 924cc5d

Browse files
authored
Merge pull request #4813 from gitbutlerapp/remove-missing-repo
Can remove repository if it's missing, from the error boundary page
2 parents ab2a9cc + 23a5bb7 commit 924cc5d

File tree

16 files changed

+265
-59
lines changed

16 files changed

+265
-59
lines changed

apps/desktop/src/app.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ declare namespace App {
77
interface Error {
88
message: string;
99
errorId?: string;
10+
errorCode?: string;
1011
}
1112
}
Lines changed: 21 additions & 0 deletions
Loading

apps/desktop/src/lib/backend/ipc.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ export enum Code {
77
Validation = 'errors.validation',
88
ProjectsGitAuth = 'errors.projects.git.auth',
99
DefaultTargetNotFound = 'errors.projects.default_target.not_found',
10-
CommitSigningFailed = 'errors.commit.signing_failed'
10+
CommitSigningFailed = 'errors.commit.signing_failed',
11+
ProjectMissing = 'errors.projects.missing'
12+
}
13+
14+
export function isUserErrorCode(something: unknown): something is Code {
15+
return Object.values(Code).includes(something as Code);
1116
}
1217

1318
export class UserError extends Error {
@@ -35,6 +40,13 @@ function capitalize(str: string): string {
3540
return str.charAt(0).toUpperCase() + str.slice(1);
3641
}
3742

43+
export function getUserErrorCode(error: unknown): Code | undefined {
44+
if (error instanceof UserError) {
45+
return error.code;
46+
}
47+
return undefined;
48+
}
49+
3850
export async function invoke<T>(command: string, params: Record<string, unknown> = {}): Promise<T> {
3951
// This commented out code can be used to delay/reject an api call
4052
// return new Promise<T>((resolve, reject) => {

apps/desktop/src/lib/backend/projects.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ export class ProjectService {
8181
this.projects.set(await this.loadAll());
8282
}
8383

84-
async getProject(projectId: string) {
85-
return plainToInstance(Project, await invoke('get_project', { id: projectId }));
84+
async getProject(projectId: string, noValidation?: boolean) {
85+
return plainToInstance(Project, await invoke('get_project', { id: projectId, noValidation }));
8686
}
8787

8888
async updateProject(project: Project) {
@@ -113,14 +113,28 @@ export class ProjectService {
113113
await invoke('open_project_in_window', { id: projectId });
114114
}
115115

116+
async relocateProject(projectId: string): Promise<void> {
117+
const path = await this.getValidPath();
118+
if (!path) return;
119+
120+
try {
121+
const project = await this.getProject(projectId, true);
122+
project.path = path;
123+
await this.updateProject(project);
124+
toasts.success(`Project ${project.title} relocated`);
125+
126+
goto(`/${project.id}/board`);
127+
} catch (error: any) {
128+
showError('Failed to relocate project:', error.message);
129+
}
130+
}
131+
116132
async addProject(path?: string) {
117133
if (!path) {
118-
path = await this.promptForDirectory();
134+
path = await this.getValidPath();
119135
if (!path) return;
120136
}
121137

122-
if (!this.validateProjectPath(path)) return;
123-
124138
try {
125139
const project = await this.add(path);
126140
if (!project) return;
@@ -132,6 +146,13 @@ export class ProjectService {
132146
}
133147
}
134148

149+
async getValidPath(): Promise<string | undefined> {
150+
const path = await this.promptForDirectory();
151+
if (!path) return undefined;
152+
if (!this.validateProjectPath(path)) return undefined;
153+
return path;
154+
}
155+
135156
validateProjectPath(path: string, showErrors = true) {
136157
if (/^\\\\wsl.localhost/i.test(path)) {
137158
if (showErrors) {

apps/desktop/src/lib/components/DecorativeSplitView.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@
160160
.img-wrapper {
161161
flex: 1;
162162
width: 100%;
163-
max-width: 400px;
163+
max-width: 440px;
164164
overflow: hidden;
165165
padding: 0 24px;
166166
}

apps/desktop/src/lib/components/NotOnGitButlerBranch.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import derectionDoubtSvg from '$lib/assets/illustrations/direction-doubt.svg?raw';
88
import { ProjectService, Project } from '$lib/backend/projects';
99
import { showError } from '$lib/notifications/toasts';
10+
import Spacer from '$lib/shared/Spacer.svelte';
1011
import { getContext } from '$lib/utils/context';
1112
import * as toasts from '$lib/utils/toasts';
1213
import { BranchController } from '$lib/vbranches/branchController';
@@ -82,6 +83,8 @@
8283
{/if}
8384
</div>
8485

86+
<Spacer dotted margin={0} />
87+
8588
<div class="switchrepo__project">
8689
<ProjectSwitcher />
8790
</div>
@@ -111,6 +114,5 @@
111114
112115
.switchrepo__project {
113116
padding-top: 24px;
114-
border-top: 1px dashed var(--clr-scale-ntrl-60);
115117
}
116118
</style>

apps/desktop/src/lib/components/ProblemLoadingRepo.svelte

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import loadErrorSvg from '$lib/assets/illustrations/load-error.svg?raw';
66
import { ProjectService, Project } from '$lib/backend/projects';
77
import { showError } from '$lib/notifications/toasts';
8+
import ProjectNameLabel from '$lib/shared/ProjectNameLabel.svelte';
9+
import Spacer from '$lib/shared/Spacer.svelte';
810
import { getContext } from '$lib/utils/context';
911
import * as toasts from '$lib/utils/toasts';
1012
import Icon from '@gitbutler/ui/Icon.svelte';
@@ -37,10 +39,12 @@
3739

3840
<DecorativeSplitView img={loadErrorSvg}>
3941
<div class="problem" data-tauri-drag-region>
40-
<p class="problem__project text-bold"><Icon name="repo-book" /> {project?.title}</p>
41-
<p class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
42+
<div class="project-name">
43+
<ProjectNameLabel projectName={project?.title} />
44+
</div>
45+
<h2 class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
4246
There was a problem loading this repo
43-
</p>
47+
</h2>
4448

4549
<div class="problem__error text-12 text-body">
4650
<Icon name="error" color="error" />
@@ -56,20 +60,17 @@
5660
/>
5761
</div>
5862

63+
<Spacer dotted margin={0} />
64+
5965
<div class="problem__switcher">
6066
<ProjectSwitcher />
6167
</div>
6268
</div>
6369
</DecorativeSplitView>
6470

6571
<style lang="postcss">
66-
.problem__project {
67-
display: flex;
68-
gap: 8px;
69-
align-items: center;
70-
line-height: 120%;
71-
color: var(--clr-scale-ntrl-30);
72-
margin-bottom: 20px;
72+
.project-name {
73+
margin-bottom: 12px;
7374
}
7475
7576
.problem__title {
@@ -96,6 +97,5 @@
9697
display: flex;
9798
justify-content: flex-end;
9899
padding-bottom: 24px;
99-
border-bottom: 1px dashed var(--clr-scale-ntrl-60);
100100
}
101101
</style>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<script lang="ts">
2+
import DecorativeSplitView from './DecorativeSplitView.svelte';
3+
import ProjectSwitcher from './ProjectSwitcher.svelte';
4+
import RemoveProjectButton from './RemoveProjectButton.svelte';
5+
import notFoundSvg from '$lib/assets/illustrations/not-found.svg?raw';
6+
import { ProjectService } from '$lib/backend/projects';
7+
import InfoMessage from '$lib/shared/InfoMessage.svelte';
8+
import Spacer from '$lib/shared/Spacer.svelte';
9+
import { getContext } from '$lib/utils/context';
10+
import Button from '@gitbutler/ui/Button.svelte';
11+
12+
const projectService = getContext(ProjectService);
13+
const id = projectService.getLastOpenedProject();
14+
const projectPromise = id
15+
? projectService.getProject(id, true)
16+
: Promise.reject('Failed to get project');
17+
18+
let deleteSucceeded: boolean | undefined = $state(undefined);
19+
let isDeleting = $state(false);
20+
21+
async function stopTracking(id: string) {
22+
isDeleting = true;
23+
deleteProject: {
24+
try {
25+
await projectService.deleteProject(id);
26+
} catch (e) {
27+
deleteSucceeded = false;
28+
break deleteProject;
29+
}
30+
deleteSucceeded = true;
31+
}
32+
isDeleting = false;
33+
}
34+
35+
async function locate(id: string) {
36+
await projectService.relocateProject(id);
37+
}
38+
39+
function getDeletionStatusMessage(repoName: string) {
40+
if (deleteSucceeded === undefined) return null;
41+
if (deleteSucceeded) return `Project "${repoName}" successfully deleted`;
42+
return `Failed to delete "${repoName}" project`;
43+
}
44+
</script>
45+
46+
<DecorativeSplitView img={notFoundSvg}>
47+
<div class="container" data-tauri-drag-region>
48+
{#if deleteSucceeded === undefined}
49+
{#await projectPromise then project}
50+
<div class="text-content">
51+
<h2 class="title-text text-18 text-body text-bold" data-tauri-drag-region>
52+
Can’t find "{project.title}"
53+
</h2>
54+
55+
<p class="description-text text-13 text-body">
56+
Sorry, we can't find the project you're looking for.
57+
<br />
58+
It might have been removed or doesn't exist.
59+
<button class="check-again-btn" onclick={() => location.reload()}>Click here</button>
60+
to check again.
61+
<br />
62+
The current project path: <span class="code-string">{project.path}</span>
63+
</p>
64+
</div>
65+
66+
<div class="button-container">
67+
<Button
68+
type="button"
69+
style="pop"
70+
kind="solid"
71+
onclick={async () => await locate(project.id)}>Locate project…</Button
72+
>
73+
<RemoveProjectButton
74+
noModal
75+
{isDeleting}
76+
onDeleteClicked={async () => await stopTracking(project.id)}
77+
/>
78+
</div>
79+
80+
{#if deleteSucceeded !== undefined}
81+
<InfoMessage filled outlined={false} style="success" icon="info">
82+
<svelte:fragment slot="content"
83+
>{getDeletionStatusMessage(project.title)}</svelte:fragment
84+
>
85+
</InfoMessage>
86+
{/if}
87+
{:catch}
88+
<div class="text-content">
89+
<h2 class="title-text text-18 text-body text-bold">Can’t find project</h2>
90+
</div>
91+
{/await}
92+
{/if}
93+
94+
<Spacer dotted margin={0} />
95+
<ProjectSwitcher />
96+
</div>
97+
</DecorativeSplitView>
98+
99+
<style lang="postcss">
100+
.container {
101+
display: flex;
102+
flex-direction: column;
103+
gap: 20px;
104+
}
105+
106+
.button-container {
107+
display: flex;
108+
gap: 8px;
109+
}
110+
111+
.text-content {
112+
display: flex;
113+
flex-direction: column;
114+
gap: 12px;
115+
}
116+
117+
.title-text {
118+
color: var(--clr-scale-ntrl-30);
119+
/* margin-bottom: 12px; */
120+
}
121+
122+
.description-text {
123+
color: var(--clr-text-2);
124+
line-height: 1.6;
125+
}
126+
127+
.check-again-btn {
128+
text-decoration: underline;
129+
}
130+
</style>

apps/desktop/src/lib/components/RemoveProjectButton.svelte

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
export let projectTitle: string = '#';
66
export let isDeleting = false;
7+
export let noModal = false;
78
export let onDeleteClicked: () => Promise<void>;
89
910
export function show() {
@@ -13,18 +14,18 @@
1314
modal.close();
1415
}
1516
17+
function handleClick() {
18+
if (noModal) {
19+
onDeleteClicked();
20+
} else {
21+
modal.show();
22+
}
23+
}
24+
1625
let modal: Modal;
1726
</script>
1827

19-
<Button
20-
style="error"
21-
kind="solid"
22-
icon="bin-small"
23-
reversedDirection
24-
onclick={() => {
25-
modal.show();
26-
}}
27-
>
28+
<Button style="error" kind="solid" icon="bin-small" reversedDirection onclick={handleClick}>
2829
Remove project…
2930
</Button>
3031

0 commit comments

Comments
 (0)