commit
e61624ce6e
@ -0,0 +1,48 @@
|
|||||||
|
<div class="file-edit-inner" fxLayout="column">
|
||||||
|
<div class="file-metadata" fxFlex="150px">
|
||||||
|
<h1>Edit File</h1>
|
||||||
|
<mat-form-field *ngIf="this.files.length === 1" appearance="fill">
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input #fileNameInput matInput (focusout)="this.changeFileName(fileNameInput.value)">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="tag-edit-list" fxFlex fxFlexFill fxFlexAlign="start">
|
||||||
|
<cdk-virtual-scroll-viewport #tagScroll itemSize="50" maxBufferPx="2000" minBufferPx="1000">
|
||||||
|
<div class="editable-tag" *cdkVirtualFor="let tag of tags">
|
||||||
|
<app-tag-item [tag]="tag"></app-tag-item>
|
||||||
|
<button class="tag-remove-button" mat-icon-button (click)="removeTag(tag)">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
</div>
|
||||||
|
<mat-divider fxFlex="1em"></mat-divider>
|
||||||
|
<div class="tag-input" fxFlex="200px">
|
||||||
|
<div class="tag-input-field">
|
||||||
|
<mat-form-field class="form-field-tag-input" appearance="fill">
|
||||||
|
<mat-label>{{modeSelect.value}} tag</mat-label>
|
||||||
|
<input [formControl]="tagInputForm" matInput [matAutocomplete]="auto" (keydown)="this.handleTagInputKeydown($event)" (submit)="this.editTag(tagInputForm.value)">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-autocomplete #auto (optionSelected)="editTagByAutocomplete($event)">
|
||||||
|
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
|
||||||
|
{{tag}}
|
||||||
|
</mat-option>
|
||||||
|
|
||||||
|
</mat-autocomplete>
|
||||||
|
<button mat-icon-button class="add-tag-button">
|
||||||
|
<mat-icon *ngIf="editMode === 'Toggle'">change_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="editMode === 'Add'">add_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="editMode === 'Remove'">remove_circle</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-form-field class="form-field-mode" appearance="fill">
|
||||||
|
<mat-label>Mode</mat-label>
|
||||||
|
<mat-select #modeSelect [(value)]="editMode">
|
||||||
|
<mat-option value="Toggle">Toggle</mat-option>
|
||||||
|
<mat-option value="Add">Add</mat-option>
|
||||||
|
<mat-option value="Remove">Remove</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,70 @@
|
|||||||
|
.file-metadata, .tag-input {
|
||||||
|
width: 100%;
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.form-field-mode {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.form-field-tag-input {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-edit-inner {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-edit-list {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
cdk-virtual-scroll-viewport {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
::ng-deep .cdk-virtual-scroll-content-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-tag {
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.2em;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
app-tag-item {
|
||||||
|
margin: auto auto auto 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove-button {
|
||||||
|
margin-right: 1em;
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
.add-tag-button {
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-divider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileEditComponent } from './file-edit.component';
|
||||||
|
|
||||||
|
describe('FileEditComponent', () => {
|
||||||
|
let component: FileEditComponent;
|
||||||
|
let fixture: ComponentFixture<FileEditComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ FileEditComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileEditComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,186 @@
|
|||||||
|
import {
|
||||||
|
Component, ElementRef,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import {FormControl} from "@angular/forms";
|
||||||
|
import {File} from "../../models/File";
|
||||||
|
import {Tag} from "../../models/Tag";
|
||||||
|
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
|
||||||
|
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {map, startWith} from "rxjs/operators";
|
||||||
|
import {TagService} from "../../services/tag/tag.service";
|
||||||
|
import {FileService} from "../../services/file/file.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-file-edit',
|
||||||
|
templateUrl: './file-edit.component.html',
|
||||||
|
styleUrls: ['./file-edit.component.scss']
|
||||||
|
})
|
||||||
|
export class FileEditComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
@Input() files: File[] = [];
|
||||||
|
public tags: Tag[] = [];
|
||||||
|
|
||||||
|
private allTags: Tag[] = [];
|
||||||
|
private fileTags: {[key: number]: Tag[]} = {};
|
||||||
|
|
||||||
|
public suggestionTags: Observable<string[]>;
|
||||||
|
public tagInputForm = new FormControl("");
|
||||||
|
public editMode: string = "Toggle";
|
||||||
|
|
||||||
|
@ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport;
|
||||||
|
@ViewChild("fileNameInput") fileNameInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private tagService: TagService,
|
||||||
|
private fileService: FileService,
|
||||||
|
) {
|
||||||
|
this.suggestionTags = this.tagInputForm.valueChanges.pipe(startWith(null),
|
||||||
|
map(
|
||||||
|
(tag: string | null) => tag ? this.filterSuggestionTag(
|
||||||
|
tag) : this.allTags.slice(0, 20).map(t => t.getNormalizedOutput())));
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.tagService.tags.subscribe(tags => this.allTags = tags);
|
||||||
|
await this.tagService.loadTags();
|
||||||
|
await this.loadFileTags();
|
||||||
|
this.resetFileNameInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
|
if (changes["files"]) {
|
||||||
|
await this.loadFileTags()
|
||||||
|
this.resetFileNameInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async changeFileName(value: string) {
|
||||||
|
const name = value.trim();
|
||||||
|
|
||||||
|
if (name.length > 0) {
|
||||||
|
const file = this.files[0];
|
||||||
|
console.log("Updating name to", name);
|
||||||
|
const responseFile = await this.fileService.updateFileName(file, name);
|
||||||
|
console.log("Updated name");
|
||||||
|
file.name = responseFile.name;
|
||||||
|
this.resetFileNameInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async editTagByAutocomplete($event: MatAutocompleteSelectedEvent) {
|
||||||
|
const tag = $event.option.value.trim();
|
||||||
|
await this.editTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async editTag(tag: string): Promise<void> {
|
||||||
|
if (tag.length > 0) {
|
||||||
|
let tagInstance = this.allTags.find(t => t.getNormalizedOutput() === tag);
|
||||||
|
|
||||||
|
if (!tagInstance) {
|
||||||
|
tagInstance = (await this.tagService.createTags([tag]))[0];
|
||||||
|
this.allTags.push(tagInstance);
|
||||||
|
}
|
||||||
|
switch (this.editMode) {
|
||||||
|
case "Toggle":
|
||||||
|
await this.toggleTag(tagInstance);
|
||||||
|
break;
|
||||||
|
case "Add":
|
||||||
|
await this.addTag(tagInstance);
|
||||||
|
break;
|
||||||
|
case "Remove":
|
||||||
|
await this.removeTag(tagInstance);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.tagInputForm.setValue("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleTag(tag: Tag) {
|
||||||
|
for (const file of this.files) {
|
||||||
|
const fileTags = this.fileTags[file.id];
|
||||||
|
let addedTags = [];
|
||||||
|
let removedTags = [];
|
||||||
|
if (fileTags.findIndex(i => i.id === tag.id) < 0) {
|
||||||
|
addedTags.push(tag.id);
|
||||||
|
} else {
|
||||||
|
removedTags.push(tag.id);
|
||||||
|
}
|
||||||
|
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id, addedTags, removedTags);
|
||||||
|
}
|
||||||
|
this.mapFileTagsToTagList();
|
||||||
|
const index = this.tags.indexOf(tag);
|
||||||
|
index >= 0 && this.tagScroll.scrollToIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTag(tag: Tag) {
|
||||||
|
for (const file of this.files) {
|
||||||
|
if (this.fileTags[file.id].findIndex(t => t.id === tag.id) < 0) {
|
||||||
|
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id,
|
||||||
|
[tag.id], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.tags.findIndex(t => t.getNormalizedOutput() === tag.getNormalizedOutput()) < 0) {
|
||||||
|
this.tags.push(tag);
|
||||||
|
this.tags = this.tags.sort(
|
||||||
|
(a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput()));
|
||||||
|
this.tags = [...this.tags]; // angular pls detect it wtf
|
||||||
|
}
|
||||||
|
const index = this.tags.indexOf(tag);
|
||||||
|
index >= 0 && this.tagScroll.scrollToIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeTag(tag: Tag) {
|
||||||
|
for (const file of this.files) {
|
||||||
|
if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) {
|
||||||
|
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id,
|
||||||
|
[], [tag.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const index = this.tags.indexOf(tag);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.tags.splice(index, 1);
|
||||||
|
this.tags = [...this.tags]; // so angular detects the change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterSuggestionTag(tag: string) {
|
||||||
|
const allTags = this.allTags.map(t => t.getNormalizedOutput());
|
||||||
|
return allTags.filter(
|
||||||
|
t => t.includes(tag))
|
||||||
|
.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFileTags() {
|
||||||
|
for (const file of this.files) {
|
||||||
|
this.fileTags[file.id] = await this.tagService.getTagsForFiles([file.hash]);
|
||||||
|
}
|
||||||
|
this.mapFileTagsToTagList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetFileNameInput() {
|
||||||
|
if (this.files.length === 1) {
|
||||||
|
this.fileNameInput.nativeElement.value = this.files[0].name ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapFileTagsToTagList() {
|
||||||
|
let tags: Tag[] = [];
|
||||||
|
for (const file of this.files) {
|
||||||
|
const fileTags = this.fileTags[file.id];
|
||||||
|
tags.push(...fileTags.filter(t => tags.findIndex(tag => tag.id === t.id) < 0));
|
||||||
|
}
|
||||||
|
this.tags = tags.sort((a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleTagInputKeydown($event: KeyboardEvent) {
|
||||||
|
if ($event.key === "Enter") {
|
||||||
|
await this.editTag(this.tagInputForm.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue