change to seperate value-picker component

This commit is contained in:
rubikscraft 2022-03-28 22:12:06 +02:00
parent bf2fa9e771
commit 87af2c47c0
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
20 changed files with 274 additions and 322 deletions

View file

@ -0,0 +1,39 @@
<mat-form-field class="value-picker" appearance="outline" color="accent">
<mat-label>{{ nameCapMul }}</mat-label>
<mat-chip-list #chipList>
<mat-chip
*ngFor="let item of this.myControl.value"
[removable]="!isDisabled(item)"
[disabled]="isDisabled(item)"
(removed)="removeItem(item)"
>
{{ item }}
<button *ngIf="!isDisabled(item)" matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
<input
placeholder="Add {{name}}..."
#fruitInput
[formControl]="inputControl"
[value]="inputControl.value"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addItemInput($event)"
autocorrect="off"
autocapitalize="none"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="addItemSelect($event)"
>
<mat-option
*ngFor="let item of selectable | async"
[value]="item"
>
{{ item }}
</mat-option>
</mat-autocomplete>
</mat-form-field>

View file

@ -0,0 +1,7 @@
mat-form-field {
width: 100%;
}
:host {
display: block;
}

View file

@ -0,0 +1,108 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import Fuse from 'fuse.js';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { BehaviorSubject } from 'rxjs';
import { Required } from 'src/app/models/decorators/required.decorator';
@Component({
selector: 'values-picker',
templateUrl: './values-picker.component.html',
styleUrls: ['./values-picker.component.scss'],
})
export class ValuesPickerComponent implements OnInit {
// Static data
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
// Ui niceties
@Input('name') @Required name: string = '';
public get nameCap(): string {
return this.name.charAt(0).toUpperCase() + this.name.slice(1);
}
public get nameCapMul(): string {
return `${this.nameCap}s`;
}
// Inputs/oututs
@Input('selection-list') @Required fullSelection: string[] = [];
@Input('control') @Required myControl: FormControl;
@Input('disabled-list') disabledSelection: string[] = [];
// Selection
private selectableSubject = new BehaviorSubject<string[]>([]);
public selectable = this.selectableSubject.asObservable();
public inputControl = new FormControl('');
public ngOnInit(): void {
this.subscribeInputValue();
this.subscribeMyValue();
}
public isDisabled(value: string): boolean {
return this.disabledSelection.includes(value);
}
// Remove/add
public removeItem(item: string) {
const selected: string[] = this.myControl.value;
const newSelection = selected.filter((s) => s !== item);
this.myControl.setValue(newSelection);
}
public addItemInput(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.addItem(value);
}
public addItemSelect(event: MatAutocompleteSelectedEvent): void {
this.addItem(event.option.viewValue);
}
private addItem(value: string) {
console.log('adding', value);
const selectable = this.selectableSubject.getValue();
if (this.isDisabled(value) || !selectable.includes(value)) return;
const selected: string[] = this.myControl.value;
this.myControl.setValue([...selected, value]);
this.inputControl.setValue('');
}
// Update and subscribe
private updateSelectable() {
const selected: string[] = this.myControl.value;
const available = this.fullSelection.filter(
(s) => !this.isDisabled(s) && !selected.includes(s)
);
const searchValue = this.inputControl.value;
if (searchValue && available.length > 0) {
const fuse = new Fuse(available);
const result = fuse.search(searchValue).map((r) => r.item);
this.selectableSubject.next(result);
} else {
this.selectableSubject.next(available);
}
}
@AutoUnsubscribe()
private subscribeInputValue() {
return this.inputControl.valueChanges.subscribe((value) => {
this.updateSelectable();
});
}
@AutoUnsubscribe()
private subscribeMyValue() {
return this.myControl.valueChanges.subscribe((value) => {
this.updateSelectable();
});
}
}

View file

@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { ValuesPickerComponent } from './values-picker.component';
@NgModule({
declarations: [ValuesPickerComponent],
imports: [
CommonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatChipsModule,
MatAutocompleteModule,
FormsModule,
ReactiveFormsModule,
],
exports: [ValuesPickerComponent],
})
export class ValuesPickerModule {}

View file

