entry edit, fix menu touch

This commit is contained in:
_Bastler 2021-11-21 16:43:36 +01:00
parent 2bd1e7a9d1
commit 185f28e262
18 changed files with 344 additions and 68 deletions

View File

@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard } from './auth/auth.guard';
import { PageBookmarks } from './pages/bookmarks/bookmarks.page';
import { PageComment } from './pages/comment/comment.page';
import { PageEntryEdit } from './pages/entry/edit/edit.page';
import { PageEntry } from './pages/entry/entry.page';
import { PageHot } from './pages/hot/hot.page';
import { PageLast } from './pages/last/last.page';
@ -37,6 +38,7 @@ const routes: Routes = [
{ path: 'settings', component: PageSettings, canActivate: [ AuthenticatedGuard ] },
{ path: 'submit', component: PageSubmission, canActivate: [ AuthenticatedGuard ] },
{ path: 'e/:id', component: PageEntry, canActivate: [ AuthenticatedGuard ] },
{ path: 'e/:id/edit', component: PageEntryEdit, canActivate: [ AuthenticatedGuard ] },
{ path: 'c/:id', component: PageComment, canActivate: [ AuthenticatedGuard ] },
{ path: 'u/:username', component: PageUser, canActivate: [ AuthenticatedGuard ] },
{ path: 'u/c/:username', component: PageUserComments, canActivate: [ AuthenticatedGuard ] },

View File

@ -20,6 +20,7 @@ import { AppComponent } from './app.component';
import { PageBookmarks } from './pages/bookmarks/bookmarks.page';
import { PageComment } from './pages/comment/comment.page';
import { PageEntry } from './pages/entry/entry.page';
import { PageEntryEdit } from './pages/entry/edit/edit.page';
import { PageEntries } from './pages/entries/entries.page';
import { PageHot } from './pages/hot/hot.page';
import { PageLast } from './pages/last/last.page';
@ -88,6 +89,7 @@ export class XhrInterceptor implements HttpInterceptor {
PageBookmarks,
PageComment,
PageEntry,
PageEntryEdit,
PageEntries,
PageHot,
PageLast,

View File

@ -0,0 +1,41 @@
<div class="container">
<form [formGroup]="form" (ngSubmit)="update()" #formDirective="ngForm">
<mat-card>
<mat-card-content>
<p>{{'submission.edit' | i18n}}</p>
<mat-form-field>
<input matInput placeholder="{{'submission.entryType' | i18n}}" formControlName="entryType" disabled>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="{{'submission.url' | i18n}}" formControlName="url" type="url"
[required]="entryType == 'LINK'" matAutofocus>
<mat-error *ngIf="hasError('url')">
{{'submission.url.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="{{'submission.title' | i18n}}" formControlName="title" type="text" required
(focus)="onTitleFocus($event)">
<mat-error>
{{'submission.title.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<textarea [mat-autosize] [matAutosizeMinRows]="3" matInput placeholder="{{'submission.text' | i18n}}" [required]="entryType != 'LINK'"
formControlName="text"></textarea>
<mat-error>
{{'submission.text.error' | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'submission.update' | i18n}}
</button>
<a *ngIf="success" mat-button color="primary">{{'submission.success' | i18n}}</a>
</mat-card-actions>
</mat-card>
</form>
</div>

View File

@ -0,0 +1,20 @@
mat-form-field {
display: block;
}
form {
margin: 5px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}

View File

@ -0,0 +1,127 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { EntriesService } from '../../../services/entries.service';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
import { distinctUntilChanged, debounceTime } from 'rxjs/operators';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'page-entry-edit',
templateUrl: './edit.page.html',
styleUrls: [ './edit.page.scss' ]
})
export class PageEntryEdit implements OnInit {
id: number;
entry: any;
entryTypes: string[] = [ 'LINK', 'DISCUSSION', 'QUESTION', 'INTERN' ];
entryType: string = this.entryTypes[ 0 ];
notfound: boolean = false;
working: boolean = false;
success: boolean = false;
form: FormGroup;
@ViewChild('formDirective') private formDirective: NgForm;
constructor(private entriesService: EntriesService,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
entryType: [ '', Validators.required ],
url: [ '', Validators.required ],
title: [ '', Validators.required ],
text: [ '', Validators.nullValidator ],
});
this.form.get('entryType').disable();
this.form.get('entryType').valueChanges.subscribe((value) => {
this.entryType = value;
switch (value) {
case 'LINK':
this.form.get('url').setValidators([ Validators.required ]);
this.form.get('text').setValidators([ Validators.nullValidator ]);
break;
default:
this.form.get('url').setValidators([ Validators.nullValidator ]);
this.form.get('text').setValidators([ Validators.required ]);
break;
}
});
this.form.get('url').valueChanges.pipe(
debounceTime(800),
distinctUntilChanged()).subscribe((value) => {
if (value && !this.form.get('title').value) {
this.entriesService.titleHelper(value).subscribe((title: string) => {
this.form.get('title').setValue(title);
})
}
})
this.id = +this.route.snapshot.paramMap.get('id');
this.refresh();
}
refresh() {
this.entriesService.getEntry(this.id).subscribe((data) => {
this.entry = data;
this.entryType = this.entry.entryType;
this.form.get("entryType").setValue(this.entry.entryType);
this.form.get("url").setValue(this.entry.url);
this.form.get("title").setValue(this.entry.title);
this.form.get("text").setValue(this.entry.text);
}, (error) => {
if (error.status == 404) {
this.notfound = true;
}
})
}
hasError(controlName: string): boolean {
return this.form.controls[ controlName ].errors != null;
}
onTitleFocus(event): void {
}
update(): void {
if (this.working) {
return;
}
this.working = true;
this.entry.url = this.form.get("url").value;
this.entry.title = this.form.get("title").value;
this.entry.text = this.form.get("text").value;
this.entriesService.update(this.entry).subscribe((data) => {
this.entry = data;
this.working = false;
this.success = true;
}, (error) => {
this.working = false;
if (error.status == 403) {
this.snackBar.open("Error");
}
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[ code.field ] = errors[ code.field ] || {};
errors[ code.field ][ code.code ] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[ code ]);
}
}
})
}
}

