Merge pull request #4813 from gitbutlerapp/remove-missing-repo

Can remove repository if it's missing, from the error boundary page
This commit is contained in:
Esteban Vega 2024-09-04 11:32:56 +02:00 committed by GitHub
commit 924cc5df1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 265 additions and 59 deletions

View File

@ -7,5 +7,6 @@ declare namespace App {
interface Error {
message: string;
errorId?: string;
errorCode?: string;
}
}

View File

@ -0,0 +1,21 @@
<svg width="100%" height="100%" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse opacity="0.12" cx="226.371" cy="363.577" rx="133.506" ry="10.2992" fill="var(--clr-illustration-outline)"/>
<path d="M291.475 362.636H149.845C162.949 325.339 197.866 301.512 231.984 297.2L253.996 164.654H262.216L280.035 238.572L291.475 362.636Z" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)"/>
<path d="M271.983 174.072L329.506 362.636H199.179C212.282 326.281 232.491 312.163 266.608 304.084L260.938 174.072H271.983Z" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)"/>
<path d="M226.002 127.399C226.374 124.106 224.006 121.136 220.713 120.764C217.42 120.393 214.449 122.761 214.078 126.054L226.002 127.399ZM129.445 154.538C135.55 169.468 143.576 181.715 152.622 190.08C161.627 198.408 172.173 203.326 182.988 202.163C193.955 200.984 203.496 193.748 210.756 181.413C217.98 169.138 223.304 151.306 226.002 127.399L214.078 126.054C211.482 149.062 206.461 165.051 200.414 175.326C194.402 185.541 187.743 189.583 181.706 190.232C175.517 190.897 168.254 188.193 160.769 181.27C153.324 174.385 146.178 163.753 140.552 149.996L129.445 154.538Z" fill="var(--clr-illustration-outline)"/>
<path d="M309.709 49.8357C319.582 50.2731 327.257 58.5901 326.9 68.4673L325.519 106.735C325.304 112.698 328.059 118.381 332.873 121.907L341.517 128.236C346.299 131.738 349.051 137.37 348.875 143.294L348.182 166.634C347.883 176.665 339.44 184.515 329.415 184.083L194.596 178.275C184.512 177.84 176.756 169.2 177.408 159.128L183.705 61.9047C184.338 52.1273 192.675 44.6522 202.464 45.0857L309.709 49.8357Z" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<path d="M324.439 155.718L338.017 150.964M324.622 162.541L338.201 157.787M324.805 169.36L338.384 164.607" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<rect opacity="0.13" x="193.614" y="53.6751" width="95.9553" height="107.539" rx="9.86" transform="rotate(2.72776 193.614 53.6751)" fill="var(--clr-illustration-outline)"/>
<path d="M223.757 155.531L222.998 170.644L236.879 165.302L249.872 171.941L250.944 157.109L236.803 161.47L223.757 155.531Z" fill="var(--clr-illustration-outline)"/>
<path d="M270.636 181.536L276.083 181.791C286.007 182.255 294.43 174.591 294.903 164.668L299.537 67.4546C300.016 57.3964 292.151 48.9013 282.086 48.6053L280.16 48.5487" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<rect x="101.224" y="94.4771" width="44.2646" height="30.1376" fill="var(--clr-illustration-outline)"/>
<path d="M316.356 139.8C316.428 136.487 313.8 133.743 310.487 133.672C307.174 133.6 304.431 136.228 304.359 139.54L316.356 139.8ZM213.91 162.23C223.964 195.671 248.023 216.085 272.11 213.641C296.853 211.13 315.367 185.471 316.356 139.8L304.359 139.54C303.41 183.353 286.206 200.149 270.899 201.702C254.936 203.322 234.612 189.41 225.402 158.775L213.91 162.23Z" fill="var(--clr-illustration-outline)"/>
<path d="M131.904 158.907C119.193 158.643 107.293 151.964 102.907 145.397L122.907 141.896L159.231 146.719C156.096 153.994 144.615 159.171 131.904 158.907Z" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<path d="M132.304 71.403L81.2697 61.0432L78.3947 151.862L138.425 144.863L154.401 151.496L268.394 126.969L270.277 100.128L158.74 61.8458L132.304 71.403Z" fill="var(--clr-illustration-outline)" stroke="var(--clr-illustration-outline)"/>
<rect x="41.8909" y="60.5723" width="67.8096" height="91.3546" rx="33.9048" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.3882 78.4682L58.3723 78.487V93.5353H71.0866V109.546H58.3723V127.911L88.5099 131.678V82.2335L58.3882 78.4682Z" fill="var(--clr-illustration-outline)"/>
<rect x="115.351" y="60.5723" width="67.8096" height="91.3546" rx="33.9048" fill="var(--clr-illustration-fill)" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.79 78.4683L132.775 78.4871V93.5353H145.489V109.546H132.775V127.911L162.912 131.678V82.2335L132.79 78.4683Z" fill="var(--clr-illustration-outline)"/>
<path d="M225.688 167.114C209.537 172.081 201.01 165.638 199.7 161.298C188.071 160.742 180.746 154.262 176.91 147.025C190.166 142.367 193.902 133.754 195.799 122.515C198.169 108.467 188.066 72.6672 200.719 69.8716C208.262 68.205 212.623 74.7544 213.754 79.404C213.52 75.6967 213.728 67.3638 221.949 67.9089C228.871 68.3679 232.914 79.7117 233.822 84.1059C233.853 80.0147 235.094 72.1515 242.017 72.6107C252.759 73.3232 254.903 109.822 249.668 130.904L261.811 128.579C257.944 139.88 251.668 148.332 239.835 152.643C242.142 155.204 237.926 163.351 225.688 167.114Z" fill="var(--clr-illustration-fill)"/>
<path d="M199.7 161.298C201.01 165.638 209.537 172.081 225.688 167.114C237.926 163.351 242.142 155.204 239.835 152.643M199.7 161.298C188.071 160.742 180.746 154.262 176.91 147.025C190.166 142.367 193.902 133.754 195.799 122.515C198.169 108.467 188.066 72.6672 200.719 69.8716C208.262 68.205 212.623 74.7544 213.754 79.404M199.7 161.298C203.689 161.402 212.988 161.216 218.272 159.64M239.835 152.643C251.668 148.332 257.944 139.88 261.811 128.579L249.668 130.904M239.835 152.643L229.475 157.012M249.668 130.904C254.903 109.822 252.759 73.3232 242.017 72.6107C235.094 72.1515 233.853 80.0147 233.822 84.1059M249.668 130.904C248.745 132.769 247.186 135.795 245.388 137.602M233.822 84.1059C232.914 79.7117 228.871 68.3679 221.949 67.9089C213.728 67.3638 213.52 75.6967 213.754 79.404M233.822 84.1059C235.71 91.7753 235.749 112.308 232.91 123.291M213.754 79.404C216.051 89.3478 217.457 113.358 214.657 123.766M210.488 132.398C209.788 135.722 207.984 142.681 206.367 143.923M220.991 133.095C220.928 134.054 219.225 141.645 217.427 143.452M231.065 133.041C230.757 135.267 229.722 140.271 228.042 142.471" stroke="var(--clr-illustration-outline)" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -7,7 +7,12 @@ export enum Code {
Validation = 'errors.validation',
ProjectsGitAuth = 'errors.projects.git.auth',
DefaultTargetNotFound = 'errors.projects.default_target.not_found',
CommitSigningFailed = 'errors.commit.signing_failed'
CommitSigningFailed = 'errors.commit.signing_failed',
ProjectMissing = 'errors.projects.missing'
}
export function isUserErrorCode(something: unknown): something is Code {
return Object.values(Code).includes(something as Code);
}
export class UserError extends Error {
@ -35,6 +40,13 @@ function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function getUserErrorCode(error: unknown): Code | undefined {
if (error instanceof UserError) {
return error.code;
}
return undefined;
}
export async function invoke<T>(command: string, params: Record<string, unknown> = {}): Promise<T> {
// This commented out code can be used to delay/reject an api call
// return new Promise<T>((resolve, reject) => {

View File

@ -81,8 +81,8 @@ export class ProjectService {
this.projects.set(await this.loadAll());
}
async getProject(projectId: string) {
return plainToInstance(Project, await invoke('get_project', { id: projectId }));
async getProject(projectId: string, noValidation?: boolean) {
return plainToInstance(Project, await invoke('get_project', { id: projectId, noValidation }));
}
async updateProject(project: Project) {
@ -113,14 +113,28 @@ export class ProjectService {
await invoke('open_project_in_window', { id: projectId });
}
async relocateProject(projectId: string): Promise<void> {
const path = await this.getValidPath();
if (!path) return;
try {
const project = await this.getProject(projectId, true);
project.path = path;
await this.updateProject(project);
toasts.success(`Project ${project.title} relocated`);
goto(`/${project.id}/board`);
} catch (error: any) {
showError('Failed to relocate project:', error.message);
}
}
async addProject(path?: string) {
if (!path) {
path = await this.promptForDirectory();
path = await this.getValidPath();
if (!path) return;
}
if (!this.validateProjectPath(path)) return;
try {
const project = await this.add(path);
if (!project) return;
@ -132,6 +146,13 @@ export class ProjectService {
}
}
async getValidPath(): Promise<string | undefined> {
const path = await this.promptForDirectory();
if (!path) return undefined;
if (!this.validateProjectPath(path)) return undefined;
return path;
}
validateProjectPath(path: string, showErrors = true) {
if (/^\\\\wsl.localhost/i.test(path)) {
if (showErrors) {

View File

@ -160,7 +160,7 @@
.img-wrapper {
flex: 1;
width: 100%;
max-width: 400px;
max-width: 440px;
overflow: hidden;
padding: 0 24px;
}

View File

@ -7,6 +7,7 @@
import derectionDoubtSvg from '$lib/assets/illustrations/direction-doubt.svg?raw';
import { ProjectService, Project } from '$lib/backend/projects';
import { showError } from '$lib/notifications/toasts';
import Spacer from '$lib/shared/Spacer.svelte';
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import { BranchController } from '$lib/vbranches/branchController';
@ -82,6 +83,8 @@
{/if}
</div>
<Spacer dotted margin={0} />
<div class="switchrepo__project">
<ProjectSwitcher />
</div>
@ -111,6 +114,5 @@
.switchrepo__project {
padding-top: 24px;
border-top: 1px dashed var(--clr-scale-ntrl-60);
}
</style>

View File

@ -5,6 +5,8 @@
import loadErrorSvg from '$lib/assets/illustrations/load-error.svg?raw';
import { ProjectService, Project } from '$lib/backend/projects';
import { showError } from '$lib/notifications/toasts';
import ProjectNameLabel from '$lib/shared/ProjectNameLabel.svelte';
import Spacer from '$lib/shared/Spacer.svelte';
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import Icon from '@gitbutler/ui/Icon.svelte';
@ -37,10 +39,12 @@
<DecorativeSplitView img={loadErrorSvg}>
<div class="problem" data-tauri-drag-region>
<p class="problem__project text-bold"><Icon name="repo-book" /> {project?.title}</p>
<p class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
<div class="project-name">
<ProjectNameLabel projectName={project?.title} />
</div>
<h2 class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
There was a problem loading this repo
</p>
</h2>
<div class="problem__error text-12 text-body">
<Icon name="error" color="error" />
@ -56,6 +60,8 @@
/>
</div>
<Spacer dotted margin={0} />
<div class="problem__switcher">
<ProjectSwitcher />
</div>
@ -63,13 +69,8 @@
</DecorativeSplitView>
<style lang="postcss">
.problem__project {
display: flex;
gap: 8px;
align-items: center;
line-height: 120%;
color: var(--clr-scale-ntrl-30);
margin-bottom: 20px;
.project-name {
margin-bottom: 12px;
}
.problem__title {
@ -96,6 +97,5 @@
display: flex;
justify-content: flex-end;
padding-bottom: 24px;
border-bottom: 1px dashed var(--clr-scale-ntrl-60);
}
</style>

View File

@ -0,0 +1,130 @@
<script lang="ts">
import DecorativeSplitView from './DecorativeSplitView.svelte';
import ProjectSwitcher from './ProjectSwitcher.svelte';
import RemoveProjectButton from './RemoveProjectButton.svelte';
import notFoundSvg from '$lib/assets/illustrations/not-found.svg?raw';
import { ProjectService } from '$lib/backend/projects';
import InfoMessage from '$lib/shared/InfoMessage.svelte';
import Spacer from '$lib/shared/Spacer.svelte';
import { getContext } from '$lib/utils/context';
import Button from '@gitbutler/ui/Button.svelte';
const projectService = getContext(ProjectService);
const id = projectService.getLastOpenedProject();
const projectPromise = id
? projectService.getProject(id, true)
: Promise.reject('Failed to get project');
let deleteSucceeded: boolean | undefined = $state(undefined);
let isDeleting = $state(false);
async function stopTracking(id: string) {
isDeleting = true;
deleteProject: {
try {
await projectService.deleteProject(id);
} catch (e) {
deleteSucceeded = false;
break deleteProject;
}
deleteSucceeded = true;
}
isDeleting = false;
}
async function locate(id: string) {
await projectService.relocateProject(id);
}
function getDeletionStatusMessage(repoName: string) {
if (deleteSucceeded === undefined) return null;
if (deleteSucceeded) return `Project "${repoName}" successfully deleted`;
return `Failed to delete "${repoName}" project`;
}
</script>
<DecorativeSplitView img={notFoundSvg}>
<div class="container" data-tauri-drag-region>
{#if deleteSucceeded === undefined}
{#await projectPromise then project}
<div class="text-content">
<h2 class="title-text text-18 text-body text-bold" data-tauri-drag-region>
Cant find "{project.title}"
</h2>
<p class="description-text text-13 text-body">
Sorry, we can't find the project you're looking for.
<br />
It might have been removed or doesn't exist.
<button class="check-again-btn" onclick={() => location.reload()}>Click here</button>
to check again.
<br />
The current project path: <span class="code-string">{project.path}</span>
</p>
</div>
<div class="button-container">
<Button
type="button"
style="pop"
kind="solid"
onclick={async () => await locate(project.id)}>Locate project…</Button
>
<RemoveProjectButton
noModal
{isDeleting}
onDeleteClicked={async () => await stopTracking(project.id)}
/>
</div>
{#if deleteSucceeded !== undefined}
<InfoMessage filled outlined={false} style="success" icon="info">
<svelte:fragment slot="content"
>{getDeletionStatusMessage(project.title)}</svelte:fragment
>
</InfoMessage>
{/if}
{:catch}
<div class="text-content">
<h2 class="title-text text-18 text-body text-bold">Cant find project</h2>
</div>
{/await}
{/if}
<Spacer dotted margin={0} />
<ProjectSwitcher />
</div>
</DecorativeSplitView>
<style lang="postcss">
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.button-container {
display: flex;
gap: 8px;
}
.text-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.title-text {
color: var(--clr-scale-ntrl-30);
/* margin-bottom: 12px; */
}
.description-text {
color: var(--clr-text-2);
line-height: 1.6;
}
.check-again-btn {
text-decoration: underline;
}
</style>

View File

@ -4,6 +4,7 @@
export let projectTitle: string = '#';
export let isDeleting = false;
export let noModal = false;
export let onDeleteClicked: () => Promise<void>;
export function show() {
@ -13,18 +14,18 @@
modal.close();
}
function handleClick() {
if (noModal) {
onDeleteClicked();
} else {
modal.show();
}
}
let modal: Modal;
</script>
<Button
style="error"
kind="solid"
icon="bin-small"
reversedDirection
onclick={() => {
modal.show();
}}
>
<Button style="error" kind="solid" icon="bin-small" reversedDirection onclick={handleClick}>
Remove project…
</Button>

View File

@ -2,46 +2,35 @@
import DecorativeSplitView from './DecorativeSplitView.svelte';
import ProjectSwitcher from './ProjectSwitcher.svelte';
import loadErrorSvg from '$lib/assets/illustrations/load-error.svg?raw';
import Icon from '@gitbutler/ui/Icon.svelte';
import InfoMessage from '$lib/shared/InfoMessage.svelte';
export let error: any = undefined;
</script>
<DecorativeSplitView img={loadErrorSvg}>
<div class="problem" data-tauri-drag-region>
<p class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
<div class="problem__container" data-tauri-drag-region>
<h2 class="problem__title text-18 text-body text-bold" data-tauri-drag-region>
There was a problem loading the app
</p>
</h2>
<div class="problem__error text-12 text-body">
<Icon name="error" color="error" />
{error ? error : 'An unknown error occurred'}
</div>
<InfoMessage filled outlined={false} style="error" icon="info">
<svelte:fragment slot="content">
{error ? error : 'An unknown error occurred'}
</svelte:fragment>
</InfoMessage>
<div class="problem__switcher">
<ProjectSwitcher />
</div>
<ProjectSwitcher />
</div>
</DecorativeSplitView>
<style lang="postcss">
.problem__container {
display: flex;
flex-direction: column;
gap: 20px;
}
.problem__title {
color: var(--clr-scale-ntrl-30);
margin-bottom: 12px;
}
.problem__switcher {
text-align: right;
margin-top: 24px;
}
.problem__error {
display: flex;
color: var(--clr-scale-ntrl-0);
gap: 12px;
padding: 20px;
background-color: var(--clr-theme-err-bg);
border-radius: var(--radius-m);
margin-bottom: 12px;
}
</style>

View File

@ -29,7 +29,7 @@
.divider {
height: 1px;
width: 100%;
opacity: 0.15;
opacity: 0.2;
}
.divider.line {

View File

@ -1,4 +1,6 @@
<script lang="ts">
import { Code, isUserErrorCode } from '$lib/backend/ipc';
import ProjectNotFound from '$lib/components/ProjectNotFound.svelte';
import SomethingWentWrong from '$lib/components/SomethingWentWrong.svelte';
import { page } from '$app/stores';
@ -9,4 +11,10 @@
: 'Unknown error';
</script>
<SomethingWentWrong error={message} />
{#if isUserErrorCode($page.error?.errorCode)}
{#if $page.error?.errorCode === Code.ProjectMissing}
<ProjectNotFound />
{/if}
{:else}
<SomethingWentWrong error={message} />
{/if}

View File

@ -1,4 +1,4 @@
import { invoke } from '$lib/backend/ipc';
import { getUserErrorCode, invoke } from '$lib/backend/ipc';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import { BranchListingService } from '$lib/branches/branchListing';
import { BranchDragActionsFactory } from '$lib/branches/dragActions.js';
@ -39,7 +39,9 @@ export const load: LayoutLoad = async ({ params, parent }) => {
project = await projectService.getProject(projectId);
await invoke('set_project_active', { id: projectId });
} catch (err: any) {
const errorCode = getUserErrorCode(err);
throw error(400, {
errorCode,
message: err.message
});
}

View File

@ -123,6 +123,14 @@ impl Controller {
self.get_inner(id, false)
}
/// Only get the project information. No state validation is done.
/// This is intended to be used only when updating the path of a missing project.
pub fn get_raw(&self, id: ProjectId) -> Result<Project> {
#[cfg_attr(not(windows), allow(unused_mut))]
let mut project = self.projects_storage.get(id)?;
Ok(project)
}
/// Like [`Self::get()`], but will assure the project still exists and is valid by
/// opening a git repository. This should only be done for critical points in time.
pub fn get_validated(&self, id: ProjectId) -> Result<Project> {
@ -147,6 +155,7 @@ impl Controller {
.context(error::Code::ProjectMissing));
}
}
if !project.gb_dir().exists() {
if let Err(error) = std::fs::create_dir_all(project.gb_dir()) {
tracing::error!(project_id = %project.id, ?error, "failed to create \"{}\" on project get", project.gb_dir().display());

View File

@ -17,6 +17,7 @@ pub struct UpdateRequest {
pub id: ProjectId,
pub title: Option<String>,
pub description: Option<String>,
pub path: Option<PathBuf>,
pub api: Option<ApiProject>,
pub gitbutler_data_last_fetched: Option<FetchResult>,
pub preferred_key: Option<AuthKey>,
@ -85,6 +86,10 @@ impl Storage {
project.description = Some(description.clone());
}
if let Some(path) = &update_request.path {
project.path = path.clone();
}
if let Some(api) = &update_request.api {
project.api = Some(api.clone());
}

View File

@ -33,8 +33,13 @@ pub mod commands {
pub fn get_project(
projects: State<'_, Controller>,
id: ProjectId,
no_validation: Option<bool>,
) -> Result<projects::Project, Error> {
Ok(projects.get_validated(id)?)
if no_validation.unwrap_or(false) {
Ok(projects.get_raw(id)?)
} else {
Ok(projects.get_validated(id)?)
}
}
#[tauri::command(async)]