Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make user Alias unique on create - prevent accidentally double … #144

Merged

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,36 @@
</span>

@if (userToUpdate()) {
<p-iconField class="p-float-label min-w-0 grow sm:col-span-2" [iconPosition]="'right'">
@if (isTokenLoading()) {
<p-inputIcon styleClass="i-[mdi--loading] animate-spin" />
} @else if (!tokenVisible()) {
<p-inputIcon
styleClass="i-[mdi--eye-outline] cursor-pointer"
(click)="loadLoginToken()"
></p-inputIcon>
} @else {
<p-inputIcon
styleClass="i-[mdi--content-copy] cursor-pointer"
(click)="copyLoginToken(loginToken())"
></p-inputIcon>
}
<input
pInputText
[id]="id('loginToken')"
[type]="tokenVisible() ? 'text' : 'password'"
[value]="loginToken() ?? 'abcdefghijklmnop'"
[readOnly]="true"
autocomplete="off"
<div class="flex flex-row gap-2 sm:col-span-2">
<p-iconField class="p-float-label min-w-0 grow" [iconPosition]="'right'">
@if (isTokenLoading()) {
<p-inputIcon styleClass="i-[mdi--loading] animate-spin" />
} @else if (!tokenVisible()) {
<p-inputIcon
styleClass="i-[mdi--eye-outline] cursor-pointer"
(click)="loadLoginToken()"
></p-inputIcon>
} @else {
<p-inputIcon
styleClass="i-[mdi--content-copy] cursor-pointer"
(click)="copyLoginToken(loginToken())"
></p-inputIcon>
}
<input
pInputText
[id]="id('loginToken')"
[type]="tokenVisible() ? 'text' : 'password'"
[value]="loginToken() ?? 'abcdefghijklmnop'"
[readOnly]="true"
autocomplete="off"
/>
<label [htmlFor]="id('loginToken')">{{ translations.users_dialog_loginToken() }}</label>
</p-iconField>
<p-button
icon="i-[mdi--book-open-blank-variant-outline]"
(onClick)="openUserWelcomeDialog()"
/>
<label [htmlFor]="id('loginToken')">{{ translations.users_dialog_loginToken() }}</label>
</p-iconField>
</div>
}

<div class="sm:col-span-2">
Expand Down Expand Up @@ -193,4 +199,4 @@ <h4 class="m-0 mb-2">{{ translations.users_dialog_roles_title() }}</h4>
</ng-template>
</p-overlayPanel>

<app-user-created-dialog />
<app-user-welcome-dialog />
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InterpolatePipe } from '@ngneers/signal-translate';
import { interpolate, InterpolatePipe } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import copyToClipboard from 'copy-to-clipboard';
Expand Down Expand Up @@ -41,8 +41,8 @@ import { TranslateService } from '../../../services/translate.service';
import { areArraysEqual } from '../../../utils/array.utils';
import { notNullish } from '../../../utils/common.utils';
import { selectSignal } from '../../../utils/ngrx.utils';
import { UserCreatedDialogComponent } from '../user-created-dialog/user-created-dialog.component';
import { UserItemComponent } from '../user-item/user-item.component';
import { UserWelcomeDialogComponent } from '../user-welcome-dialog/user-welcome-dialog.component';

