agenda hierarchy tree
This commit is contained in:
parent
793066935e
commit
064a3f7d3c
@ -3,13 +3,15 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
|
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
|
||||||
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||||
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
|
||||||
import { SpeakerListComponent } from './components/speaker-list/speaker-list.component';
|
import { SpeakerListComponent } from './components/speaker-list/speaker-list.component';
|
||||||
|
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: AgendaListComponent },
|
{ path: '', component: AgendaListComponent },
|
||||||
{ path: 'import', component: AgendaImportListComponent },
|
{ path: 'import', component: AgendaImportListComponent },
|
||||||
{ path: 'topics/new', component: TopicDetailComponent },
|
{ path: 'topics/new', component: TopicDetailComponent },
|
||||||
|
{ path: 'sort-agenda', component: AgendaSortComponent },
|
||||||
{ path: 'topics/:id', component: TopicDetailComponent },
|
{ path: 'topics/:id', component: TopicDetailComponent },
|
||||||
{ path: ':id/speakers', component: SpeakerListComponent }
|
{ path: ':id/speakers', component: SpeakerListComponent }
|
||||||
];
|
];
|
||||||
|
@ -7,6 +7,7 @@ import { ItemInfoDialogComponent } from './components/item-info-dialog/item-info
|
|||||||
import { AgendaRoutingModule } from './agenda-routing.module';
|
import { AgendaRoutingModule } from './agenda-routing.module';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
||||||
|
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppModule for the agenda and it's children.
|
* AppModule for the agenda and it's children.
|
||||||
@ -14,6 +15,12 @@ import { TopicDetailComponent } from './components/topic-detail/topic-detail.com
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, AgendaRoutingModule, SharedModule],
|
imports: [CommonModule, AgendaRoutingModule, SharedModule],
|
||||||
entryComponents: [ItemInfoDialogComponent],
|
entryComponents: [ItemInfoDialogComponent],
|
||||||
declarations: [AgendaListComponent, TopicDetailComponent, ItemInfoDialogComponent, AgendaImportListComponent]
|
declarations: [
|
||||||
|
AgendaListComponent,
|
||||||
|
TopicDetailComponent,
|
||||||
|
ItemInfoDialogComponent,
|
||||||
|
AgendaImportListComponent,
|
||||||
|
AgendaSortComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AgendaModule {}
|
export class AgendaModule {}
|
||||||
|
@ -11,10 +11,9 @@
|
|||||||
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
|
||||||
<span>{{ selectedRows.length }} </span><span translate>selected</span>
|
<span>{{ selectedRows.length }} </span><span translate>selected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
<mat-drawer-container class="on-transition-fade">
|
<mat-drawer-container class="on-transition-fade">
|
||||||
<os-sort-filter-bar [filterService] = "filterService" (searchFieldChange)="searchFilter($event)"></os-sort-filter-bar>
|
<os-sort-filter-bar [filterService]="filterService" (searchFieldChange)="searchFilter($event)"></os-sort-filter-bar>
|
||||||
|
|
||||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||||
<!-- selector column -->
|
<!-- selector column -->
|
||||||
@ -30,8 +29,10 @@
|
|||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
|
||||||
<!-- <mat-cell (click)="onTitleColumn(item)" *matCellDef="let item"> -->
|
<!-- <mat-cell (click)="onTitleColumn(item)" *matCellDef="let item"> -->
|
||||||
<mat-cell (click)="selectItem(item, $event)" *matCellDef="let item">
|
<mat-cell (click)="selectItem(item, $event)" *matCellDef="let item">
|
||||||
<span *ngIf="item.closed"> <mat-icon class="done-check">check</mat-icon> </span>
|
<div [ngStyle]="{'margin-left': item.agendaListLevel * 25 + 'px' }">
|
||||||
<span> {{ item.getListTitle() }} </span>
|
<span *ngIf="item.closed"> <mat-icon class="done-check">check</mat-icon> </span>
|
||||||
|
<span class="table-view-list-title">{{ item.getListTitle() }}</span>
|
||||||
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -61,7 +62,10 @@
|
|||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Speakers</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let item">
|
<mat-cell *matCellDef="let item">
|
||||||
<button mat-icon-button (click)="onSpeakerIcon(item)">
|
<button mat-icon-button (click)="onSpeakerIcon(item)">
|
||||||
<mat-icon [matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null" matBadgeColor="accent">
|
<mat-icon
|
||||||
|
[matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null"
|
||||||
|
matBadgeColor="accent"
|
||||||
|
>
|
||||||
mic
|
mic
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -102,14 +106,18 @@
|
|||||||
<mat-icon>format_list_numbered</mat-icon>
|
<mat-icon>format_list_numbered</mat-icon>
|
||||||
<span translate>Numbering</span>
|
<span translate>Numbering</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item routerLink="sort-agenda">
|
||||||
|
<mat-icon>sort</mat-icon>
|
||||||
|
<span translate>Sort</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button mat-menu-item (click)="csvExportItemList();">
|
<button mat-menu-item (click)="csvExportItemList()">
|
||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export as CSV</span>
|
<span translate>Export as CSV</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item *osPerms="'agenda.can_manage'" routerLink="import">
|
<button mat-menu-item *osPerms="'agenda.can_manage'" routerLink="import">
|
||||||
<mat-icon>save_alt</mat-icon>
|
<mat-icon>save_alt</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span><span> ...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -179,7 +187,10 @@
|
|||||||
|
|
||||||
<!-- List of speakers for mobile -->
|
<!-- List of speakers for mobile -->
|
||||||
<button mat-menu-item (click)="onSpeakerIcon(item)" *ngIf="vp.isMobile">
|
<button mat-menu-item (click)="onSpeakerIcon(item)" *ngIf="vp.isMobile">
|
||||||
<mat-icon [matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null" matBadgeColor="accent">
|
<mat-icon
|
||||||
|
[matBadge]="item.waitingSpeakerAmount > 0 ? item.waitingSpeakerAmount : null"
|
||||||
|
matBadgeColor="accent"
|
||||||
|
>
|
||||||
mic
|
mic
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<span translate>List of speakers</span>
|
<span translate>List of speakers</span>
|
||||||
|
@ -81,8 +81,9 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
|||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('Agenda');
|
super.setTitle('Agenda');
|
||||||
this.initTable();
|
this.initTable();
|
||||||
this.filterService.filter().subscribe(newAgendaItem => {
|
this.filterService.filter().subscribe(newAgendaItems => {
|
||||||
this.dataSource.data = newAgendaItem;
|
newAgendaItems.sort((a, b) => a.agendaListWeight - b.agendaListWeight);
|
||||||
|
this.dataSource.data = newAgendaItems;
|
||||||
this.checkSelection();
|
this.checkSelection();
|
||||||
});
|
});
|
||||||
this.config
|
this.config
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<os-head-bar [nav]="false">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot"><h2 translate>Sort agenda</h2></div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<mat-card>
|
||||||
|
<div>
|
||||||
|
<span translate>
|
||||||
|
Drag and drop items to change the order of the agenda. Your modification will be saved immediately.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button mat-button (click)="expandCollapseAll(true)">{{ 'Expand all' | translate }}</button>
|
||||||
|
<button mat-button (click)="expandCollapseAll(false)">{{ 'Collapse all' | translate }}</button>
|
||||||
|
<os-sorting-tree
|
||||||
|
#sorter
|
||||||
|
(sort)="sort($event)"
|
||||||
|
parentIdKey="parent_id"
|
||||||
|
weightKey="weight"
|
||||||
|
[modelsObservable]="itemsObservable"
|
||||||
|
[expandCollapseAll]="expandCollapse"
|
||||||
|
>
|
||||||
|
</os-sorting-tree>
|
||||||
|
</mat-card>
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AgendaSortComponent } from './agenda-sort.component';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('AgendaSortComponent', () => {
|
||||||
|
let component: AgendaSortComponent;
|
||||||
|
let fixture: ComponentFixture<AgendaSortComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [AgendaSortComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AgendaSortComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Component, EventEmitter } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
|
||||||
|
import { BaseViewComponent } from '../../../base/base-view';
|
||||||
|
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
|
import { ViewItem } from '../../models/view-item';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort view for the agenda.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-agenda-sort',
|
||||||
|
templateUrl: './agenda-sort.component.html'
|
||||||
|
})
|
||||||
|
export class AgendaSortComponent extends BaseViewComponent {
|
||||||
|
/**
|
||||||
|
* All agendaItems sorted by their virtual weight {@link ViewItem.agendaListWeight}
|
||||||
|
*/
|
||||||
|
public itemsObservable: Observable<ViewItem[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true for expand and false for collapse. Informs the sorter component about this actions.
|
||||||
|
*/
|
||||||
|
public readonly expandCollapse: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the incoming/changing agenda items.
|
||||||
|
* @param title
|
||||||
|
* @param translate
|
||||||
|
* @param matSnackBar
|
||||||
|
* @param agendaRepo
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
title: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
private agendaRepo: AgendaRepositoryService
|
||||||
|
) {
|
||||||
|
super(title, translate, matSnackBar);
|
||||||
|
this.itemsObservable = this.agendaRepo.getViewModelListObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the sort event. The data to change is given to the repo, sending it to the server.
|
||||||
|
*
|
||||||
|
* @param data The event data. The representation fits the servers requirements, so it can directly
|
||||||
|
* be send to the server via the repository.
|
||||||
|
*/
|
||||||
|
public sort(data: OSTreeSortEvent<ViewItem>): void {
|
||||||
|
this.agendaRepo.sortItems(data).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires the expandCollapse event emitter.
|
||||||
|
*
|
||||||
|
* @param expand True, if the tree should be expanded. Otherwise collapsed
|
||||||
|
*/
|
||||||
|
public expandCollapseAll(expand: boolean): void {
|
||||||
|
this.expandCollapse.emit(expand);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,19 @@ export class ViewItem extends BaseViewModel {
|
|||||||
private _item: Item;
|
private _item: Item;
|
||||||
private _contentObject: AgendaBaseModel;
|
private _contentObject: AgendaBaseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* virtual weight defined by the order in the agenda tree, representing a shortcut to sorting by
|
||||||
|
* weight, parent_id and the parents' weight(s)
|
||||||
|
* TODO will be accurate if the viewMotion is observed via {@link getViewModelListObservable}, else, it will be undefined
|
||||||
|
*/
|
||||||
|
public agendaListWeight: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of parents in the agenda list tree.
|
||||||
|
* TODO will be accurate if the viewMotion is observed via {@link getViewModelListObservable}, else, it will be undefined
|
||||||
|
*/
|
||||||
|
public agendaListLevel: number;
|
||||||
|
|
||||||
public get item(): Item {
|
public get item(): Item {
|
||||||
return this._item;
|
return this._item;
|
||||||
}
|
}
|
||||||
@ -67,6 +80,21 @@ export class ViewItem extends BaseViewModel {
|
|||||||
return this.item ? this.item.speakers : [];
|
return this.item ? this.item.speakers : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the weight the server assigns to that item. Mostly useful for sorting within
|
||||||
|
* it's own hierarchy level (items sharing a parent)
|
||||||
|
*/
|
||||||
|
public get weight(): number {
|
||||||
|
return this.item ? this.item.weight : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the parent's id of that item (0 if no parent is set).
|
||||||
|
*/
|
||||||
|
public get parent_id(): number {
|
||||||
|
return this.item ? this.item.parent_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(item: Item, contentObject: AgendaBaseModel) {
|
public constructor(item: Item, contentObject: AgendaBaseModel) {
|
||||||
super();
|
super();
|
||||||
this._item = item;
|
this._item = item;
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { map } from 'rxjs/operators';
|
import { tap, map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { BaseRepository } from '../../base/base-repository';
|
import { BaseRepository } from '../../base/base-repository';
|
||||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
|
||||||
import { Item } from '../../../shared/models/agenda/item';
|
|
||||||
import { ViewItem } from '../models/view-item';
|
|
||||||
import { AgendaBaseModel } from '../../../shared/models/base/agenda-base-model';
|
import { AgendaBaseModel } from '../../../shared/models/base/agenda-base-model';
|
||||||
import { BaseModel } from '../../../shared/models/base/base-model';
|
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
|
||||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||||
import { ViewSpeaker } from '../models/view-speaker';
|
|
||||||
import { Speaker } from 'app/shared/models/agenda/speaker';
|
|
||||||
import { User } from 'app/shared/models/users/user';
|
|
||||||
import { HttpService } from 'app/core/services/http.service';
|
|
||||||
import { ConfigService } from 'app/core/services/config.service';
|
import { ConfigService } from 'app/core/services/config.service';
|
||||||
import { DataSendService } from 'app/core/services/data-send.service';
|
import { DataSendService } from 'app/core/services/data-send.service';
|
||||||
|
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||||
|
import { HttpService } from 'app/core/services/http.service';
|
||||||
|
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||||
|
import { Item } from '../../../shared/models/agenda/item';
|
||||||
|
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
|
import { Speaker } from 'app/shared/models/agenda/speaker';
|
||||||
|
import { User } from 'app/shared/models/users/user';
|
||||||
|
import { ViewItem } from '../models/view-item';
|
||||||
|
import { ViewSpeaker } from '../models/view-speaker';
|
||||||
|
import { TreeService } from 'app/core/services/tree.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository service for users
|
* Repository service for users
|
||||||
@ -34,13 +36,15 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
|||||||
* @param mapperService OpenSlides mapping service for collection strings
|
* @param mapperService OpenSlides mapping service for collection strings
|
||||||
* @param config Read config variables
|
* @param config Read config variables
|
||||||
* @param dataSend send models to the server
|
* @param dataSend send models to the server
|
||||||
|
* @param treeService sort the data according to weight and parents
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
protected DS: DataStoreService,
|
protected DS: DataStoreService,
|
||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
mapperService: CollectionStringModelMapperService,
|
mapperService: CollectionStringModelMapperService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private dataSend: DataSendService
|
private dataSend: DataSendService,
|
||||||
|
private treeService: TreeService
|
||||||
) {
|
) {
|
||||||
super(DS, mapperService, Item);
|
super(DS, mapperService, Item);
|
||||||
}
|
}
|
||||||
@ -229,4 +233,37 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
|||||||
public getDefaultAgendaVisibility(): Observable<number> {
|
public getDefaultAgendaVisibility(): Observable<number> {
|
||||||
return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key));
|
return this.config.get('agenda_new_items_default_visibility').pipe(map(key => +key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the changed nodes to the server.
|
||||||
|
*
|
||||||
|
* @param data The reordered data from the sorting
|
||||||
|
*/
|
||||||
|
public async sortItems(data: OSTreeSortEvent<ViewItem>): Promise<void> {
|
||||||
|
const url = '/rest/agenda/item/sort/';
|
||||||
|
await this.httpService.post(url, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom hook into the observables. The ViewItems get a virtual agendaListWeight (a sequential number)
|
||||||
|
* for the agenda topic order, and a virtual level for the hierarchy in the agenda list tree. Both values can be used
|
||||||
|
* for sorting and ordering instead of dealing with the sort parent id and weight.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public getViewModelListObservable(): Observable<ViewItem[]> {
|
||||||
|
return super.getViewModelListObservable().pipe(
|
||||||
|
tap(items => {
|
||||||
|
const iterator = this.treeService.traverseItems(items, 'weight', 'parent_id');
|
||||||
|
let m: IteratorResult<ViewItem>;
|
||||||
|
let virtualWeightCounter = 0;
|
||||||
|
while (!(m = iterator.next()).done) {
|
||||||
|
m.value.agendaListWeight = virtualWeightCounter++;
|
||||||
|
m.value.agendaListLevel = m.value.parent_id
|
||||||
|
? this.getViewModel(m.value.parent_id).agendaListLevel + 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,3 +551,8 @@ button.mat-menu-item.selected {
|
|||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-view-list-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user