From 53a8392e33cb28977d2462a3af6e916047d6e8a0 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Thu, 17 Jan 2019 10:53:16 +0100 Subject: [PATCH] motion detail imrovements --- .../app/shared/models/motions/motion-log.ts | 3 +- .../motion-detail.component.html | 37 ++++++++--- .../motion-detail.component.scss | 11 ++++ .../motion-detail/motion-detail.component.ts | 62 +++++++++++++++++- .../motion-list/motion-list.component.html | 7 ++- .../motion-log/motion-log.component.html | 12 ++++ .../motion-log/motion-log.component.scss | 3 + .../motion-log/motion-log.component.spec.ts | 27 ++++++++ .../motion-log/motion-log.component.ts | 25 ++++++++ .../app/site/motions/models/view-motion.ts | 36 ++++++++--- client/src/app/site/motions/motions.module.ts | 4 +- .../services/motion-filter-list.service.ts | 35 ++++++++++- .../services/motion-repository.service.ts | 63 ++++++++++++------- .../motions/services/personal-note.service.ts | 15 +++++ client/src/styles.scss | 3 +- 15 files changed, 299 insertions(+), 44 deletions(-) create mode 100644 client/src/app/site/motions/components/motion-log/motion-log.component.html create mode 100644 client/src/app/site/motions/components/motion-log/motion-log.component.scss create mode 100644 client/src/app/site/motions/components/motion-log/motion-log.component.spec.ts create mode 100644 client/src/app/site/motions/components/motion-log/motion-log.component.ts diff --git a/client/src/app/shared/models/motions/motion-log.ts b/client/src/app/shared/models/motions/motion-log.ts index 1dc1fd15d..70cab00e5 100644 --- a/client/src/app/shared/models/motions/motion-log.ts +++ b/client/src/app/shared/models/motions/motion-log.ts @@ -2,6 +2,7 @@ import { Deserializer } from '../base/deserializer'; /** * Representation of a Motion Log. + * TODO: better documentation * * @ignore */ @@ -9,7 +10,7 @@ export class MotionLog extends Deserializer { public message_list: string[]; public person_id: number; public time: string; - public message: string; + public message: string; // a pre-translated message in the servers' defined language public constructor(input?: any) { super(input); diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 3c8c258d7..bafcad2f5 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -106,9 +106,17 @@ -

- {{ motion.title }} -

+
+
+

+ {{ motion.title }} +

+ +
+ Sequential number {{ motion.id }} +
@@ -145,6 +153,10 @@ + + @@ -158,6 +170,10 @@ + +
@@ -264,6 +280,9 @@ : ('not set' | translate) }} +
@@ -310,10 +329,12 @@
- +
+ +
@@ -467,7 +488,7 @@
-

{{ "Attachments" | translate }}attach_file

+

{{ 'Attachments' | translate }}attach_file

