From cd40c13131b06e7c04996717bd8efe2f60128564 Mon Sep 17 00:00:00 2001 From: alexcibotari Date: Thu, 14 May 2026 14:03:32 +0200 Subject: [PATCH 01/18] Update dependencies and refactor notification service for improved action handling --- functions/package-lock.json | 70 ++++++++--------- functions/package.json | 10 +-- src/app/app.component.html | 2 + src/app/app.component.ts | 3 +- src/app/features/features.component.html | 1 - src/app/features/features.component.ts | 18 ++--- .../spaces/assets/assets.component.ts | 21 ++--- .../spaces/contents/contents.component.ts | 14 ++-- .../edit-document-schema.component.ts | 7 +- .../markdown-editor.component.ts | 7 +- .../spaces/schemas/schemas.component.ts | 14 ++-- .../translations/translations.component.ts | 21 ++--- .../custom-snack-bar.component.html | 9 --- .../custom-snack-bar.component.scss | 36 --------- .../custom-snack-bar.component.ts | 28 ------- .../custom-snack-bar.model.ts | 9 --- .../shared/services/notification.service.ts | 76 ++++++++++--------- 17 files changed, 135 insertions(+), 211 deletions(-) delete mode 100644 src/app/shared/components/custom-snack-bar/custom-snack-bar.component.html delete mode 100644 src/app/shared/components/custom-snack-bar/custom-snack-bar.component.scss delete mode 100644 src/app/shared/components/custom-snack-bar/custom-snack-bar.component.ts delete mode 100644 src/app/shared/components/custom-snack-bar/custom-snack-bar.model.ts diff --git a/functions/package-lock.json b/functions/package-lock.json index 722156f5..b784f835 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,18 +8,18 @@ "name": "functions", "version": "3.0.1", "dependencies": { - "@google-cloud/translate": "^9.3.0", + "@google-cloud/translate": "^9.4.1", "compressing": "^2.1.1", "cors": "^2.8.6", - "deepl-node": "^1.26.0", - "exiftool-vendored": "^35.18.0", + "deepl-node": "^1.27.0", + "exiftool-vendored": "^35.20.0", "express": "^5.2.1", - "firebase-admin": "^13.8.0", + "firebase-admin": "^13.9.0", "firebase-functions": "^7.2.5", "fluent-ffmpeg": "^2.1.3", "sharp": "^0.34.5", "uuid": "^14.0.0", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@types/express": "^5.0.6", @@ -693,9 +693,9 @@ } }, "node_modules/@google-cloud/translate": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/translate/-/translate-9.3.0.tgz", - "integrity": "sha512-OgZ2bCu3P0ZzMhEdYubwyCo/eFFlJMYalozmgOxlVcD51vCYelYUJeVnGlS+3cFQTJQX4RE84bYTKu7W0wqByw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@google-cloud/translate/-/translate-9.4.1.tgz", + "integrity": "sha512-3KPzuTdo50iatTf5jOeBreFFFtwruwuvn7gYm4vL+XgihDZvcHfFF6cGKGXzGIhLJTis9CfePO8ZHQVtoPWC9A==", "license": "Apache-2.0", "dependencies": { "@google-cloud/common": "^6.0.0", @@ -2802,29 +2802,19 @@ "license": "MIT" }, "node_modules/deepl-node": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.26.0.tgz", - "integrity": "sha512-KaMZ248O/N2HrevQnsJoCoHxDUNBbsQ7+OZJa2YAOhym4qtVCNTzRst+4rLX9B9T84SVN4xVQSuJyFIdlI/x1w==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.27.0.tgz", + "integrity": "sha512-OEiprFdxqr7aS0+gmv8vsZLBLoDarBppG1zkTWUlrAvPoYdwSWKFWjcZgr9XSV3FWlPy8g10WdJjWkxDn9P56w==", "license": "MIT", "dependencies": { "@types/node": ">=12.0", "adm-zip": "^0.5.16", "axios": "^1.7.4", "form-data": "^3.0.4", - "loglevel": ">=1.6.2", - "uuid": "^8.3.2" + "loglevel": ">=1.6.2" }, "engines": { - "node": ">=12.0" - } - }, - "node_modules/deepl-node/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node": ">=14.17" } }, "node_modules/define-data-property": { @@ -3518,9 +3508,9 @@ } }, "node_modules/exiftool-vendored": { - "version": "35.18.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-35.18.0.tgz", - "integrity": "sha512-QBtYNz71VAwZWqxFP1iWWS9qwOx3b9MSpk0GAMyIfS8gupUWsOyhn4i2WrB4OlRSQPuQ2YeKSw2fygi6E0LGiw==", + "version": "35.20.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-35.20.0.tgz", + "integrity": "sha512-Yn66dSBaWGcUaSbm5Nl4G28rxtceLlWf4PstqJMbLix9sN7w0okWHPEvdudiP56Q5Cjl7v3TLyKKwowUFlbD8g==", "license": "MIT", "dependencies": { "@photostructure/tz-lookup": "^11.5.0", @@ -3533,14 +3523,14 @@ "node": ">=20.0.0" }, "optionalDependencies": { - "exiftool-vendored.exe": "13.57.0", - "exiftool-vendored.pl": "13.57.0" + "exiftool-vendored.exe": "13.58.0", + "exiftool-vendored.pl": "13.58.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "13.57.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.57.0.tgz", - "integrity": "sha512-9ENCWzUiFy6F/O4jSX50ygSGrTOtvoqJFWE0zAOl7VL/EFooLWNF0LkaNSox0ibbIsQz5rWSKi0TPlEbF4qBIw==", + "version": "13.58.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.58.0.tgz", + "integrity": "sha512-pV7SjQeOu4Q77DWuyF+hlRYWVlRcSAqfqTTujBZeGUy/Q9+RPAy877YgSZIxKOYW1TxmmL8KyBGxaG0JKYG8BQ==", "license": "MIT", "optional": true, "os": [ @@ -3548,9 +3538,9 @@ ] }, "node_modules/exiftool-vendored.pl": { - "version": "13.57.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.57.0.tgz", - "integrity": "sha512-7HYhrIygbfKD+E/sUF9L8YEs7qCEFLFWKoeevJllnD9jxVvZ09tfFsjbBPQ7SAgGwWSHW//SVULFHLgrO8JsBw==", + "version": "13.58.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.58.0.tgz", + "integrity": "sha512-+Z2xhZrYLMu/anO/s14AaS/K5HMJ5Cw9C3KefIeYNpkZRN4RRBJHm7R34yjj9Pv+elqYRZrQV9NcqvkBLn/68w==", "license": "MIT", "optional": true, "os": [ @@ -3816,9 +3806,9 @@ } }, "node_modules/firebase-admin": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.8.0.tgz", - "integrity": "sha512-iawoQkmZbsA+2DY5UEuB8f6jSlskzzySoye0D2F6e3zlDZX9DUcXf0HhZqLUn/P6WhLGvTf6ZtCmshZvhAgTYg==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.9.0.tgz", + "integrity": "sha512-qiCVBBFH+kfLiCXuuE9eAbBQSckPuA43fbQ/MNvQfd9nZcHFQExmQICD/N0sZrNZDNy8FSywhjFzJJGVQzG5UA==", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", @@ -8281,9 +8271,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/functions/package.json b/functions/package.json index 80580a0d..550a697e 100644 --- a/functions/package.json +++ b/functions/package.json @@ -19,18 +19,18 @@ }, "main": "lib/index.js", "dependencies": { - "@google-cloud/translate": "^9.3.0", + "@google-cloud/translate": "^9.4.1", "compressing": "^2.1.1", "cors": "^2.8.6", - "deepl-node": "^1.26.0", - "exiftool-vendored": "^35.18.0", + "deepl-node": "^1.27.0", + "exiftool-vendored": "^35.20.0", "express": "^5.2.1", - "firebase-admin": "^13.8.0", + "firebase-admin": "^13.9.0", "firebase-functions": "^7.2.5", "fluent-ffmpeg": "^2.1.3", "sharp": "^0.34.5", "uuid": "^14.0.0", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@types/express": "^5.0.6", diff --git a/src/app/app.component.html b/src/app/app.component.html index 0680b43f..f90d16dd 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,3 @@ + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3faa1774..eba86913 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,12 +3,13 @@ import { Analytics } from '@angular/fire/analytics'; import { Performance } from '@angular/fire/performance'; import { MatIconRegistry } from '@angular/material/icon'; import { RouterModule } from '@angular/router'; +import { HlmToasterImports } from '@spartan-ng/helm/sonner'; @Component({ selector: 'll-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], - imports: [RouterModule], + imports: [RouterModule, HlmToasterImports], }) export class AppComponent { private readonly performance = inject(Performance); diff --git a/src/app/features/features.component.html b/src/app/features/features.component.html index c2f5aa02..a468b41e 100644 --- a/src/app/features/features.component.html +++ b/src/app/features/features.component.html @@ -238,5 +238,4 @@

Debug Settings

- diff --git a/src/app/features/features.component.ts b/src/app/features/features.component.ts index 115b3c98..7b47442f 100644 --- a/src/app/features/features.component.ts +++ b/src/app/features/features.component.ts @@ -44,6 +44,7 @@ import { USER_PERMISSIONS_IMPORT_EXPORT, UserPermission } from '@shared/models/u import { Version } from '@shared/models/version.model'; import { CanUserPerformPipe } from '@shared/pipes/can-user-perform.pipe'; import { ContentService } from '@shared/services/content.service'; +import { NotificationService } from '@shared/services/notification.service'; import { SchemaService } from '@shared/services/schema.service'; import { VersionService } from '@shared/services/version.service'; import { AppSettingsStore } from '@shared/stores/app-settings.store'; @@ -59,11 +60,9 @@ import { HlmIconImports } from '@spartan-ng/helm/icon'; import { HlmSeparatorImports } from '@spartan-ng/helm/separator'; import { HlmSheetImports } from '@spartan-ng/helm/sheet'; import { HlmSidebarImports, HlmSidebarService } from '@spartan-ng/helm/sidebar'; -import { HlmToasterImports } from '@spartan-ng/helm/sonner'; import { HlmSwitchImports } from '@spartan-ng/helm/switch'; import { HlmTooltipImports } from '@spartan-ng/helm/tooltip'; import { cva } from 'class-variance-authority'; -import { toast } from 'ngx-sonner'; import { filter, interval, mergeMap } from 'rxjs'; import { environment } from '../../environments/environment'; @@ -116,7 +115,6 @@ interface SideMenuItem { HlmFieldImports, HlmSwitchImports, ReactiveFormsModule, - HlmToasterImports, ], providers: [ provideIcons({ @@ -160,6 +158,7 @@ export class FeaturesComponent implements OnInit { private readonly contentService = inject(ContentService); private readonly schemaService = inject(SchemaService); private readonly versionService = inject(VersionService); + private readonly notificationService = inject(NotificationService); public readonly sidebarService = inject(HlmSidebarService); public readonly spaceStore = inject(SpaceStore); public readonly userStore = inject(UserStore); @@ -293,17 +292,12 @@ export class FeaturesComponent implements OnInit { if (currentVersion.version !== remoteVersion.version) { window.location.reload(); } else if (currentVersion.gitCommitSha !== remoteVersion.gitCommitSha) { - toast.info('New version is available', { + this.notificationService.info('New version is available', { position: 'bottom-left', - description: 'We’ve just rolled out an update! Refresh the page to get the latest improvements.', + description: "We've just rolled out an update! Refresh the page to get the latest improvements.", duration: 300000, - action: { - label: 'Reload', - onClick: () => window.location.reload(), - }, - cancel: { - label: 'Skip', - }, + action: { type: 'action', label: 'Reload', onClick: () => window.location.reload() }, + cancel: { label: 'Skip' }, }); } } else { diff --git a/src/app/features/spaces/assets/assets.component.ts b/src/app/features/spaces/assets/assets.component.ts index f388530c..333d1114 100644 --- a/src/app/features/spaces/assets/assets.component.ts +++ b/src/app/features/spaces/assets/assets.component.ts @@ -513,12 +513,13 @@ export class AssetsComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Assets Import Task has been created.', [ - { + this.notificationService.success('Assets Import Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: () => { this.notificationService.error('Assets Import Task can not be created.'); @@ -541,12 +542,13 @@ export class AssetsComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Assets Export Task has been created.', [ - { + this.notificationService.success('Assets Export Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: (err: unknown) => { console.error(err); @@ -571,12 +573,13 @@ export class AssetsComponent implements OnInit { .subscribe({ next: () => { this.cd.markForCheck(); - this.notificationService.success('Assets Regenerate Metadata Task has been created.', [ - { + this.notificationService.success('Assets Regenerate Metadata Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: (err: unknown) => { console.error(err); diff --git a/src/app/features/spaces/contents/contents.component.ts b/src/app/features/spaces/contents/contents.component.ts index f6e17d77..cb212995 100644 --- a/src/app/features/spaces/contents/contents.component.ts +++ b/src/app/features/spaces/contents/contents.component.ts @@ -478,12 +478,13 @@ export class ContentsComponent { ) .subscribe({ next: () => { - this.notificationService.success('Content Import Task has been created.', [ - { + this.notificationService.success('Content Import Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: () => { this.notificationService.error('Content Import Task can not be created.'); @@ -506,12 +507,13 @@ export class ContentsComponent { ) .subscribe({ next: () => { - this.notificationService.success('Content Export Task has been created.', [ - { + this.notificationService.success('Content Export Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: (err: unknown) => { console.error(err); diff --git a/src/app/features/spaces/contents/edit-document-schema/edit-document-schema.component.ts b/src/app/features/spaces/contents/edit-document-schema/edit-document-schema.component.ts index 8588702c..6bea5ec8 100644 --- a/src/app/features/spaces/contents/edit-document-schema/edit-document-schema.component.ts +++ b/src/app/features/spaces/contents/edit-document-schema/edit-document-schema.component.ts @@ -533,12 +533,13 @@ export class EditDocumentSchemaComponent implements OnInit, OnChanges { }, error: err => { console.error(err); - this.notificationService.error('Can not be translation.', [ - { + this.notificationService.error('Can not be translation.', { + action: { + type: 'link', label: 'Documentation', link: 'https://localess.org/docs/setup/firebase#errors-in-the-user-interface', }, - ]); + }); }, }); } diff --git a/src/app/features/spaces/contents/shared/markdown-editor/markdown-editor.component.ts b/src/app/features/spaces/contents/shared/markdown-editor/markdown-editor.component.ts index b41d212f..42dd9195 100644 --- a/src/app/features/spaces/contents/shared/markdown-editor/markdown-editor.component.ts +++ b/src/app/features/spaces/contents/shared/markdown-editor/markdown-editor.component.ts @@ -94,12 +94,13 @@ export class MarkdownEditorComponent { }, error: err => { console.error(err); - this.notificationService.error('Can not be translation.', [ - { + this.notificationService.error('Can not be translation.', { + action: { + type: 'link', label: 'Documentation', link: 'https://localess.org/docs/setup/firebase#errors-in-the-user-interface', }, - ]); + }); }, }); } diff --git a/src/app/features/spaces/schemas/schemas.component.ts b/src/app/features/spaces/schemas/schemas.component.ts index 2fd93e2b..b90c9454 100644 --- a/src/app/features/spaces/schemas/schemas.component.ts +++ b/src/app/features/spaces/schemas/schemas.component.ts @@ -301,12 +301,13 @@ export class SchemasComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Schema Import Task has been created.', [ - { + this.notificationService.success('Schema Import Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: () => { this.notificationService.error('Schema Import Task can not be created.'); @@ -326,12 +327,13 @@ export class SchemasComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Schema Export Task has been created.', [ - { + this.notificationService.success('Schema Export Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: (err: unknown) => { console.error(err); diff --git a/src/app/features/spaces/translations/translations.component.ts b/src/app/features/spaces/translations/translations.component.ts index 25e48bf6..dfb07e96 100644 --- a/src/app/features/spaces/translations/translations.component.ts +++ b/src/app/features/spaces/translations/translations.component.ts @@ -498,12 +498,13 @@ export class TranslationsComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Translation Import Task has been created.', [ - { + this.notificationService.success('Translation Import Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: () => { this.notificationService.error('Translation Import Task can not be created.'); @@ -534,12 +535,13 @@ export class TranslationsComponent implements OnInit { ) .subscribe({ next: () => { - this.notificationService.success('Translation Export Task has been created.', [ - { + this.notificationService.success('Translation Export Task has been created.', { + action: { + type: 'route', label: 'To Tasks', link: `/features/spaces/${this.spaceId()}/tasks`, }, - ]); + }); }, error: (err: unknown) => { console.error(err); @@ -751,12 +753,13 @@ export class TranslationsComponent implements OnInit { }, error: (err: unknown) => { console.error(err); - this.notificationService.error('Can not be translation.', [ - { + this.notificationService.error('Can not be translation.', { + action: { + type: 'link', label: 'Documentation', link: 'https://localess.org/docs/setup/firebase#errors-in-the-user-interface', }, - ]); + }); }, complete: () => { console.log('complete'); diff --git a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.html b/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.html deleted file mode 100644 index ce77486b..00000000 --- a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
{{ data.message }}
- -@if (data.actions) { -
- @for (action of data.actions; track action.label) { - - } -
-} diff --git a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.scss b/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.scss deleted file mode 100644 index 6087c217..00000000 --- a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -@use 'sass:math'; - -$button-horizontal-margin: 8px !default; -$button-height: 36px !default; -$line-height: 20px !default; -// Button vertical margin is used to ensure that a button height of 36px, when the containing -// space falls below 36px. -$button-vertical-margin: -(math.div($button-height - $line-height, 2)); - -.mat-mdc-simple-snack-bar { - display: flex; - justify-content: space-between; - align-items: center; - line-height: $line-height; - opacity: 1; -} - -.mat-simple-snackbar-action { - flex-shrink: 0; - margin: $button-vertical-margin $button-horizontal-margin * -1 $button-vertical-margin $button-horizontal-margin; - - button { - max-height: $button-height; - min-width: 0; - } - - [dir='rtl'] & { - margin-left: -$button-horizontal-margin; - margin-right: $button-horizontal-margin; - } -} - -.mat-simple-snack-bar-content { - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.ts b/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.ts deleted file mode 100644 index e8bbb2b1..00000000 --- a/src/app/shared/components/custom-snack-bar/custom-snack-bar.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ChangeDetectionStrategy, Component, HostBinding, inject, ViewEncapsulation } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MAT_SNACK_BAR_DATA, MatSnackBarModule } from '@angular/material/snack-bar'; -import { Router } from '@angular/router'; -import { CustomSnackBarModel } from '@shared/components/custom-snack-bar/custom-snack-bar.model'; - -@Component({ - selector: 'll-custom-snack-bar', - templateUrl: 'custom-snack-bar.component.html', - styleUrls: ['custom-snack-bar.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MatSnackBarModule, MatButtonModule], -}) -export class CustomSnackBarComponent { - private readonly router = inject(Router); - readonly data = inject(MAT_SNACK_BAR_DATA); - - @HostBinding('class') class = 'mat-mdc-simple-snack-bar'; - - navigate(link: string): void { - if (link.startsWith('https://') || link.startsWith('http://')) { - window.open(link); - } else { - this.router.navigate([link]); - } - } -} diff --git a/src/app/shared/components/custom-snack-bar/custom-snack-bar.model.ts b/src/app/shared/components/custom-snack-bar/custom-snack-bar.model.ts deleted file mode 100644 index 9675ddb0..00000000 --- a/src/app/shared/components/custom-snack-bar/custom-snack-bar.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface CustomSnackBarModel { - message: string; - actions?: ActionRoute[]; -} - -export interface ActionRoute { - label: string; - link: string; -} diff --git a/src/app/shared/services/notification.service.ts b/src/app/shared/services/notification.service.ts index fef2fdf2..fa41dd89 100644 --- a/src/app/shared/services/notification.service.ts +++ b/src/app/shared/services/notification.service.ts @@ -1,46 +1,54 @@ import { inject, Injectable } from '@angular/core'; -import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; -import { CustomSnackBarComponent } from '@shared/components/custom-snack-bar/custom-snack-bar.component'; -import { ActionRoute, CustomSnackBarModel } from '@shared/components/custom-snack-bar/custom-snack-bar.model'; +import { Router } from '@angular/router'; +import { ExternalToast, toast } from 'ngx-sonner'; + +export type ToastAction = + | { type: 'route'; label: string; link: string } + | { type: 'link'; label: string; link: string } + | { type: 'action'; label: string; onClick: () => void }; + +export type NotificationOptions = Omit & { + action?: ToastAction; +}; @Injectable({ providedIn: 'root' }) export class NotificationService { - private readonly snackBar = inject(MatSnackBar); - - default(message: string, actions?: ActionRoute[]) { - this.show({ - duration: 2000, - panelClass: 'default-notification-overlay', - data: { - message: message, - actions: actions, - }, - }); + private readonly router = inject(Router); + + default(message: string, options?: NotificationOptions) { + toast(message, this.toExternalToast(options)); + } + + success(message: string, options?: NotificationOptions) { + toast.success(message, this.toExternalToast(options)); + } + + info(message: string, options?: NotificationOptions) { + toast.info(message, this.toExternalToast(options)); + } + + warning(message: string, options?: NotificationOptions) { + toast.warning(message, this.toExternalToast(options)); } - success(message: string, actions?: ActionRoute[]) { - this.show({ - duration: 2000, - panelClass: 'success-notification-overlay', - data: { - message: message, - actions: actions, - }, - }); + error(message: string, options?: NotificationOptions) { + toast.error(message, { duration: 6000, ...this.toExternalToast(options) }); } - error(message: string, actions?: ActionRoute[]) { - this.show({ - duration: 6000, - panelClass: 'error-notification-overlay', - data: { - message: message, - actions: actions, - }, - }); + private toExternalToast(options?: NotificationOptions): ExternalToast | undefined { + if (!options) return undefined; + const { action, ...rest } = options; + return { ...rest, action: action ? this.toAction(action) : undefined }; } - private show(configuration: MatSnackBarConfig) { - this.snackBar.openFromComponent(CustomSnackBarComponent, configuration); + private toAction(action: ToastAction): ExternalToast['action'] { + switch (action.type) { + case 'route': + return { label: action.label, onClick: () => this.router.navigateByUrl(action.link) }; + case 'link': + return { label: action.label, onClick: () => window.open(action.link, '_blank') }; + case 'action': + return { label: action.label, onClick: action.onClick }; + } } } From 2e9c0aeaa40ff7ec7e666df5ee4e5d1f1d59e72b Mon Sep 17 00:00:00 2001 From: alexcibotari Date: Thu, 14 May 2026 15:57:56 +0200 Subject: [PATCH 02/18] Migrate user dialog and invite dialog to Spartan UI components, update permissions handling --- .github/copilot-instructions.md | 1 + CLAUDE.md | 1 + docs/frontend-architecture.md | 4 +- docs/spartan-ui-migration.md | 297 ++++++++++++++++++ libs/ui/checkbox/src/lib/hlm-checkbox.ts | 220 ++++++------- package-lock.json | 50 ++- package.json | 4 +- .../user-dialog/user-dialog.component.html | 217 +++---------- .../user-dialog/user-dialog.component.scss | 3 - .../user-dialog/user-dialog.component.ts | 52 ++- .../user-invite-dialog.component.html | 251 +++++---------- .../user-invite-dialog.component.scss | 3 - .../user-invite-dialog.component.ts | 55 ++-- .../features/admin/users/user-permissions.ts | 72 +++++ 14 files changed, 719 insertions(+), 511 deletions(-) create mode 100644 docs/spartan-ui-migration.md create mode 100644 src/app/features/admin/users/user-permissions.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 80fb4305..2785e2b7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -127,6 +127,7 @@ Detailed documentation lives in `docs/`. Read the relevant file when working on | Frontend architecture, routing, libs/ui | [docs/frontend-architecture.md](../docs/frontend-architecture.md) | Any Angular feature work | | NgRx Signal stores, state patterns | [docs/frontend-state.md](../docs/frontend-state.md) | Adding/editing stores or components | | User roles, route guards, UI permissions | [docs/frontend-permissions.md](../docs/frontend-permissions.md) | Auth, guards, user management | +| Spartan UI migration (checkbox, select, notifications) | [docs/spartan-ui-migration.md](../docs/spartan-ui-migration.md) | Migrating Material → Spartan, dialogs, forms | | **Feature modules — Admin** | | | | Admin overview (users, spaces, settings) | [docs/features/admin/overview.md](../docs/features/admin/overview.md) | Any admin feature | | Admin → Users | [docs/features/admin/admin-users.md](../docs/features/admin/admin-users.md) | `features/admin/users/` | diff --git a/CLAUDE.md b/CLAUDE.md index d93f19a3..488f3383 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -123,6 +123,7 @@ Detailed documentation lives in `docs/`. Read the relevant file when working on | Frontend architecture, routing, libs/ui | [docs/frontend-architecture.md](docs/frontend-architecture.md) | Any Angular feature work | | NgRx Signal stores, state patterns | [docs/frontend-state.md](docs/frontend-state.md) | Adding/editing stores or components | | User roles, route guards, UI permissions | [docs/frontend-permissions.md](docs/frontend-permissions.md) | Auth, guards, user management | +| Spartan UI migration (checkbox, select, notifications) | [docs/spartan-ui-migration.md](docs/spartan-ui-migration.md) | Migrating Material → Spartan, dialogs, forms | | **Feature modules — Admin** | | | | Admin overview (users, spaces, settings) | [docs/features/admin/overview.md](docs/features/admin/overview.md) | Any admin feature | | Admin → Users | [docs/features/admin/admin-users.md](docs/features/admin/admin-users.md) | `features/admin/users/` | diff --git a/docs/frontend-architecture.md b/docs/frontend-architecture.md index 1de94b28..dfe3caea 100644 --- a/docs/frontend-architecture.md +++ b/docs/frontend-architecture.md @@ -1,6 +1,6 @@ # Frontend Architecture -> Related: [State Management](frontend-state.md) · [User Roles & Permissions](frontend-permissions.md) · [Concepts](concepts.md) +> Related: [State Management](frontend-state.md) · [User Roles & Permissions](frontend-permissions.md) · [Concepts](concepts.md) · [Spartan UI Migration](spartan-ui-migration.md) ## Tech Stack @@ -8,7 +8,7 @@ |-------|-----------| | Framework | Angular 21 (standalone, zoneless, signals) | | State | NgRx Signals (`@ngrx/signals`) | -| UI Components | Angular Material + custom Spartan/Helm (`libs/ui/`) | +| UI Components | Angular Material (being migrated) + Spartan/Helm (`libs/ui/`) | | Styling | Tailwind CSS 4 + SCSS | | Backend SDK | AngularFire (Firestore, Auth, Storage, Functions, Remote Config) | | Rich Text | TipTap editor | diff --git a/docs/spartan-ui-migration.md b/docs/spartan-ui-migration.md new file mode 100644 index 00000000..6f5e52ba --- /dev/null +++ b/docs/spartan-ui-migration.md @@ -0,0 +1,297 @@ +# Spartan UI Migration Guide + +> This document captures hard-won knowledge from migrating Angular Material components to the Spartan/Helm UI library (`libs/ui/`). Read this before touching any dialog, form, or notification code. + +--- + +## Migration Philosophy + +- **Dialog frame stays Material** — `MatDialogModule` (`mat-dialog-title`, `mat-dialog-content`, `mat-dialog-actions`, `[mat-dialog-close]`) is kept for the dialog container. Only form and interactive elements inside are replaced with Spartan. +- **Spartan components are headless primitives** — they render with `display: contents` or inject host classes. Layout is your responsibility. +- **All components are standalone** — import via `*Imports` barrel constants (e.g. `HlmButtonImports`, `HlmCheckboxImports`). + +--- + +## Component Replacement Table + +| Angular Material | Spartan Equivalent | Import | +|---|---|---| +| `MatButtonModule` | `hlmBtn` directive | `HlmButtonImports` | +| `MatFormFieldModule` | `hlmField` + `hlmFieldLabel` | `HlmFieldImports` | +| `MatInputModule` | `hlmInput` directive | `HlmInputImports` | +| `MatSelectModule` | `hlm-select` + related | `HlmSelectImports` | +| `MatSlideToggleModule` | `hlm-switch` | `HlmSwitchImports` | +| `MatCheckboxModule` / `mat-selection-list` | `hlm-checkbox` | `HlmCheckboxImports` | +| `MatDividerModule` | `hlm-separator` | `HlmSeparatorImports` | +| `MatTooltipModule` | `hlmTooltip` directive | `HlmTooltipImports` | +| `MatSnackBar` | `toast` from `ngx-sonner` | see Notifications section | + +--- + +## `hlm-checkbox` — Layout Pitfalls + +### Host renders as `display: contents` + +`HlmCheckbox` has `host: { class: 'contents peer' }`. The host element disappears from the layout tree; its inner `brn-checkbox` becomes the actual flex item. This is correct and intentional — do not add margin/padding to `hlm-checkbox` itself. + +### ❌ Wrong — description nested inside `