change to seperate value-picker component
This commit is contained in:
parent
bf2fa9e771
commit
87af2c47c0
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
15
frontend/src/app/models/decorators/required.decorator.ts
Normal file
15
frontend/src/app/models/decorators/required.decorator.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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 ||
|
||||||
|
|
|
@ -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('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
mat-form-field {
|
values-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
mat-form-field {
|
values-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
Loading…
Reference in a new issue