mirror of https://github.com/Squidex/squidex.git
34 changed files with 686 additions and 238 deletions
@ -0,0 +1 @@ |
|||||
|
<sqx-comments [commentsId]="commentsId"></sqx-comments> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,30 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
import { allParams } from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comments-page', |
||||
|
styleUrls: ['./comments-page.component.scss'], |
||||
|
templateUrl: './comments-page.component.html' |
||||
|
}) |
||||
|
export class CommentsPageComponent implements OnInit { |
||||
|
public commentsId: string; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly route: ActivatedRoute |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.commentsId = allParams(this.route)['contentId']; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,19 @@ |
|||||
|
<div class="comment row no-gutters"> |
||||
|
<div class="col col-auto"> |
||||
|
<img class="user-picture" [attr.title]="comment.user | sqxUserNameRef:null" [attr.src]="comment.user | sqxUserPictureRef" /> |
||||
|
</div> |
||||
|
<div class="col pl-2"> |
||||
|
<div class="comment-message"> |
||||
|
<div class="user-row"> |
||||
|
<div class="user-ref">{{comment.user | sqxUserNameRef:null}}</div> |
||||
|
|
||||
|
<button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-link btn-danger item-remove" (click)="deleting.emit()!"> |
||||
|
<i class="icon-bin2"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div>{{comment.text}}</div> |
||||
|
<div class="comment-created text-muted">{{comment.time | sqxFromNow}}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,43 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.user-ref { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.item-remove { |
||||
|
@include absolute(-5px, -15px, auto, auto); |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.user-row { |
||||
|
& { |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
.item-remove { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.comment { |
||||
|
& { |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
margin-bottom: .75rem; |
||||
|
} |
||||
|
|
||||
|
&-message { |
||||
|
margin-bottom: .375rem; |
||||
|
} |
||||
|
|
||||
|
&-created { |
||||
|
font-size: .75rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.text-muted { |
||||
|
color: $color-history; |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
|
||||
|
import { CommentDto, UpsertCommentForm } from '@app/shared/internal'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comment', |
||||
|
styleUrls: ['./comment.component.scss'], |
||||
|
templateUrl: './comment.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class CommentComponent { |
||||
|
public editForm = new UpsertCommentForm(this.formBuilder); |
||||
|
|
||||
|
@Input() |
||||
|
public comment: CommentDto; |
||||
|
|
||||
|
@Input() |
||||
|
public userId: string; |
||||
|
|
||||
|
@Output() |
||||
|
public deleting = new EventEmitter(); |
||||
|
|
||||
|
@Output() |
||||
|
public updated = new EventEmitter<string>(); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false" contentClass="grid"> |
||||
|
<ng-container title> |
||||
|
Comments |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div class="grid-content" #scrollMe [scrollTop]="scrollMe.scrollHeight"> |
||||
|
<sqx-comment *ngFor="let comment of state.comments | async; trackBy: trackByComment" |
||||
|
[comment]="comment" |
||||
|
[userId]="userId" |
||||
|
(updated)="update(comment, $event)" |
||||
|
(deleting)="delete(comment)"> |
||||
|
</sqx-comment> |
||||
|
</div> |
||||
|
|
||||
|
<div class="grid-footer"> |
||||
|
<form [formGroup]="commentForm.form" (submit)="comment()"> |
||||
|
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment" /> |
||||
|
</form> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</sqx-panel> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,11 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.grid-footer { |
||||
|
border-top-width: 1px; |
||||
|
} |
||||
|
|
||||
|
.grid-body, |
||||
|
.grid-footer { |
||||
|
padding: 1rem; |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
import { Subscription, timer } from 'rxjs'; |
||||
|
import { onErrorResumeNext, switchMap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
AppsState, |
||||
|
AuthService, |
||||
|
CommentDto, |
||||
|
CommentsService, |
||||
|
CommentsState, |
||||
|
DialogService, |
||||
|
fadeAnimation, |
||||
|
UpsertCommentForm |
||||
|
} from '@app/shared/internal'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-comments', |
||||
|
styleUrls: ['./comments.component.scss'], |
||||
|
templateUrl: './comments.component.html', |
||||
|
animations: [ |
||||
|
fadeAnimation |
||||
|
] |
||||
|
}) |
||||
|
export class CommentsComponent implements OnDestroy, OnInit { |
||||
|
private timer: Subscription; |
||||
|
|
||||
|
public state: CommentsState; |
||||
|
|
||||
|
public userId: string; |
||||
|
|
||||
|
public commentForm = new UpsertCommentForm(this.formBuilder); |
||||
|
|
||||
|
@Input() |
||||
|
public commentsId: string; |
||||
|
|
||||
|
constructor(authService: AuthService, |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly commentsService: CommentsService, |
||||
|
private readonly dialogs: DialogService, |
||||
|
private readonly formBuilder: FormBuilder |
||||
|
) { |
||||
|
this.userId = authService.user!.token; |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.timer.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.state = new CommentsState(this.appsState, this.commentsId, this.commentsService, this.dialogs); |
||||
|
|
||||
|
this.timer = timer(0, 4000).pipe(switchMap(() => this.state.load()), onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public delete(comment: CommentDto) { |
||||
|
this.state.delete(comment.id).pipe(onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public update(comment: CommentDto, text: string) { |
||||
|
this.state.update(comment.id, text).pipe(onErrorResumeNext()).subscribe(); |
||||
|
} |
||||
|
|
||||
|
public comment() { |
||||
|
const value = this.commentForm.submit(); |
||||
|
|
||||
|
if (value) { |
||||
|
this.state.create(value.text).pipe(onErrorResumeNext()).subscribe(); |
||||
|
|
||||
|
this.commentForm.submitCompleted({}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public trackByComment(index: number, comment: CommentDto) { |
||||
|
return comment.id; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
|
||||
|
import { Form } from '@app/framework'; |
||||
|
|
||||
|
export class UpsertCommentForm extends Form<FormGroup> { |
||||
|
constructor(formBuilder: FormBuilder) { |
||||
|
super(formBuilder.group({ |
||||
|
text: ['', |
||||
|
[ |
||||
|
Validators.required |
||||
|
] |
||||
|
] |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { of } from 'rxjs'; |
||||
|
import { IMock, It, Mock, Times } from 'typemoq'; |
||||
|
|
||||
|
import { |
||||
|
AppsState, |
||||
|
CommentDto, |
||||
|
CommentsDto, |
||||
|
CommentsService, |
||||
|
CommentsState, |
||||
|
DateTime, |
||||
|
DialogService, |
||||
|
ImmutableArray, |
||||
|
UpsertCommentDto, |
||||
|
Version |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
describe('CommentsState', () => { |
||||
|
const app = 'my-app'; |
||||
|
const commentsId = 'my-comments'; |
||||
|
const now = DateTime.today(); |
||||
|
const user = 'not-me'; |
||||
|
|
||||
|
const oldComments = new CommentsDto([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2', user) |
||||
|
], [], [], new Version('1')); |
||||
|
|
||||
|
let dialogs: IMock<DialogService>; |
||||
|
let appsState: IMock<AppsState>; |
||||
|
let commentsService: IMock<CommentsService>; |
||||
|
let commentsState: CommentsState; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
dialogs = Mock.ofType<DialogService>(); |
||||
|
|
||||
|
appsState = Mock.ofType<AppsState>(); |
||||
|
|
||||
|
appsState.setup(x => x.appName) |
||||
|
.returns(() => app); |
||||
|
|
||||
|
commentsService = Mock.ofType<CommentsService>(); |
||||
|
|
||||
|
commentsService.setup(x => x.getComments(app, commentsId, new Version(''))) |
||||
|
.returns(() => of(oldComments)); |
||||
|
|
||||
|
commentsState = new CommentsState(appsState.object, commentsId, commentsService.object, dialogs.object); |
||||
|
commentsState.load().subscribe(); |
||||
|
}); |
||||
|
|
||||
|
it('should load and merge comments', () => { |
||||
|
const newComments = new CommentsDto([ |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
], [ |
||||
|
new CommentDto('2', now, 'text2_2', user) |
||||
|
], ['1'], new Version('2')); |
||||
|
|
||||
|
commentsService.setup(x => x.getComments(app, commentsId, new Version('1'))) |
||||
|
.returns(() => of(newComments)); |
||||
|
|
||||
|
commentsState.load().subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.isLoaded).toBeTruthy(); |
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('2', now, 'text2_2', user), |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.getComments(app, commentsId, It.isAny()), Times.exactly(2)); |
||||
|
}); |
||||
|
|
||||
|
it('should add comment to snapshot when created', () => { |
||||
|
const newComment = new CommentDto('3', now, 'text3', user); |
||||
|
|
||||
|
commentsService.setup(x => x.postComment(app, commentsId, new UpsertCommentDto('text3'))) |
||||
|
.returns(() => of(newComment)); |
||||
|
|
||||
|
commentsState.create('text3').subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2', user), |
||||
|
new CommentDto('3', now, 'text3', user) |
||||
|
])); |
||||
|
}); |
||||
|
|
||||
|
it('should update properties when updated', () => { |
||||
|
commentsService.setup(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2'))) |
||||
|
.returns(() => of({})); |
||||
|
|
||||
|
commentsState.update('2', 'text2_2', now).subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user), |
||||
|
new CommentDto('2', now, 'text2_2', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')), Times.once()); |
||||
|
}); |
||||
|
|
||||
|
it('should remove comment from snapshot when deleted', () => { |
||||
|
commentsService.setup(x => x.deleteComment(app, commentsId, '2')) |
||||
|
.returns(() => of({})); |
||||
|
|
||||
|
commentsState.delete('2').subscribe(); |
||||
|
|
||||
|
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ |
||||
|
new CommentDto('1', now, 'text1', user) |
||||
|
])); |
||||
|
|
||||
|
commentsService.verify(x => x.deleteComment(app, commentsId, '2'), Times.once()); |
||||
|
}); |
||||
|
}); |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue