Pull request #54: User management
Merge in RED/ui from user-management to master * commit 'db56af4758786161396ec05eb2b3496a83788df8': Routing, breadcrumbs, users page
This commit is contained in:
commit
5915635e4e
@ -89,11 +89,112 @@ import { DictionaryOverviewScreenComponent } from './screens/admin/dictionary-ov
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
import { AceEditorModule } from 'ng2-ace-editor';
|
||||
import { TeamMembersComponent } from './components/team-members/team-members.component';
|
||||
import { AdminBreadcrumbsComponent } from './components/admin-page-header/admin-breadcrumbs.component';
|
||||
import { UserListingScreenComponent } from './screens/admin/users/user-listing-screen.component';
|
||||
|
||||
export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'ui/projects',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'auth-error',
|
||||
component: AuthErrorComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'info',
|
||||
component: AppInfoComponent
|
||||
},
|
||||
{
|
||||
path: 'ui',
|
||||
component: BaseScreenComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectListingScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'projects/:projectId',
|
||||
component: ProjectOverviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'projects/:projectId/file/:fileId',
|
||||
component: FilePreviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
children: [
|
||||
{ path: '', redirectTo: 'dictionaries', pathMatch: 'full' },
|
||||
{
|
||||
path: 'dictionaries',
|
||||
component: DictionaryListingScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dictionaries/:type',
|
||||
component: DictionaryOverviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: UserListingScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const matImports = [
|
||||
MatDialogModule,
|
||||
MatNativeDateModule,
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatButtonToggleModule,
|
||||
MatFormFieldModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatCheckboxModule,
|
||||
MatListModule,
|
||||
MatDatepickerModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
@ -116,7 +217,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
AuthErrorComponent,
|
||||
HumanizePipe,
|
||||
CommentsComponent,
|
||||
HumanizePipe,
|
||||
ToastComponent,
|
||||
FilterComponent,
|
||||
AppInfoComponent,
|
||||
@ -144,7 +244,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
SyncWidthDirective,
|
||||
AddEditDictionaryDialogComponent,
|
||||
DictionaryOverviewScreenComponent,
|
||||
TeamMembersComponent
|
||||
TeamMembersComponent,
|
||||
AdminBreadcrumbsComponent,
|
||||
UserListingScreenComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -155,8 +257,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
AuthModule,
|
||||
IconsModule,
|
||||
ApiModule,
|
||||
MatDialogModule,
|
||||
MatNativeDateModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
@ -164,93 +264,16 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'ui/projects',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'auth-error',
|
||||
component: AuthErrorComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'info',
|
||||
component: AppInfoComponent
|
||||
},
|
||||
{
|
||||
path: 'ui',
|
||||
component: BaseScreenComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectListingScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'projects/:projectId',
|
||||
component: ProjectOverviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'projects/:projectId/file/:fileId',
|
||||
component: FilePreviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'admin-dictionaries',
|
||||
component: DictionaryListingScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dictionary-overview/:type',
|
||||
component: DictionaryOverviewScreenComponent,
|
||||
canActivate: [CompositeRouteGuard],
|
||||
data: {
|
||||
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
RouterModule.forRoot(routes),
|
||||
NgpSortModule,
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
MatButtonToggleModule,
|
||||
MatFormFieldModule,
|
||||
...matImports,
|
||||
ToastrModule.forRoot({
|
||||
closeButton: true,
|
||||
enableHtml: true,
|
||||
toastComponent: ToastComponent
|
||||
}),
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
FileUploadModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
MatProgressSpinnerModule,
|
||||
MatCheckboxModule,
|
||||
MatListModule,
|
||||
MatDatepickerModule,
|
||||
MatInputModule,
|
||||
ColorPickerModule,
|
||||
AceEditorModule
|
||||
],
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { AppLoadStateService } from '../utils/app-load-state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
class UrlTree {}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@ -22,7 +19,7 @@ export class RedRoleGuard implements CanActivate {
|
||||
obs.complete();
|
||||
} else {
|
||||
if (!this._userService.isUser() && state.url.startsWith('/ui/projects')) {
|
||||
this._router.navigate(['/ui/admin-dictionaries']);
|
||||
this._router.navigate(['/ui/admin']);
|
||||
obs.next(false);
|
||||
obs.complete();
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a
|
||||
class="breadcrumb"
|
||||
routerLink="/ui/admin/dictionaries"
|
||||
translate="dictionaries"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
*ngIf="screen === 'dictionaries' || root"
|
||||
></a>
|
||||
|
||||
<a
|
||||
class="ml-32 breadcrumb"
|
||||
[routerLink]="'/ui/admin/users'"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
routerLinkActive="active"
|
||||
translate="user-management"
|
||||
*ngIf="screen === 'users' || root"
|
||||
></a>
|
||||
|
||||
<ng-container *ngIf="dictionary">
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
<a class="breadcrumb" [routerLink]="'/ui/admin/dictionaries/' + dictionary.type" routerLinkActive="active">
|
||||
{{ dictionary.type | humanize }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
@ -0,0 +1,3 @@
|
||||
.ml-32 {
|
||||
margin-left: 32px;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TypeValue } from '@redaction/red-ui-http';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-admin-breadcrumbs',
|
||||
templateUrl: './admin-breadcrumbs.component.html',
|
||||
styleUrls: ['./admin-breadcrumbs.component.scss']
|
||||
})
|
||||
export class AdminBreadcrumbsComponent implements OnInit {
|
||||
public dictionary: TypeValue;
|
||||
public root: boolean;
|
||||
public screen: string;
|
||||
|
||||
constructor(private readonly _activatedRoute: ActivatedRoute, private _appStateService: AppStateService) {
|
||||
this._activatedRoute.params.subscribe((params) => {
|
||||
const url = this._activatedRoute.snapshot.url;
|
||||
this.root = url.length === 1;
|
||||
this.screen = url[0].path;
|
||||
if (this.screen === 'dictionaries' && url.length === 2) {
|
||||
this.dictionary = this._appStateService.dictionaryData[params.type];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
@ -1,9 +1,6 @@
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a class="breadcrumb" routerLink="/ui/admin-dictionaries" translate="dictionaries"></a>
|
||||
<div> </div>
|
||||
</div>
|
||||
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
|
||||
|
||||
<div class="actions">
|
||||
<redaction-icon-button
|
||||
@ -60,7 +57,7 @@
|
||||
[withSort]="true"
|
||||
></redaction-table-col-name>
|
||||
<redaction-table-col-name label="dictionary-listing.table-col-names.hint-redaction" class="flex-center"></redaction-table-col-name>
|
||||
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
|
||||
<div class="placeholder-bottom-border"></div>
|
||||
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
|
||||
</div>
|
||||
|
||||
@ -69,7 +66,7 @@
|
||||
<div
|
||||
class="table-item pointer"
|
||||
*ngFor="let dict of dictionaries | sortBy: sortingOption.order:sortingOption.column"
|
||||
[routerLink]="['/ui/dictionary-overview/' + dict.type]"
|
||||
[routerLink]="['/ui/admin/dictionaries/' + dict.type]"
|
||||
>
|
||||
<div class="pr-0" (click)="toggleDictSelected($event, dict)">
|
||||
<div *ngIf="!isDictSelected(dict)" class="select-oval"></div>
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a class="breadcrumb" routerLink="/ui/admin-dictionaries" translate="dictionaries"></a>
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
<a class="breadcrumb" [routerLink]="'/ui/dictionary-overview/' + dictionary.type">
|
||||
{{ dictionary.type | humanize }}
|
||||
</a>
|
||||
</div>
|
||||
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
|
||||
|
||||
<div class="actions">
|
||||
<redaction-circle-button
|
||||
@ -29,7 +23,7 @@
|
||||
|
||||
<redaction-circle-button
|
||||
class="ml-6"
|
||||
[routerLink]="['/ui/admin-dictionaries/']"
|
||||
[routerLink]="['/ui/admin/dictionaries/']"
|
||||
tooltip="common.close"
|
||||
tooltipPosition="before"
|
||||
icon="red:close"
|
||||
|
||||
@ -55,7 +55,7 @@ export class DictionaryOverviewScreenComponent {
|
||||
this._activatedRoute.params.subscribe((params) => {
|
||||
this.dictionary = this._appStateService.dictionaryData[params.type];
|
||||
if (!this.dictionary) {
|
||||
this._router.navigate(['/ui/admin-dictionaries']);
|
||||
this._router.navigate(['/ui/admin/dictionaries']);
|
||||
} else {
|
||||
this._initialize();
|
||||
}
|
||||
@ -87,7 +87,7 @@ export class DictionaryOverviewScreenComponent {
|
||||
openDeleteDictionaryDialog($event: any) {
|
||||
this._dialogService.openDeleteDictionaryDialog($event, this.dictionary, async () => {
|
||||
await this._appStateService.loadDictionaryData();
|
||||
this._router.navigate(['/ui/admin-dictionaries']);
|
||||
this._router.navigate(['/ui/admin/dictionaries']);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
|
||||
<div class="actions">
|
||||
<redaction-circle-button
|
||||
class="ml-6"
|
||||
*ngIf="permissionsService.isUser()"
|
||||
[routerLink]="['/ui/projects/']"
|
||||
tooltip="common.close"
|
||||
tooltipPosition="before"
|
||||
icon="red:close"
|
||||
></redaction-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="red-content-inner">
|
||||
<div class="left-container">
|
||||
<div class="header-item">
|
||||
<span class="all-caps-label">
|
||||
{{ 'user-listing.table-header.title' | translate: { length: users.length } }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-header" redactionSyncWidth="table-item">
|
||||
<redaction-table-col-name label="user-listing.table-col-names.name"></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name label="user-listing.table-col-names.email"></redaction-table-col-name>
|
||||
|
||||
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<!-- Table lines -->
|
||||
<div class="table-item" *ngFor="let user of users">
|
||||
<div>
|
||||
<redaction-initials-avatar [userId]="user.userId" [withName]="true" size="large"></redaction-initials-avatar>
|
||||
</div>
|
||||
<div>{{ user.email || '-' }}</div>
|
||||
<div class="scrollbar-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,23 @@
|
||||
.left-container {
|
||||
width: calc(100vw - 353px);
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: 1fr 1fr 11px;
|
||||
|
||||
&:hover {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.table-item {
|
||||
> div {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-container {
|
||||
display: flex;
|
||||
width: 353px;
|
||||
min-width: 353px;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PermissionsService } from '../../../common/service/permissions.service';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
import { User } from '@redaction/red-ui-http';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-user-listing-screen',
|
||||
templateUrl: './user-listing-screen.component.html',
|
||||
styleUrls: ['./user-listing-screen.component.scss']
|
||||
})
|
||||
export class UserListingScreenComponent implements OnInit {
|
||||
public users: User[];
|
||||
|
||||
constructor(public readonly permissionsService: PermissionsService, private readonly userService: UserService) {
|
||||
this.users = this.userService.allUsers;
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
@ -20,9 +20,21 @@
|
||||
</mat-menu>
|
||||
</div>
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container" *ngIf="permissionsService.isUser()">
|
||||
<a class="breadcrumb" routerLink="/ui/projects" translate="top-bar.navigation-items.projects"></a>
|
||||
<a
|
||||
class="breadcrumb"
|
||||
routerLink="/ui/projects"
|
||||
translate="top-bar.navigation-items.projects"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
></a>
|
||||
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon>
|
||||
<a *ngIf="appStateService.activeProject" class="breadcrumb" [routerLink]="'/ui/projects/' + appStateService.activeProjectId">
|
||||
<a
|
||||
*ngIf="appStateService.activeProject"
|
||||
class="breadcrumb"
|
||||
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
{{ appStateService.activeProject.project.projectName }}
|
||||
</a>
|
||||
<mat-icon svgIcon="red:arrow-right" *ngIf="appStateService.activeFile"></mat-icon>
|
||||
@ -30,6 +42,8 @@
|
||||
*ngIf="appStateService.activeFile"
|
||||
class="breadcrumb"
|
||||
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
{{ appStateService.activeFile.filename }}
|
||||
</a>
|
||||
@ -47,9 +61,9 @@
|
||||
<button
|
||||
*ngIf="permissionsService.isManager()"
|
||||
(click)="appStateService.reset()"
|
||||
[routerLink]="'/ui/admin-dictionaries'"
|
||||
[routerLink]="'/ui/admin'"
|
||||
mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.admin-dictionaries"
|
||||
translate="top-bar.navigation-items.my-account.children.admin"
|
||||
></button>
|
||||
<button [matMenuTriggerFor]="language" mat-menu-item translate="top-bar.navigation-items.my-account.children.language.label"></button>
|
||||
<mat-menu #language="matMenu">
|
||||
|
||||
@ -43,19 +43,19 @@ export class UserService {
|
||||
this._keycloakService.logout(window.location.origin);
|
||||
}
|
||||
|
||||
get userId() {
|
||||
get userId(): string {
|
||||
return this._currentUser.id;
|
||||
}
|
||||
|
||||
get allUsers() {
|
||||
get allUsers(): User[] {
|
||||
return this._allUsers;
|
||||
}
|
||||
|
||||
get managerUsers() {
|
||||
get managerUsers(): User[] {
|
||||
return this._allUsers.filter((u) => u.roles.indexOf('RED_MANAGER') >= 0);
|
||||
}
|
||||
|
||||
get eligibleUsers() {
|
||||
get eligibleUsers(): User[] {
|
||||
return this._allUsers.filter((u) => u.roles.indexOf('RED_USER') >= 0 || u.roles.indexOf('RED_MANAGER') >= 0);
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
"projects": "Projects",
|
||||
"my-account": {
|
||||
"children": {
|
||||
"admin-dictionaries": "Manage Dictionaries",
|
||||
"admin": "Management",
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"english": "English",
|
||||
@ -535,5 +535,15 @@
|
||||
"hint-redaction": "Hint/Redaction"
|
||||
}
|
||||
},
|
||||
"dictionaries": "Dictionaries"
|
||||
"user-listing": {
|
||||
"table-header": {
|
||||
"title": "{{length}} users"
|
||||
},
|
||||
"table-col-names": {
|
||||
"name": "Name",
|
||||
"email": "Email"
|
||||
}
|
||||
},
|
||||
"dictionaries": "Dictionaries",
|
||||
"user-management": "User Management"
|
||||
}
|
||||
|
||||
@ -14,9 +14,12 @@
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
color: $primary;
|
||||
@include line-clamp(1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user