diff --git a/client/src/app/core/services/app-load.service.ts b/client/src/app/core/services/app-load.service.ts index f0ef8a4c1..374bad9fc 100644 --- a/client/src/app/core/services/app-load.service.ts +++ b/client/src/app/core/services/app-load.service.ts @@ -10,6 +10,7 @@ import { ConfigAppConfig } from '../../site/config/config.config'; import { AgendaAppConfig } from '../../site/agenda/agenda.config'; import { AssignmentsAppConfig } from '../../site/assignments/assignments.config'; import { UsersAppConfig } from '../../site/users/users.config'; +import { TagAppConfig } from '../../site/tags/tag.config'; import { MainMenuService } from './main-menu.service'; /** @@ -22,6 +23,7 @@ const appConfigs: AppConfig[] = [ AssignmentsAppConfig, MotionsAppConfig, MediafileAppConfig, + TagAppConfig, UsersAppConfig ]; diff --git a/client/src/app/site/site-routing.module.ts b/client/src/app/site/site-routing.module.ts index af4feb22b..4c9487b9c 100644 --- a/client/src/app/site/site-routing.module.ts +++ b/client/src/app/site/site-routing.module.ts @@ -42,6 +42,10 @@ const routes: Routes = [ { path: 'users', loadChildren: './users/users.module#UsersModule' + }, + { + path: 'tags', + loadChildren: './tags/tag.module#TagModule' } ], canActivateChild: [AuthGuard] diff --git a/client/src/app/site/tags/components/tag-list.component.css b/client/src/app/site/tags/components/tag-list.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/tags/components/tag-list.component.html b/client/src/app/site/tags/components/tag-list.component.html new file mode 100644 index 000000000..7188d218d --- /dev/null +++ b/client/src/app/site/tags/components/tag-list.component.html @@ -0,0 +1,33 @@ + + + + + Tags + + + + A tag name is required + + + + + + + + delete + Delete + + + + + + + Name + {{tag.getTitle()}} + + + + + + diff --git a/client/src/app/site/tags/components/tag-list.component.spec.ts b/client/src/app/site/tags/components/tag-list.component.spec.ts new file mode 100644 index 000000000..2a17b6e37 --- /dev/null +++ b/client/src/app/site/tags/components/tag-list.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TagListComponent } from './tag-list.component'; +import { E2EImportsModule } from '../../../../e2e-imports.module'; + +describe('TagListComponent', () => { + let component: TagListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [TagListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TagListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/tags/components/tag-list.component.ts b/client/src/app/site/tags/components/tag-list.component.ts new file mode 100644 index 000000000..e857e4efe --- /dev/null +++ b/client/src/app/site/tags/components/tag-list.component.ts @@ -0,0 +1,140 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Title } from '@angular/platform-browser'; +import { Tag } from '../../../shared/models/core/tag'; +import { ListViewBaseComponent } from '../../base/list-view-base'; +import { TagRepositoryService } from '../services/tag-repository.service'; +import { ViewTag } from '../models/view-tag'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { PromptService } from '../../../core/services/prompt.service'; + +/** + * Listview for the complete lsit of available Tags + * ### Usage: + * ```html + * + * ``` + */ +@Component({ + selector: 'os-tag-list', + templateUrl: './tag-list.component.html', + styleUrls: ['./tag-list.component.css'] +}) +export class TagListComponent extends ListViewBaseComponent implements OnInit { + public editTag = false; + public newTag = false; + public selectedTag: ViewTag; + + @ViewChild('tagForm') + public tagForm: FormGroup; + + /** + * Constructor. + * @param titleService + * @param translate + * @param repo the repository + */ + public constructor( + titleService: Title, + translate: TranslateService, + private repo: TagRepositoryService, + private promptService: PromptService + ) { + super(titleService, translate); + } + + /** + * Init function. + * Sets the title, inits the table and calls the repo. + */ + public ngOnInit(): void { + super.setTitle('Tags'); + this.initTable(); + this.tagForm = new FormGroup({ name: new FormControl('', Validators.required) }); + this.repo.getViewModelListObservable().subscribe(newTags => { + this.dataSource.data = newTags; + }); + } + + /** + * Sends a new or updates tag to the dataStore + */ + public saveTag(): void { + if (this.editTag && this.newTag) { + this.submitNewTag(); + } else if (this.editTag && !this.newTag) { + this.submitEditedTag(); + } + } + + /** + * Saves a newly created tag. + */ + public submitNewTag(): void { + if (this.tagForm.value && this.tagForm.valid) { + this.repo.create(this.tagForm.value).subscribe(response => { + if (response) { + this.tagForm.reset(); + this.cancelEditing(); + } + }); + } + } + + /** + * Saves an edited tag. + */ + public submitEditedTag(): void { + if (this.tagForm.value && this.tagForm.valid) { + const updateData = new Tag({ name: this.tagForm.value.name }); + + this.repo.update(updateData, this.selectedTag).subscribe(response => { + if (response) { + this.cancelEditing(); + } + }); + } + } + + /** + * Deletes the selected Tag after a successful confirmation. + * @async + */ + public async deleteSelectedTag(): Promise { + const content = this.translate.instant('Delete') + ` ${this.selectedTag.name}?`; + if (await this.promptService.open(this.translate.instant('Are you sure?'), content)) { + this.repo.delete(this.selectedTag).subscribe(response => { + this.cancelEditing(); + }); + } + } + + public cancelEditing(): void { + this.newTag = false; + this.editTag = false; + this.tagForm.reset(); + } + + /** + * Select a row in the table + * @param viewTag + */ + public selectTag(viewTag: ViewTag): void { + this.selectedTag = viewTag; + this.setEditMode(true, false); + this.tagForm.setValue({ name: this.selectedTag.name }); + } + + public setEditMode(mode: boolean, newTag: boolean = true): void { + this.editTag = mode; + this.newTag = newTag; + if (!mode) { + this.cancelEditing(); + } + } + public keyDownFunction(event: KeyboardEvent): void { + if (event.keyCode === 27) { + this.cancelEditing(); + } + } +} diff --git a/client/src/app/site/tags/models/view-tag.ts b/client/src/app/site/tags/models/view-tag.ts new file mode 100644 index 000000000..806472a6e --- /dev/null +++ b/client/src/app/site/tags/models/view-tag.ts @@ -0,0 +1,42 @@ +import { Tag } from '../../../shared/models/core/tag'; +import { BaseViewModel } from '../../base/base-view-model'; + +/** + * Tag view class + * + * Stores a Tag including all (implicit) references + * Provides "safe" access to variables and functions in {@link Tag} + * @ignore + */ +export class ViewTag extends BaseViewModel { + private _tag: Tag; + + public constructor(tag: Tag) { + super(); + this._tag = tag; + } + + public get tag(): Tag { + return this._tag; + } + + public get id(): number { + return this.tag ? this.tag.id : null; + } + + public get name(): string { + return this.tag ? this.tag.name : null; + } + + public getTitle(): string { + return this.name; + } + + /** + * Updates the local objects if required + * @param update + */ + public updateValues(update: Tag): void { + this._tag = update; + } +} diff --git a/client/src/app/site/tags/services/tag-repository.service.spec.ts b/client/src/app/site/tags/services/tag-repository.service.spec.ts new file mode 100644 index 000000000..92abad209 --- /dev/null +++ b/client/src/app/site/tags/services/tag-repository.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { TagRepositoryService } from './tag-repository.service'; +import { E2EImportsModule } from '../../../../e2e-imports.module'; + +describe('TagRepositoryService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [TagRepositoryService] + }); + }); + + it('should be created', () => { + const service = TestBed.get(TagRepositoryService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/tags/services/tag-repository.service.ts b/client/src/app/site/tags/services/tag-repository.service.ts new file mode 100644 index 000000000..5bc15c423 --- /dev/null +++ b/client/src/app/site/tags/services/tag-repository.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { Tag } from '../../../shared/models/core/tag'; +import { ViewTag } from '../models/view-tag'; +import { DataSendService } from '../../../core/services/data-send.service'; +import { Observable } from 'rxjs'; +import { DataStoreService } from '../../../core/services/data-store.service'; +import { BaseRepository } from '../../base/base-repository'; +import { HTTPMethod } from 'app/core/services/http.service'; + +/** + * Repository Services for Tags + * + * The repository is meant to process domain objects (those found under + * shared/models), so components can display them and interact with them. + * + * Rather than manipulating models directly, the repository is meant to + * inform the {@link DataSendService} about changes which will send + * them to the Server. + */ +@Injectable({ + providedIn: 'root' +}) +export class TagRepositoryService extends BaseRepository { + /** + * Creates a TagRepository + * Converts existing and incoming Tags to ViewTags + * Handles CRUD using an observer to the DataStore + * @param DataSend + */ + public constructor(protected DS: DataStoreService, private dataSend: DataSendService) { + super(DS, Tag); + } + + protected createViewModel(tag: Tag): ViewTag { + return new ViewTag(tag); + } + + public create(update: Tag): Observable { + const newTag = new Tag(); + newTag.patchValues(update); + return this.dataSend.createModel(newTag); + } + + public update(update: Partial, viewTag: ViewTag): Observable { + const updateTag = new Tag(); + updateTag.patchValues(viewTag.tag); + updateTag.patchValues(update); + return this.dataSend.updateModel(updateTag, HTTPMethod.PUT); + } + + public delete(viewTag: ViewTag): Observable { + return this.dataSend.deleteModel(viewTag.tag); + } +} diff --git a/client/src/app/site/tags/tag-routing.module.ts b/client/src/app/site/tags/tag-routing.module.ts new file mode 100644 index 000000000..28d767198 --- /dev/null +++ b/client/src/app/site/tags/tag-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { TagListComponent } from './components/tag-list.component'; + +const routes: Routes = [{ path: '', component: TagListComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class TagRoutingModule {} diff --git a/client/src/app/site/tags/tag.config.ts b/client/src/app/site/tags/tag.config.ts new file mode 100644 index 000000000..c44a41a80 --- /dev/null +++ b/client/src/app/site/tags/tag.config.ts @@ -0,0 +1,16 @@ +import { AppConfig } from '../base/app-config'; +import { Tag } from '../../shared/models/core/tag'; + +export const TagAppConfig: AppConfig = { + name: 'tag', + models: [{ collectionString: 'core/tag', model: Tag }], + mainMenuEntries: [ + { + route: '/tags', + displayName: 'Tags', + icon: 'turned_in', + weight: 1100, + permission: 'core.can_manage_tags' + } + ] +}; diff --git a/client/src/app/site/tags/tag.module.spec.ts b/client/src/app/site/tags/tag.module.spec.ts new file mode 100644 index 000000000..c0767e697 --- /dev/null +++ b/client/src/app/site/tags/tag.module.spec.ts @@ -0,0 +1,13 @@ +import { TagModule } from './tag.module'; + +describe('MotionsModule', () => { + let tagModule: TagModule; + + beforeEach(() => { + tagModule = new TagModule(); + }); + + it('should create an instance', () => { + expect(tagModule).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/tags/tag.module.ts b/client/src/app/site/tags/tag.module.ts new file mode 100644 index 000000000..24d8ffed5 --- /dev/null +++ b/client/src/app/site/tags/tag.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TagRoutingModule } from './tag-routing.module'; +import { SharedModule } from '../../shared/shared.module'; +import { TagListComponent } from './components/tag-list.component'; + +@NgModule({ + imports: [CommonModule, TagRoutingModule, SharedModule], + declarations: [TagListComponent] +}) +export class TagModule {} diff --git a/client/src/karma.conf.js b/client/src/karma.conf.js index f905be2e3..5f8f30cc8 100644 --- a/client/src/karma.conf.js +++ b/client/src/karma.conf.js @@ -26,6 +26,12 @@ module.exports = function(config) { logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], - singleRun: false + singleRun: false, + proxies: { + '/apps/': 'http://localhost:8000/apps/', + '/media/': 'http://localhost:8000/media/', + '/rest/': 'http://localhost:8000/rest/', + '/ws/site/': 'ws://localhost:8000/ws/site' + } }); }; diff --git a/openslides/core/static/templates/core/tag-list.html b/openslides/core/static/templates/core/tag-list.html index 9bfd028fd..f119f14e7 100644 --- a/openslides/core/static/templates/core/tag-list.html +++ b/openslides/core/static/templates/core/tag-list.html @@ -29,12 +29,12 @@ - - - Name - + + Name + - +