add invites

This commit is contained in:
_Bastler 2021-05-06 21:26:57 +02:00
parent 3e02cbb353
commit c8a10eb73c
12 changed files with 1213 additions and 892 deletions

View File

@ -23,6 +23,7 @@ import {UserComponent} from './pages/user/user.component'
import {JitsiComponent} from './pages/jitsi/jitsi.component' import {JitsiComponent} from './pages/jitsi/jitsi.component'
import {AliasesComponent} from './pages/account/aliases/aliases.component'; import {AliasesComponent} from './pages/account/aliases/aliases.component';
import {DomainsComponent} from './pages/account/domains/domains.component'; import {DomainsComponent} from './pages/account/domains/domains.component';
import {InvitesComponent} from './pages/invites/invites.component';
const routes: Routes = [ const routes: Routes = [
{path: '', redirectTo: "/services", pathMatch: 'full'}, {path: '', redirectTo: "/services", pathMatch: 'full'},
@ -50,6 +51,7 @@ const routes: Routes = [
{path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]}, {path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]},
{path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]}, {path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]},
{path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]}, {path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]},
{path: 'invites/:quota', component: InvitesComponent, canActivate: [AuthenticatedGuard]},
{path: 'unavailable', component: UnavailableComponent}, {path: 'unavailable', component: UnavailableComponent},
{path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]}, {path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]},
{path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]}, {path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]},

View File

