added tags + filtering
This commit is contained in:
parent
1da276c670
commit
979324ccc8
@ -49,6 +49,7 @@ import { ConfirmDialog } from './ui/confirm/confirm.component'
|
||||
import { I18nService, I18nPaginatorIntl } from './services/i18n.service';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { UiTagsPicker } from './ui/tags/tagspicker.ui';
|
||||
|
||||
|
||||
export function fetchI18n(i18n: I18nService) {
|
||||
@ -112,6 +113,7 @@ export class XhrInterceptor implements HttpInterceptor {
|
||||
UiMain,
|
||||
UiPoints,
|
||||
ConfirmDialog,
|
||||
UiTagsPicker
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -1 +1 @@
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate" [gravityFilter]="gravityFilter"></ui-entries>
|
@ -12,10 +12,13 @@ export class PageEntries implements OnInit {
|
||||
|
||||
settings: any;
|
||||
@Input() fetch: Function;
|
||||
@Input() gravityFilter: boolean = false;
|
||||
entries: any;
|
||||
asc: boolean = false;
|
||||
boundRefresh: Function;
|
||||
boundUpdate: Function;
|
||||
init: boolean = true;
|
||||
filterOpen: boolean = false;
|
||||
|
||||
constructor(
|
||||
private settingsService: SettingsService, private router: Router, private route: ActivatedRoute) { }
|
||||
@ -25,7 +28,7 @@ export class PageEntries implements OnInit {
|
||||
this.boundUpdate = this.update.bind(this);
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (this.init) {
|
||||
this.entries = {};
|
||||
this.entries = { filter: {} };
|
||||
if (params[ 'p' ]) {
|
||||
this.entries.number = +params[ 'p' ] - 1;
|
||||
if (this.entries.number < 0) {
|
||||
@ -37,6 +40,16 @@ export class PageEntries implements OnInit {
|
||||
this.entries.size = +params[ 's' ];
|
||||
}
|
||||
|
||||
if (params[ 'asc' ]) {
|
||||
this.asc = true;
|
||||
}
|
||||
|
||||
for (const param in params) {
|
||||
if (param != 's' && param != 'p' && param != 'asc') {
|
||||
this.entries.filter[ param ] = params[ param ];
|
||||
}
|
||||
}
|
||||
|
||||
this.settingsService.settings.subscribe((settings) => {
|
||||
this.settings = settings;
|
||||
this.refresh();
|
||||
@ -51,9 +64,11 @@ export class PageEntries implements OnInit {
|
||||
this.entries = {};
|
||||
}
|
||||
|
||||
const filter = JSON.parse(JSON.stringify(this.entries.filter || {}))
|
||||
this.entries.content = null;
|
||||
this.fetch(this.entries.number || 0, this.entries.size || this.settings.pageSize).subscribe((data: any) => {
|
||||
this.fetch(this.entries.number || 0, this.entries.size || this.settings.pageSize, this.asc, this.entries.filter).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
this.entries.filter = filter;
|
||||
}, (error) => { })
|
||||
|
||||
}
|
||||
@ -70,6 +85,12 @@ export class PageEntries implements OnInit {
|
||||
params.s = event.pageSize;
|
||||
}
|
||||
|
||||
if (this.entries.filter) {
|
||||
for (const param in this.entries.filter) {
|
||||
params[ param ] = this.entries.filter[ param ];
|
||||
}
|
||||
}
|
||||
|
||||
this.router.navigate(
|
||||
[],
|
||||
{
|
||||
@ -78,9 +99,13 @@ export class PageEntries implements OnInit {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
|
||||
this.fetch(event.pageIndex, event.pageSize).subscribe((data: any) => {
|
||||
const filter = JSON.parse(JSON.stringify(this.entries.filter || {}))
|
||||
this.fetch(event.pageIndex, event.pageSize, this.asc, this.entries.filter).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
}, (error) => { })
|
||||
this.entries.filter = filter;
|
||||
}, (error) => {
|
||||
this.entries = {};
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<div class="container">
|
||||
<form [formGroup]="form" (ngSubmit)="update()" #formDirective="ngForm">
|
||||
<mat-progress-bar *ngIf="!entry" mode="indeterminate"></mat-progress-bar>
|
||||
<form [formGroup]="form" (ngSubmit)="update()" #formDirective="ngForm" *ngIf="entry">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<p>{{'submission.edit' | i18n}}</p>
|
||||
@ -23,18 +24,21 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea [mat-autosize] [matAutosizeMinRows]="3" matInput placeholder="{{'submission.text' | i18n}}" [required]="entryType != 'LINK'"
|
||||
formControlName="text"></textarea>
|
||||
<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>
|
||||
|
||||
<ui-tagspicker [(model)]="entry.tags" placeholder="{{'submission.tags' | i18n}}"></ui-tagspicker>
|
||||
|
||||
</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>
|
||||
<a *ngIf="success" mat-button color="primary" routerLink="/e/{{entry.id}}">{{'submission.success' | i18n}}</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
|
@ -2,6 +2,11 @@ mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-chip mat-icon.mat-icon-inline {
|
||||
margin-top: -12px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 5px;
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { EntriesService } from '../../../services/entries.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { TagsService } from 'src/app/services/tags.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-entry-edit',
|
||||
@ -20,9 +23,11 @@ export class PageEntryEdit implements OnInit {
|
||||
working: boolean = false;
|
||||
success: boolean = false;
|
||||
form: FormGroup;
|
||||
readonly tagsSeparatorKeysCodes = [ ENTER, COMMA, SPACE ] as const;
|
||||
@ViewChild('formDirective') private formDirective: NgForm;
|
||||
|
||||
constructor(private entriesService: EntriesService,
|
||||
private tagsService: TagsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private snackBar: MatSnackBar) { }
|
||||
@ -73,6 +78,11 @@ export class PageEntryEdit implements OnInit {
|
||||
this.form.get("url").setValue(this.entry.url);
|
||||
this.form.get("title").setValue(this.entry.title);
|
||||
this.form.get("text").setValue(this.entry.text);
|
||||
if (!this.entry.metadata.edit) {
|
||||
this.form.get("url").disable();
|
||||
this.form.get("title").disable();
|
||||
this.form.get("text").disable();
|
||||
}
|
||||
}, (error) => {
|
||||
if (error.status == 404) {
|
||||
this.notfound = true;
|
||||
@ -88,6 +98,25 @@ export class PageEntryEdit implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
let value = (event.value || "").trim();
|
||||
if (value.startsWith('#')) {
|
||||
value = value.replace('#', '');
|
||||
}
|
||||
value = value.split('#').join('-');
|
||||
if (value) {
|
||||
this.entry.tags.push(value);
|
||||
}
|
||||
event.chipInput!.clear();
|
||||
}
|
||||
|
||||
removeTag(tag: string): void {
|
||||
const index = this.entry.tags.indexOf(tag);
|
||||
if (index >= 0) {
|
||||
this.entry.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
update(): void {
|
||||
|
||||
if (this.working) {
|
||||
@ -96,6 +125,7 @@ export class PageEntryEdit implements OnInit {
|
||||
|
||||
this.working = true;
|
||||
|
||||
if (this.entry.metadata.edit) {
|
||||
this.entry.url = this.form.get("url").value;
|
||||
this.entry.title = this.form.get("title").value;
|
||||
this.entry.text = this.form.get("text").value;
|
||||
@ -121,6 +151,29 @@ export class PageEntryEdit implements OnInit {
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.tagsService.setTags(this.entry.id, this.entry.tags).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 ]);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<page-notfound *ngIf="notfound"></page-notfound>
|
||||
<ng-container *ngIf="entry">
|
||||
<ui-entry [entry]="entry" [change]="boundRefresh"></ui-entry>
|
||||
<ui-entry [entry]="entry" [change]="boundRefresh" [linkedTag]="false"></ui-entry>
|
||||
|
||||
<p class="text" [innerHTML]="entry.text | urltext"></p>
|
||||
|
||||
|
@ -33,6 +33,7 @@ export class PageEntry implements OnInit {
|
||||
this.entry = data;
|
||||
}, (error) => {
|
||||
if (error.status == 404) {
|
||||
this.entry = false;
|
||||
this.notfound = true;
|
||||
}
|
||||
})
|
||||
|
@ -1 +1 @@
|
||||
<page-entries [fetch]="boundFetch"></page-entries>
|
||||
<page-entries [fetch]="boundFetch" [gravityFilter]="true"></page-entries>
|
@ -19,8 +19,8 @@ export class PageHot implements OnInit {
|
||||
this.boundFetch = this.fetch.bind(this);
|
||||
}
|
||||
|
||||
fetch(page: number, size: number) {
|
||||
return this.entriesService.getComments(page,size);
|
||||
fetch(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.entriesService.getComments(page, size, asc, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ export class PageLast implements OnInit {
|
||||
this.boundFetch = this.fetch.bind(this);
|
||||
}
|
||||
|
||||
fetch(page: number, size: number) {
|
||||
return this.entriesService.getLastComment(page,size);
|
||||
fetch(page: number, size: number,asc: boolean, filter: any) {
|
||||
return this.entriesService.getLastComment(page, size,asc, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
||||
<span fxFlexOffset="auto"></span>
|
||||
<div class="container" *ngIf="!internalLogin">
|
||||
<small>
|
||||
<a href="/login?all">{{'login.local' | i18n}}</a>
|
||||
<a href="javascript:" (click)="internalLogin = true;">{{'login.local' | i18n}}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
@ -18,8 +18,8 @@ export class PageNew implements OnInit {
|
||||
this.boundFetch = this.fetch.bind(this);
|
||||
}
|
||||
|
||||
fetch(page: number, size: number) {
|
||||
return this.entriesService.getNew(page,size);
|
||||
fetch(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.entriesService.getNew(page, size, asc, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,12 +31,15 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea [mat-autosize] [matAutosizeMinRows]="3" matInput placeholder="{{'submission.text' | i18n}}" [required]="entryType != 'LINK'"
|
||||
formControlName="text"></textarea>
|
||||
<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>
|
||||
|
||||
<ui-tagspicker [(model)]="tags" placeholder="{{'submission.tags' | i18n}}"></ui-tagspicker>
|
||||
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
|
||||
|
@ -2,6 +2,11 @@ mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ui-tagspicker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 5px;
|
||||
|
||||
|
@ -2,7 +2,9 @@ import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { EntriesService } from '../../services/entries.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
|
||||
import { tap, distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
|
||||
@Component({
|
||||
selector: 'page-submission',
|
||||
@ -15,6 +17,8 @@ export class PageSubmission implements OnInit {
|
||||
entryType: string = this.entryTypes[ 0 ];
|
||||
working: boolean = false;
|
||||
form: FormGroup;
|
||||
readonly tagsSeparatorKeysCodes = [ ENTER, COMMA, SPACE ] as const;
|
||||
tags: string[] = [];
|
||||
@ViewChild('formDirective') private formDirective: NgForm;
|
||||
|
||||
constructor(private entriesService: EntriesService,
|
||||
@ -64,6 +68,25 @@ export class PageSubmission implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
let value = (event.value || "").trim();
|
||||
if (value.startsWith('#')) {
|
||||
value = value.replace('#', '');
|
||||
}
|
||||
value = value.split('#').join('-');
|
||||
if (value) {
|
||||
this.tags.push(value);
|
||||
}
|
||||
event.chipInput!.clear();
|
||||
}
|
||||
|
||||
removeTag(tag: string): void {
|
||||
const index = this.tags.indexOf(tag);
|
||||
if (index >= 0) {
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
create(): void {
|
||||
|
||||
if (this.working) {
|
||||
@ -77,6 +100,7 @@ export class PageSubmission implements OnInit {
|
||||
entry.entryType = this.entryType;
|
||||
entry.title = this.form.get("title").value;
|
||||
entry.text = this.form.get("text").value;
|
||||
entry.tags = this.tags;
|
||||
|
||||
this.entriesService.create(entry).subscribe((data) => {
|
||||
this.router.navigateByUrl('/');
|
||||
|
@ -1 +1 @@
|
||||
<page-entries [fetch]="boundFetch"></page-entries>
|
||||
<page-entries [fetch]="boundFetch" [gravityFilter]="true"></page-entries>
|
@ -16,8 +16,8 @@ export class PageTop implements OnInit {
|
||||
this.boundFetch = this.fetch.bind(this);
|
||||
}
|
||||
|
||||
fetch(page: number, size: number) {
|
||||
return this.entriesService.getRanked(page,size);
|
||||
fetch(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.entriesService.getRanked(page, size, asc, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ export class PageUserEntries implements OnInit {
|
||||
this.boundFetch = this.fetch.bind(this);
|
||||
}
|
||||
|
||||
fetch(page: number, size: number) {
|
||||
return this.entriesService.getByUser(this.username, page, size);
|
||||
fetch(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.entriesService.getByUser(this.username, page, size, asc, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
@ -10,24 +10,48 @@ export class EntriesService {
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
getRanked(page: number, size: number) {
|
||||
return this.http.get(environment.apiUrl + "/entries?page=" + page + "&size=" + size);
|
||||
fetch(path: string, page: number, size: number, asc: boolean, filter: any) {
|
||||
let httpParams = new HttpParams();
|
||||
if (page != undefined) {
|
||||
httpParams = httpParams.set("page", "" + page);
|
||||
}
|
||||
if (size != undefined) {
|
||||
httpParams = httpParams.set("size", "" + size);
|
||||
}
|
||||
|
||||
getNew(page: number, size: number) {
|
||||
return this.http.get(environment.apiUrl + "/entries/new?page=" + page + "&size=" + size);
|
||||
if (asc) {
|
||||
httpParams = httpParams.set("asc", "" + asc);
|
||||
}
|
||||
|
||||
getComments(page: number, size: number) {
|
||||
return this.http.get(environment.apiUrl + "/entries/comments?page=" + page + "&size=" + size);
|
||||
if (filter) {
|
||||
for (const param in filter) {
|
||||
if (filter[ param ]) {
|
||||
httpParams = httpParams.set(param, "" + filter[ param ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getLastComment(page: number, size: number) {
|
||||
return this.http.get(environment.apiUrl + "/entries/last?page=" + page + "&size=" + size);
|
||||
return this.http.get(environment.apiUrl + "/entries" + path, { params: httpParams });
|
||||
}
|
||||
|
||||
getByUser(username: string, page: number, size: number) {
|
||||
return this.http.get(environment.apiUrl + "/entries/byuser/" + username + "?page=" + page + "&size=" + size);
|
||||
getRanked(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.fetch("", page, size, asc, filter);
|
||||
}
|
||||
|
||||
getNew(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.fetch("/new", page, size, asc, filter);
|
||||
}
|
||||
|
||||
getComments(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.fetch("/comments", page, size, asc, filter);
|
||||
}
|
||||
|
||||
getLastComment(page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.fetch("/last", page, size, asc, filter);
|
||||
}
|
||||
|
||||
getByUser(username: string, page: number, size: number, asc: boolean, filter: any) {
|
||||
return this.fetch("/byuser/" + username, page, size, asc, filter);
|
||||
}
|
||||
|
||||
getEntry(id: number) {
|
||||
|
22
src/app/services/tags.service.ts
Normal file
22
src/app/services/tags.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TagsService {
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
return this.http.get(environment.apiUrl + "/tags?q=" + query);
|
||||
}
|
||||
|
||||
setTags(id: number, tags: string[]) {
|
||||
return this.http.patch(environment.apiUrl + "/tags/entry/" + id, tags);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<mat-progress-bar *ngIf="!entries || !entries.content" mode="indeterminate"></mat-progress-bar>
|
||||
|
||||
<div *ngIf="entries && entries.content" fxLayout="column" fxFlexFill>
|
||||
<mat-list>
|
||||
<div *ngIf="entries" fxLayout="column" fxFlexFill>
|
||||
<mat-list *ngIf="entries.content">
|
||||
<ng-container *ngFor="let entry of entries.content; let i = index">
|
||||
<mat-divider *ngIf="i > 0"></mat-divider>
|
||||
<mat-list-item class="entry-item">
|
||||
@ -19,7 +19,33 @@
|
||||
|
||||
<span fxFlexOffset="auto"></span>
|
||||
|
||||
<mat-paginator *ngIf="entries.totalElements > 0" [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.number"
|
||||
[length]="entries.totalElements" [pageSize]="entries.size" (page)="update && update($event)" showFirstLastButtons>
|
||||
<div class="mat-paginator" fxLayout="row" fxLayout.xs="column" fxLayoutAlign.xs="start start">
|
||||
<div class="filter-container">
|
||||
<a mat-icon-button mat-button (click)="filterOpen=!filterOpen" title="{{'entries.filter' | i18n}}">
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
</a>
|
||||
<div *ngIf="filterOpen">
|
||||
<ui-tagspicker [(model)]="entries.filter.tag" singleton="true" placeholder="{{'entries.filter.tag' | i18n}}" [change]="boundTagspickerChange"></ui-tagspicker>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput [matDatepicker]="picker" [value]="entries && entries.filter && entries.filter.date"
|
||||
(dateChange)="setFilter('date', $event.value && $event.value.toISOString() || undefined)"
|
||||
placeholder="{{'entries.filter.date' | i18n}}">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker touchUi></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field *ngIf="gravityFilter">
|
||||
<input matInput type="number" step="0.01" min="0"
|
||||
[value]="entries && entries.filter && entries.filter.gravity"
|
||||
(change)="setFilter('gravity', $event.target && $event.target.value || undefined)"
|
||||
placeholder="{{'entries.filter.gravity' | i18n}}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<span fxFlexOffset="auto" fxFlexOffset.xs="none"></span>
|
||||
<mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.number" [length]="entries.totalElements"
|
||||
[pageSize]="entries.size" (page)="update && update($event)" showFirstLastButtons>
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
@ -1,5 +1,6 @@
|
||||
.entry {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -7,3 +8,23 @@
|
||||
height: auto;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding-left: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
mat-form-field {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-chip mat-icon.mat-icon-inline {
|
||||
margin-top: -12px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.mat-option .mat-icon {
|
||||
margin-right: -2px;
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Component, OnInit, Input, ViewChild } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { Observable } from 'rxjs';
|
||||
import { debounceTime, switchMap } from 'rxjs/operators';
|
||||
import { TagsService } from 'src/app/services/tags.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-entries',
|
||||
@ -7,19 +13,55 @@ import { Component, OnInit, Input } from '@angular/core';
|
||||
})
|
||||
export class UiEntries implements OnInit {
|
||||
|
||||
|
||||
@Input() entries: any;
|
||||
@Input() update: Function;
|
||||
@Input() refresh: Function;
|
||||
@Input() gravityFilter: boolean = false;
|
||||
@ViewChild(MatPaginator) matPaginator;
|
||||
pageSizeOptions: number[] = [ 1, 2, 3, 4, 5, 10, 30, 50, 100 ];
|
||||
filterOpen: boolean = false;
|
||||
searchTags: Observable<Object>;
|
||||
boundTagspickerChange: Function;
|
||||
|
||||
constructor() { }
|
||||
searchFormControl = new FormControl();
|
||||
|
||||
readonly separatorKeysCodes = [ ENTER, COMMA, SPACE ] as const;
|
||||
|
||||
constructor(private tagsService: TagsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.checkFilterOpen();
|
||||
this.boundTagspickerChange = this.tagspickerChange.bind(this);
|
||||
this.searchTags = this.searchFormControl
|
||||
.valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
switchMap(value => this.tagsService.search(value))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
checkFilterOpen() {
|
||||
this.filterOpen = false;
|
||||
if (this.entries.filter) {
|
||||
for (const param in this.entries.filter) {
|
||||
if (this.entries.filter[ param ]) {
|
||||
this.filterOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagspickerChange(value: any) {
|
||||
console.log("change", value);
|
||||
this.setFilter('tag', value);
|
||||
}
|
||||
|
||||
setFilter(key: string, value) {
|
||||
if (value != this.entries.filter[ key ]) {
|
||||
this.entries.filter[ key ] = value;
|
||||
this.entries.number = 0;
|
||||
this.update({ pageIndex: this.entries.number, pageSize: this.entries.size, length: this.entries.totalElements });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@
|
||||
<a class="title" *ngIf="entry.url" [href]="entry.url" target="_blank">{{entry.title}}</a>
|
||||
<a class="title" *ngIf="!entry.url" routerLink="/e/{{entry.id}}">{{entry.title}}</a>
|
||||
<span *ngIf="entry.url" class="urlbase">(<a [href]="entry.url">{{entry.url | urlbase}}</a>)</span>
|
||||
<a *ngFor="let tag of entry.tags" class="tag" (click)="applyTag(tag)">#{{tag}}</a>
|
||||
<a *ngIf="entry.metadata.author && !entry.metadata.edit" class="tags-edit tag"
|
||||
routerLink="/e/{{entry.id}}/edit"><mat-icon inline="true">local_offer</mat-icon>{{'entry.edit.tags' | i18n}}</a>
|
||||
</div>
|
||||
<div mat-line>
|
||||
<small>
|
||||
@ -31,8 +34,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.edit"> | </span>
|
||||
<a *ngIf="entry.metadata.edit" 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}}">
|
||||
@ -56,8 +59,8 @@
|
||||
i18n}}">
|
||||
<mat-icon inline="true">flag</mat-icon>
|
||||
</a>
|
||||
<span *ngIf="canEdit()"> | </span>
|
||||
<a *ngIf="canEdit()" href="javascript:" (click)="deleteEntry()">{{'entry.delete' | i18n}}</a>
|
||||
<span *ngIf="entry.metadata.edit"> | </span>
|
||||
<a *ngIf="entry.metadata.edit" 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' |
|
||||
|
@ -52,3 +52,48 @@ span.mod {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
|
||||
.tag {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
font-size: 9px;
|
||||
background-color: #616161;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 2px 5px;
|
||||
margin-left: 3px;
|
||||
top: -1px;
|
||||
|
||||
.mat-icon {
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag::after {
|
||||
background-color: #000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
transition: opacity 200ms cubic-bezier(0.35, 0, 0.25, 1);
|
||||
}
|
||||
|
||||
.tag:hover::after {
|
||||
opacity: .12;
|
||||
}
|
||||
|
||||
.tags-edit.tag {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.tags-edit:hover.tag {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ 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';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-entry',
|
||||
@ -16,19 +17,18 @@ import { EntriesService } from 'src/app/services/entries.service';
|
||||
})
|
||||
export class UiEntry implements OnInit {
|
||||
|
||||
author: boolean = false;
|
||||
moderator: boolean = false;
|
||||
@Input() entry: any;
|
||||
@Input() index: number;
|
||||
@Input() linkedTag: boolean = true;
|
||||
@Input() change: Function;
|
||||
|
||||
constructor(private authService: AuthService, private entriesService: EntriesService, private voteService: VoteService, private flagService: FlagService,
|
||||
private moderationService: ModerationService, private bookmarksService: BookmarksService, public dialog: MatDialog) { }
|
||||
private moderationService: ModerationService, private bookmarksService: BookmarksService, public dialog: MatDialog, private router: Router, private route: ActivatedRoute) { }
|
||||
|
||||
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;
|
||||
@ -38,6 +38,19 @@ export class UiEntry implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
applyTag(tag) {
|
||||
if (this.linkedTag) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.router.navigate(
|
||||
[],
|
||||
{
|
||||
relativeTo: this.route,
|
||||
queryParams: { 'tag': tag },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
voteUp() {
|
||||
this.voteService.voteEntryUp(this.entry.id).subscribe((result) => {
|
||||
this.change && this.change();
|
||||
@ -84,11 +97,6 @@ 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: {
|
||||
|
18
src/app/ui/tags/tagspicker.ui.html
Normal file
18
src/app/ui/tags/tagspicker.ui.html
Normal file
@ -0,0 +1,18 @@
|
||||
<mat-form-field [ngClass]="singleton ? 'singleton' : ''">
|
||||
<mat-chip-list #tagList>
|
||||
<mat-chip *ngFor="let tag of tags" [removable]="true" (removed)="removeTag(tag)">
|
||||
<mat-icon inline="true">tag</mat-icon>{{tag}}
|
||||
<button matChipRemove>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip>
|
||||
<input *ngIf="!singleton || !tags || tags.length < 1" #tagsInput placeholder="{{placeholder}}"
|
||||
[formControl]="searchFormControl" [matAutocomplete]="auto" [matChipInputFor]="tagList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" (matChipInputTokenEnd)="addInputTag($event)">
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="addOptionTag($event)">
|
||||
<mat-option *ngFor="let option of searchTags | async" [value]="option">
|
||||
<mat-icon inline="true">tag</mat-icon>{{option}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
16
src/app/ui/tags/tagspicker.ui.scss
Normal file
16
src/app/ui/tags/tagspicker.ui.scss
Normal file
@ -0,0 +1,16 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-form-field.singleton {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
mat-chip mat-icon.mat-icon-inline {
|
||||
margin-top: -12px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.mat-option .mat-icon {
|
||||
margin-right: -2px;
|
||||
}
|
95
src/app/ui/tags/tagspicker.ui.ts
Normal file
95
src/app/ui/tags/tagspicker.ui.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { Observable } from 'rxjs';
|
||||
import { debounceTime, switchMap } from 'rxjs/operators';
|
||||
import { TagsService } from 'src/app/services/tags.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-tagspicker',
|
||||
templateUrl: './tagspicker.ui.html',
|
||||
styleUrls: [ './tagspicker.ui.scss' ]
|
||||
})
|
||||
export class UiTagsPicker implements OnInit {
|
||||
|
||||
@Input() change: Function;
|
||||
@Input() model: any;
|
||||
@Input() placeholder: string;
|
||||
@Input() singleton: boolean = false;
|
||||
tags: string[] = [];
|
||||
searchTags: Observable<Object>;
|
||||
|
||||
@ViewChild('tagsInput') tagsInput: ElementRef<HTMLInputElement>;
|
||||
searchFormControl = new FormControl();
|
||||
|
||||
readonly separatorKeysCodes = [ ENTER, COMMA, SPACE ] as const;
|
||||
|
||||
constructor(private tagsService: TagsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchTags = this.searchFormControl
|
||||
.valueChanges
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
switchMap(value => this.tagsService.search(value))
|
||||
);
|
||||
|
||||
if (this.singleton) {
|
||||
if (this.model) {
|
||||
this.tags = [ this.model ];
|
||||
}
|
||||
} else {
|
||||
this.tags = this.model || [];
|
||||
}
|
||||
}
|
||||
|
||||
addTag(tag: string) {
|
||||
if (tag.startsWith('#')) {
|
||||
tag = tag.replace('#', '');
|
||||
}
|
||||
tag = tag.split('#').join('-');
|
||||
if (tag && this.tags.indexOf(tag) == -1) {
|
||||
this.tags.push(tag);
|
||||
}
|
||||
if (this.singleton) {
|
||||
this.model = this.tags && this.tags[ 0 ] || undefined;
|
||||
} else {
|
||||
this.model = this.tags;
|
||||
}
|
||||
if (this.change) {
|
||||
this.change(this.model);
|
||||
}
|
||||
this.tagsInput.nativeElement.value = '';
|
||||
}
|
||||
|
||||
addInputTag(event: MatChipInputEvent): void {
|
||||
this.addTag((event.value || "").trim())
|
||||
event.chipInput!.clear();
|
||||
}
|
||||
|
||||
addOptionTag(event: MatAutocompleteSelectedEvent): void {
|
||||
this.addTag((event.option && event.option.value || "").trim());
|
||||
}
|
||||
|
||||
removeTag(tag: string): void {
|
||||
const index = this.tags.indexOf(tag);
|
||||
if (index >= 0) {
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.singleton) {
|
||||
this.model = undefined;
|
||||
} else {
|
||||
this.model = this.tags;
|
||||
}
|
||||
|
||||
if (this.change) {
|
||||
this.change(this.model);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user