User management updates
This commit is contained in:
parent
fe57bba05e
commit
7289c8675e
@ -30,6 +30,7 @@ import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-scr
|
||||
import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
|
||||
import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component';
|
||||
import { ConfirmDeleteUserDialogComponent } from './dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component';
|
||||
import { UsersStatsComponent } from './components/users-stats/users-stats.component';
|
||||
|
||||
const dialogs = [
|
||||
AddEditRuleSetDialogComponent,
|
||||
@ -63,6 +64,7 @@ const components = [
|
||||
TabsComponent,
|
||||
ComboChartComponent,
|
||||
ComboSeriesVerticalComponent,
|
||||
UsersStatsComponent,
|
||||
...dialogs,
|
||||
...screens
|
||||
];
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
<div class="collapsed-wrapper">
|
||||
<redaction-circle-button (action)="toggleCollapse.emit()" icon="red:expand" tooltip="user-stats.expand" tooltipPosition="before"></redaction-circle-button>
|
||||
<div class="all-caps-label" translate="user-stats.title"></div>
|
||||
</div>
|
||||
|
||||
<div class="header-wrapper mt-8">
|
||||
<div class="heading-xl flex-1" translate="user-stats.title"></div>
|
||||
<redaction-circle-button
|
||||
(action)="toggleCollapse.emit()"
|
||||
icon="red:collapse"
|
||||
tooltip="user-stats.collapse"
|
||||
tooltipPosition="before"
|
||||
></redaction-circle-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-44">
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="chartData"
|
||||
[strokeWidth]="15"
|
||||
[radius]="63"
|
||||
[subtitle]="'user-stats.chart.users'"
|
||||
totalType="sum"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
@ -0,0 +1,19 @@
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
.heading-xl {
|
||||
max-width: 88%;
|
||||
}
|
||||
|
||||
redaction-circle-button {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 270px;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-44 {
|
||||
margin-top: 44px;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-users-stats',
|
||||
templateUrl: './users-stats.component.html',
|
||||
styleUrls: ['./users-stats.component.scss']
|
||||
})
|
||||
export class UsersStatsComponent implements OnInit {
|
||||
@Output() public toggleCollapse = new EventEmitter();
|
||||
@Input() chartData: DoughnutChartConfig[];
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
@ -28,13 +28,7 @@
|
||||
<div class="red-input-group">
|
||||
<label translate="add-edit-user.form.role"></label>
|
||||
<div class="roles-wrapper">
|
||||
<mat-checkbox
|
||||
*ngFor="let role of ROLES"
|
||||
color="primary"
|
||||
[checked]="getChecked(role)"
|
||||
[disabled]="getDisabled(role)"
|
||||
(change)="changeRoleValue(role, $event)"
|
||||
>
|
||||
<mat-checkbox [formControlName]="role" *ngFor="let role of ROLES" color="primary">
|
||||
{{ 'roles.' + role | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { User, UserControllerService } from '@redaction/red-ui-http';
|
||||
import { User } from '@redaction/red-ui-http';
|
||||
import { UserService } from '../../../../services/user.service';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { AdminDialogService } from '../../services/admin-dialog-service.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-add-edit-user-dialog',
|
||||
@ -17,19 +15,39 @@ export class AddEditUserDialogComponent {
|
||||
|
||||
constructor(
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _userControllerService: UserControllerService,
|
||||
private readonly _userService: UserService,
|
||||
public dialogRef: MatDialogRef<AddEditUserDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public user: User
|
||||
) {
|
||||
const rolesControls = this.ROLES.reduce(
|
||||
(prev, role) => ({
|
||||
...prev,
|
||||
[role]: [this.user && this.user.roles.indexOf(role) !== -1]
|
||||
}),
|
||||
{}
|
||||
);
|
||||
this.userForm = this._formBuilder.group({
|
||||
firstName: [this.user?.firstName, Validators.required],
|
||||
lastName: [this.user?.lastName, Validators.required],
|
||||
email: [{ value: this.user?.email, disabled: !!user }, [Validators.required, Validators.email]],
|
||||
// password: [this.user?.password, Validators.required],
|
||||
admin: [user && this._userService.isAdmin(user)],
|
||||
roles: [user?.roles || []]
|
||||
...rolesControls
|
||||
});
|
||||
this._setRolesRequirements();
|
||||
}
|
||||
|
||||
private _setRolesRequirements() {
|
||||
const requirements = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
|
||||
for (const key of Object.keys(requirements)) {
|
||||
this.userForm.controls[key].valueChanges.subscribe((checked) => {
|
||||
if (checked) {
|
||||
this.userForm.patchValue({ [requirements[key]]: true });
|
||||
this.userForm.controls[requirements[key]].disable();
|
||||
} else {
|
||||
this.userForm.controls[requirements[key]].enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get changed(): boolean {
|
||||
@ -44,79 +62,23 @@ export class AddEditUserDialogComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _updateProfile() {
|
||||
const profileKeys = ['firstName', 'lastName'];
|
||||
let profileChanged = false;
|
||||
for (const key of profileKeys) {
|
||||
if (this.userForm.get(key).value !== this.user[key]) {
|
||||
profileChanged = true;
|
||||
}
|
||||
}
|
||||
if (!profileChanged) {
|
||||
return;
|
||||
}
|
||||
await this._userControllerService.updateProfile(this.userForm.getRawValue()).toPromise();
|
||||
}
|
||||
|
||||
private async _updateRoles() {
|
||||
const newAdminValue = this.userForm.get('admin').value;
|
||||
if (newAdminValue === this._userService.isAdmin(this.user)) {
|
||||
return;
|
||||
}
|
||||
let roles = [...this.user.roles];
|
||||
if (newAdminValue) {
|
||||
roles.push('RED_ADMIN');
|
||||
} else {
|
||||
roles = roles.filter((role) => role !== 'RED_ADMIN');
|
||||
}
|
||||
await this._userControllerService.addRoleToUsers(roles, this.user.userId).toPromise();
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
await this._updateProfile();
|
||||
await this._updateRoles();
|
||||
}
|
||||
|
||||
private async _create() {
|
||||
const roles = ['RED_USER'];
|
||||
if (this.userForm.get('admin').value) {
|
||||
roles.push('RED_ADMIN');
|
||||
}
|
||||
await this._userControllerService.createUser({ ...this.userForm.getRawValue(), roles: [] }, 'body').toPromise();
|
||||
}
|
||||
|
||||
public async save() {
|
||||
if (this.user) {
|
||||
await this._update();
|
||||
} else {
|
||||
await this._create();
|
||||
}
|
||||
this.dialogRef.close(true);
|
||||
this.dialogRef.close({
|
||||
action: this.user ? 'UPDATE' : 'CREATE',
|
||||
user: { ...this.userForm.getRawValue(), roles: this.activeRoles }
|
||||
});
|
||||
}
|
||||
|
||||
public async delete() {
|
||||
this.dialogRef.close('DELETE');
|
||||
}
|
||||
|
||||
public changeRoleValue(role: string, { checked }: MatCheckboxChange) {
|
||||
const roles = [...this.activeRoles];
|
||||
if (checked) {
|
||||
roles.push(role);
|
||||
} else {
|
||||
roles.splice(roles.indexOf(role), 1);
|
||||
}
|
||||
this.userForm.patchValue({ roles });
|
||||
}
|
||||
|
||||
public get activeRoles(): string[] {
|
||||
return this.userForm.get('roles').value;
|
||||
}
|
||||
|
||||
public getChecked(role: string) {
|
||||
return this.activeRoles.indexOf(role) !== -1;
|
||||
}
|
||||
|
||||
public getDisabled(role: string) {
|
||||
return this.getChecked(role) && role === 'RED_USER' && this.getChecked('RED_ADMIN');
|
||||
return this.ROLES.reduce((acc, role) => {
|
||||
if (this.userForm.get(role).value) {
|
||||
acc.push(role);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="heading" translate="confirm-delete-user.warning"></div>
|
||||
|
||||
<mat-checkbox *ngFor="let checkbox of checkboxes; let idx = index" [(ngModel)]="checkbox.value" color="primary">
|
||||
{{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { count: 20 } }}
|
||||
{{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { User, UserControllerService } from '@redaction/red-ui-http';
|
||||
import { User } from '@redaction/red-ui-http';
|
||||
import { AppStateService } from '../../../../state/app-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-confirm-delete-user-dialog',
|
||||
@ -10,18 +11,20 @@ import { User, UserControllerService } from '@redaction/red-ui-http';
|
||||
export class ConfirmDeleteUserDialogComponent implements OnInit {
|
||||
public checkboxes = [{ value: false }, { value: false }];
|
||||
public showToast = false;
|
||||
public projectsCount: number;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public user: User,
|
||||
private readonly _userControllerService: UserControllerService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
public dialogRef: MatDialogRef<ConfirmDeleteUserDialogComponent>
|
||||
) {}
|
||||
) {
|
||||
this.projectsCount = this._appStateService.allProjects.filter((pw) => pw.memberIds.indexOf(user.userId) !== -1).length;
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
async deleteUser() {
|
||||
if (this.valid) {
|
||||
await this._userControllerService.deleteUser(this.user.userId).toPromise();
|
||||
this.dialogRef.close(true);
|
||||
} else {
|
||||
this.showToast = true;
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
|
||||
<div class="red-content-inner">
|
||||
<div class="left-container">
|
||||
<div class="left-container" [class.extended]="collapsedDetails">
|
||||
<div class="header-item">
|
||||
<span class="all-caps-label">
|
||||
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }}
|
||||
@ -78,7 +78,9 @@
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
|
||||
<div class="right-container"></div>
|
||||
<div class="right-container" redactionHasScrollbar [class.collapsed]="collapsedDetails">
|
||||
<redaction-users-stats (toggleCollapse)="toggleCollapsedDetails()" [chartData]="chartData"></redaction-users-stats>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -28,6 +28,15 @@
|
||||
display: flex;
|
||||
width: 353px;
|
||||
min-width: 353px;
|
||||
padding: 16px 16px 16px 24px;
|
||||
|
||||
&.has-scrollbar:hover {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
redaction-users-stats {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header .actions {
|
||||
|
||||
@ -6,6 +6,8 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { debounce } from '../../../../utils/debounce';
|
||||
import { AdminDialogService } from '../../services/admin-dialog-service.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { TranslateChartService } from '../../../../services/translate-chart.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-user-listing-screen',
|
||||
@ -14,7 +16,8 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
})
|
||||
export class UserListingScreenComponent implements OnInit {
|
||||
public viewReady = false;
|
||||
|
||||
public collapsedDetails = false;
|
||||
public chartData: DoughnutChartConfig[];
|
||||
public users: User[];
|
||||
public displayedUsers: User[] = [];
|
||||
public searchForm: FormGroup;
|
||||
@ -25,7 +28,8 @@ export class UserListingScreenComponent implements OnInit {
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _adminDialogService: AdminDialogService,
|
||||
private readonly _userControllerService: UserControllerService
|
||||
private readonly _userControllerService: UserControllerService,
|
||||
private readonly _translateChartService: TranslateChartService
|
||||
) {
|
||||
this.searchForm = this._formBuilder.group({
|
||||
query: ['']
|
||||
@ -46,10 +50,16 @@ export class UserListingScreenComponent implements OnInit {
|
||||
|
||||
public openAddEditUserDialog($event: MouseEvent, user?: User) {
|
||||
$event.stopPropagation();
|
||||
this._adminDialogService.openAddEditUserDialog(user, async (action) => {
|
||||
if (action === 'DELETE') {
|
||||
this._adminDialogService.openAddEditUserDialog(user, async (result) => {
|
||||
if (result === 'DELETE') {
|
||||
this.openDeleteUserDialog(user);
|
||||
} else {
|
||||
this.viewReady = false;
|
||||
if (result.action === 'CREATE') {
|
||||
await this._userControllerService.createUser(result.user).toPromise();
|
||||
} else if (result.action === 'UPDATE') {
|
||||
await this._userControllerService.updateProfile(result.user).toPromise();
|
||||
}
|
||||
await this._loadData();
|
||||
}
|
||||
});
|
||||
@ -58,6 +68,8 @@ export class UserListingScreenComponent implements OnInit {
|
||||
public openDeleteUserDialog(user: User, $event?: MouseEvent) {
|
||||
$event?.stopPropagation();
|
||||
this._adminDialogService.openConfirmDeleteUserDialog(user, async () => {
|
||||
this.viewReady = false;
|
||||
await this._userControllerService.deleteUser(user.userId).toPromise();
|
||||
await this._loadData();
|
||||
});
|
||||
}
|
||||
@ -66,9 +78,47 @@ export class UserListingScreenComponent implements OnInit {
|
||||
this.viewReady = false;
|
||||
this.users = (await this._userControllerService.getAllUsers({}).toPromise()).users;
|
||||
this._executeSearch();
|
||||
this._computeStats();
|
||||
this.viewReady = true;
|
||||
}
|
||||
|
||||
private _computeStats() {
|
||||
this.chartData = this._translateChartService.translateRoles(
|
||||
[
|
||||
{
|
||||
value: this.users.filter((user) => !this.userService.isActive(user)).length,
|
||||
color: 'INACTIVE',
|
||||
label: 'INACTIVE'
|
||||
},
|
||||
{
|
||||
value: this.users.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length,
|
||||
color: 'REGULAR',
|
||||
label: 'REGULAR'
|
||||
},
|
||||
{
|
||||
value: this.users.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length,
|
||||
color: 'MANAGER',
|
||||
label: 'RED_MANAGER'
|
||||
},
|
||||
{
|
||||
value: this.users.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length,
|
||||
color: 'MANAGER_ADMIN',
|
||||
label: 'MANAGER_ADMIN'
|
||||
},
|
||||
{
|
||||
value: this.users.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length,
|
||||
color: 'USER_ADMIN',
|
||||
label: 'RED_USER_ADMIN'
|
||||
},
|
||||
{
|
||||
value: this.users.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length,
|
||||
color: 'ADMIN',
|
||||
label: 'RED_ADMIN'
|
||||
}
|
||||
].filter((type) => type.value > 0)
|
||||
);
|
||||
}
|
||||
|
||||
public getDisplayRoles(user: User) {
|
||||
return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE');
|
||||
}
|
||||
@ -79,4 +129,8 @@ export class UserListingScreenComponent implements OnInit {
|
||||
await this._userControllerService.addRoleToUsers(user.roles, user.userId).toPromise();
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
public toggleCollapsedDetails() {
|
||||
this.collapsedDetails = !this.collapsedDetails;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="appStateService.activeProject">
|
||||
<div class="collapsed-wrapper mt-8">
|
||||
<div class="collapsed-wrapper">
|
||||
<redaction-circle-button
|
||||
(action)="toggleCollapse.emit()"
|
||||
icon="red:expand"
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
redaction-circle-button {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
top: -8px;
|
||||
left: 290px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
&:not(.column) {
|
||||
> *:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
|
||||
.text-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@ -11,4 +11,8 @@ export class TranslateChartService {
|
||||
public translateStatus(config: DoughnutChartConfig[]): DoughnutChartConfig[] {
|
||||
return config.map((val) => ({ ...val, label: this._translateService.instant(val.label) }));
|
||||
}
|
||||
|
||||
public translateRoles(config: DoughnutChartConfig[]): DoughnutChartConfig[] {
|
||||
return config.map((val) => ({ ...val, label: this._translateService.instant(`roles.${val.label}`).toLowerCase() }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,6 +114,13 @@ export class UserService {
|
||||
return user.roles?.indexOf('RED_USER') >= 0;
|
||||
}
|
||||
|
||||
isUserAdmin(user?: User): boolean {
|
||||
if (!user) {
|
||||
user = this.user;
|
||||
}
|
||||
return user.roles?.indexOf('RED_USER_ADMIN') >= 0;
|
||||
}
|
||||
|
||||
isAdmin(user?: User): boolean {
|
||||
if (!user) {
|
||||
user = this.user;
|
||||
|
||||
@ -761,8 +761,8 @@
|
||||
"confirm-delete-user": {
|
||||
"title": "Delete User from Workspace",
|
||||
"warning": "Warning: this cannot be undone!",
|
||||
"checkbox-1": "{{count}} projects will be impacted",
|
||||
"checkbox-2": "{{count}} documents waiting review will be impacted",
|
||||
"checkbox-1": "{{projectsCount}} projects will be impacted",
|
||||
"checkbox-2": "All documents waiting review from the user will be impacted",
|
||||
"delete": "Delete User",
|
||||
"cancel": "Keep User",
|
||||
"toast-error": "Please confirm that you understand the ramifications of your action!"
|
||||
@ -807,6 +807,14 @@
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"user-stats": {
|
||||
"title": "Users",
|
||||
"chart": {
|
||||
"users": "Users in Workspace"
|
||||
},
|
||||
"expand": "Show Details",
|
||||
"collapse": "Hide Details"
|
||||
},
|
||||
"rules-screen": {
|
||||
"error": {
|
||||
"generic": "Something went wrong... Rules update failed!"
|
||||
@ -1057,6 +1065,9 @@
|
||||
"RED_MANAGER": "Manager",
|
||||
"RED_USER_ADMIN": "Users Admin",
|
||||
"RED_ADMIN": "Application Admin",
|
||||
"NO_ROLE": "No role defined"
|
||||
"NO_ROLE": "No role defined",
|
||||
"INACTIVE": "Inactive",
|
||||
"MANAGER_ADMIN": "Manager & Admin",
|
||||
"REGULAR": "Regular"
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +111,8 @@
|
||||
background-color: $grey-3;
|
||||
}
|
||||
|
||||
.UNDER_REVIEW {
|
||||
.UNDER_REVIEW,
|
||||
.REGULAR {
|
||||
stroke: $yellow-1;
|
||||
background-color: $yellow-1;
|
||||
}
|
||||
@ -121,7 +122,8 @@
|
||||
background-color: $blue-4;
|
||||
}
|
||||
|
||||
.APPROVED {
|
||||
.APPROVED,
|
||||
.ADMIN {
|
||||
stroke: $blue-3;
|
||||
background-color: $blue-3;
|
||||
}
|
||||
@ -131,7 +133,8 @@
|
||||
background-color: $grey-1;
|
||||
}
|
||||
|
||||
.OCR_PROCESSING {
|
||||
.OCR_PROCESSING,
|
||||
.USER_ADMIN {
|
||||
stroke: $green-2;
|
||||
background-color: $green-2;
|
||||
}
|
||||
@ -161,6 +164,17 @@
|
||||
background-color: rgba(#0389ec, 0.1);
|
||||
}
|
||||
|
||||
.INACTIVE {
|
||||
stroke: $grey-5;
|
||||
background-color: $grey-5;
|
||||
}
|
||||
|
||||
.MANAGER,
|
||||
.MANAGER_ADMIN {
|
||||
stroke: $red-1;
|
||||
background-color: $red-1;
|
||||
}
|
||||
|
||||
.select-oval {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user