@ -19,6 +19,7 @@ import {LoginTotpComponent} from './pages/login-totp/login-totp.component';
import {FormLoginComponent} from './pages/form-login/form-login.component'; import {FormLoginComponent} from './pages/form-login/form-login.component';
import {FormLoginTotpComponent} from './pages/form-login-totp/form-login-totp.component'; import {FormLoginTotpComponent} from './pages/form-login-totp/form-login-totp.component';
import {TokensComponent} from './pages/tokens/tokens.component'; import {TokensComponent} from './pages/tokens/tokens.component';
import {InvitesComponent} from './pages/invites/invites.component';
import {PermissionsComponent} from './ui/permissions/permissions.component'; import {PermissionsComponent} from './ui/permissions/permissions.component';
import {ProfileFieldDialog, ProfileFieldsComponent, ProfileFieldBlob} from './ui/profilefields/profilefields.component'; import {ProfileFieldDialog, ProfileFieldsComponent, ProfileFieldBlob} from './ui/profilefields/profilefields.component';
import {QuotasComponent} from './ui/quotas/quotas.component'; import {QuotasComponent} from './ui/quotas/quotas.component';
@ -70,6 +71,7 @@ export class XhrInterceptor implements HttpInterceptor {
FormLoginComponent, FormLoginComponent,
FormLoginTotpComponent, FormLoginTotpComponent,
TokensComponent, TokensComponent,
InvitesComponent,
ServicesComponent, ServicesComponent,
PermissionsComponent, PermissionsComponent,
ProfileFieldsComponent, ProfileFieldDialog, ProfileFieldBlob, ProfileFieldsComponent, ProfileFieldDialog, ProfileFieldBlob,

View File

@ -0,0 +1,63 @@
<h3>{{'invites' | i18n}}</h3>
<table mat-table matSort [dataSource]="invites">
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'invite.starts' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.starts | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'invite.expires' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.expires | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="link">
<th mat-header-cell *matHeaderCellDef> {{'invite.link' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> <a href="{{ invite.codeLink}}" target="_blank" mat-button color="accent">
{{
invite.code}}</a> </td>
</ng-container>
<ng-container matColumnDef="note">
<th mat-header-cell *matHeaderCellDef> {{'invite.note' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.note}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="inviteColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: inviteColumns"></tr>
</table>
<mat-card>
<mat-card-content>
<p>{{'invites.info' | i18n}}</p>
<p *ngIf="!inviteQuota">{{'invites.noQuota' | i18n}}</p>
<div *ngIf="inviteQuota">
<p>{{'invites.left' | i18n:inviteQuota}}</p>
</div>
</mat-card-content>
<mat-card-actions>
<button *ngIf="inviteQuota && !working" mat-raised-button color="primary" (click)="create()">
{{'invite.create' | i18n}}
</button>
</mat-card-actions>
</mat-card>
<div *ngIf="others && others.content">
<h4>{{'invites.others' | i18n}}</h4>
<mat-form-field>
<input matInput [formControl]="searchFormControl" placeholder="{{'invites.search' | i18n}}">
</mat-form-field>
<table mat-table matSort [dataSource]="others.content">
<ng-container matColumnDef="note">
<th mat-header-cell *matHeaderCellDef> {{'invite.note' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.note}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="otherColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: otherColumns"></tr>
</table>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [length]="others.totalElements" [pageSize]="others.size" (page)="updateOthers($event)" showFirstLastButtons></mat-paginator>
</div>

View File

@ -0,0 +1,3 @@
mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InvitesComponent } from './invites.component';
describe('InvitesComponent', () => {
let component: InvitesComponent;
let fixture: ComponentFixture<InvitesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InvitesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InvitesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,99 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {PageEvent} from '@angular/material/paginator';
import {AuthService} from '../../services/auth.service';
import {I18nService} from '../../services/i18n.service';
import {QuotaService} from '../../services/quota.service';
import {InviteService} from '../../services/invites.service';
import {FormControl} from '@angular/forms';
import {debounceTime} from 'rxjs/operators';
@Component({
selector: 'app-invites',
templateUrl: './invites.component.html',
styleUrls: ['./invites.component.scss']
})
export class InvitesComponent implements OnInit {
quota: string;
invites: any[];
others: any;
inviteQuota: number = 0;
success: boolean;
working: boolean;
datetimeformat: string;
pageSizeOptions: number[] = [5, 10, 25, 50];
searchFormControl = new FormControl();
inviteColumns = ["starts", "expires", "link", "note"];
otherColumns = ["note"];
constructor(
private authService: AuthService,
private inviteService: InviteService,
private i18n: I18nService,
private quotaService: QuotaService,
private router: Router,
private route: ActivatedRoute) {
}
async ngOnInit() {
this.datetimeformat = this.i18n.get('format.datetime', []);
this.quota = this.route.snapshot.paramMap.get('quota');
this.searchFormControl.valueChanges.pipe(debounceTime(500)).subscribe(value => {
this.inviteService.getOthersPages(this.quota, 0, this.others.size, value).subscribe((data: any) => {
this.others = data;
}, (error) => {})
})
this.update();
}
update(): void {
this.inviteQuota = 0;
this.quotaService.quotas().subscribe((data: any) => {
for(let quota of data) {
if(quota.name == "invite_" + this.quota) {
this.inviteQuota = quota.value;
}
}
})
this.inviteService.get(this.quota).subscribe((data: any) => {
this.invites = data;
})
this.inviteService.getOthers(this.quota).subscribe((data: any) => {
this.others = data;
}, (error) => {})
}
create(): void {
this.working = true;
this.inviteService.create(this.quota, {}).subscribe(response => {
this.update();
this.working = false;
}, (error) => {
this.working = false;
if(error.status == 409) {
let errors = {};
for(let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
}
})
}
updateOthers(event: PageEvent) {
this.inviteService.getOthersPages(this.quota, event.pageIndex, event.pageSize, "").subscribe((data: any) => {
this.others = data;
}, (error) => {})
}
}

View File

@ -2,24 +2,34 @@
<p *ngIf="!services || services.length == 0">{{'services.empty' | i18n}}</p> <p *ngIf="!services || services.length == 0">{{'services.empty' | i18n}}</p>
<div fxLayout="row wrap" fxLayoutGap="16px grid">
<div fxFlex="33.33%" fxFlex.sm="50%" fxFlex.xs="100%" *ngFor="let service of services"> <table *ngIf="services && services.length > 0" mat-table matSort [dataSource]="services" (matSortChange)="sortData($event)" matSortActive="name" matSortDirection="desc">
<mat-card>
<mat-card-header> <ng-container matColumnDef="icon">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let service">
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon> <mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
<mat-card-title>{{'services.' + service.name + '.title' | i18n}}</mat-card-title> </td>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle> </ng-container>
</mat-card-header>
<mat-card-content>
<p> <ng-container matColumnDef="name">
{{ 'services.' + service.name + '.text' | i18n}} <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'service.name' | i18n}} </th>
</p> <td mat-cell *matCellDef="let service" class="text-center">
</mat-card-content> <a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-button color="accent">
<mat-card-actions> <strong>{{'services.' + service.name + '.title' | i18n}}</strong>
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-raised-button <mat-icon *ngIf="!service.sameSite" inline="true">
color="accent">{{'services.goto' | i18n}} <mat-icon *ngIf="!service.sameSite" inline="true"> open_in_new</mat-icon>
open_in_new</mat-icon></a> </a>
</mat-card-actions> <div><small>{{'services.' + service.name + '.subtitle' | i18n}}</small></div>
</mat-card> </td>
</div> </ng-container>
</div>
<ng-container matColumnDef="text">
<th mat-header-cell *matHeaderCellDef> {{'service.text' | i18n}} </th>
<td mat-cell *matCellDef="let service">{{ 'services.' + service.name + '.text' | i18n}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="serviceColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: serviceColumns"></tr>
</table>

View File

@ -1,5 +1,7 @@
import { Component, OnInit, Input } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Sort} from '@angular/material/sort';
import {ServiceService} from '../../services/service.service'; import {ServiceService} from '../../services/service.service';
import {I18nService} from '../../services/i18n.service';
@Component({ @Component({
selector: 'app-services', selector: 'app-services',
@ -9,13 +11,34 @@ import { ServiceService } from '../../services/service.service';
export class ServicesComponent implements OnInit { export class ServicesComponent implements OnInit {
services = []; services = [];
serviceColumns = ["icon", "name", "text"];
constructor(private serviceService: ServiceService) { } constructor(private serviceService: ServiceService, private i18n: I18nService) {}
ngOnInit(): void { ngOnInit(): void {
this.serviceService.services().subscribe((data: any) => { this.serviceService.services().subscribe((data: any) => {
this.services = data; this.services = data;
this.sortData({"active": "name", "direction": "desc"});
}) })
} }
sortData(sort: Sort) {
const data = this.services.slice();
if(!sort.active || sort.direction === '') {
this.services = data;
return;
}
this.services = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch(sort.active) {
case 'name': return this.compare(this.i18n.get('services.' + a.name + '.title', []), this.i18n.get('services.' + b.name + '.title', []), isAsc);
default: return 0;
}
});
}
compare(a: number | string | String, b: number | string | String, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
} }

View File

@ -0,0 +1,35 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class InviteService {
constructor(private http: HttpClient) {
}
get(quota: string) {
return this.http.get(environment.apiUrl + "/invites" + (quota ? "?quota=" + quota : ""));
}
getOthers(quota: string) {
return this.http.get(environment.apiUrl + "/invites/" + quota + "/others");
}
getOthersPages(quota: string, page : number, size : number, search : string) {
return this.http.get(environment.apiUrl + "/invites/" + quota + "/others?page=" + page + "&size=" + size + "&search=" + search);
}
create(quota: string, invite: any) {
return this.http.post(environment.apiUrl + "/invites/" + quota, invite);
}
update(invite: any) {
return this.http.post(environment.apiUrl + "/invites", invite);
}
}

View File

@ -1,5 +1,4 @@
<table mat-table matSort [dataSource]="profileFields" (matSortChange)="sortData($event)" matSortActive="index" <table mat-table matSort [dataSource]="profileFields" (matSortChange)="sortData($event)" matSortActive="index" matSortDirection="asc">
matSortDirection="asc">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'profileField.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'profileField.name' | i18n}} </th>
@ -15,11 +14,9 @@
<span *ngSwitchCase="'DATETIME'">{{profileField.value | date:datetimeformat}}</span> <span *ngSwitchCase="'DATETIME'">{{profileField.value | date:datetimeformat}}</span>
<span *ngSwitchCase="'TIME'">{{profileField.value | date:timeformat}}</span> <span *ngSwitchCase="'TIME'">{{profileField.value | date:timeformat}}</span>
<a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a> <a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a>
<a *ngSwitchCase="'EMAIL'" class="accent" <a *ngSwitchCase="'EMAIL'" class="accent" href="mailto:{{profileField.value}}">{{profileField.value}}</a>
href="mailto:{{profileField.value}}">{{profileField.value}}</a>
<span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span> <span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span>
<button *ngSwitchCase="'BLOB'" mat-raised-buttonu <button *ngSwitchCase="'BLOB'" mat-raised-button (click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
(click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
<mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled> <mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>

View File

@ -27,6 +27,21 @@
"info": { "info": {
".": "Info" ".": "Info"
}, },
"invite": {
".": "Einleidung",
"create": "Einladung erstellen",
"expires": "Gültig bis",
"link": "Link",
"note": "Notiz",
"starts": "Gültig ab"
},
"invites": {
".": "Einladungen",
"info": "Hier kannst du neue Einladungen erstellen. Um die Einladung zu bearbeiten klicke einfach auf den Link. Wenn du authoriziert bist, kannst du dort direkt den persönlichen Einladungstext bearbeiten oder eine Notiz hinzufügen.",
"left": "Du kannst noch {0} Einladungen erstellen.",
"others": "Einladungen anderer Mitglieder",
"search": "Suche"
},
"jitsi": { "jitsi": {
"rooms": { "rooms": {
".": "Jitsi Räume", ".": "Jitsi Räume",
@ -286,6 +301,10 @@
"support": "Support", "support": "Support",
"text": "Zurzeit scheint der Dienst nicht erreichbar zu sein. Wenn diese Meldung länger besteht, melde dich beim Support!" "text": "Zurzeit scheint der Dienst nicht erreichbar zu sein. Wenn diese Meldung länger besteht, melde dich beim Support!"
}, },
"service": {
"name": "Name",
"text": "Beschreibung"
},
"services": { "services": {
".": "Dienste", ".": "Dienste",
"alias_creation": { "alias_creation": {
@ -302,6 +321,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "Zum Dienst", "goto": "Zum Dienst",
"invite_partey": {
"icon": "cake",
"subtitle": "Einladung zur Partey",
"text": "Hier kannst du Einladungen für die Eröffnungs-Partey erstellen.",
"title": "Partey-Einladung"
},
"jitsi": { "jitsi": {
"icon": "video_call", "icon": "video_call",
"subtitle": "Video Konferenzen", "subtitle": "Video Konferenzen",
@ -320,6 +345,12 @@
"text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.", "text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.",
"title": "Matrix" "title": "Matrix"
}, },
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status und Perfomance der aktuellen Systeme",
"title": "Monitoring"
},
"nextcloud": { "nextcloud": {
"icon": "cloud", "icon": "cloud",
"subtitle": "Cloud Plattform", "subtitle": "Cloud Plattform",

View File

@ -27,6 +27,21 @@
"info": { "info": {
".": "Info" ".": "Info"
}, },
"invite": {
".": "Invite",
"create": "Create Invite",
"expires": "Expires",
"link": "Link",
"note": "Note",
"starts": "Starts"
},
"invites": {
".": "Invites",
"info": "You can create new invites here. To edit an invite like adding a note or change the personal invite message just click on the invite link. If you are authorized, you can change the texts directly on the invite page.",
"left": "You have {0} invites left.",
"others": "Other's invites",
"search": "Search"
},
"jitsi": { "jitsi": {
"rooms": { "rooms": {
".": "Jitsi Rooms", ".": "Jitsi Rooms",
@ -273,6 +288,10 @@
"support": "Support", "support": "Support",
"text": "The service seems currently unavailable. If this message appears over a longer period, please contact our support!" "text": "The service seems currently unavailable. If this message appears over a longer period, please contact our support!"
}, },
"service": {
"name": "Name",
"text": "Description"
},
"services": { "services": {
".": "Services", ".": "Services",
"alias_creation": { "alias_creation": {
@ -289,6 +308,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "To service", "goto": "To service",
"invite_partey": {
"icon": "cake",
"subtitle": "Invite to Partey",
"text": "Create Invites for Opening Partey.",
"title": "Partey-Invites"
},
"jitsi": { "jitsi": {
"icon": "video_call", "icon": "video_call",
"subtitle": "Video conferencing", "subtitle": "Video conferencing",
@ -307,6 +332,12 @@
"text": "talk, exchange, discuss with others", "text": "talk, exchange, discuss with others",
"title": "Matrix" "title": "Matrix"
}, },
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status and perfomance of current systems",
"title": "Monitoring"
},
"nextcloud": { "nextcloud": {
"icon": "cloud", "icon": "cloud",
"subtitle": "Cloud service", "subtitle": "Cloud service",