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 {AliasesComponent} from './pages/account/aliases/aliases.component';
import {DomainsComponent} from './pages/account/domains/domains.component';
import {InvitesComponent} from './pages/invites/invites.component';
const routes: Routes = [
{path: '', redirectTo: "/services", pathMatch: 'full'},
@ -50,13 +51,14 @@ const routes: Routes = [
{path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]},
{path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]},
{path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]},
{path: 'invites/:quota', component: InvitesComponent, canActivate: [AuthenticatedGuard]},
{path: 'unavailable', component: UnavailableComponent},
{path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]},
{path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy' })],
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy'})],
exports: [RouterModule]
})
export class AppRoutingModule {}

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 {FormLoginTotpComponent} from './pages/form-login-totp/form-login-totp.component';
import {TokensComponent} from './pages/tokens/tokens.component';
import {InvitesComponent} from './pages/invites/invites.component';
import {PermissionsComponent} from './ui/permissions/permissions.component';
import {ProfileFieldDialog, ProfileFieldsComponent, ProfileFieldBlob} from './ui/profilefields/profilefields.component';
import {QuotasComponent} from './ui/quotas/quotas.component';
@ -70,6 +71,7 @@ export class XhrInterceptor implements HttpInterceptor {
FormLoginComponent,
FormLoginTotpComponent,
TokensComponent,
InvitesComponent,
ServicesComponent,
PermissionsComponent,
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>
<div fxLayout="row wrap" fxLayoutGap="16px grid">
<div fxFlex="33.33%" fxFlex.sm="50%" fxFlex.xs="100%" *ngFor="let service of services">
<mat-card>
<mat-card-header>
<table *ngIf="services && services.length > 0" mat-table matSort [dataSource]="services" (matSortChange)="sortData($event)" matSortActive="name" matSortDirection="desc">
<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-card-title>{{'services.' + service.name + '.title' | i18n}}</mat-card-title>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{ 'services.' + service.name + '.text' | i18n}}
</p>
</mat-card-content>
<mat-card-actions>
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-raised-button
color="accent">{{'services.goto' | i18n}} <mat-icon *ngIf="!service.sameSite" inline="true">
open_in_new</mat-icon></a>
</mat-card-actions>
</mat-card>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'service.name' | i18n}} </th>
<td mat-cell *matCellDef="let service" class="text-center">
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-button color="accent">
<strong>{{'services.' + service.name + '.title' | i18n}}</strong>
<mat-icon *ngIf="!service.sameSite" inline="true">
open_in_new</mat-icon>
</a>
<div><small>{{'services.' + service.name + '.subtitle' | i18n}}</small></div>
</td>
</ng-container>
<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 { ServiceService } from '../../services/service.service';
import {Component, OnInit} from '@angular/core';
import {Sort} from '@angular/material/sort';
import {ServiceService} from '../../services/service.service';
import {I18nService} from '../../services/i18n.service';
@Component({
selector: 'app-services',
@ -9,13 +11,34 @@ import { ServiceService } from '../../services/service.service';
export class ServicesComponent implements OnInit {
services = [];
serviceColumns = ["icon", "name", "text"];
constructor(private serviceService: ServiceService) { }
constructor(private serviceService: ServiceService, private i18n: I18nService) {}
ngOnInit(): void {
this.serviceService.services().subscribe((data: any) => {
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"
matSortDirection="asc">
<table mat-table matSort [dataSource]="profileFields" (matSortChange)="sortData($event)" matSortActive="index" matSortDirection="asc">
<ng-container matColumnDef="name">
<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="'TIME'">{{profileField.value | date:timeformat}}</span>
<a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a>
<a *ngSwitchCase="'EMAIL'" class="accent"
href="mailto:{{profileField.value}}">{{profileField.value}}</a>
<a *ngSwitchCase="'EMAIL'" class="accent" href="mailto:{{profileField.value}}">{{profileField.value}}</a>
<span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span>
<button *ngSwitchCase="'BLOB'" mat-raised-buttonu
(click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
<button *ngSwitchCase="'BLOB'" mat-raised-button (click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
<mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled>
</mat-slide-toggle>
</div>

View File

@ -27,6 +27,21 @@
"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": {
"rooms": {
".": "Jitsi Räume",
@ -35,7 +50,7 @@
"delete": "Löschen",
"error": {
"expires": "Ungültiges Ende.",
"moderationStarts" : "Ungültiger Beginn Moderation. Moderation muss vor Beginn liegen.",
"moderationStarts": "Ungültiger Beginn Moderation. Moderation muss vor Beginn liegen.",
"room": "Bitte gebe einen anderen Namen an. Der Name ist schon vergeben oder enthält ungültige Zeichen. Erlaubt sind nur Buchstaben und Zahlen.",
"starts": "Ungültiger Beginn."
},
@ -43,9 +58,9 @@
"info": "Du kannst hier Jitsi Räume erstellen. Die Anzahl wird über eine Quota begrenzt.",
"left": "Du kannst noch {0} Jitsi Raum/Räume erstellen.",
"moderationUrl": "Url für Moderation",
"moderationStarts" : "Beginn Moderation",
"moderationStarts": "Beginn Moderation",
"noQuota": "Deine Quota für Jitsi Räume ist leider aufgebraucht.",
"notStarted" : "Die Konferenz hat noch nicht begonnen.",
"notStarted": "Die Konferenz hat noch nicht begonnen.",
"room": "Name",
"starts": "Beginn"
},
@ -60,12 +75,12 @@
".": "Via E-Mail teilen",
"subject": "Einladung in Videokonferenz {0}"
},
"text" : {
"both" : "Die Konferenz beginnt am {0} und endet am {1}.",
"expires" : "Die Konferenz endet am {0}.",
"intro" : "Hallo, ich möchte dich zu einer Videokonferenz einladen.",
"outro" : "Du kannst der Konferenz unter folgendem Link beitreten:\n {0}",
"starts" : "Die Konferenz beginnt am {0}."
"text": {
"both": "Die Konferenz beginnt am {0} und endet am {1}.",
"expires": "Die Konferenz endet am {0}.",
"intro": "Hallo, ich möchte dich zu einer Videokonferenz einladen.",
"outro": "Du kannst der Konferenz unter folgendem Link beitreten:\n {0}",
"starts": "Die Konferenz beginnt am {0}."
}
}
},
@ -286,6 +301,10 @@
"support": "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": {
".": "Dienste",
"alias_creation": {
@ -302,6 +321,12 @@
"title": "Gitea"
},
"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": {
"icon": "video_call",
"subtitle": "Video Konferenzen",
@ -320,6 +345,12 @@
"text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.",
"title": "Matrix"
},
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status und Perfomance der aktuellen Systeme",
"title": "Monitoring"
},
"nextcloud": {
"icon": "cloud",
"subtitle": "Cloud Plattform",

View File

@ -27,6 +27,21 @@
"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": {
"rooms": {
".": "Jitsi Rooms",
@ -35,17 +50,17 @@
"delete": "Delete",
"error": {
"expires": "Invalid expiry.",
"moderationStarts" : "Invalid moderation starts. Moderation have to start before conference starts",
"moderationStarts": "Invalid moderation starts. Moderation have to start before conference starts",
"room": "Please choose a different name. The name is already taken or contains invalid characters. Only letters and numbers are allowed.",
"starts": "Invalid start."
},
"expires": "Expires",
"info": "You can create new Jitsi Rooms here. The number is limited due to a quota.",
"left": "You have {0} Jitsi Room(s) left.",
"moderationStarts" : "Moderation starts",
"moderationStarts": "Moderation starts",
"moderationUrl": "Moderation url",
"noQuota": "Your quota for Jitsi Rooms is depleted.",
"notStarted" : "The conference has not started yet.",
"notStarted": "The conference has not started yet.",
"room": "Name",
"starts": "Starts"
},
@ -273,6 +288,10 @@
"support": "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",
"alias_creation": {
@ -289,6 +308,12 @@
"title": "Gitea"
},
"goto": "To service",
"invite_partey": {
"icon": "cake",
"subtitle": "Invite to Partey",
"text": "Create Invites for Opening Partey.",
"title": "Partey-Invites"
},
"jitsi": {
"icon": "video_call",
"subtitle": "Video conferencing",
@ -307,6 +332,12 @@
"text": "talk, exchange, discuss with others",
"title": "Matrix"
},
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status and perfomance of current systems",
"title": "Monitoring"
},
"nextcloud": {
"icon": "cloud",
"subtitle": "Cloud service",