From bc74482098a0ade2189c4ac2a562d1a71acef982 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 6 Nov 2021 12:43:39 +0100 Subject: [PATCH] Add UI to edit tags Signed-off-by: trivernis --- mediarepo-ui/src/app/app.module.ts | 2 + .../file-edit/file-edit.component.html | 48 +++++++++ .../file-edit/file-edit.component.scss | 70 ++++++++++++ .../file-edit/file-edit.component.spec.ts | 25 +++++ .../file-edit/file-edit.component.ts | 100 ++++++++++++++++++ .../file-search/file-search.component.ts | 2 +- .../tag-item/tag-item.component.scss | 2 + .../files-tab-sidebar.component.html | 3 + .../files-tab-sidebar.component.scss | 12 +-- .../files-tab-sidebar.component.ts | 8 +- 10 files changed, 261 insertions(+), 11 deletions(-) create mode 100644 mediarepo-ui/src/app/components/file-edit/file-edit.component.html create mode 100644 mediarepo-ui/src/app/components/file-edit/file-edit.component.scss create mode 100644 mediarepo-ui/src/app/components/file-edit/file-edit.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/file-edit/file-edit.component.ts diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index ce93bac..f443a4e 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -48,6 +48,7 @@ import {ConfirmDialogComponent} from './components/confirm-dialog/confirm-dialog import {FilesTabSidebarComponent} from './pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component'; import {MatExpansionModule} from "@angular/material/expansion"; import {TagItemComponent} from './components/tag-item/tag-item.component'; +import { FileEditComponent } from './components/file-edit/file-edit.component'; @NgModule({ declarations: [ @@ -67,6 +68,7 @@ import {TagItemComponent} from './components/tag-item/tag-item.component'; ConfirmDialogComponent, FilesTabSidebarComponent, TagItemComponent, + FileEditComponent, ], imports: [ BrowserModule, diff --git a/mediarepo-ui/src/app/components/file-edit/file-edit.component.html b/mediarepo-ui/src/app/components/file-edit/file-edit.component.html new file mode 100644 index 0000000..37352c2 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-edit/file-edit.component.html @@ -0,0 +1,48 @@ +
+ +
+ +
+ + +
+
+
+ +
+
+ + {{modeSelect.value}} tag + + + + + + {{tag}} + + + + +
+ + Mode + + Toggle + Add + Remove + + +
+
diff --git a/mediarepo-ui/src/app/components/file-edit/file-edit.component.scss b/mediarepo-ui/src/app/components/file-edit/file-edit.component.scss new file mode 100644 index 0000000..92d6ccc --- /dev/null +++ b/mediarepo-ui/src/app/components/file-edit/file-edit.component.scss @@ -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%; +} diff --git a/mediarepo-ui/src/app/components/file-edit/file-edit.component.spec.ts b/mediarepo-ui/src/app/components/file-edit/file-edit.component.spec.ts new file mode 100644 index 0000000..51ab4bb --- /dev/null +++ b/mediarepo-ui/src/app/components/file-edit/file-edit.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FileEditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/file-edit/file-edit.component.ts b/mediarepo-ui/src/app/components/file-edit/file-edit.component.ts new file mode 100644 index 0000000..b11e6bf --- /dev/null +++ b/mediarepo-ui/src/app/components/file-edit/file-edit.component.ts @@ -0,0 +1,100 @@ +import {Component, Input, OnInit, 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"; + +@Component({ + selector: 'app-file-edit', + templateUrl: './file-edit.component.html', + styleUrls: ['./file-edit.component.scss'] +}) +export class FileEditComponent implements OnInit { + + @Input() files: File[] = []; + @Input() tags: Tag[] = []; + + private allTags: Tag[] = []; + + public suggestionTags: Observable; + public tagInputForm = new FormControl(""); + public editMode: string = "Toggle"; + + @ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport; + + constructor( + private tagService: TagService, + ) { + 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(); + } + + public async editTagByAutocomplete($event: MatAutocompleteSelectedEvent) { + const tag = $event.option.value.trim(); + await this.editTag(tag); + } + + private async editTag(tag: string): Promise { + if (tag.length > 0) { + let tagInstance = this.allTags.find(t => t.getNormalizedOutput() === tag); + + if (!tagInstance) { + // TODO: Create tag + tagInstance = new Tag(0, "", undefined); + } + 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) { + + } + + async addTag(tag: Tag) { + 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) { + 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); + } +} diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.ts b/mediarepo-ui/src/app/components/file-search/file-search.component.ts index e58462a..19da0a3 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.ts +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.ts @@ -135,7 +135,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit { return this.validTags.filter( t => t.includes(normalizedTag) && this.searchTags.findIndex( - s => s.name === t) < 0) + s => s.getNormalizedTag() === t) < 0) .map(t => negated ? "-" + t : t) .slice(0, 20); } diff --git a/mediarepo-ui/src/app/components/tag-item/tag-item.component.scss b/mediarepo-ui/src/app/components/tag-item/tag-item.component.scss index 5e91837..6911049 100644 --- a/mediarepo-ui/src/app/components/tag-item/tag-item.component.scss +++ b/mediarepo-ui/src/app/components/tag-item/tag-item.component.scss @@ -2,6 +2,8 @@ display: inline; width: 100%; padding: 0.25em; + word-break: break-all; + text-overflow: ellipsis; } .tag-item-namespace { diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html index b5b3c49..a86bcb8 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html @@ -21,5 +21,8 @@ + + + diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.scss b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.scss index 30b9a87..fcdf3f8 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.scss +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.scss @@ -8,7 +8,7 @@ app-file-search { overflow: hidden; } -mat-tab-group, mat-tab, .file-tag-list { +mat-tab-group, mat-tab, .file-tag-list, app-file-edit { height: 100%; width: 100%; } @@ -45,17 +45,13 @@ mat-selection-list { cursor: pointer; } -.file-tag-list-inner { - display: block; - height: 100%; - width: 100%; - overflow: hidden; -} - cdk-virtual-scroll-viewport { height: 100%; width: 100%; overflow-y: auto; + ::ng-deep .cdk-virtual-scroll-content-wrapper { + width: 100%; + } } mat-divider { diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts index 69b2706..062c8c2 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts @@ -14,6 +14,7 @@ import {FileService} from "../../../../services/file/file.service"; import {File} from "../../../../models/File"; import {FileSearchComponent} from "../../../../components/file-search/file-search.component"; import {RepositoryService} from "../../../../services/repository/repository.service"; +import {FileEditComponent} from "../../../../components/file-edit/file-edit.component"; @Component({ selector: 'app-files-tab-sidebar', @@ -27,10 +28,12 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { @Output() searchEndEvent = new EventEmitter(); @ViewChild('filesearch') fileSearch!: FileSearchComponent; + @ViewChild("fileedit") fileEdit: FileEditComponent | undefined; public tagsOfFiles: Tag[] = []; public tags: Tag[] = []; public files: File[] = []; + public tagsOfSelection: Tag[] = []; constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) { this.fileService.displayedFiles.subscribe(async files => { @@ -72,9 +75,10 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { } async showFileDetails(files: File[]) { - this.tags = await this.tagService.getTagsForFiles(files.map(f => f.hash)) - this.tags = this.tags.sort( + this.tagsOfSelection = await this.tagService.getTagsForFiles(files.map(f => f.hash)) + this.tagsOfSelection = this.tagsOfSelection.sort( (a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput())); + this.tags = this.tagsOfSelection; } private async refreshFileSelection() {