Add UI to edit tags

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent df437a6cec
commit bc74482098

@ -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 {FilesTabSidebarComponent} from './pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component';
import {MatExpansionModule} from "@angular/material/expansion"; import {MatExpansionModule} from "@angular/material/expansion";
import {TagItemComponent} from './components/tag-item/tag-item.component'; import {TagItemComponent} from './components/tag-item/tag-item.component';
import { FileEditComponent } from './components/file-edit/file-edit.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -67,6 +68,7 @@ import {TagItemComponent} from './components/tag-item/tag-item.component';
ConfirmDialogComponent, ConfirmDialogComponent,
FilesTabSidebarComponent, FilesTabSidebarComponent,
TagItemComponent, TagItemComponent,
FileEditComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

@ -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 matInput>
</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">
</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,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<string[]>;
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<void> {
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);
}
}

@ -135,7 +135,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
return this.validTags.filter( return this.validTags.filter(
t => t.includes(normalizedTag) && this.searchTags.findIndex( t => t.includes(normalizedTag) && this.searchTags.findIndex(
s => s.name === t) < 0) s => s.getNormalizedTag() === t) < 0)
.map(t => negated ? "-" + t : t) .map(t => negated ? "-" + t : t)
.slice(0, 20); .slice(0, 20);
} }

@ -2,6 +2,8 @@
display: inline; display: inline;
width: 100%; width: 100%;
padding: 0.25em; padding: 0.25em;
word-break: break-all;
text-overflow: ellipsis;
} }
.tag-item-namespace { .tag-item-namespace {

@ -21,5 +21,8 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="Edit" *ngIf="this.selectedFiles.length > 0">
<app-file-edit #fileedit [files]="this.selectedFiles" [tags]="tagsOfSelection"></app-file-edit>
</mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>

@ -8,7 +8,7 @@ app-file-search {
overflow: hidden; overflow: hidden;
} }
mat-tab-group, mat-tab, .file-tag-list { mat-tab-group, mat-tab, .file-tag-list, app-file-edit {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
@ -45,17 +45,13 @@ mat-selection-list {
cursor: pointer; cursor: pointer;
} }
.file-tag-list-inner {
display: block;
height: 100%;
width: 100%;
overflow: hidden;
}
cdk-virtual-scroll-viewport { cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%;
}
} }
mat-divider { mat-divider {

@ -14,6 +14,7 @@ import {FileService} from "../../../../services/file/file.service";
import {File} from "../../../../models/File"; import {File} from "../../../../models/File";
import {FileSearchComponent} from "../../../../components/file-search/file-search.component"; import {FileSearchComponent} from "../../../../components/file-search/file-search.component";
import {RepositoryService} from "../../../../services/repository/repository.service"; import {RepositoryService} from "../../../../services/repository/repository.service";
import {FileEditComponent} from "../../../../components/file-edit/file-edit.component";
@Component({ @Component({
selector: 'app-files-tab-sidebar', selector: 'app-files-tab-sidebar',
@ -27,10 +28,12 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
@Output() searchEndEvent = new EventEmitter<void>(); @Output() searchEndEvent = new EventEmitter<void>();
@ViewChild('filesearch') fileSearch!: FileSearchComponent; @ViewChild('filesearch') fileSearch!: FileSearchComponent;
@ViewChild("fileedit") fileEdit: FileEditComponent | undefined;
public tagsOfFiles: Tag[] = []; public tagsOfFiles: Tag[] = [];
public tags: Tag[] = []; public tags: Tag[] = [];
public files: File[] = []; public files: File[] = [];
public tagsOfSelection: Tag[] = [];
constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) { constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) {
this.fileService.displayedFiles.subscribe(async files => { this.fileService.displayedFiles.subscribe(async files => {
@ -72,9 +75,10 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
} }
async showFileDetails(files: File[]) { async showFileDetails(files: File[]) {
this.tags = await this.tagService.getTagsForFiles(files.map(f => f.hash)) this.tagsOfSelection = await this.tagService.getTagsForFiles(files.map(f => f.hash))
this.tags = this.tags.sort( this.tagsOfSelection = this.tagsOfSelection.sort(
(a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput())); (a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput()));
this.tags = this.tagsOfSelection;
} }
private async refreshFileSelection() { private async refreshFileSelection() {

Loading…
Cancel
Save