View File

@ -1,51 +1,59 @@
<div class="container">
<mat-card *ngIf="externals && externals.length > 0" class="box">
<mat-card-content>
<h2>{{'login.external' | i18n}}</h2>
<mat-error *ngIf="externalLoginInvalid">
{{'login.external.invalid' | i18n}}
</mat-error>
</mat-card-content>
<mat-card-actions>
<a class="external-login" (click)="externalLogin(client)" *ngFor="let client of externals" mat-raised-button
color="accent">{{'login.external.client' | i18n:client.id}}</a>
<mat-slide-toggle [(ngModel)]="autologin">
{{'login.autologin' | i18n}}
</mat-slide-toggle>
</mat-card-actions>
</mat-card>
<form action="{{apiUrl}}/login" method="POST" #loginForm class="box">
<mat-card *ngIf="internalLogin || externals && externals.length < 1">
<div fxLayout="column" fxFlexFill>
<div class="container">
<mat-card *ngIf="externals && externals.length > 0" class="box">
<mat-card-content>
<h2>{{'login.internal' | i18n}}</h2>
<mat-error *ngIf="loginInvalid">
{{'login.invalid' | i18n}}
<h2>{{'login.external' | i18n}}</h2>
<mat-error *ngIf="externalLoginInvalid">
{{'login.external.invalid' | i18n}}
</mat-error>
<mat-form-field>
<input id="username" name="username" matInput placeholder="{{'username' | i18n}}" required
matAutofocus>
<mat-error>
{{'username.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input id="password" name="password" matInput type="password" placeholder="{{'password' | i18n}}"
required>
<mat-error>
{{'password.invalid.hint' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle id="remember-me" name="remember-me">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button type="submit" (click)="loginForm.submit()" mat-raised-button color="primary"
[disabled]="loginForm.invalid">{{'login' |
i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button>
<a class="external-login" (click)="externalLogin(client)" *ngFor="let client of externals"
mat-raised-button color="accent">{{'login.external.client' | i18n:client.id}}</a>
<mat-slide-toggle [(ngModel)]="autologin">
{{'login.autologin' | i18n}}
</mat-slide-toggle>
</mat-card-actions>
</mat-card>
</form>
<form action="{{apiUrl}}/login" method="POST" #loginForm class="box">
<mat-card *ngIf="internalLogin || externals && externals.length < 1">
<mat-card-content>
<h2>{{'login.internal' | i18n}}</h2>
<mat-error *ngIf="loginInvalid">
{{'login.invalid' | i18n}}
</mat-error>
<mat-form-field>
<input id="username" name="username" matInput placeholder="{{'username' | i18n}}" required
matAutofocus>
<mat-error>
{{'username.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input id="password" name="password" matInput type="password"
placeholder="{{'password' | i18n}}" required>
<mat-error>
{{'password.invalid.hint' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle id="remember-me" name="remember-me">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button type="submit" (click)="loginForm.submit()" mat-raised-button color="primary"
[disabled]="loginForm.invalid">{{'login' |
i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button>
</mat-card-actions>
</mat-card>
</form>
</div>
<span fxFlexOffset="auto"></span>
<div class="container" *ngIf="!internalLogin">
<small>
<a href="/login?all">{{'login.local' | i18n}}</a>
</small>
</div>
</div>

View File

@ -32,6 +32,21 @@
{{'settings.gravity.zero' | i18n}}
</mat-hint>
</mat-form-field>
<mat-form-field>
<button
*ngIf="user.settings.entryDelay || form.get('entryDelay').value != settings.defaultEntryDelay"
matPrefix mat-icon-button (click)="resetEntryDelay()">
<mat-icon>cancel</mat-icon>
</button>
<input type="number" min="0" max="15" step="1" matInput placeholder="{{'settings.entryDelay' | i18n}}"
formControlName="entryDelay">
<mat-hint *ngIf="form.get('entryDelay').value != 0">
{{'settings.entryDelay.hint' | i18n}}
</mat-hint>
<mat-hint *ngIf="form.get('entryDelay').value == 0">
{{'settings.entryDelay.zero' | i18n}}
</mat-hint>
</mat-form-field>
<mat-form-field>
<button
*ngIf="user.settings.commentDelay || form.get('commentDelay').value != settings.defaultCommentDelay"

View File

@ -30,6 +30,7 @@ export class PageSettings implements OnInit {
email: [ '', Validators.nullValidator ],
about: [ '', Validators.nullValidator ],
gravity: [ '', Validators.nullValidator ],
entryDelay: [ '', Validators.nullValidator ],
commentDelay: [ '', Validators.nullValidator ],
});
@ -47,6 +48,7 @@ export class PageSettings implements OnInit {
this.settingsService.settings.subscribe((settings) => {
this.settings = settings;
this.form.get('gravity').setValue(this.user.settings.gravity || this.settings.defaultGravity);
this.form.get('entryDelay').setValue(this.user.settings.entryDelay || this.settings.defaultEntryDelay);
this.form.get('commentDelay').setValue(this.user.settings.commentDelay || this.settings.defaultCommentDelay);
});
})
@ -63,6 +65,11 @@ export class PageSettings implements OnInit {
this.form.get('gravity').setValue(this.settings.defaultGravity);
}
resetEntryDelay(): void {
this.user.settings.entryDelay = null;
this.form.get('entryDelay').setValue(this.settings.defaultEntryDelay);
}
resetCommentDelay(): void {
this.user.settings.commentDelay = null;
this.form.get('commentDelay').setValue(this.settings.defaultCommentDelay);
@ -89,10 +96,16 @@ export class PageSettings implements OnInit {
this.user.settings.gravity = this.form.get('gravity').value;
}
if (this.form.get('entryDelay').value != this.settings.defaultEntryDelay && !this.user.settings.entryDelay) {
this.user.settings.entryDelay = this.form.get('entryDelay').value;
} else if (this.user.settings.entryDelay) {
this.user.settings.entryDelay = this.form.get('entryDelay').value;
}
if (this.form.get('commentDelay').value != this.settings.defaultCommentDelay && !this.user.settings.commentDelay) {
this.user.settings.commentDelay = this.form.get('commentDelay').value;
} else if (this.user.settings.commentDelay) {
this.user.settings.commentDelay = this.form.get('gravity').value;
this.user.settings.commentDelay = this.form.get('commentDelay').value;
}
this.userService.update(this.user).subscribe((data) => {

View File

@ -39,6 +39,15 @@ export class EntriesService {
return this.http.post(environment.apiUrl + "/entries", entry);
}
update(entry: any) {
entry.type = 'ENTRY';
return this.http.patch(environment.apiUrl + "/entries", entry);
}
delete(id: number) {
return this.http.delete(environment.apiUrl + "/entries/" + id);
}
titleHelper(url: string) {
return this.http.get(environment.apiUrl + "/entries/helper/title?url=" + encodeURIComponent(url));
}

View File

@ -51,7 +51,7 @@
'comment.replyHide' : 'comment.reply') | i18n}}</a>
<span *ngIf="canEdit()"> | </span>
<a *ngIf="canEdit()" href="javascript:" (click)="edit()">{{'comment.edit' | i18n}}</a>
<ng-container *ngIf="moderator">
<span class="mod" *ngIf="moderator">
<span *ngIf="comment.metadata.flagged"> | </span>
<a *ngIf="comment.metadata.flagged" href="javascript:"
(click)="modUnflagComment()">{{'moderation.comment.unflag' |
@ -59,7 +59,7 @@
<span> | </span>
<a href="javascript:" (click)="modDeleteComment(comment)">{{'moderation.comment.delete' |
i18n}}</a>
</ng-container>
</span>
</small>
</div>
</ng-container>

View File

@ -31,6 +31,10 @@ small .mat-icon {
margin-right: 0px;
}
span.mod {
font-style: italic;
opacity: 0.7;
}
mat-form-field {
display: block;
@ -52,4 +56,4 @@ form {
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
}

View File

@ -95,8 +95,6 @@ export class UiComment implements OnInit {
this.comment.metadata.comments = (this.comment.metadata.comments || 0) + 1;
}
canEdit(): boolean {
const canEdit = this.author && (new Date(this.comment.created).getTime() > new Date().getTime());

View File

@ -42,7 +42,6 @@ export class UiComments implements OnInit {
}
}
addComment(comment: any): void {
if (!this.comments) {
this.comments = { "content": [] };

View File

@ -31,6 +31,8 @@
<span> | </span>
<a routerLink="/e/{{entry.id}}">{{(entry.metadata && entry.metadata.comments == 1 ? 'entry.comment' :
'entry.comments') | i18n:(entry.metadata && entry.metadata.comments)}}</a>
<span *ngIf="canEdit()"> | </span>
<a *ngIf="canEdit()" routerLink="/e/{{entry.id}}/edit">{{'entry.edit' | i18n}}</a>
<span *ngIf="entry.metadata && entry.metadata.bookmark"> | </span>
<a *ngIf="entry.metadata && entry.metadata.bookmark" href="javascript:" (click)="addBookmark()"
matTooltip="{{'bookmarks.add' | i18n}}">
@ -54,12 +56,14 @@
i18n}}">
<mat-icon inline="true">flag</mat-icon>
</a>
<ng-container *ngIf="moderator">
<span *ngIf="canEdit()"> | </span>
<a *ngIf="canEdit()" href="javascript:" (click)="deleteEntry()">{{'entry.delete' | i18n}}</a>
<span *ngIf="moderator" class="mod">
<span *ngIf="entry.metadata.flagged"> | </span>
<a *ngIf="entry.metadata.flagged" href="javascript:"
(click)="modUnflagEntry()">{{'moderation.entry.unflag' | i18n}}</a>
<a *ngIf="entry.metadata.flagged" href="javascript:" (click)="modUnflagEntry()">{{'moderation.entry.unflag' |
i18n}}</a>
<span> | </span>
<a href="javascript:" (click)="modDeleteEntry()">{{'moderation.entry.delete' | i18n}}</a>
</ng-container>
</span>
</small>
</div>

View File

@ -47,3 +47,8 @@ small {
span.voted {
opacity: 0.5;
}
span.mod {
font-style: italic;
opacity: 0.7;
}

View File

@ -7,6 +7,7 @@ import { FlagService } from '../../services/flag.service';
import { BookmarksService } from '../../services/bookmarks.service';
import { ModerationService } from '../../services/moderarion.service';
import { ConfirmDialog } from '../../ui/confirm/confirm.component';
import { EntriesService } from 'src/app/services/entries.service';
@Component({
selector: 'ui-entry',
@ -15,17 +16,19 @@ import { ConfirmDialog } from '../../ui/confirm/confirm.component';
})
export class UiEntry implements OnInit {
author: boolean = false;
moderator: boolean = false;
@Input() entry: any;
@Input() index: number;
@Input() change: Function;
constructor(private authService: AuthService, private voteService: VoteService, private flagService: FlagService,
constructor(private authService: AuthService, private entriesService: EntriesService, private voteService: VoteService, private flagService: FlagService,
private moderationService: ModerationService, private bookmarksService: BookmarksService, public dialog: MatDialog) { }
ngOnInit(): void {
this.authService.auth.subscribe((auth: any) => {
if (auth && auth.authorities) {
this.author = auth.username == this.entry.author;
for (let role of auth.authorities) {
if (role.authority == 'ROLE_ADMIN' || role.authority == 'ROLE_MOD') {
this.moderator = true;
@ -81,6 +84,29 @@ export class UiEntry implements OnInit {
});
}
canEdit(): boolean {
const canEdit = this.author && (new Date(this.entry.created).getTime() > new Date().getTime());
return canEdit;
}
deleteEntry() {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'entry.confirmDelete',
'args': [ this.entry.title, this.entry.author ]
}
})
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.entriesService.delete(this.entry.id).subscribe((result: any) => {
this.entry = null;
this.change && this.change();
})
}
});
}
modDeleteEntry() {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {

View File

@ -1,6 +1,6 @@
<mat-toolbar color="primary">
<a href="javascript:" mat-icon-button>
<mat-icon (click)="sidenav.toggle()">menu</mat-icon>
<a href="javascript:" mat-icon-button (click)="sidenav.toggle()">
<mat-icon>menu</mat-icon>
</a>
<mat-icon svgIcon="logo"></mat-icon>
<span>
@ -41,7 +41,6 @@
<mat-sidenav-container>
<mat-sidenav #sidenav [mode]="isBiggerScreen() ? 'side' : 'over'" [(opened)]="opened"
(click)="!isBiggerScreen() && opened=false">
<mat-nav-list>
<a *ngIf="authenticated" routerLink="/top" routerLinkActive="active" mat-list-item>
<mat-icon>trending_up</mat-icon> {{'page.top' | i18n}}

View File

@ -1,4 +1,4 @@
import { Component, HostListener } from '@angular/core';
import { Component, HostListener, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
@ -9,6 +9,7 @@ import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
import { I18nService } from '../../services/i18n.service';
import { SettingsService } from '../../services/settings.service';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'ui-main',
@ -16,7 +17,8 @@ import { SettingsService } from '../../services/settings.service';
styleUrls: [ './main.ui.scss' ]
})
export class UiMain {
opened = true;
opened: boolean = true;
darkTheme: boolean = false;
title = 'bstlboard';
currentLocale: String;
@ -143,7 +145,6 @@ export class UiMain {
fromEvent(document, 'touchstart').subscribe((event: TouchEvent) => {
if (event.touches[ 0 ]) {
this.touchStartX = event.touches[ 0 ].screenX;
}
})
@ -154,12 +155,15 @@ export class UiMain {
})
fromEvent(document, 'touchend').subscribe((event: TouchEvent) => {
const touchDiff = this.touchStartX - this.touchX;
if (touchDiff < 0 && touchDiff < (this.touchThresh * -1) && !this.opened) {
this.opened = true;
} else if (touchDiff > 0 && touchDiff > this.touchThresh && this.opened) {
this.opened = false;
if (this.touchX != 0) {
const touchDiff = this.touchStartX - this.touchX;
this.touchStartX = 0;
this.touchX = 0;
if (touchDiff < 0 && touchDiff < (this.touchThresh * -1) && !this.opened) {
this.opened = true;
} else if (touchDiff > 0 && touchDiff > this.touchThresh && this.opened) {
this.opened = false;
}
}
})
}