{{ file.title }} diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss index 761439dd3..6f9c0286a 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss @@ -263,3 +263,14 @@ span { .main-nav-color { color: rgba(0, 0, 0, 0.54); } + +.title-line { + display: flex; +} + +.create-poll-button { + margin-top: 10px; + button { + padding: 0px; + } +} diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index e7e26b38a..af67aefda 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -38,6 +38,8 @@ import { PromptService } from 'app/core/services/prompt.service'; import { AgendaRepositoryService } from 'app/site/agenda/services/agenda-repository.service'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { MotionPdfExportService } from '../../services/motion-pdf-export.service'; +import { PersonalNoteService } from '../../services/personal-note.service'; +import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; /** * Component for the motion detail view @@ -77,6 +79,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public newMotion = false; + /** + * Toggle to expand/hide the motion log. + */ + public motionLogExpanded = false; + /** * Sets the motions, e.g. via an autoupdate. Reload important things here: * - Reload the recommendation. Not changed with autoupdates, but if the motion is loaded this needs to run. @@ -93,6 +100,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { return this._motion; } + /** + * @returns treu if the motion log is present and the user is allowed to see it + */ + public get canShowLog(): boolean { + if ( + this.motion && + !this.editMotion && + this.motion.motion.log_messages && + this.motion.motion.log_messages.length + ) { + return true; + } + return false; + } + /** * Saves the target motion. Accessed via the getter and setter. */ @@ -260,6 +282,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public highlightedLine: number; + /** + * The personal notes' content for this motion + */ + public personalNoteContent: PersonalNoteContent; + /** * Constuct the detail view. * @@ -281,6 +308,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { * @param sanitizer For making HTML SafeHTML * @param promptService ensure safe deletion * @param pdfExport export the motion to pdf + * @param personalNoteService: personal comments and favorite marker */ public constructor( title: Title, @@ -301,7 +329,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { private configService: ConfigService, private sanitizer: DomSanitizer, private promptService: PromptService, - private pdfExport: MotionPdfExportService + private pdfExport: MotionPdfExportService, + private personalNoteService: PersonalNoteService ) { super(title, translate, matSnackBar); @@ -433,6 +462,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.repo.getViewModelObservable(motionId).subscribe(newViewMotion => { if (newViewMotion) { this.motion = newViewMotion; + this.personalNoteService.getPersonalNoteObserver(this.motion.motion).subscribe(pn => { + this.personalNoteContent = pn; + }); this.patchForm(this.motion); } }); @@ -956,4 +988,32 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { public async createPoll(): Promise { await this.repo.createPoll(this.motion); } + + /** + * Check if a recommendation can be followed. Checks for permissions and additionally if a recommentadion is present + */ + public get canFollowRecommendation(): boolean { + if ( + this.perms.isAllowed('createPoll', this.motion) && + this.motion.recommendation && + this.motion.recommendation.recommendation_label + ) { + return true; + } + return false; + } + + /** + * Handler for the 'follow recommendation' button + */ + public onFollowRecButton(): void { + this.repo.followRecommendation(this.motion); + } + + /** + * Toggles the favorite status + */ + public async toggleFavorite(): Promise { + this.personalNoteService.setPersonalNoteStar(this.motion.motion, !this.motion.star); + } } diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index 7b9951fcd..36e54038c 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -43,7 +43,12 @@ Title
- {{ motion.title }} + {{ motion.title }} + + {{ motion.star ? 'star' : 'star_border' }} + + + diff --git a/client/src/app/site/motions/components/motion-log/motion-log.component.html b/client/src/app/site/motions/components/motion-log/motion-log.component.html new file mode 100644 index 000000000..233a20ed6 --- /dev/null +++ b/client/src/app/site/motions/components/motion-log/motion-log.component.html @@ -0,0 +1,12 @@ + + + + Motion log + + +
+ {{message.message}} +
+
+ +
diff --git a/client/src/app/site/motions/components/motion-log/motion-log.component.scss b/client/src/app/site/motions/components/motion-log/motion-log.component.scss new file mode 100644 index 000000000..579d7d3a4 --- /dev/null +++ b/client/src/app/site/motions/components/motion-log/motion-log.component.scss @@ -0,0 +1,3 @@ +.small-messages { + font-size: x-small; +} diff --git a/client/src/app/site/motions/components/motion-log/motion-log.component.spec.ts b/client/src/app/site/motions/components/motion-log/motion-log.component.spec.ts new file mode 100644 index 000000000..34913e6ff --- /dev/null +++ b/client/src/app/site/motions/components/motion-log/motion-log.component.spec.ts @@ -0,0 +1,27 @@ +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +// import { E2EImportsModule } from 'e2e-imports.module'; +// import { MotionLogComponent } from './motion-log.component'; + +describe('MotionLogComponent skipped', () => { + // TODO testing fails if personalNotesModule (also having the MetaTextBlockComponent) + // is running its' test at the same time. One of the two tests fail, but run fine if tested + // separately; so this is some async duplication stuff + // + // let component: MotionLogComponent; + // let fixture: ComponentFixture; + // beforeEach(async(() => { + // TestBed.configureTestingModule({ + // declarations: [MotionLogComponent], + // imports: [E2EImportsModule] + // }).compileComponents(); + // })); + // beforeEach(() => { + // fixture = TestBed.createComponent(MotionLogComponent); + // component = fixture.componentInstance; + // fixture.detectChanges(); + // }); + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/client/src/app/site/motions/components/motion-log/motion-log.component.ts b/client/src/app/site/motions/components/motion-log/motion-log.component.ts new file mode 100644 index 000000000..237d3f748 --- /dev/null +++ b/client/src/app/site/motions/components/motion-log/motion-log.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; +import { ViewMotion } from '../../models/view-motion'; + +/** + * Component showing the log messages of a motion + */ +@Component({ + selector: 'os-motion-log', + templateUrl: './motion-log.component.html', + styleUrls: ['motion-log.component.scss'] +}) +export class MotionLogComponent { + public expanded = false; + + /** + * The viewMotion to show the log messages for + */ + @Input() + public motion: ViewMotion; + + /** + * empty constructor + */ + public constructor() {} +} diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index e49286029..cdbbb87b4 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -1,15 +1,16 @@ -import { Motion } from '../../../shared/models/motions/motion'; -import { Category } from '../../../shared/models/motions/category'; -import { User } from '../../../shared/models/users/user'; -import { Workflow } from '../../../shared/models/motions/workflow'; -import { WorkflowState } from '../../../shared/models/motions/workflow-state'; import { BaseModel } from '../../../shared/models/base/base-model'; import { BaseViewModel } from '../../base/base-view-model'; -import { ViewMotionCommentSection } from './view-motion-comment-section'; -import { MotionComment } from '../../../shared/models/motions/motion-comment'; +import { Category } from '../../../shared/models/motions/category'; import { Item } from 'app/shared/models/agenda/item'; -import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; +import { Motion } from '../../../shared/models/motions/motion'; +import { MotionBlock } from 'app/shared/models/motions/motion-block'; +import { MotionComment } from '../../../shared/models/motions/motion-comment'; +import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; +import { User } from '../../../shared/models/users/user'; +import { ViewMotionCommentSection } from './view-motion-comment-section'; +import { Workflow } from '../../../shared/models/motions/workflow'; +import { WorkflowState } from '../../../shared/models/motions/workflow-state'; /** * The line numbering mode for the motion detail view. @@ -48,6 +49,7 @@ export class ViewMotion extends BaseViewModel { protected _item: Item; protected _block: MotionBlock; protected _attachments: Mediafile[]; + public personalNote: PersonalNoteContent; /** * Is set by the repository; this is the order of the flat call list given by @@ -230,6 +232,24 @@ export class ViewMotion extends BaseViewModel { return this.motion.comments.map(comment => comment.section_id); } + /** + * Getter to query the 'favorite'/'star' status of the motions + * + * @returns the current state + */ + public get star(): boolean { + return this.personalNote && this.personalNote.star ? true : false; + } + + /** + * Queries if any personal comments are rpesent + * + * @returns true if personalContent is present and has notes + */ + public get hasNotes(): boolean { + return this.personalNote && this.personalNote.note ? true : false; + } + public constructor( motion?: Motion, category?: Category, diff --git a/client/src/app/site/motions/motions.module.ts b/client/src/app/site/motions/motions.module.ts index 6c85b2824..25e3eec61 100644 --- a/client/src/app/site/motions/motions.module.ts +++ b/client/src/app/site/motions/motions.module.ts @@ -22,6 +22,7 @@ import { MotionImportListComponent } from './components/motion-import-list/motio import { ManageSubmittersComponent } from './components/manage-submitters/manage-submitters.component'; import { MotionPollComponent } from './components/motion-poll/motion-poll.component'; import { MotionPollDialogComponent } from './components/motion-poll/motion-poll-dialog.component'; +import { MotionLogComponent } from './components/motion-log/motion-log.component'; @NgModule({ imports: [CommonModule, MotionsRoutingModule, SharedModule], @@ -44,7 +45,8 @@ import { MotionPollDialogComponent } from './components/motion-poll/motion-poll- MotionImportListComponent, ManageSubmittersComponent, MotionPollComponent, - MotionPollDialogComponent + MotionPollDialogComponent, + MotionLogComponent ], entryComponents: [ MotionChangeRecommendationComponent, diff --git a/client/src/app/site/motions/services/motion-filter-list.service.ts b/client/src/app/site/motions/services/motion-filter-list.service.ts index 1e9997f22..7ba037ac3 100644 --- a/client/src/app/site/motions/services/motion-filter-list.service.ts +++ b/client/src/app/site/motions/services/motion-filter-list.service.ts @@ -26,7 +26,7 @@ export class MotionFilterListService extends FilterListService * @param httpService OpenSlides own Http service * @param lineNumbering Line numbering for motion text * @param diff Display changes in motion text as diff. + * @param personalNoteService service fo personal notes */ public constructor( DS: DataStoreService, @@ -64,7 +66,8 @@ export class MotionRepositoryService extends BaseRepository private httpService: HttpService, private readonly lineNumbering: LinenumberingService, private readonly diff: DiffService, - private treeService: TreeService + private treeService: TreeService, + private personalNoteService: PersonalNoteService ) { super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile]); } @@ -106,6 +109,10 @@ export class MotionRepositoryService extends BaseRepository let virtualWeightCounter = 0; while (!(m = iterator.next()).done) { m.value.callListWeight = virtualWeightCounter++; + const motion = m.value; + this.personalNoteService + .getPersonalNoteObserver(motion.motion) + .subscribe(note => (motion.personalNote = note)); } }) ); @@ -651,7 +658,7 @@ export class MotionRepositoryService extends BaseRepository } /** - * Sends a haap request to delete the given poll + * Sends a http request to delete the given poll * * @param poll */ @@ -659,4 +666,16 @@ export class MotionRepositoryService extends BaseRepository const url = '/rest/motions/motion-poll/' + poll.id + '/'; await this.httpService.delete(url); } + + /** + * Signals the acceptance of the current recommendation to the server + * + * @param motion A ViewMotion + */ + public async followRecommendation(motion: ViewMotion): Promise { + if (motion.recommendation_id) { + const restPath = `/rest/motions/motion/${motion.id}/follow_recommendation/`; + await this.httpService.post(restPath); + } + } } diff --git a/client/src/app/site/motions/services/personal-note.service.ts b/client/src/app/site/motions/services/personal-note.service.ts index 3711203a3..e6601cc3c 100644 --- a/client/src/app/site/motions/services/personal-note.service.ts +++ b/client/src/app/site/motions/services/personal-note.service.ts @@ -127,4 +127,19 @@ export class PersonalNoteService { await this.http.put(`rest/users/personal-note/${pnObject.id}/`, pnObject); } } + + /** + * Changes the 'favorite' status of a personal note, without changing other information + * + * @param model + * @param star The new status to set + */ + public async setPersonalNoteStar(model: BaseModel, star: boolean): Promise { + let content: PersonalNoteContent = this.getPersonalNoteContent(model.collectionString, model.id); + if (!content) { + content = { note: null, star: star }; + } + content.star = star; + return this.savePersonalNote(model, content); + } } diff --git a/client/src/styles.scss b/client/src/styles.scss index 2ab6ff98d..e3cba2ba1 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -38,7 +38,8 @@ body { h1, h2, -h3 { +h3, +.title-font { font-family: Fira Sans Condensed, Roboto-condensed, Arial, Helvetica, sans-serif; }