Skip to content

Commit

Permalink
Project reset fixups
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Oct 20, 2023
1 parent 7ce7f3e commit a9e536d
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 61 deletions.
3 changes: 3 additions & 0 deletions backend/LexBoxApi/Services/HgService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,13 @@ public async Task ResetRepo(string code)
{
string timestamp = FileUtils.ToTimestamp(DateTimeOffset.UtcNow);
await SoftDeleteRepo(code, $"{timestamp}__reset");
await InitRepo(code); // we don't want 404s
}

public async Task FinishReset(string code, Stream zipFile)
{
using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read);
await DeleteRepo(code);
var repoPath = Path.Combine(_options.Value.RepoPath, code);
Directory.CreateDirectory(repoPath);
archive.ExtractToDirectory(repoPath);
Expand All @@ -120,6 +122,7 @@ public async Task FinishReset(string code, Stream zipFile)
if (hgFolder is null)
{
await DeleteRepo(code);
await InitRepo(code); // we don't want 404s
//not sure if this is the best way to handle this, might need to catch it further up to expose the error properly to tus
throw ProjectResetException.ZipMissingHgFolder();
}
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/Services/ProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public async Task FinishReset(string code, Stream zipFile)
if (project.ResetStatus != ResetStatus.InProgress) throw ProjectResetException.NotReadyForUpload(code);
await _hgService.FinishReset(code, zipFile);
project.ResetStatus = ResetStatus.None;
project.LastCommit = await _hgService.GetLastCommitTimeFromHg(project.Code, project.MigrationStatus);
await _dbContext.SaveChangesAsync();
}

Expand Down
5 changes: 1 addition & 4 deletions frontend/src/lib/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ table tr:nth-last-child(-n + 2) .dropdown {
}

.file-input {
/* some make-up for what looks like a Chrome rendering bug */
border-width: 2px;
}

.hide-modal-actions .modal-action {
display: none !important;
}
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Badges/Badge.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" context="module">
// Add more as necessary. Should be as limited as possible to maximize consistency. https://daisyui.com/components/badge/
export type BadgeVariant = 'badge-neutral' | 'badge-info' | 'badge-primary';
export type BadgeVariant = 'badge-neutral' | 'badge-info' | 'badge-primary' | 'badge-warning';
export type BadgeSize = 'badge-lg' | 'badge-md';
export function badgePadding(size: BadgeSize): string {
return size === 'badge-lg' ? 'p-4' : 'py-2 px-2';
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/lib/components/TusUpload.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
export let accept: string;
export let uploadLabel: string = $t('tus.upload');
export let inputLabel: string = $t('tus.select_file');
export let inputDescription: string | undefined = undefined;
const dispatch = createEventDispatcher<{
uploadComplete: { upload: Upload }
}>();
Expand All @@ -37,6 +38,7 @@
let inputElement = e.target as HTMLInputElement;
if (!inputElement.files?.length) return;
let file = inputElement.files[0];
console.log(file);
if (accept === 'application/zip' && !file.name.endsWith('.zip')) {
fileError = $t('tus.zip_only');
return;
Expand Down Expand Up @@ -121,7 +123,7 @@

<div class="space-y-4">
<form>
<FormField label={inputLabel} id="tus-upload" error={fileError}>
<FormField label={inputLabel} id="tus-upload" error={fileError} description={inputDescription}>
<input id="tus-upload"
type="file"
{accept}
Expand All @@ -131,10 +133,12 @@
</FormField>
<FormError {error}/>
</form>
<p>{$t('tus.status', {status})}</p>
<progress class="progress progress-success" class:progress-error={error} value={percent} max="100"/>
</div>

<div class="mt-6">
<div class="mt-6 flex items-center gap-6">
<Button style="btn-success" disabled={status > UploadStatus.Ready} on:click={startUpload}>{uploadLabel}</Button>
<div class="flex-1">
<p class="label label-text py-0">Upload progress</p>
<progress class="progress progress-success" class:progress-error={error} value={percent} max="100"/>
</div>
</div>
4 changes: 2 additions & 2 deletions frontend/src/lib/components/modals/ConfirmDeleteModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<script lang="ts">
import Input from '$lib/forms/Input.svelte';
import { tScoped, type I18nShapeKey } from '$lib/i18n';
import { tTypeScoped, type I18nShapeKey } from '$lib/i18n';
import { z } from 'zod';
import { FormModal } from '$lib/components/modals';
import type { FormModalResult, FormSubmitCallback } from '$lib/components/modals/FormModal.svelte';
Expand All @@ -26,7 +26,7 @@
return await deletionFormModal.open(onSubmit);
}
$: t = tScoped<DeleteModalI18nShape>(i18nScope);
$: t = tTypeScoped<DeleteModalI18nShape>(i18nScope);
const verify = z.object({
keyphrase: z.string().refine((value) => value.match(`^${$t('enter_to_delete.value')}$`)),
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/lib/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type I18n from '../i18n/locales/en.json';
import { derived, type Readable } from 'svelte/store';
// @ts-ignore there's an error here because this is a synthetic path
import en from '$locales/en';
import type { Get } from 'type-fest';

export async function loadI18n(): Promise<void> {
addMessages('en', en);
Expand All @@ -32,10 +33,17 @@ export default t;

export type Translater = StoreType<typeof t>;

export function tScoped<Shape extends object>(scope: I18nShapeKey<Shape>): Readable<(key: DeepPathsToString<Shape>, values?: InterpolationValues) => string> {
export function tScoped<Scope extends I18nScope>(scope: Scope): Readable<(key: DeepPathsToString<I18nShape<Scope>>, values?: InterpolationValues) => string> {
// I can't quite figure out why this needs to be cast
return tTypeScoped<I18nShape<Scope>>(scope as I18nShapeKey<I18nShape<Scope>>);
}

export function tTypeScoped<Shape extends object>(scope: I18nShapeKey<Shape>): Readable<(key: DeepPathsToString<Shape>, values?: InterpolationValues) => string> {
return derived(t, tFunc => (key: DeepPathsToString<Shape>, values?: InterpolationValues) =>
tFunc(`${String(scope)}.${String(key)}`, values));
}

type I18nKey = DeepPaths<typeof I18n>;
type I18nScope = DeepPathsToType<typeof I18n, I18nKey, object>;
type I18nShape<Scope extends I18nScope> = Get<typeof I18n, Scope>;
export type I18nShapeKey<Shape> = DeepPathsToType<typeof I18n, I18nKey, Shape>;
15 changes: 9 additions & 6 deletions frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,21 +160,24 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
"title": "Choose role for {name}"
},
"reset_project_modal": {
"title": "Reset project",
"title": "Reset project {code}",
"submit": "Reset project",
"close": "Close",
"i_have_working_backup": "I have a working backup",
"next": "Next",
"download_button": "Download backup file",
"confirm_downloaded": "I confirm that I have downloaded the backup file and verified that it works. I am ready to completely reset the repository history.",
"back": "Back",
"download_button": "Download project backup",
"confirm_downloaded": "I confirm that I have downloaded a backup of the project and verified that it works. I am ready to completely reset/delete the contents of the project repository.",
"confirm_downloaded_error": "Please check the box to confirm you have downloaded the backup file",
"confirm_project_code": "Enter project code to confirm reset",
"confirm_project_code_error": "Please type the project code to confirm reset",
"reset_project_notification": "Successfully reset project {code}",
"upload_project": "Upload Project",
"select_zip": "Project zip file, should only contain the .hg folder at the root",
"download_step": "Download",
"select_zip": "Project zip file",
"should_only_contain_hg": "Should only contain the .hg folder at the root",
"backup_step": "Backup",
"reset_step": "Reset",
"upload_step": "Upload",
"restore_step": "Restore",
"finished_step": "Finished",
},
"notifications": {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/lib/type.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { Readable } from 'svelte/store';
*/
type OmitNever<T> = { [K in keyof T as IfNever<T[K], never, K>]: T[K] };

type OrIfNever<T, N> = IfNever<T, N, T>;

/**
* Creates a union of all possible deep paths / nested keys (e.g. `obj.nestedObj.prop`) of an object
*/
Expand All @@ -20,9 +22,9 @@ export type DeepPaths<ObjectType extends object> =
/**
* Create a union of all possible deep paths of an object who's value fulfill `Condition`
*/
export type DeepPathsToType<Base, Path extends string, Type> = keyof OmitNever<{
export type DeepPathsToType<Base, Path extends string, Type> = OrIfNever<keyof OmitNever<{
[Property in Path]: Get<Base, Property> extends Type ? Property : never;
}>;
}>, Readonly<`No paths match type:`> | Type>;

/**
* Create a union of all possible deep paths of an object who's value type is `string`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,9 @@
</button>
<AdminContent>
<button class="btn btn-accent" on:click={() => resetProject()}>
{$t('project_page.reset_project_modal.title')}<CircleArrowIcon />
{$t('project_page.reset_project_modal.submit')}<CircleArrowIcon />
</button>
<ResetProjectModal bind:this={resetProjectModal} i18nScope="project_page.reset_project_modal" />
<ResetProjectModal bind:this={resetProjectModal} />
</AdminContent>
</MoreSettings>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,10 @@ export async function _refreshProjectStatus(projectCode: string): Promise<void>
migrationStatus
}
}
`), { projectCode }, {requestPolicy: 'network-only'});
`), { projectCode }, { requestPolicy: 'network-only' });

if (result.error) {
// this should be meaningless, but just in case and it makes the linter happy
throw result.error;
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
<script context="module" lang="ts">
export type ResetProjectModalI18nShape = {
title: string,
submit: string,
close: string,
next: string,
/* eslint-disable @typescript-eslint/naming-convention */
download_button: string,
confirm_downloaded: string,
confirm_downloaded_error: string,
confirm_project_code: string,
confirm_project_code_error: string,
reset_project_notification: string,
upload_project: string,
select_zip: string,
download_step: string,
reset_step: string,
upload_step: string,
finished_step: string,
/* eslint-enable @typescript-eslint/naming-convention */
};
</script>

<script lang="ts">
import Input from '$lib/forms/Input.svelte';
import Checkbox from '$lib/forms/Checkbox.svelte';
import {tScoped, type I18nShapeKey} from '$lib/i18n';
import {tScoped} from '$lib/i18n';
import {z} from 'zod';
import {CircleArrowIcon} from '$lib/icons';
import Modal from '$lib/components/modals/Modal.svelte';
Expand All @@ -47,7 +24,9 @@
currentStep++;
}
export let i18nScope: I18nShapeKey<ResetProjectModalI18nShape>;
function previousStep(): void {
currentStep--;
}
let code: string;
let modal: Modal;
Expand All @@ -61,7 +40,7 @@
return currentStep == ResetSteps.Finished;
}
$: t = tScoped<ResetProjectModalI18nShape>(i18nScope);
const t = tScoped('project_page.reset_project_modal');
let verify = z.object({
confirmProjectCode: z.string().refine((value) => value === code, () => ({message: $t('confirm_project_code_error')})),
Expand Down Expand Up @@ -93,23 +72,23 @@

<div class="reset-modal contents" class:hide-modal-actions={currentStep === ResetSteps.Upload}>
<Modal bind:this={modal} on:close={onClose} showCloseButton={false}>
<h2 class="text-xl mb-4">{$t('title')}</h2>
<h2 class="text-xl mb-4">{$t('title', { code })}</h2>
<ul class="steps w-full mb-2">
<li class="step step-primary">{$t('download_step')}</li>
<li class="step step-primary">{$t('backup_step')}</li>
<li class="step" class:step-primary={currentStep >= ResetSteps.Reset}>{$t('reset_step')}</li>
<li class="step" class:step-primary={currentStep >= ResetSteps.Upload}>{$t('upload_step')}</li>
<li class="step" class:step-primary={currentStep >= ResetSteps.Upload}>{$t('restore_step')}</li>
<li class="step" class:step-primary={currentStep >= ResetSteps.Finished}>{$t('finished_step')}</li>
</ul>

{#if currentStep === ResetSteps.Download}
<div class="form-control">
<a rel="external" href="/api/project/backupProject/{code}"
class="btn btn-success w-96" download>
{$t('download_button')}
<span class="i-mdi-download text-2xl"/>
</a>
</div>
<div class="divider my-2" />

{#if currentStep === ResetSteps.Download}
<p class="mb-2 label">First, download a backup of the project that you can use to restore it in step 3:</p>
<a rel="external" href="/api/project/backupProject/{code}"
class="btn btn-success" download>
{$t('download_button')}
<span class="i-mdi-download text-2xl"/>
</a>
{:else if currentStep === ResetSteps.Reset}
<Form id="reset-form" {enhance}>
<Checkbox
Expand All @@ -127,9 +106,13 @@
</Form>

{:else if currentStep === ResetSteps.Upload}
<div class="label">
The project repository was successfully reset. Now upload a zip file to restore it:
</div>
<TusUpload endpoint={'/api/project/upload-zip/' + code}
uploadLabel={$t('upload_project')}
inputLabel={$t('select_zip')}
inputDescription={$t('should_only_contain_hg')}
accept="application/zip"
on:uploadComplete={uploadComplete}/>
{:else if currentStep === ResetSteps.Finished}
Expand All @@ -139,10 +122,18 @@
{:else}
<span>Unknown step</span>
{/if}
<svelte:fragment slot="extraActions">
{#if currentStep === ResetSteps.Reset}
<button class="btn btn-secondary" on:click={previousStep}>
<span class="i-mdi-chevron-left text-2xl"/>
{$t('back')}
</button>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
{#if currentStep === ResetSteps.Download}
<button class="btn btn-primary" on:click={nextStep}>
{$t('next')}
{$t('i_have_working_backup')}
<span class="i-mdi-chevron-right text-2xl"/>
</button>
{:else if currentStep === ResetSteps.Reset}
Expand All @@ -158,3 +149,9 @@
</svelte:fragment>
</Modal>
</div>

<style>
:global(.hide-modal-actions .modal-action) {
display: none !important;
}
</style>

0 comments on commit a9e536d

Please sign in to comment.