From 9c0449e280f03ba660409cd5e1bef4a8635d2fea Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 24 Oct 2025 14:44:39 +0200 Subject: [PATCH 01/20] Initial level edit page --- src/app/api/client.service.ts | 13 + .../api/types/levels/level-update-request.ts | 10 + src/app/api/types/levels/level.ts | 14 +- src/app/api/types/users/user-roles.ts | 9 + src/app/app.routes.ts | 12 + src/app/helpers/data-fetching.ts | 2 +- .../level-edit/level-edit.component.html | 83 ++++++ .../pages/level-edit/level-edit.component.ts | 276 ++++++++++++++++++ 8 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 src/app/api/types/levels/level-update-request.ts create mode 100644 src/app/api/types/users/user-roles.ts create mode 100644 src/app/pages/level-edit/level-edit.component.html create mode 100644 src/app/pages/level-edit/level-edit.component.ts diff --git a/src/app/api/client.service.ts b/src/app/api/client.service.ts index 425dfcd5..c07d43e2 100644 --- a/src/app/api/client.service.ts +++ b/src/app/api/client.service.ts @@ -16,6 +16,7 @@ import {Contest} from "./types/contests/contest"; import {Score} from "./types/levels/score"; import { LevelRelations } from './types/levels/level-relations'; import { Asset } from './types/asset'; +import { LevelUpdateRequest } from './types/levels/level-update-request'; export const defaultPageSize: number = 40; @@ -55,6 +56,18 @@ export class ClientService extends ApiImplementation { getLevelById(id: number) { return this.http.get(`/levels/id/${id}`); } + + updateLevelById(id: number, data: LevelUpdateRequest, isCurator: boolean) { + return this.http.patch(`${isCurator ? '/admin' : ''}/levels/id/${id}`, data); + } + + updateLevelIconById(id: number, hash: string, isCurator: boolean) { + return this.http.patch(`${isCurator ? '/admin' : ''}/levels/id/${id}`, {iconHash: hash}); + } + + deleteLevelById(id: number, isModerator: boolean) { + return this.http.delete(`${isModerator ? '/admin' : ''}/levels/id/${id}`); + } getScoresForLevel(id: number, scoreType: number, skip: number, count: number = defaultPageSize, params: Params | null = null) { return this.http.get>(`/scores/${id}/${scoreType}`, {params: this.setPageQuery(params, skip, count)}); diff --git a/src/app/api/types/levels/level-update-request.ts b/src/app/api/types/levels/level-update-request.ts new file mode 100644 index 00000000..04b85e87 --- /dev/null +++ b/src/app/api/types/levels/level-update-request.ts @@ -0,0 +1,10 @@ +import { GameVersion } from "../game-version"; + +export interface LevelUpdateRequest { + title: string | undefined; + description: string | undefined; + + gameVersion: GameVersion | undefined; + isReUpload: boolean | undefined; + originalPublisher: string | undefined; +} \ No newline at end of file diff --git a/src/app/api/types/levels/level.ts b/src/app/api/types/levels/level.ts index 22b2c3ef..676aefdc 100644 --- a/src/app/api/types/levels/level.ts +++ b/src/app/api/types/levels/level.ts @@ -5,16 +5,20 @@ export interface Level { title: string; description: string; iconHash: string; + publisher: User | undefined; + originalPublisher: string | undefined; + isReUpload: boolean; + teamPicked: boolean; + dateTeamPicked : Date; + gameVersion: number; + score: number; + slotType: number; publishDate: Date; updateDate: Date; + booRatings: number; yayRatings: number; hearts: number; totalPlays: number; uniquePlays: number; - publisher: User | undefined; - teamPicked: boolean; - gameVersion: number; - score: number; - slotType: number; } diff --git a/src/app/api/types/users/user-roles.ts b/src/app/api/types/users/user-roles.ts new file mode 100644 index 00000000..685c53e6 --- /dev/null +++ b/src/app/api/types/users/user-roles.ts @@ -0,0 +1,9 @@ +export enum UserRoles { + Admin = 127, + Moderator = 96, + Curator = 64, + Trusted = 1, + User = 0, + Restricted = -126, + Banned = 127, +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f66bc763..d72b3fed 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -29,6 +29,18 @@ export const routes: Routes = [ }, ...alias("level/:id/:slug", "slot/:id/:slug"), ...alias("level/:id", "slot/:id",), + { + path: 'level/:id/:slug/edit', + loadComponent: () => import('./pages/level-edit/level-edit.component').then(x => x.LevelEditComponent), + data: {title: "Edit Level"}, + }, + { + path: 'level/:id/edit', + loadComponent: () => import('./pages/level-edit/level-edit.component').then(x => x.LevelEditComponent), + data: {title: "Edit Level"}, + }, + ...alias("level/:id/:slug/edit", "slot/:id/:slug/edit"), + ...alias("level/:id/edit", "slot/:id/edit",), { path: 'photos', loadComponent: () => import('./pages/photo-listing/photo-listing.component').then(x => x.PhotoListingComponent), diff --git a/src/app/helpers/data-fetching.ts b/src/app/helpers/data-fetching.ts index a683895a..698cb0c9 100644 --- a/src/app/helpers/data-fetching.ts +++ b/src/app/helpers/data-fetching.ts @@ -1,7 +1,7 @@ import {ImageLoaderConfig} from "@angular/common"; export function getApiBaseUrl(): string { - return "https://lbp.littlebigrefresh.com/api/v3"; + return "http://localhost:10061/api/v3"; //"https://lbp.littlebigrefresh.com/api/v3"; } export function getImageLink(hash: string): string { diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html new file mode 100644 index 00000000..cf283c20 --- /dev/null +++ b/src/app/pages/level-edit/level-edit.component.html @@ -0,0 +1,83 @@ +@if (level && ownUser && level.publisher?.userId != ownUser.userId && ownUser.role < 64) +{ +

You may not edit this level. Go away.

+} +@if (level && ownUser) { + +

(id: {{ level.levelId }})

+ +
+

Admin/Curator Settings

+
+
+

Game version

+
+
+
+ + + + + + + +
+
+
+
+ +
+ + +
+ + +
+
+ + + +
+ + +
+ + + +
+
+

Metadata

+ + +
+ +
+

Preview

+
+
+
+ + + +
+ +
+ + +
+
+ {{ displayTitle.length == 0 ? 'Unnamed Level' : displayTitle }} +
+ +

{{ displayDescription.length == 0 ? 'No description was provided for this level.' : displayDescription }}

+
+
+
+
+
+} diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts new file mode 100644 index 00000000..b255af43 --- /dev/null +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -0,0 +1,276 @@ +import {Component, Inject, PLATFORM_ID} from '@angular/core'; +import {Level} from "../../api/types/levels/level"; +import {ClientService} from "../../api/client.service"; +import {ActivatedRoute, Router} from "@angular/router"; +import { AsyncPipe, isPlatformBrowser, NgClass, NgIf } from "@angular/common"; +import {LevelAvatarComponent} from "../../components/ui/photos/level-avatar.component"; +import {GamePipe} from "../../pipes/game.pipe"; +import {LayoutService} from "../../services/layout.service"; +import {DividerComponent} from "../../components/ui/divider.component"; +import {AuthenticationService} from "../../api/authentication.service"; +import { ExtendedUser } from '../../api/types/users/extended-user'; +import { UserRoles } from '../../api/types/users/user-roles'; +import { PageTitleComponent } from "../../components/ui/text/page-title.component"; +import { ButtonComponent } from "../../components/ui/form/button.component"; +import { faCertificate, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; +import { TextboxComponent } from '../../components/ui/form/textbox.component'; +import { FormControl, FormGroup } from '@angular/forms'; +import { LevelUpdateRequest } from '../../api/types/levels/level-update-request'; +import { BannerService } from '../../banners/banner.service'; +import { RefreshApiError } from '../../api/refresh-api-error'; +import { sha1Async } from '../../helpers/crypto'; +import { CheckboxComponent } from "../../components/ui/form/checkbox.component"; +import { GameVersion } from '../../api/types/game-version'; +import { FaIconComponent } from "@fortawesome/angular-fontawesome"; +import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; +import { DarkContainerComponent } from "../../components/ui/dark-container.component"; + + +@Component({ + selector: 'app-level-edit', + imports: [ + LevelAvatarComponent, + AsyncPipe, + DividerComponent, + PageTitleComponent, + ButtonComponent, + TextboxComponent, + NgClass, + NgIf, + CheckboxComponent, + FaIconComponent, + RadioButtonComponent, + DarkContainerComponent, + GamePipe, +], + templateUrl: './level-edit.component.html' +}) +export class LevelEditComponent { + level: Level | undefined | null; + + protected readonly isBrowser: boolean; + protected isMobile: boolean = false; + protected ownUser: ExtendedUser | undefined; + + settingsForm = new FormGroup({ + title: new FormControl(), + description: new FormControl(), + }); + + curatorForm = new FormGroup({ + isReupload: new FormControl(), + originalPublisher: new FormControl(), + isTeamPicked: new FormControl(), + gameVersion: new FormControl(0), + }); + + iconHash: string = "0"; + + hasTitleChanged: boolean = false; + hasDescriptionChanged: boolean = false; + + hasReuploadChanged: boolean = false; + hasOriginalPublisherChanged: boolean = false; + hasTeamPickedChanged: boolean = false; + hasGameChanged: boolean = false; + + hasPendingChanges: boolean = false; + showGameMenu: boolean = false; + displayTitle: string = "Unnamed Level"; + displayDescription: string = "No desk"; + + protected readonly curatorRoleValue: UserRoles = UserRoles.Curator; + protected readonly moderatorRoleValue: UserRoles = UserRoles.Moderator; + + constructor(private client: ClientService, protected banner: BannerService, route: ActivatedRoute, + protected layout: LayoutService, private auth: AuthenticationService, + @Inject(PLATFORM_ID) platformId: Object, private router: Router) + { + this.isBrowser = isPlatformBrowser(platformId); + + route.params.subscribe(params => { + const id: number = +params['id']; + this.client.getLevelById(id).subscribe(data => { + if(data) { + this.level = data; + } + }); + + this.auth.user.subscribe(user => { + if(user) { + this.ownUser = user; + } + }); + }); + + this.layout.isMobile.subscribe(v => this.isMobile = v); + } + + checkTitleChanges() { + this.hasTitleChanged = this.settingsForm.controls.title.getRawValue() != this.level?.title; + this.doesPageHavePendingChanges(); + } + + checkDescriptionChanges() { + this.hasDescriptionChanged = this.settingsForm.controls.description.getRawValue() != this.level?.description; + this.doesPageHavePendingChanges(); + } + + setGame(input: GameVersion) { + this.curatorForm.controls.gameVersion.setValue(input); + this.hasGameChanged = input != this.level!.gameVersion; + this.doesPageHavePendingChanges(); + } + + checkIsReuploadChanges() { + this.hasReuploadChanged = this.curatorForm.controls.isReupload.getRawValue() != this.level?.isReUpload; + this.doesPageHavePendingChanges(); + + let isReupload: boolean | undefined = this.curatorForm.controls.originalPublisher.getRawValue(); + if (isReupload != true) { + this.curatorForm.controls.originalPublisher.setValue(""); + this.curatorForm.controls.originalPublisher.disable(); + } + else + { + this.curatorForm.controls.originalPublisher.enable(); + } + } + + checkOriginalPublisherChanges() { + this.hasOriginalPublisherChanged = this.curatorForm.controls.originalPublisher.getRawValue() != this.level?.originalPublisher; + this.doesPageHavePendingChanges(); + } + + checkTeamPickChanges() { + this.hasTeamPickedChanged = this.curatorForm.controls.isTeamPicked.getRawValue() != this.level?.teamPicked; + this.doesPageHavePendingChanges(); + } + + doesPageHavePendingChanges() { + this.hasPendingChanges = + this.hasTitleChanged + || this.hasDescriptionChanged + || this.hasTeamPickedChanged + || this.hasGameChanged + || this.hasReuploadChanged + || this.hasOriginalPublisherChanged; + } + + updateInputs(level: Level) { + this.hasPendingChanges = false; + + this.iconHash = level.iconHash; + this.settingsForm.controls.title.setValue(level.title); + this.settingsForm.controls.description.setValue(level.description); + + this.curatorForm.controls.gameVersion.setValue(level.gameVersion); + this.curatorForm.controls.isTeamPicked.setValue(level.teamPicked); + this.curatorForm.controls.isReupload.setValue(level.isReUpload); + this.curatorForm.controls.originalPublisher.setValue(level.originalPublisher); + + this.displayTitle = level.title; + this.displayDescription = level.description; + } + + uploadChanges() { + if (!this.hasPendingChanges) return; + + let request: LevelUpdateRequest = { + title: this.settingsForm.controls.title.getRawValue(), + description: this.settingsForm.controls.description.getRawValue(), + + gameVersion: this.curatorForm.controls.gameVersion.getRawValue()!, + isReUpload: this.curatorForm.controls.isReupload.getRawValue(), + originalPublisher: this.curatorForm.controls.originalPublisher.getRawValue(), + }; + + this.client.updateLevelById(this.level!.levelId, request, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to update the level", apiError == null ? error.message : apiError.message); + }, + next: response => { + this.banner.success("Level updated!", "The level was successfully updated."); + + // Update level data + this.level = response; + } + }); + } + + async iconChanged($event: any) { + const file: File = $event.target.files[0] + console.log(file); + + const data: ArrayBuffer = await file.arrayBuffer(); + const hash: string = await sha1Async(data); + + this.client.uploadAsset(hash, data).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to upload new level icon", apiError == null ? error.message : apiError.message); + }, + next: _ => { + this.updateLevelIcon(hash); + } + }); + } + + updateLevelIcon(hash: string) + { + this.client.updateLevelIconById(this.level!.levelId, hash, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to update the level icon", apiError == null ? error.message : apiError.message); + }, + next: response => { + this.banner.success("Level updated!", "The level icon was successfully updated."); + + // Update level data + this.level = response; + } + }); + } + + avatarErr(img: EventTarget | null): void { + if(!(img instanceof HTMLImageElement)) return; + img.srcset = "/assets/missingLevel.svg"; + } + + delete() { + if (true) { + this.banner.success("DELETE LOL", "yuo pressed oit!!"); + return; + } + + /* + if (this.level == undefined) return; + + this.client.deleteLevelById(this.level.levelId, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to delete the level", apiError == null ? error.message : apiError.message); + + // Update level data + this.client.getLevelById(this.level!.levelId).subscribe(data => { + this.level = data; + }); + }, + next: response => { + this.banner.success("Level deleted!", "The level was successfully deleted."); + + // Navigate (TODO: redirect to page before both the edit and the details page) + this.router.navigate(['**']); + } + }); + */ + } + + protected readonly faFloppyDisk = faFloppyDisk; + protected readonly faPencil = faPencil; + protected readonly faTrash = faTrash; + protected readonly faUser = faUser; + protected readonly faCertificate = faCertificate; + protected readonly faClone = faClone; +} From 6380f080d9ee1c5db11f4567c14ba7fce2bb5ef9 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 24 Oct 2025 19:10:12 +0200 Subject: [PATCH 02/20] Improve level editing page, remove preview --- src/app/api/client.service.ts | 8 ++ .../components/ui/form/textbox.component.ts | 12 ++- .../level-edit/level-edit.component.html | 86 +++++++++-------- .../pages/level-edit/level-edit.component.ts | 96 ++++++++++++++----- 4 files changed, 132 insertions(+), 70 deletions(-) diff --git a/src/app/api/client.service.ts b/src/app/api/client.service.ts index c07d43e2..494b80f1 100644 --- a/src/app/api/client.service.ts +++ b/src/app/api/client.service.ts @@ -148,6 +148,14 @@ export class ClientService extends ApiImplementation { return this.http.post(`/levels/id/${id}/setAsOverride`, null); } + teamPickLevel(id: number) { + return this.http.post(`/admin/levels/id/${id}/teamPick`, null); + } + + unTeamPickLevel(id: number) { + return this.http.post(`/admin/levels/id/${id}/removeTeamPick`, null); + } + uploadAsset(hash: string, data: ArrayBuffer) { return this.http.post(`/assets/${hash}`, data); } diff --git a/src/app/components/ui/form/textbox.component.ts b/src/app/components/ui/form/textbox.component.ts index 3f1953ff..49d7cc57 100644 --- a/src/app/components/ui/form/textbox.component.ts +++ b/src/app/components/ui/form/textbox.component.ts @@ -2,21 +2,23 @@ import {Component, Input} from '@angular/core'; import {IconProp} from "@fortawesome/fontawesome-svg-core"; import {FaIconComponent} from "@fortawesome/angular-fontawesome"; import {FormGroup, ReactiveFormsModule} from "@angular/forms"; +import { NgClass } from "@angular/common"; @Component({ selector: 'app-textbox', imports: [ - FaIconComponent, - ReactiveFormsModule - ], + FaIconComponent, + ReactiveFormsModule, + NgClass +], template: ` @if (label.length > 0) { }
- +
` }) @@ -30,4 +32,6 @@ export class TextboxComponent { @Input() required: boolean = true; @Input() type: string = "text"; + + @Input() wrapInput: boolean = false; } diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index cf283c20..e6934437 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -6,13 +6,46 @@

(id: {{ level.levelId }})

+
+
+

Metadata

+
+
+
+ + + +
+
+ +
+ +
+
+ + + +
+ +
+ + +

Admin/Curator Settings

Game version

+ +
-
@@ -29,55 +62,24 @@

Admin/Curator Settings

+
- +

+ since +

+
+
- +
-
- +
+ -
- - - -
-
-

Metadata

- - -
- -
-

Preview

-
-
-
- - - -
- -
- - -
-
- {{ displayTitle.length == 0 ? 'Unnamed Level' : displayTitle }} -
- -

{{ displayDescription.length == 0 ? 'No description was provided for this level.' : displayDescription }}

-
-
-
-
+ [icon]="faFloppyDisk" text="DELETE level" color="bg-red" (click)="delete()">
} diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index b255af43..a3dc8390 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -12,7 +12,7 @@ import { ExtendedUser } from '../../api/types/users/extended-user'; import { UserRoles } from '../../api/types/users/user-roles'; import { PageTitleComponent } from "../../components/ui/text/page-title.component"; import { ButtonComponent } from "../../components/ui/form/button.component"; -import { faCertificate, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; +import { faCertificate, faChevronDown, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; import { TextboxComponent } from '../../components/ui/form/textbox.component'; import { FormControl, FormGroup } from '@angular/forms'; import { LevelUpdateRequest } from '../../api/types/levels/level-update-request'; @@ -23,7 +23,8 @@ import { CheckboxComponent } from "../../components/ui/form/checkbox.component"; import { GameVersion } from '../../api/types/game-version'; import { FaIconComponent } from "@fortawesome/angular-fontawesome"; import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; -import { DarkContainerComponent } from "../../components/ui/dark-container.component"; +import { TextAreaComponent } from "../../components/ui/form/textarea.component"; +import { DateComponent } from "../../components/ui/info/date.component"; @Component({ @@ -40,8 +41,9 @@ import { DarkContainerComponent } from "../../components/ui/dark-container.compo CheckboxComponent, FaIconComponent, RadioButtonComponent, - DarkContainerComponent, GamePipe, + TextAreaComponent, + DateComponent ], templateUrl: './level-edit.component.html' }) @@ -63,9 +65,8 @@ export class LevelEditComponent { isTeamPicked: new FormControl(), gameVersion: new FormControl(0), }); - + iconHash: string = "0"; - hasTitleChanged: boolean = false; hasDescriptionChanged: boolean = false; @@ -74,7 +75,9 @@ export class LevelEditComponent { hasTeamPickedChanged: boolean = false; hasGameChanged: boolean = false; - hasPendingChanges: boolean = false; + hasPendingChangesTotal: boolean = false; + hasPendingChangesLevel: boolean = false; + showGameMenu: boolean = false; displayTitle: string = "Unnamed Level"; displayDescription: string = "No desk"; @@ -93,6 +96,7 @@ export class LevelEditComponent { this.client.getLevelById(id).subscribe(data => { if(data) { this.level = data; + this.updateInputs(data); } }); @@ -123,10 +127,9 @@ export class LevelEditComponent { } checkIsReuploadChanges() { - this.hasReuploadChanged = this.curatorForm.controls.isReupload.getRawValue() != this.level?.isReUpload; - this.doesPageHavePendingChanges(); + let isReupload: boolean | undefined = this.curatorForm.controls.isReupload.getRawValue(); + this.hasReuploadChanged = isReupload != this.level?.isReUpload; - let isReupload: boolean | undefined = this.curatorForm.controls.originalPublisher.getRawValue(); if (isReupload != true) { this.curatorForm.controls.originalPublisher.setValue(""); this.curatorForm.controls.originalPublisher.disable(); @@ -135,6 +138,8 @@ export class LevelEditComponent { { this.curatorForm.controls.originalPublisher.enable(); } + + this.checkOriginalPublisherChanges() } checkOriginalPublisherChanges() { @@ -148,19 +153,24 @@ export class LevelEditComponent { } doesPageHavePendingChanges() { - this.hasPendingChanges = + this.hasPendingChangesTotal = + this.doesLevelHavePendingChanges() + || this.hasTeamPickedChanged; + } + + doesLevelHavePendingChanges(): boolean { + return this.hasPendingChangesLevel = this.hasTitleChanged || this.hasDescriptionChanged - || this.hasTeamPickedChanged || this.hasGameChanged || this.hasReuploadChanged || this.hasOriginalPublisherChanged; } updateInputs(level: Level) { - this.hasPendingChanges = false; + this.hasPendingChangesTotal = false; + this.hasPendingChangesLevel = false; - this.iconHash = level.iconHash; this.settingsForm.controls.title.setValue(level.title); this.settingsForm.controls.description.setValue(level.description); @@ -173,9 +183,43 @@ export class LevelEditComponent { this.displayDescription = level.description; } + async gameButtonClick() { + this.showGameMenu = !this.showGameMenu; + } + uploadChanges() { - if (!this.hasPendingChanges) return; + if (!this.hasPendingChangesTotal) return; + + if (this.hasTeamPickedChanged) this.updateTeamPick(); + if (this.hasPendingChangesLevel) this.updateLevel(); + } + updateTeamPick() { + if (this.curatorForm.controls.isTeamPicked.getRawValue()) { + this.client.teamPickLevel(this.level!.levelId).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to team pick level", apiError == null ? error.message : apiError.message); + }, + next: response => { + this.banner.success("Level team picked!", "The level was successfully team picked."); + } + }); + } + else { + this.client.unTeamPickLevel(this.level!.levelId).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.error("Failed to remove team pick", apiError == null ? error.message : apiError.message); + }, + next: response => { + this.banner.success("Team pick removed!", "The level is no longer team picked."); + } + }); + } + } + + updateLevel() { let request: LevelUpdateRequest = { title: this.settingsForm.controls.title.getRawValue(), description: this.settingsForm.controls.description.getRawValue(), @@ -185,7 +229,7 @@ export class LevelEditComponent { originalPublisher: this.curatorForm.controls.originalPublisher.getRawValue(), }; - this.client.updateLevelById(this.level!.levelId, request, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + this.client.updateLevelById(this.level!.levelId, request, this.isUserPublisher()).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to update the level", apiError == null ? error.message : apiError.message); @@ -199,8 +243,8 @@ export class LevelEditComponent { }); } - async iconChanged($event: any) { - const file: File = $event.target.files[0] + async uploadIcon($event: any) { + const file: File = $event.target.files[0]; console.log(file); const data: ArrayBuffer = await file.arrayBuffer(); @@ -209,7 +253,7 @@ export class LevelEditComponent { this.client.uploadAsset(hash, data).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; - this.banner.error("Failed to upload new level icon", apiError == null ? error.message : apiError.message); + this.banner.warn("Failed to upload level icon", apiError == null ? error.message : apiError.message); }, next: _ => { this.updateLevelIcon(hash); @@ -217,9 +261,8 @@ export class LevelEditComponent { }); } - updateLevelIcon(hash: string) - { - this.client.updateLevelIconById(this.level!.levelId, hash, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + updateLevelIcon(hash: string) { + this.client.updateLevelIconById(this.level!.levelId, hash, this.isUserPublisher()).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to update the level icon", apiError == null ? error.message : apiError.message); @@ -234,8 +277,12 @@ export class LevelEditComponent { } avatarErr(img: EventTarget | null): void { - if(!(img instanceof HTMLImageElement)) return; - img.srcset = "/assets/missingLevel.svg"; + if(!(img instanceof HTMLImageElement)) return; + img.srcset = "/assets/missingLevel.svg"; + } + + isUserPublisher(): boolean { + return this.ownUser!.userId != this.level!.publisher?.userId; } delete() { @@ -247,7 +294,7 @@ export class LevelEditComponent { /* if (this.level == undefined) return; - this.client.deleteLevelById(this.level.levelId, this.ownUser!.userId != this.level!.publisher?.userId).subscribe({ + this.client.deleteLevelById(this.level.levelId, this.isUserPublisher()).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to delete the level", apiError == null ? error.message : apiError.message); @@ -273,4 +320,5 @@ export class LevelEditComponent { protected readonly faUser = faUser; protected readonly faCertificate = faCertificate; protected readonly faClone = faClone; + protected readonly faChevronDown = faChevronDown; } From cd2d57b3eb67dee197428cbfadb357ed74015fcf Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 24 Oct 2025 20:33:07 +0200 Subject: [PATCH 03/20] Fix change detection --- .../pages/level-edit/level-edit.component.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index a3dc8390..f952c761 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -127,23 +127,12 @@ export class LevelEditComponent { } checkIsReuploadChanges() { - let isReupload: boolean | undefined = this.curatorForm.controls.isReupload.getRawValue(); - this.hasReuploadChanged = isReupload != this.level?.isReUpload; - - if (isReupload != true) { - this.curatorForm.controls.originalPublisher.setValue(""); - this.curatorForm.controls.originalPublisher.disable(); - } - else - { - this.curatorForm.controls.originalPublisher.enable(); - } - - this.checkOriginalPublisherChanges() + this.hasReuploadChanged = this.curatorForm.controls.isReupload.getRawValue() != this.level?.isReUpload; + this.doesPageHavePendingChanges(); } checkOriginalPublisherChanges() { - this.hasOriginalPublisherChanged = this.curatorForm.controls.originalPublisher.getRawValue() != this.level?.originalPublisher; + this.hasOriginalPublisherChanged = this.curatorForm.controls.originalPublisher.getRawValue() != (this.level?.originalPublisher ?? ""); this.doesPageHavePendingChanges(); } From 499706c2bfe17c4df8a58ff4d72c95e7381341b1 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 24 Oct 2025 21:12:10 +0200 Subject: [PATCH 04/20] page adjustments, over-engineered game button --- .../level-edit/level-edit.component.html | 99 +++++++++---------- .../pages/level-edit/level-edit.component.ts | 15 +++ tailwind.config.js | 5 + 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index e6934437..15f97006 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -3,18 +3,20 @@

You may not edit this level. Go away.

} @if (level && ownUser) { - -

(id: {{ level.levelId }})

+
+ +

(Level ID: {{ level.levelId }})

+
-
-

Metadata

-
+
+ +
-
@@ -25,61 +27,58 @@

Metadata

- - +
+
+ + +
-

Admin/Curator Settings

-
-
-

Game version

- - -
-
-
- - - - - - - -
-
+ +
+

Game version:

+
+ + +
+
+ + + + + + +
- -
-
- -

- since -

-
- +
+
+
+
+
+ +

+ since +

+ +
- + + +
- - - -
- - -
} diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index f952c761..a0620fab 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -81,6 +81,7 @@ export class LevelEditComponent { showGameMenu: boolean = false; displayTitle: string = "Unnamed Level"; displayDescription: string = "No desk"; + gameButtonColor: string = "bg-secondary"; protected readonly curatorRoleValue: UserRoles = UserRoles.Curator; protected readonly moderatorRoleValue: UserRoles = UserRoles.Moderator; @@ -124,6 +125,19 @@ export class LevelEditComponent { this.curatorForm.controls.gameVersion.setValue(input); this.hasGameChanged = input != this.level!.gameVersion; this.doesPageHavePendingChanges(); + this.setGameButtonColor(input); + } + + setGameButtonColor(game: GameVersion) { + switch (game) { + case 0: this.gameButtonColor = "bg-dark-pink"; break; + case 1: this.gameButtonColor = "bg-dark-green"; break; + case 2: this.gameButtonColor = "bg-orange"; break; + case 3: this.gameButtonColor = "bg-purple"; break; + case 4: this.gameButtonColor = "bg-light-blue"; break; + case 6: this.gameButtonColor = "bg-blue"; break; + default: this.gameButtonColor = "bg-tertiary"; break; + } } checkIsReuploadChanges() { @@ -170,6 +184,7 @@ export class LevelEditComponent { this.displayTitle = level.title; this.displayDescription = level.description; + this.setGameButtonColor(level.gameVersion); } async gameButtonClick() { diff --git a/tailwind.config.js b/tailwind.config.js index 6f11369e..165fc15e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -21,9 +21,14 @@ const defaultColors = { "red": "#E52E2E", "green": "#52BC24", + "dark-green": "#4a9e27ff", + "light-blue": "#2d92e5ff", "blue": "#2D43E5", "yellow": "#F2AA00", + "orange": "#f8640eff", + "dark-pink": "#f145a4ff", "pink": "#ff68f4", + "purple": "#A13DE3", "rank-gold": "#FFD234", "rank-silver": "#f9f4f9", From aeb16c8ae94578b7fc121519d4749e882854346c Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 24 Oct 2025 21:51:25 +0200 Subject: [PATCH 05/20] Use and show a max length on text inputs --- .../components/ui/form/textarea.component.ts | 12 +- .../components/ui/form/textbox.component.ts | 10 +- .../level-edit/level-edit.component.html | 130 +++++++++--------- 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/src/app/components/ui/form/textarea.component.ts b/src/app/components/ui/form/textarea.component.ts index b75c46c1..d9024388 100644 --- a/src/app/components/ui/form/textarea.component.ts +++ b/src/app/components/ui/form/textarea.component.ts @@ -14,8 +14,13 @@ import {FormGroup, ReactiveFormsModule} from "@angular/forms"; }
- - +
+ + @if (showMaxLength == true) { +

{{maxLength - (form.get(ctrlName)?.value?.length ?? 0)}}

+ } +
+
` }) @@ -28,4 +33,7 @@ export class TextAreaComponent { @Input({required: true}) ctrlName: string = ""; @Input() required: boolean = true; + + @Input() maxLength: number = 4096; + @Input() showMaxLength: boolean = false; } diff --git a/src/app/components/ui/form/textbox.component.ts b/src/app/components/ui/form/textbox.component.ts index 49d7cc57..9499774b 100644 --- a/src/app/components/ui/form/textbox.component.ts +++ b/src/app/components/ui/form/textbox.component.ts @@ -2,7 +2,6 @@ import {Component, Input} from '@angular/core'; import {IconProp} from "@fortawesome/fontawesome-svg-core"; import {FaIconComponent} from "@fortawesome/angular-fontawesome"; import {FormGroup, ReactiveFormsModule} from "@angular/forms"; -import { NgClass } from "@angular/common"; @Component({ @@ -10,7 +9,6 @@ import { NgClass } from "@angular/common"; imports: [ FaIconComponent, ReactiveFormsModule, - NgClass ], template: ` @if (label.length > 0) { @@ -18,7 +16,10 @@ import { NgClass } from "@angular/common"; }
- + + @if (showMaxLength == true) { +

{{maxLength - (form.get(ctrlName)?.value?.length ?? 0)}}

+ }
` }) @@ -33,5 +34,6 @@ export class TextboxComponent { @Input() required: boolean = true; @Input() type: string = "text"; - @Input() wrapInput: boolean = false; + @Input() maxLength: number = 256; + @Input() showMaxLength: boolean = false; } diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index 15f97006..7c51e36b 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -3,82 +3,84 @@

You may not edit this level. Go away.

} @if (level && ownUser) { -
- -

(Level ID: {{ level.levelId }})

-
+
+
+ +

(Level ID: {{ level.levelId }})

+
-
-
- -
-
-
- - - -
-
+
+
-
- +
+
+
+ + + +
+
+ +
+ +
-
- -
- -
- - + +
+ +
+ + +
-
- + -
- -
-

Game version:

-
- - -
-
- - - - - - - +
+ + +
+

Game version:

+
+ + +
+
+ + + + + + + +
-
-
-
-
- -

- since -

+ +
+
+
+ +

+ since +

+
+
- -
- - - + +
} From eab02f8946dd53efd5199a9fb5da7d4c340f8c4e Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 12:43:25 +0200 Subject: [PATCH 06/20] Refactor dropdown menu into own component, textarea default row count --- .../components/ui/form/button.component.ts | 9 +++-- .../ui/form/dropdown-menu.component.ts | 27 ++++++++++++++ .../ui/form/radio-button.component.ts | 6 +-- .../components/ui/form/textarea.component.ts | 11 ++++-- .../level-edit/level-edit.component.html | 37 +++++++++---------- .../pages/level-edit/level-edit.component.ts | 8 ++-- 6 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 src/app/components/ui/form/dropdown-menu.component.ts diff --git a/src/app/components/ui/form/button.component.ts b/src/app/components/ui/form/button.component.ts index a0f6e326..94a189a1 100644 --- a/src/app/components/ui/form/button.component.ts +++ b/src/app/components/ui/form/button.component.ts @@ -10,12 +10,12 @@ import { NgClass } from "@angular/common"; NgClass ], template: ` - ` }) @@ -28,6 +28,7 @@ export class ButtonComponent { @Input() type: "submit" | "reset" | "button" = "button"; @Input() enabled: boolean = true; + @Input() width: string = ""; // actions @Input() routerLink: any[] | string | null | undefined diff --git a/src/app/components/ui/form/dropdown-menu.component.ts b/src/app/components/ui/form/dropdown-menu.component.ts new file mode 100644 index 00000000..20384d7b --- /dev/null +++ b/src/app/components/ui/form/dropdown-menu.component.ts @@ -0,0 +1,27 @@ +import {Component, Input} from '@angular/core'; +import {ReactiveFormsModule} from "@angular/forms"; +import { NgClass } from "@angular/common"; + +@Component({ + selector: 'app-dropdown-menu', + imports: [ + ReactiveFormsModule, + NgClass +], + template: ` +
+ +
+ +
+
+ ` +}) +export class DropdownMenuComponent { + @Input() offsets: string = "" + @Input({required: true}) width: number = 0; + + @Input() showMenu: boolean = false; +} diff --git a/src/app/components/ui/form/radio-button.component.ts b/src/app/components/ui/form/radio-button.component.ts index 1d23051b..78417cae 100644 --- a/src/app/components/ui/form/radio-button.component.ts +++ b/src/app/components/ui/form/radio-button.component.ts @@ -7,11 +7,11 @@ import {FormGroup, ReactiveFormsModule} from "@angular/forms"; ReactiveFormsModule ], template: ` -
- +
+ @if (label.length > 0) { - + }
` diff --git a/src/app/components/ui/form/textarea.component.ts b/src/app/components/ui/form/textarea.component.ts index d9024388..6c1a2077 100644 --- a/src/app/components/ui/form/textarea.component.ts +++ b/src/app/components/ui/form/textarea.component.ts @@ -6,9 +6,9 @@ import {FormGroup, ReactiveFormsModule} from "@angular/forms"; @Component({ selector: 'app-textarea', imports: [ - FaIconComponent, - ReactiveFormsModule - ], + FaIconComponent, + ReactiveFormsModule +], template: ` @if (label.length > 0) { @@ -20,7 +20,8 @@ import {FormGroup, ReactiveFormsModule} from "@angular/forms";

{{maxLength - (form.get(ctrlName)?.value?.length ?? 0)}}

}
- +
` }) @@ -36,4 +37,6 @@ export class TextAreaComponent { @Input() maxLength: number = 4096; @Input() showMaxLength: boolean = false; + + @Input() defRows: number = 4; } diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index 7c51e36b..8e55ae82 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -33,7 +33,7 @@
+ [icon]="faTrash" text="DELETE level" color="bg-red" (click)="delete()">
@@ -41,31 +41,28 @@
- + -
-

Game version:

-
- +

Game version:

+ + -
-
- - - - - - - -
+
+ + + + + + +
-
+
diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index a0620fab..f974971c 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -12,7 +12,7 @@ import { ExtendedUser } from '../../api/types/users/extended-user'; import { UserRoles } from '../../api/types/users/user-roles'; import { PageTitleComponent } from "../../components/ui/text/page-title.component"; import { ButtonComponent } from "../../components/ui/form/button.component"; -import { faCertificate, faChevronDown, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; +import { faCertificate, faChevronDown, faChevronUp, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; import { TextboxComponent } from '../../components/ui/form/textbox.component'; import { FormControl, FormGroup } from '@angular/forms'; import { LevelUpdateRequest } from '../../api/types/levels/level-update-request'; @@ -25,6 +25,7 @@ import { FaIconComponent } from "@fortawesome/angular-fontawesome"; import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; import { TextAreaComponent } from "../../components/ui/form/textarea.component"; import { DateComponent } from "../../components/ui/info/date.component"; +import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; @Component({ @@ -36,14 +37,14 @@ import { DateComponent } from "../../components/ui/info/date.component"; PageTitleComponent, ButtonComponent, TextboxComponent, - NgClass, NgIf, CheckboxComponent, FaIconComponent, RadioButtonComponent, GamePipe, TextAreaComponent, - DateComponent + DateComponent, + DropdownMenuComponent ], templateUrl: './level-edit.component.html' }) @@ -325,4 +326,5 @@ export class LevelEditComponent { protected readonly faCertificate = faCertificate; protected readonly faClone = faClone; protected readonly faChevronDown = faChevronDown; + protected readonly faChevronUp = faChevronUp; } From eeee4a41a06df8c8eaf2bf1f5a1db057c1ab05a9 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 13:05:39 +0200 Subject: [PATCH 07/20] Also use app-dropdown-menu in FancyHeaderButtonsComponent --- .../layouts/fancy-header-buttons.component.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/components/ui/layouts/fancy-header-buttons.component.ts b/src/app/components/ui/layouts/fancy-header-buttons.component.ts index e6266145..3d897bd5 100644 --- a/src/app/components/ui/layouts/fancy-header-buttons.component.ts +++ b/src/app/components/ui/layouts/fancy-header-buttons.component.ts @@ -1,33 +1,29 @@ import { Component, Input, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core"; import { ButtonComponent } from "../form/button.component"; -import { NgClass } from "@angular/common"; -import {faEllipsisV} from "@fortawesome/free-solid-svg-icons"; +import {faEllipsisV, faXmark} from "@fortawesome/free-solid-svg-icons"; +import { DropdownMenuComponent } from "../form/dropdown-menu.component"; @Component({ selector: 'app-fancy-header-buttons', imports: [ - ButtonComponent, - NgClass - ], + ButtonComponent, + DropdownMenuComponent +], template: ` - -
-
-
-
-
-
-
+ + +
+
+
+
`, styles: `` }) @@ -70,4 +66,5 @@ export class FancyHeaderButtonsComponent { } protected readonly faEllipsisV = faEllipsisV; + protected readonly faXmark = faXmark; } \ No newline at end of file From d0653b5a6d32ffb45e08d3c52265d3b9fdeff53d Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 13:29:21 +0200 Subject: [PATCH 08/20] Add Edit button to level buttons, dropdown menu positioning fix --- .../layouts/fancy-header-buttons.component.ts | 2 +- .../fancy-header-level-buttons.component.ts | 42 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/app/components/ui/layouts/fancy-header-buttons.component.ts b/src/app/components/ui/layouts/fancy-header-buttons.component.ts index 3d897bd5..0b5d3781 100644 --- a/src/app/components/ui/layouts/fancy-header-buttons.component.ts +++ b/src/app/components/ui/layouts/fancy-header-buttons.component.ts @@ -19,7 +19,7 @@ import { DropdownMenuComponent } from "../form/dropdown-menu.component"; - +
diff --git a/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts b/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts index 27ff6c4f..7278bf81 100644 --- a/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts +++ b/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts @@ -11,17 +11,21 @@ import { faBellSlash, faHeart, faHeartCrack, + faPencil, faPlay } from "@fortawesome/free-solid-svg-icons"; import { FancyHeaderButtonsComponent } from "./fancy-header-buttons.component"; import { areGameVersionsCompatible } from "../../../helpers/game-versioning"; +import { UserRoles } from "../../../api/types/users/user-roles"; +import { Router } from "@angular/router"; +import { SlugPipe } from "../../../pipes/slug.pipe"; @Component({ selector: 'app-fancy-header-level-buttons', imports: [ - ButtonOrNavItemComponent, - FancyHeaderButtonsComponent - ], + ButtonOrNavItemComponent, + FancyHeaderButtonsComponent, +], template: ` + + + + + @if (buttonsInitialized) { @@ -88,14 +105,24 @@ export class FancyHeaderLevelButtonsComponent { @ViewChild('playNowButtonTemplate') playNowButtonTemplateRef!: TemplateRef; @ViewChild('queueButtonTemplate') queueButtonTemplateRef!: TemplateRef; @ViewChild('heartButtonTemplate') heartButtonTemplateRef!: TemplateRef; + @ViewChild('editButtonTemplate') editButtonTemplateRef!: TemplateRef; buttonTemplateRefs: TemplateRef[] = []; ownUserRoom: Room | undefined; buttonsInitialized: boolean = false; + levelTitleSlug: string = "title"; - constructor(private client: ClientService, private bannerService: BannerService) {} + constructor(private client: ClientService, private bannerService: BannerService, + protected router: Router, private slug: SlugPipe) {} ngAfterViewInit() { + const isPublisher: boolean = this.level.publisher != null && this.level.publisher.userId == this.ownUser.userId; + + // Edit button at the top, if level is published by the user + if (isPublisher) { + this.buttonTemplateRefs.push(this.editButtonTemplateRef); + } + // Play Now button, if level is compatible with the game currently played by the player this.ownUserRoom = this.ownUser.activeRoom; if (this.ownUserRoom != undefined && areGameVersionsCompatible(this.level.gameVersion, this.ownUserRoom.game)) { @@ -110,6 +137,12 @@ export class FancyHeaderLevelButtonsComponent { // Heart Button this.buttonTemplateRefs.push(this.heartButtonTemplateRef); + // Edit button further below, if the user is a curator or above and not already the publisher aswell + if (!isPublisher && this.ownUser.role >= UserRoles.Curator) { + this.buttonTemplateRefs.push(this.editButtonTemplateRef); + } + + this.levelTitleSlug = this.slug.transform(this.level.title); this.buttonsInitialized = true; } @@ -150,4 +183,5 @@ export class FancyHeaderLevelButtonsComponent { protected readonly faBellSlash = faBellSlash; protected readonly faHeart = faHeart; protected readonly faHeartCrack = faHeartCrack; + protected readonly faPencil = faPencil; } \ No newline at end of file From a067d126bbe86c69041857ecdd49e183526c1e45 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 13:35:45 +0200 Subject: [PATCH 09/20] Remove non-slug level edit route --- src/app/app.routes.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d72b3fed..2b8a3d53 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -34,13 +34,7 @@ export const routes: Routes = [ loadComponent: () => import('./pages/level-edit/level-edit.component').then(x => x.LevelEditComponent), data: {title: "Edit Level"}, }, - { - path: 'level/:id/edit', - loadComponent: () => import('./pages/level-edit/level-edit.component').then(x => x.LevelEditComponent), - data: {title: "Edit Level"}, - }, ...alias("level/:id/:slug/edit", "slot/:id/:slug/edit"), - ...alias("level/:id/edit", "slot/:id/edit",), { path: 'photos', loadComponent: () => import('./pages/photo-listing/photo-listing.component').then(x => x.PhotoListingComponent), From 1232ab91554753bef42c3e0f05010de9261e2274 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 14:26:52 +0200 Subject: [PATCH 10/20] Fix determining whether to send level updates as publisher or curator, cache some user-level info --- .../level-edit/level-edit.component.html | 78 ++++++++++--------- .../pages/level-edit/level-edit.component.ts | 54 ++++++++----- 2 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index 8e55ae82..d9f0f133 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -1,8 +1,8 @@ -@if (level && ownUser && level.publisher?.userId != ownUser.userId && ownUser.role < 64) +@if (level && ownUser && !isUserPublisher && !isUserCurator) {

You may not edit this level. Go away.

} -@if (level && ownUser) { +@else if (level && ownUser) {
@@ -32,52 +32,54 @@
-
+ + @if (isUserCurator) { + - +
+ -
- +
+

Game version:

+ + + +
+ + + + + + + +
+
+
-
-

Game version:

- - - -
- - - - - - - +
+
+
+ +

+ since +

+
+
- -
-
-
-
- -

- since -

-
- +
- -
-
+ }
} diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index f974971c..f470b414 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -78,14 +78,14 @@ export class LevelEditComponent { hasPendingChangesTotal: boolean = false; hasPendingChangesLevel: boolean = false; + hasPendingCuratorChanges: boolean = false; showGameMenu: boolean = false; - displayTitle: string = "Unnamed Level"; - displayDescription: string = "No desk"; gameButtonColor: string = "bg-secondary"; - protected readonly curatorRoleValue: UserRoles = UserRoles.Curator; - protected readonly moderatorRoleValue: UserRoles = UserRoles.Moderator; + isUserCurator: boolean = false; + isUserModerator: boolean = false; + isUserPublisher: boolean = false; constructor(private client: ClientService, protected banner: BannerService, route: ActivatedRoute, protected layout: LayoutService, private auth: AuthenticationService, @@ -96,15 +96,18 @@ export class LevelEditComponent { route.params.subscribe(params => { const id: number = +params['id']; this.client.getLevelById(id).subscribe(data => { - if(data) { + if(!this.level && data) { this.level = data; this.updateInputs(data); - } - }); - this.auth.user.subscribe(user => { - if(user) { - this.ownUser = user; + this.auth.user.subscribe(user => { + if(user) { + this.ownUser = user; + this.isUserPublisher = data.publisher != null && user.userId == data.publisher?.userId; + this.isUserCurator = user.role >= UserRoles.Curator; + this.isUserModerator = user.role >= UserRoles.Moderator; + } + }); } }); }); @@ -166,7 +169,12 @@ export class LevelEditComponent { return this.hasPendingChangesLevel = this.hasTitleChanged || this.hasDescriptionChanged - || this.hasGameChanged + || this.doesLevelHavePendingCuratorSettingChanges(); + } + + doesLevelHavePendingCuratorSettingChanges(): boolean { + return this.hasPendingCuratorChanges = + this.hasGameChanged || this.hasReuploadChanged || this.hasOriginalPublisherChanged; } @@ -174,6 +182,7 @@ export class LevelEditComponent { updateInputs(level: Level) { this.hasPendingChangesTotal = false; this.hasPendingChangesLevel = false; + this.hasPendingCuratorChanges = false; this.settingsForm.controls.title.setValue(level.title); this.settingsForm.controls.description.setValue(level.description); @@ -183,9 +192,14 @@ export class LevelEditComponent { this.curatorForm.controls.isReupload.setValue(level.isReUpload); this.curatorForm.controls.originalPublisher.setValue(level.originalPublisher); - this.displayTitle = level.title; - this.displayDescription = level.description; this.setGameButtonColor(level.gameVersion); + + this.hasTitleChanged = false; + this.hasDescriptionChanged = false; + this.hasTeamPickedChanged = false; + this.hasReuploadChanged = false; + this.hasOriginalPublisherChanged = false; + this.hasGameChanged = false; } async gameButtonClick() { @@ -197,6 +211,7 @@ export class LevelEditComponent { if (this.hasTeamPickedChanged) this.updateTeamPick(); if (this.hasPendingChangesLevel) this.updateLevel(); + this.hasPendingChangesTotal = false; } updateTeamPick() { @@ -207,6 +222,8 @@ export class LevelEditComponent { this.banner.error("Failed to team pick level", apiError == null ? error.message : apiError.message); }, next: response => { + this.hasTeamPickedChanged = false; + this.level!.teamPicked = true; this.banner.success("Level team picked!", "The level was successfully team picked."); } }); @@ -218,6 +235,8 @@ export class LevelEditComponent { this.banner.error("Failed to remove team pick", apiError == null ? error.message : apiError.message); }, next: response => { + this.hasTeamPickedChanged = false; + this.level!.teamPicked = false; this.banner.success("Team pick removed!", "The level is no longer team picked."); } }); @@ -234,7 +253,7 @@ export class LevelEditComponent { originalPublisher: this.curatorForm.controls.originalPublisher.getRawValue(), }; - this.client.updateLevelById(this.level!.levelId, request, this.isUserPublisher()).subscribe({ + this.client.updateLevelById(this.level!.levelId, request, this.hasPendingCuratorChanges).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to update the level", apiError == null ? error.message : apiError.message); @@ -244,6 +263,7 @@ export class LevelEditComponent { // Update level data this.level = response; + this.updateInputs(response); } }); } @@ -267,7 +287,7 @@ export class LevelEditComponent { } updateLevelIcon(hash: string) { - this.client.updateLevelIconById(this.level!.levelId, hash, this.isUserPublisher()).subscribe({ + this.client.updateLevelIconById(this.level!.levelId, hash, !this.isUserPublisher).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to update the level icon", apiError == null ? error.message : apiError.message); @@ -286,10 +306,6 @@ export class LevelEditComponent { img.srcset = "/assets/missingLevel.svg"; } - isUserPublisher(): boolean { - return this.ownUser!.userId != this.level!.publisher?.userId; - } - delete() { if (true) { this.banner.success("DELETE LOL", "yuo pressed oit!!"); From 2fec4b384bdfe84ff0aeb5cce00b9e87f0cd6910 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 14:50:20 +0200 Subject: [PATCH 11/20] level edit page GUI improvements --- src/app/api/types/levels/level.ts | 2 +- src/app/pages/level-edit/level-edit.component.html | 4 +++- src/app/pages/level-edit/level-edit.component.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/api/types/levels/level.ts b/src/app/api/types/levels/level.ts index 676aefdc..e56ec3b3 100644 --- a/src/app/api/types/levels/level.ts +++ b/src/app/api/types/levels/level.ts @@ -9,7 +9,7 @@ export interface Level { originalPublisher: string | undefined; isReUpload: boolean; teamPicked: boolean; - dateTeamPicked : Date; + dateTeamPicked: Date; gameVersion: number; score: number; slotType: number; diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index d9f0f133..f6678f3c 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -34,7 +34,9 @@
- +
+ +
diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index f470b414..98dc22b1 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -224,6 +224,7 @@ export class LevelEditComponent { next: response => { this.hasTeamPickedChanged = false; this.level!.teamPicked = true; + this.level!.dateTeamPicked = new Date(); this.banner.success("Level team picked!", "The level was successfully team picked."); } }); @@ -278,7 +279,7 @@ export class LevelEditComponent { this.client.uploadAsset(hash, data).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; - this.banner.warn("Failed to upload level icon", apiError == null ? error.message : apiError.message); + this.banner.warn("Failed to upload new icon", apiError == null ? error.message : apiError.message); }, next: _ => { this.updateLevelIcon(hash); From 8eb8adcc038a7178d2f7449c1b0071528d42f54f Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 15:50:42 +0200 Subject: [PATCH 12/20] Fix slug for level edit routes --- .../components/ui/form/dropdown-menu.component.ts | 5 ++++- .../layouts/fancy-header-level-buttons.component.ts | 4 ++-- src/app/pages/level-edit/level-edit.component.ts | 13 ++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/components/ui/form/dropdown-menu.component.ts b/src/app/components/ui/form/dropdown-menu.component.ts index 20384d7b..cf7d1ee6 100644 --- a/src/app/components/ui/form/dropdown-menu.component.ts +++ b/src/app/components/ui/form/dropdown-menu.component.ts @@ -1,4 +1,4 @@ -import {Component, Input} from '@angular/core'; +import {Component, Injectable, Input} from '@angular/core'; import {ReactiveFormsModule} from "@angular/forms"; import { NgClass } from "@angular/common"; @@ -19,6 +19,9 @@ import { NgClass } from "@angular/common";
` }) +@Injectable({ + providedIn: 'root' +}) export class DropdownMenuComponent { @Input() offsets: string = "" @Input({required: true}) width: number = 0; diff --git a/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts b/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts index 7278bf81..3867d468 100644 --- a/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts +++ b/src/app/components/ui/layouts/fancy-header-level-buttons.component.ts @@ -117,6 +117,7 @@ export class FancyHeaderLevelButtonsComponent { ngAfterViewInit() { const isPublisher: boolean = this.level.publisher != null && this.level.publisher.userId == this.ownUser.userId; + this.levelTitleSlug = this.slug.transform(this.level.title); // Edit button at the top, if level is published by the user if (isPublisher) { @@ -140,9 +141,8 @@ export class FancyHeaderLevelButtonsComponent { // Edit button further below, if the user is a curator or above and not already the publisher aswell if (!isPublisher && this.ownUser.role >= UserRoles.Curator) { this.buttonTemplateRefs.push(this.editButtonTemplateRef); - } + } - this.levelTitleSlug = this.slug.transform(this.level.title); this.buttonsInitialized = true; } diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index 98dc22b1..fc1be34c 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -26,7 +26,7 @@ import { RadioButtonComponent } from "../../components/ui/form/radio-button.comp import { TextAreaComponent } from "../../components/ui/form/textarea.component"; import { DateComponent } from "../../components/ui/info/date.component"; import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; - +import { SlugPipe } from '../../pipes/slug.pipe'; @Component({ selector: 'app-level-edit', @@ -46,6 +46,9 @@ import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.co DateComponent, DropdownMenuComponent ], + providers: [ + SlugPipe + ], templateUrl: './level-edit.component.html' }) export class LevelEditComponent { @@ -89,7 +92,7 @@ export class LevelEditComponent { constructor(private client: ClientService, protected banner: BannerService, route: ActivatedRoute, protected layout: LayoutService, private auth: AuthenticationService, - @Inject(PLATFORM_ID) platformId: Object, private router: Router) + @Inject(PLATFORM_ID) platformId: Object, private router: Router, private slug: SlugPipe) { this.isBrowser = isPlatformBrowser(platformId); @@ -100,6 +103,10 @@ export class LevelEditComponent { this.level = data; this.updateInputs(data); + if(this.isBrowser) { + window.history.replaceState({}, '', `/level/${data.levelId}/${this.slug.transform(data.title)}/edit`); + } + this.auth.user.subscribe(user => { if(user) { this.ownUser = user; @@ -316,7 +323,7 @@ export class LevelEditComponent { /* if (this.level == undefined) return; - this.client.deleteLevelById(this.level.levelId, this.isUserPublisher()).subscribe({ + this.client.deleteLevelById(this.level.levelId, !this.isUserPublisher).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to delete the level", apiError == null ? error.message : apiError.message); From e7c43278b8e3cb110820792490a9e4acb52ecafe Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 16:01:44 +0200 Subject: [PATCH 13/20] Align remaining char number on textarea --- src/app/components/ui/form/textarea.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/ui/form/textarea.component.ts b/src/app/components/ui/form/textarea.component.ts index 6c1a2077..816e9349 100644 --- a/src/app/components/ui/form/textarea.component.ts +++ b/src/app/components/ui/form/textarea.component.ts @@ -14,8 +14,8 @@ import {FormGroup, ReactiveFormsModule} from "@angular/forms"; }
-
- +
+ @if (showMaxLength == true) {

{{maxLength - (form.get(ctrlName)?.value?.length ?? 0)}}

} From e39d0ba42ecfc55f0d595ca9629d503f633a4ed4 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 16:11:32 +0200 Subject: [PATCH 14/20] Implement level delete button --- src/app/pages/level-edit/level-edit.component.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index fc1be34c..072733f3 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -315,32 +315,20 @@ export class LevelEditComponent { } delete() { - if (true) { - this.banner.success("DELETE LOL", "yuo pressed oit!!"); - return; - } - - /* if (this.level == undefined) return; this.client.deleteLevelById(this.level.levelId, !this.isUserPublisher).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to delete the level", apiError == null ? error.message : apiError.message); - - // Update level data - this.client.getLevelById(this.level!.levelId).subscribe(data => { - this.level = data; - }); }, next: response => { this.banner.success("Level deleted!", "The level was successfully deleted."); - // Navigate (TODO: redirect to page before both the edit and the details page) - this.router.navigate(['**']); + // Navigate (TODO: redirect to page before the details page, or atleast last viewed level category) + this.router.navigate(['/levels']); } }); - */ } protected readonly faFloppyDisk = faFloppyDisk; From 43e444127b315937dcf32c3df16a6883a31a0aba Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 16:24:51 +0200 Subject: [PATCH 15/20] Allow buttons to have no text, make more button on level details page no longer change width when clicked --- src/app/components/ui/form/button.component.ts | 8 +++++--- .../ui/layouts/fancy-header-buttons.component.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/components/ui/form/button.component.ts b/src/app/components/ui/form/button.component.ts index 94a189a1..b8ddd9b9 100644 --- a/src/app/components/ui/form/button.component.ts +++ b/src/app/components/ui/form/button.component.ts @@ -10,18 +10,20 @@ import { NgClass } from "@angular/common"; NgClass ], template: ` - ` }) export class ButtonComponent { // metadata - @Input({required: true}) text: string = "Button"; + @Input() text: string | undefined; @Input() icon: IconProp | undefined; @Input() color: string = "bg-secondary"; diff --git a/src/app/components/ui/layouts/fancy-header-buttons.component.ts b/src/app/components/ui/layouts/fancy-header-buttons.component.ts index 0b5d3781..9bdbe967 100644 --- a/src/app/components/ui/layouts/fancy-header-buttons.component.ts +++ b/src/app/components/ui/layouts/fancy-header-buttons.component.ts @@ -12,7 +12,7 @@ import { DropdownMenuComponent } from "../form/dropdown-menu.component"; template: ` From d665732616113b06868a1df687f5f538c0550446 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 25 Oct 2025 17:41:40 +0200 Subject: [PATCH 16/20] Level deletion prompt --- src/app/components/ui/dialog.component.ts | 2 +- .../level-edit/level-edit.component.html | 25 ++++++++++++++++++- .../pages/level-edit/level-edit.component.ts | 16 ++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/app/components/ui/dialog.component.ts b/src/app/components/ui/dialog.component.ts index 1bd33b06..c1b1a489 100644 --- a/src/app/components/ui/dialog.component.ts +++ b/src/app/components/ui/dialog.component.ts @@ -4,7 +4,7 @@ import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core selector: 'app-dialog', imports: [], template: ` - + `, diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index f6678f3c..28284cf6 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -33,7 +33,7 @@
+ [icon]="faTrash" text="DELETE level" color="bg-red" (click)="deleteButtonClick()">
@@ -84,4 +84,27 @@
}
+ + + @defer (when showDeletionPrompt) { @if (showDeletionPrompt) { + +
+ +
+ + +
+
+
+ }} } diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index 072733f3..ba1ead84 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -12,7 +12,7 @@ import { ExtendedUser } from '../../api/types/users/extended-user'; import { UserRoles } from '../../api/types/users/user-roles'; import { PageTitleComponent } from "../../components/ui/text/page-title.component"; import { ButtonComponent } from "../../components/ui/form/button.component"; -import { faCertificate, faChevronDown, faChevronUp, faClone, faFloppyDisk, faPencil, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; +import { faCertificate, faChevronDown, faChevronUp, faClone, faFloppyDisk, faPencil, faSignOutAlt, faTrash, faUser } from '@fortawesome/free-solid-svg-icons'; import { TextboxComponent } from '../../components/ui/form/textbox.component'; import { FormControl, FormGroup } from '@angular/forms'; import { LevelUpdateRequest } from '../../api/types/levels/level-update-request'; @@ -27,6 +27,7 @@ import { TextAreaComponent } from "../../components/ui/form/textarea.component"; import { DateComponent } from "../../components/ui/info/date.component"; import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; import { SlugPipe } from '../../pipes/slug.pipe'; +import { DialogComponent } from "../../components/ui/dialog.component"; @Component({ selector: 'app-level-edit', @@ -44,7 +45,8 @@ import { SlugPipe } from '../../pipes/slug.pipe'; GamePipe, TextAreaComponent, DateComponent, - DropdownMenuComponent + DropdownMenuComponent, + DialogComponent ], providers: [ SlugPipe @@ -83,6 +85,7 @@ export class LevelEditComponent { hasPendingChangesLevel: boolean = false; hasPendingCuratorChanges: boolean = false; + showDeletionPrompt: boolean = false; showGameMenu: boolean = false; gameButtonColor: string = "bg-secondary"; @@ -314,6 +317,14 @@ export class LevelEditComponent { img.srcset = "/assets/missingLevel.svg"; } + async deleteButtonClick() { + this.showDeletionPrompt = !this.showDeletionPrompt; + } + + async deleteCancelClick() { + this.showDeletionPrompt = false; + } + delete() { if (this.level == undefined) return; @@ -339,4 +350,5 @@ export class LevelEditComponent { protected readonly faClone = faClone; protected readonly faChevronDown = faChevronDown; protected readonly faChevronUp = faChevronUp; + protected readonly faSignOutAlt = faSignOutAlt; } From 2e7bc7b3f4592609064124b4d78e2e84a45ef02c Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sun, 26 Oct 2025 11:18:30 +0100 Subject: [PATCH 17/20] Correct server url back --- src/app/helpers/data-fetching.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/helpers/data-fetching.ts b/src/app/helpers/data-fetching.ts index 698cb0c9..a683895a 100644 --- a/src/app/helpers/data-fetching.ts +++ b/src/app/helpers/data-fetching.ts @@ -1,7 +1,7 @@ import {ImageLoaderConfig} from "@angular/common"; export function getApiBaseUrl(): string { - return "http://localhost:10061/api/v3"; //"https://lbp.littlebigrefresh.com/api/v3"; + return "https://lbp.littlebigrefresh.com/api/v3"; } export function getImageLink(hash: string): string { From 643b9ea5386fe42a72943cd68fc84f4a10f65c0a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sun, 26 Oct 2025 12:37:14 +0100 Subject: [PATCH 18/20] More accurately determine whether to send admin or non-admin level edit requests --- src/app/pages/level-edit/level-edit.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index ba1ead84..7e99bac7 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -264,7 +264,10 @@ export class LevelEditComponent { originalPublisher: this.curatorForm.controls.originalPublisher.getRawValue(), }; - this.client.updateLevelById(this.level!.levelId, request, this.hasPendingCuratorChanges).subscribe({ + // Only make an admin request if the curator has either changed any curator options on the level, + // or this is another user's level. This way they may still edit the metadata of their own level + // without having that be counted as moderation action. + this.client.updateLevelById(this.level!.levelId, request, this.hasPendingCuratorChanges || !this.isUserPublisher).subscribe({ error: error => { const apiError: RefreshApiError | undefined = error.error?.error; this.banner.error("Failed to update the level", apiError == null ? error.message : apiError.message); From 71d93aa2acd5f6b2933786c36d7aa225046a2c42 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 1 Nov 2025 17:03:20 +0100 Subject: [PATCH 19/20] Fix re-opening delete dialog after closing dialog itself (e.g. pressing escape) --- src/app/components/ui/dialog.component.ts | 9 +++++++-- src/app/pages/level-edit/level-edit.component.html | 4 ++-- src/app/pages/level-edit/level-edit.component.ts | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/components/ui/dialog.component.ts b/src/app/components/ui/dialog.component.ts index c1b1a489..96599f17 100644 --- a/src/app/components/ui/dialog.component.ts +++ b/src/app/components/ui/dialog.component.ts @@ -1,10 +1,10 @@ -import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, OnDestroy, OnInit, Output, ViewChild, EventEmitter} from '@angular/core'; @Component({ selector: 'app-dialog', imports: [], template: ` - + `, @@ -12,11 +12,16 @@ import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core }) export class DialogComponent implements OnInit, OnDestroy { @ViewChild('dialog', { static: true }) dialog!: ElementRef; + @Output() onDialogClose = new EventEmitter; ngOnInit(): void { this.dialog.nativeElement.showModal(); } + close(): void { + this.onDialogClose.emit(); + } + ngOnDestroy(): void { this.dialog.nativeElement.close(); } diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index 28284cf6..c7421d56 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -87,7 +87,7 @@ @defer (when showDeletionPrompt) { @if (showDeletionPrompt) { - +
@@ -101,7 +101,7 @@ text="No, Go back!" [icon]="faSignOutAlt" color="bg-secondary" - (click)="deleteCancelClick()" + (click)="closeDeleteDialog()" >
diff --git a/src/app/pages/level-edit/level-edit.component.ts b/src/app/pages/level-edit/level-edit.component.ts index 7e99bac7..b429d56b 100644 --- a/src/app/pages/level-edit/level-edit.component.ts +++ b/src/app/pages/level-edit/level-edit.component.ts @@ -324,7 +324,7 @@ export class LevelEditComponent { this.showDeletionPrompt = !this.showDeletionPrompt; } - async deleteCancelClick() { + async closeDeleteDialog() { this.showDeletionPrompt = false; } From 94f27e6653d5eb63a23d2d074c47a997e4950a70 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sat, 1 Nov 2025 19:32:32 +0100 Subject: [PATCH 20/20] Remove delete dialog margin workaround, properly close search dialog --- src/app/components/ui/dialog.component.ts | 2 +- src/app/overlays/search.component.ts | 2 +- src/app/pages/level-edit/level-edit.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/ui/dialog.component.ts b/src/app/components/ui/dialog.component.ts index 96599f17..8a894e45 100644 --- a/src/app/components/ui/dialog.component.ts +++ b/src/app/components/ui/dialog.component.ts @@ -4,7 +4,7 @@ import {Component, ElementRef, OnDestroy, OnInit, Output, ViewChild, EventEmitte selector: 'app-dialog', imports: [], template: ` - + `, diff --git a/src/app/overlays/search.component.ts b/src/app/overlays/search.component.ts index 9a10d6a1..6b6b2d4e 100644 --- a/src/app/overlays/search.component.ts +++ b/src/app/overlays/search.component.ts @@ -39,7 +39,7 @@ import {debounceTime} from "rxjs"; @defer (when show) { @if (show) { - +
@if (!(layout.isMobile | async)) { diff --git a/src/app/pages/level-edit/level-edit.component.html b/src/app/pages/level-edit/level-edit.component.html index c7421d56..3d609442 100644 --- a/src/app/pages/level-edit/level-edit.component.html +++ b/src/app/pages/level-edit/level-edit.component.html @@ -88,7 +88,7 @@ @defer (when showDeletionPrompt) { @if (showDeletionPrompt) { -
+