@ -0,0 +1,15 @@
export function Required(target: object, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
throw new Error(`Attribute ${propertyKey} is required`);
},
set(value) {
Object.defineProperty(target, propertyKey, {
value,
writable: true,
configurable: true,
});
},
configurable: true,
});
}

View file

@ -3,13 +3,18 @@ import { Route } from '@angular/router';
export type PRouteData = { export type PRouteData = {
page?: { page?: {
// This is not the tab-title, but the title in the sidenav
title?: string; title?: string;
// This is not the favicon, but the icon in the sidenav
icon?: string; icon?: string;
category?: string; category?: string;
}; };
permissions?: string[]; permissions?: string[];
noContainer?: boolean; noContainer?: boolean;
sidebar?: ComponentType<unknown>; sidebar?: ComponentType<unknown>;
// This is not meant to be set by the user, but by a resolver service
// It just cant be stored anywhere else
_sidebar_portal?: Portal<unknown>; _sidebar_portal?: Portal<unknown>;
}; };

View file

@ -20,6 +20,7 @@ export class LoginControl {
return CreatePasswordError(this.password.errors); return CreatePasswordError(this.password.errors);
} }
// This getter firstly verifies the form, RawData does not
public getData(): Failable<UserPassModel> { public getData(): Failable<UserPassModel> {
if (this.username.errors || this.password.errors) if (this.username.errors || this.password.errors)
return Fail('Invalid username or password'); return Fail('Invalid username or password');

View file

@ -3,7 +3,10 @@ import { Fail, Failable } from 'picsur-shared/dist/types';
import { UserPassModel } from '../forms-dto/userpass.dto'; import { UserPassModel } from '../forms-dto/userpass.dto';
import { Compare } from '../validators/compare.validator'; import { Compare } from '../validators/compare.validator';
import { import {
CreatePasswordError, CreateUsernameError, PasswordValidators, UsernameValidators CreatePasswordError,
CreateUsernameError,
PasswordValidators,
UsernameValidators
} from '../validators/user.validator'; } from '../validators/user.validator';
export class RegisterControl { export class RegisterControl {
@ -26,6 +29,7 @@ export class RegisterControl {
return CreatePasswordError(this.passwordConfirm.errors); return CreatePasswordError(this.passwordConfirm.errors);
} }
// This getter firstly verifies the form, RawData does not
public getData(): Failable<UserPassModel> { public getData(): Failable<UserPassModel> {
if ( if (
this.username.errors || this.username.errors ||

View file

@ -1,24 +1,11 @@
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { BehaviorSubject, Subscription } from 'rxjs';
import { RoleModel } from '../forms-dto/role.dto'; import { RoleModel } from '../forms-dto/role.dto';
import { RoleNameValidators } from '../validators/role.validator'; import { RoleNameValidators } from '../validators/role.validator';
import { CreateUsernameError } from '../validators/user.validator'; import { CreateUsernameError } from '../validators/user.validator';
export class UpdateRoleControl { export class UpdateRoleControl {
// Set once
private permissions: string[] = [];
// Variables
private selectablePermissionsSubject = new BehaviorSubject<string[]>([]);
private permissionsInputSubscription: null | Subscription;
public rolename = new FormControl('', RoleNameValidators); public rolename = new FormControl('', RoleNameValidators);
public permissions = new FormControl([]);
public permissionControl = new FormControl('', []);
public selectablePermissions =
this.selectablePermissionsSubject.asObservable();
public selectedPermissions: string[] = [];
public get rolenameValue() { public get rolenameValue() {
return this.rolename.value; return this.rolename.value;
@ -28,48 +15,18 @@ export class UpdateRoleControl {
return CreateUsernameError(this.rolename.errors); return CreateUsernameError(this.rolename.errors);
} }
constructor() { public get selectedPermissions() {
this.permissionsInputSubscription = return this.permissions.value;
this.permissionControl.valueChanges.subscribe((roles) => {
this.updateSelectablePermissions();
});
}
public destroy() {
if (this.permissionsInputSubscription) {
this.permissionsInputSubscription.unsubscribe();
this.permissionsInputSubscription = null;
}
}
public addPermission(permission: string) {
if (!this.selectablePermissionsSubject.value.includes(permission)) return;
this.selectedPermissions.push(permission);
this.clearInput();
}
public removePermission(permission: string) {
this.selectedPermissions = this.selectedPermissions.filter(
(r) => r !== permission
);
this.updateSelectablePermissions();
} }
// Data interaction // Data interaction
public putAllPermissions(permissions: string[]) {
this.permissions = permissions;
this.updateSelectablePermissions();
}
public putRoleName(rolename: string) { public putRoleName(rolename: string) {
this.rolename.setValue(rolename); this.rolename.setValue(rolename);
} }
public putPermissions(permissions: string[]) { public putPermissions(permissions: string[]) {
this.selectedPermissions = permissions; this.permissions.setValue(permissions);
this.updateSelectablePermissions();
} }
public getData(): RoleModel { public getData(): RoleModel {
@ -78,28 +35,4 @@ export class UpdateRoleControl {
permissions: this.selectedPermissions, permissions: this.selectedPermissions,
}; };
} }
// Logic
private updateSelectablePermissions() {
const availablePermissins = this.permissions.filter(
(r) => !this.selectedPermissions.includes(r)
);
const searchValue = this.permissionControl.value;
if (searchValue && availablePermissins.length > 0) {
const fuse = new Fuse(availablePermissins);
const result = fuse
.search(this.permissionControl.value ?? '')
.map((r) => r.item);
this.selectablePermissionsSubject.next(result);
} else {
this.selectablePermissionsSubject.next(availablePermissins);
}
}
private clearInput() {
this.permissionControl.setValue('');
}
} }

View file

@ -1,7 +1,4 @@
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { BehaviorSubject, Subscription } from 'rxjs';
import { FullUserModel } from '../forms-dto/fulluser.dto'; import { FullUserModel } from '../forms-dto/fulluser.dto';
import { import {
CreatePasswordError, CreatePasswordError,
@ -11,23 +8,9 @@ import {
} from '../validators/user.validator'; } from '../validators/user.validator';
export class UpdateUserControl { export class UpdateUserControl {
// Special roles
private SoulBoundRolesList: string[] = [];
// Set once
private fullRoles: ERole[] = [];
private roles: string[] = [];
// Variables
private selectableRolesSubject = new BehaviorSubject<string[]>([]);
private rolesInputSubscription: null | Subscription;
public username = new FormControl('', UsernameValidators); public username = new FormControl('', UsernameValidators);
public password = new FormControl('', PasswordValidators); public password = new FormControl('', PasswordValidators);
public roles = new FormControl([]);
public rolesControl = new FormControl('', []);
public selectableRoles = this.selectableRolesSubject.asObservable();
public selectedRoles: string[] = [];
public get usernameValue() { public get usernameValue() {
return this.username.value; return this.username.value;
@ -41,74 +24,18 @@ export class UpdateUserControl {
return CreatePasswordError(this.password.errors); return CreatePasswordError(this.password.errors);
} }
constructor() { public get selectedRoles(): string[] {
this.rolesInputSubscription = this.rolesControl.valueChanges.subscribe( return this.roles.value;
(roles) => {
this.updateSelectableRoles();
}
);
}
public destroy() {
if (this.rolesInputSubscription) {
this.rolesInputSubscription.unsubscribe();
this.rolesInputSubscription = null;
}
}
public addRole(role: string) {
if (!this.selectableRolesSubject.value.includes(role)) return;
this.selectedRoles.push(role);
this.clearInput();
}
public removeRole(role: string) {
this.selectedRoles = this.selectedRoles.filter((r) => r !== role);
this.updateSelectableRoles();
}
public isRemovable(role: string) {
if (this.SoulBoundRolesList.includes(role)) return false;
return true;
}
public getEffectivePermissions(): string[] {
const permissions: string[] = [];
for (const role of this.selectedRoles) {
const fullRole = this.fullRoles.find((r) => r.name === role);
if (!fullRole) {
console.warn(`Role ${role} not found`);
continue;
}
permissions.push(
...fullRole.permissions.filter((p) => !permissions.includes(p))
);
}
return permissions;
} }
// Data interaction // Data interaction
public putAllRoles(roles: ERole[]) {
this.fullRoles = roles;
this.roles = roles.map((role) => role.name);
this.updateSelectableRoles();
}
public putUsername(username: string) { public putUsername(username: string) {
this.username.setValue(username); this.username.setValue(username);
} }
public putRoles(roles: string[]) { public putRoles(roles: string[]) {
this.selectedRoles = roles; this.roles.setValue(roles);
this.updateSelectableRoles();
}
public putSoulBoundRoles(roles: string[]) {
this.SoulBoundRolesList = roles;
} }
public getData(): FullUserModel { public getData(): FullUserModel {
@ -118,30 +45,4 @@ export class UpdateUserControl {
roles: this.selectedRoles, roles: this.selectedRoles,
}; };
} }
// Logic
private updateSelectableRoles() {
const availableRoles = this.roles.filter(
// Not available if either already selected, or the role is not addable/removable
(r) =>
!(this.selectedRoles.includes(r) || this.SoulBoundRolesList.includes(r))
);
const searchValue = this.rolesControl.value;
if (searchValue && availableRoles.length > 0) {
const fuse = new Fuse(availableRoles);
const result = fuse
.search(this.rolesControl.value ?? '')
.map((r) => r.item);
this.selectableRolesSubject.next(result);
} else {
this.selectableRolesSubject.next(availableRoles);
}
}
private clearInput() {
this.rolesControl.setValue('');
}
} }

View file

@ -1,2 +1 @@
<h1>Settings</h1> <h1>Settings</h1>

View file

@ -6,7 +6,5 @@ import { Component, OnInit } from '@angular/core';
export class SettingsGeneralComponent implements OnInit { export class SettingsGeneralComponent implements OnInit {
constructor() {} constructor() {}
ngOnInit(): void { ngOnInit(): void {}
}
} }

View file

@ -28,43 +28,11 @@
<div class="row"> <div class="row">
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<mat-form-field appearance="outline" color="accent"> <values-picker
<mat-label>Permissions</mat-label> name="permission"
<mat-chip-list #chipList aria-label="Permissions Selection"> [control]="model.permissions"
<mat-chip [selection-list]="allPermissions"
*ngFor="let permission of model.selectedPermissions" ></values-picker>
(removed)="removePermission(permission)"
>
{{ uiFriendlyPermission(permission) }}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
<input
placeholder="Add permission..."
#fruitInput
[formControl]="model.permissionControl"
[value]="model.permissionControl.value"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addPermission($event)"
autocorrect="off"
autocapitalize="none"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="selectedPermission($event)"
>
<mat-option
*ngFor="let permission of model.selectablePermissions | async"
[value]="permission"
>
{{ permission }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div> </div>
</div> </div>
@ -86,3 +54,5 @@
</div> </div>
</div> </div>
</form> </form>
<div class="value-picker"></div>

View file

@ -1,3 +1,3 @@
mat-form-field { values-picker {
width: 100%; width: 100%;
} }

View file

@ -1,12 +1,7 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
import { SnackBarType } from "src/app/models/dto/snack-bar-type.dto";
import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control'; import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control';
import { PermissionService } from 'src/app/services/api/permission.service'; import { PermissionService } from 'src/app/services/api/permission.service';
import { RolesService } from 'src/app/services/api/roles.service'; import { RolesService } from 'src/app/services/api/roles.service';
@ -23,11 +18,10 @@ enum EditMode {
styleUrls: ['./settings-roles-edit.component.scss'], styleUrls: ['./settings-roles-edit.component.scss'],
}) })
export class SettingsRolesEditComponent implements OnInit { export class SettingsRolesEditComponent implements OnInit {
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
private mode: EditMode = EditMode.edit; private mode: EditMode = EditMode.edit;
model = new UpdateRoleControl(); model = new UpdateRoleControl();
allPermissions: string[] = [];
get adding() { get adding() {
return this.mode === EditMode.add; return this.mode === EditMode.add;
@ -49,26 +43,29 @@ export class SettingsRolesEditComponent implements OnInit {
} }
private async initRole() { private async initRole() {
// Check if adding or editing
const rolename = this.route.snapshot.paramMap.get('role'); const rolename = this.route.snapshot.paramMap.get('role');
if (!rolename) { if (!rolename) {
this.mode = EditMode.add; this.mode = EditMode.add;
return; return;
} }
// Set data thats already known
this.mode = EditMode.edit; this.mode = EditMode.edit;
this.model.putRoleName(rolename); this.model.putRoleName(rolename);
// Fetch data and populate form
const role = await this.rolesService.getRole(rolename); const role = await this.rolesService.getRole(rolename);
if (HasFailed(role)) { if (HasFailed(role)) {
this.utilService.showSnackBar('Failed to get role', SnackBarType.Error); this.utilService.showSnackBar('Failed to get role', SnackBarType.Error);
return; return;
} }
this.model.putRoleName(role.name); this.model.putRoleName(role.name);
this.model.putPermissions(role.permissions); this.model.putPermissions(role.permissions);
} }
private async initPermissions() { private async initPermissions() {
// Get a list of all permissions so that we can select them
const allPermissions = await this.permissionsService.fetchAllPermission(); const allPermissions = await this.permissionsService.fetchAllPermission();
if (HasFailed(allPermissions)) { if (HasFailed(allPermissions)) {
this.utilService.showSnackBar( this.utilService.showSnackBar(
@ -78,30 +75,13 @@ export class SettingsRolesEditComponent implements OnInit {
return; return;
} }
this.model.putAllPermissions(allPermissions); this.allPermissions = allPermissions;
}
removePermission(permission: string) {
this.model.removePermission(permission);
}
addPermission(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.model.addPermission(value);
}
selectedPermission(event: MatAutocompleteSelectedEvent): void {
this.model.addPermission(event.option.viewValue);
} }
cancel() { cancel() {
this.router.navigate(['/settings/roles']); this.router.navigate(['/settings/roles']);
} }
uiFriendlyPermission(permission: string) {
return UIFriendlyPermissions[permission as Permission] ?? permission;
}
async updateUser() { async updateUser() {
const data = this.model.getData(); const data = this.model.getData();

View file

@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@ -9,6 +8,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ValuesPickerModule } from 'src/app/components/values-picker/values-picker.module';
import { SettingsRolesEditComponent } from './settings-roles-edit/settings-roles-edit.component'; import { SettingsRolesEditComponent } from './settings-roles-edit/settings-roles-edit.component';
import { SettingsRolesComponent } from './settings-roles.component'; import { SettingsRolesComponent } from './settings-roles.component';
import { SettingsRolesRoutingModule } from './settings-roles.routing.module'; import { SettingsRolesRoutingModule } from './settings-roles.routing.module';
@ -27,7 +27,7 @@ import { SettingsRolesRoutingModule } from './settings-roles.routing.module';
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
ReactiveFormsModule, ReactiveFormsModule,
MatAutocompleteModule ValuesPickerModule
], ],
}) })
export class SettingsRolesRouteModule {} export class SettingsRolesRouteModule {}

View file

@ -46,45 +46,12 @@
<div class="row" *ngIf="!isLockedPerms()"> <div class="row" *ngIf="!isLockedPerms()">
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<mat-form-field appearance="outline" color="accent"> <values-picker
<mat-label>Roles</mat-label> name="role"
<mat-chip-list #chipList aria-label="Roles Selection"> [control]="model.roles"
<mat-chip [disabled-list]="soulBoundRoles"
*ngFor="let role of model.selectedRoles" [selection-list]="allRoles"
[removable]="model.isRemovable(role)" ></values-picker>
[disabled]="!model.isRemovable(role)"
(removed)="removeRole(role)"
>
{{ role }}
<button *ngIf="model.isRemovable(role)" matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
<input
placeholder="Add role..."
#fruitInput
[formControl]="model.rolesControl"
[value]="model.rolesControl.value"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRole($event)"
autocorrect="off"
autocapitalize="none"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="selectedRole($event)"
>
<mat-option
*ngFor="let role of model.selectableRoles | async"
[value]="role"
>
{{ role }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div> </div>
</div> </div>

View file

@ -1,3 +1,3 @@
mat-form-field { values-picker {
width: 100%; width: 100%;
} }

View file

@ -1,12 +1,10 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n';
import { SnackBarType } from "src/app/models/dto/snack-bar-type.dto"; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
import { UpdateUserControl } from 'src/app/models/forms/updateuser.control'; import { UpdateUserControl } from 'src/app/models/forms/updateuser.control';
import { RolesService } from 'src/app/services/api/roles.service'; import { RolesService } from 'src/app/services/api/roles.service';
import { UserManageService } from 'src/app/services/api/usermanage.service'; import { UserManageService } from 'src/app/services/api/usermanage.service';
@ -24,12 +22,14 @@ enum EditMode {
}) })
export class SettingsUsersEditComponent implements OnInit { export class SettingsUsersEditComponent implements OnInit {
private ImmutableUsersList: string[] = []; private ImmutableUsersList: string[] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
private mode: EditMode = EditMode.edit; private mode: EditMode = EditMode.edit;
model = new UpdateUserControl(); model = new UpdateUserControl();
allFullRoles: ERole[] = [];
get allRoles(): string[] {
return this.allFullRoles.map((role) => role.name);
}
soulBoundRoles: string[] = [];
get adding() { get adding() {
return this.mode === EditMode.add; return this.mode === EditMode.add;
@ -53,14 +53,21 @@ export class SettingsUsersEditComponent implements OnInit {
private async initUser() { private async initUser() {
const username = this.route.snapshot.paramMap.get('username'); const username = this.route.snapshot.paramMap.get('username');
const { DefaultRoles, SoulBoundRoles } = const SpecialRoles = await this.rolesService.getSpecialRoles();
await this.rolesService.getSpecialRolesOptimistic(); if (HasFailed(SpecialRoles)) {
this.model.putSoulBoundRoles(SoulBoundRoles); this.utilService.showSnackBar(
'Failed to get special roles',
SnackBarType.Error
);
return;
}
this.soulBoundRoles = SpecialRoles.SoulBoundRoles;
if (!username) { if (!username) {
this.mode = EditMode.add; this.mode = EditMode.add;
this.model.putRoles(DefaultRoles); this.model.putRoles(SpecialRoles.DefaultRoles);
return; return;
} }
@ -88,29 +95,25 @@ export class SettingsUsersEditComponent implements OnInit {
return; return;
} }
this.model.putAllRoles(roles); this.allFullRoles = roles;
} }
getEffectivePermissions() { public getEffectivePermissions(): string[] {
return this.model const permissions: string[] = [];
.getEffectivePermissions()
.map( for (const role of this.model.selectedRoles) {
(permission) => const fullRole = this.allFullRoles.find((r) => r.name === role);
UIFriendlyPermissions[permission as Permission] ?? permission if (!fullRole) {
console.warn(`Role ${role} not found`);
continue;
}
permissions.push(
...fullRole.permissions.filter((p) => !permissions.includes(p))
); );
} }
removeRole(role: string) { return permissions.map((p) => UIFriendlyPermissions[p as Permission] ?? p);
this.model.removeRole(role);
}
addRole(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.model.addRole(value);
}
selectedRole(event: MatAutocompleteSelectedEvent): void {
this.model.addRole(event.option.viewValue);
} }
cancel() { cancel() {

View file

@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@ -9,15 +8,13 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ValuesPickerModule } from 'src/app/components/values-picker/values-picker.module';
import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component'; import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component';
import { SettingsUsersComponent } from './settings-users.component'; import { SettingsUsersComponent } from './settings-users.component';
import { SettingsUsersRoutingModule } from './settings-users.routing.module'; import { SettingsUsersRoutingModule } from './settings-users.routing.module';
@NgModule({ @NgModule({
declarations: [ declarations: [SettingsUsersComponent, SettingsUsersEditComponent],
SettingsUsersComponent,
SettingsUsersEditComponent,
],
imports: [ imports: [
CommonModule, CommonModule,
SettingsUsersRoutingModule, SettingsUsersRoutingModule,
@ -28,9 +25,9 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module';
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatChipsModule, MatChipsModule,
MatAutocompleteModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ValuesPickerModule,
], ],
}) })
export class SettingsUsersRouteModule {} export class SettingsUsersRouteModule {}