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