@Component({
selector: 'app-user-dialog',
Expand All @@ -61,8 +61,8 @@ import { UserItemComponent } from '../user-item/user-item.component';
MessagesModule,
OverlayPanelModule,
ReactiveFormsModule,
UserCreatedDialogComponent,
UserItemComponent,
UserWelcomeDialogComponent,
],
templateUrl: './user-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand All @@ -74,7 +74,7 @@ export class UserDialogComponent {
private readonly _allUsers = selectSignal(userSelectors.selectEntities);
private readonly _randomId = Math.random().toString(36).substring(2, 9);

private readonly _userCreatedDialog = viewChild.required(UserCreatedDialogComponent);
private readonly _userWelcomeDialog = viewChild.required(UserWelcomeDialogComponent);

protected readonly form = this._formBuilder.group({
id: new FormControl<string | null>(null),
Expand Down Expand Up @@ -131,7 +131,7 @@ export class UserDialogComponent {
.subscribe(({ type, response }) => {
this.close();
if (type === addUserAction.success.type) {
this._userCreatedDialog().open(response);
this._userWelcomeDialog().open(response);
}
});
actions$
Expand Down Expand Up @@ -229,6 +229,17 @@ export class UserDialogComponent {
}

protected submit() {
if (Object.values(this._allUsers()).some(x => x?.alias === this.form.value.alias)) {
this._messageService.add({
severity: 'error',
summary: interpolate(this.translations.users_dialog_error_exists(), {
user: this.form.value.alias,
}),
life: 2000,
});
return;
}

if (!this.form.valid) {
this.form.markAllAsTouched();
return;
Expand Down Expand Up @@ -274,6 +285,13 @@ export class UserDialogComponent {
return `${purpose}-${this._randomId}`;
}

protected openUserWelcomeDialog() {
const user = this.userToUpdate();
if (user) {
this._userWelcomeDialog().open(user);
}
}

private getUsersByIds(ids: string[]) {
return ids.map(id => this._allUsers()[id]).filter(notNullish);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<p-dialog
[visible]="visible()"
(visibleChange)="visible.set($event)"
[modal]="true"
[draggable]="false"
[resizable]="false"
>
<ng-template pTemplate="header">
<h2 class="max-w-72 sm:max-w-none">
{{ translations.users_userWelcomeDialog_title() | interpolate: user() }}
</h2>
</ng-template>
@if (isLoading()) {
<div class="flex flex-row justify-center">
<p-progressSpinner class="m-4 self-center" styleClass="h-16 w-16" strokeWidth="4" />
</div>
} @else {
<div class="flex flex-col items-end gap-2">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<p-button
styleClass="w-full"
[icon]="'i-[mdi--text-account]'"
[label]="translations.users_userWelcomeDialog_copyWelcomeMessage()"
(onClick)="copyWelcomeMessage()"
/>
@if (loginToken(); as loginToken) {
<p-button
styleClass="w-full"
[icon]="'i-[mdi--lock]'"
[label]="translations.users_userWelcomeDialog_copyPassword()"
(onClick)="copyPassword(loginToken)"
/>
}
</div>
</div>
}
<ng-template pTemplate="footer">
<p-button
styleClass="w-full"
[icon]="'i-[mdi--check]'"
[label]="translations.shared_done()"
(onClick)="visible.set(false)"
[text]="true"
/>
</ng-template>
</p-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interpolate, InterpolatePipe } from '@ngneers/signal-translate';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import copyToClipboard from 'copy-to-clipboard';
import { MessageService } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { ProgressSpinnerModule } from 'primeng/progressspinner';

import { loadUserLoginTokenAction, selectUserLoginToken } from '../../../+state/users';
import { User } from '../../../models/parsed-models';
import { TranslateService } from '../../../services/translate.service';
import { selectSignal } from '../../../utils/ngrx.utils';

@Component({
selector: 'app-user-welcome-dialog',
standalone: true,
imports: [ButtonModule, CommonModule, DialogModule, InterpolatePipe, ProgressSpinnerModule],
templateUrl: './user-welcome-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserWelcomeDialogComponent {
private readonly _messageService = inject(MessageService);
private readonly _store = inject(Store);
protected readonly translations = inject(TranslateService).translations;

protected readonly visible = signal(false);
protected readonly user = signal<User | undefined>(undefined);
protected readonly isLoading = signal(true);
AnSch1510 marked this conversation as resolved.
Show resolved Hide resolved
protected readonly loginToken = selectSignal(
computed(() => selectUserLoginToken(this.user()?.id))
);

constructor() {
const actions$ = inject(Actions);
actions$.pipe(ofType(loadUserLoginTokenAction.success), takeUntilDestroyed()).subscribe(() => {
this.isLoading.set(false);
});
}

protected loadLoginToken() {
if (this.loginToken()) {
this.isLoading.set(false);
return;
}

const user = this.user();
if (!user) return;

this._store.dispatch(loadUserLoginTokenAction({ userId: user.id }));
}

public open(user: User) {
this.user.set(user);
this.isLoading.set(true);
this.loadLoginToken();
this.visible.set(true);
}

protected copyWelcomeMessage() {
const message = interpolate(this.translations.users_userWelcomeDialog_welcomeMessage(), {
url: (document.head.querySelector('base') as HTMLBaseElement).href,
});
copyToClipboard(message);
this._messageService.add({
severity: 'success',
summary: this.translations.users_userWelcomeDialog_welcomeMessageCopied(),
life: 2000,
});
}

protected copyPassword(password: string) {
copyToClipboard(password);
this._messageService.add({
severity: 'success',
summary: this.translations.users_dialog_loginTokenCopied(),
life: 2000,
});
}
}
7 changes: 4 additions & 3 deletions src/client/src/app/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@
"developer": "Entwickler"
},
"error": {
"save": "Fehler beim Speichern des Benutzers."
"save": "Fehler beim Speichern des Benutzers.",
"exists": "Benutzer \"{{user}}\" existiert bereits"
}
},
"error": {
Expand All @@ -207,8 +208,8 @@
"body": "Inhalt",
"bodyDefault": "Benachrichtigungen funktionieren!!! Wohoo ⛳"
},
"userCreatedDialog": {
"title": "User \"{{alias}}\" created",
"userWelcomeDialog": {
"title": "Willkommen {{alias}}",
"copyWelcomeMessage": "Willkommensnachricht kopieren",
"copyPassword": "Passwort kopieren",
"welcomeMessage": "Hallo Liebe Minigolffreunde!\nDie Anmeldung zum Minigolffreitag läuft ab jetzt über die Web-basierte App, welche du unter:\n{{url}}\naufrufen kannst.\nSpeichere sie dir als Lesezeichen, oder füge sie zu deinem Home-Bildschirm hinzu um jederzeit darauf zugreifen zu können.\nMelde dich dort bitte mit deinem Passwort an und registriere dich selber für deine Spielzeiten.\nBitte schreibe doch trotzdem im Facebook Beitrag, dass du dich registriert hast, damit dort die Konversation aufrecht erhalten wird.\nViel Spaß beim Minigolfen!\nDein Orga-Team",
Expand Down
Loading
Loading