From 3f5253d90741fbb914720aef70daa56c5b685854 Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Thu, 30 Apr 2026 13:43:16 -0700 Subject: [PATCH 1/3] feat(Discussion): Hide hidden response content in teacher summary - Allow teacher to view hidden posts and replies - Add tests --- .../class-response-teacher.component.html | 183 +++++++------ .../class-response-teacher.component.spec.ts | 251 ++++++++++++++++++ .../class-response-teacher.component.ts | 33 +++ .../class-response.component.ts | 1 + 4 files changed, 386 insertions(+), 82 deletions(-) create mode 100644 src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.spec.ts diff --git a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html index 33c4de92e5b..30c61ddab9c 100644 --- a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html +++ b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html @@ -1,4 +1,30 @@ + @if (isHidden(response)) { +
+ + This post is hidden from students. + (tap to view) + + + + + +
+ } @else { + + + } +
+ +
- - @if ( - response.latestInappropriateFlagAnnotation == null || - response.latestInappropriateFlagAnnotation.data == null || - response.latestInappropriateFlagAnnotation.data.action != 'Delete' - ) { + @if (showHideButton) { + - } @else if ( - response.latestInappropriateFlagAnnotation != null && - response.latestInappropriateFlagAnnotation.data.action === 'Delete' - ) { - }
@@ -52,11 +61,12 @@ Post attachment } +
+ + @if (response.replies.length > 0) {
- @if (mode === 'student' || response.replies.length > 0) { - - } +
@if (response.replies.length === 1) { Comments ({{ response.replies.length }}) @@ -71,69 +81,78 @@
@for (reply of repliesToShow; track reply) {
-
- - account_circle - -
- {{ reply.usernames }} - -
- - @if ( - (response.latestInappropriateFlagAnnotation == null || - response.latestInappropriateFlagAnnotation.data.action !== 'Delete') && - (reply.latestInappropriateFlagAnnotation == null || - reply.latestInappropriateFlagAnnotation.data.action != 'Delete') - ) { - - } @else if ( - response.latestInappropriateFlagAnnotation != null && - response.latestInappropriateFlagAnnotation.data.action === 'Delete' - ) { - visibility_off - } @else if ( - reply.latestInappropriateFlagAnnotation != null && - reply.latestInappropriateFlagAnnotation.data.action === 'Delete' - ) { - - } -
-
+ + @if (parentHidden) { + Comment hidden because parent post is hidden. + } @else { + This comment is hidden from students. + } + (tap to view) + + @if (!parentHidden && isHidden(reply)) { + + } + + + + } @else { + + }
}
} - + + + +
+ + account_circle + +
+ {{ reply.usernames }} + +
+ @if (showHideButton) { + + + } +
+
+
diff --git a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.spec.ts b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.spec.ts new file mode 100644 index 00000000000..17eccb29ef3 --- /dev/null +++ b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.spec.ts @@ -0,0 +1,251 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ClassResponseTeacherComponent } from './class-response-teacher.component'; +import { MockComponent, MockProvider } from 'ng-mocks'; +import { SaveTimeMessageComponent } from '../../../common/save-time-message/save-time-message.component'; +import { ConfigService } from '../../../services/configService'; +import { provideRouter } from '@angular/router'; + +let fixture: ComponentFixture; +let component: ClassResponseTeacherComponent; + +describe('ClassResponseTeacherComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ClassResponseTeacherComponent, MockComponent(SaveTimeMessageComponent)], + providers: [MockProvider(ConfigService), provideRouter([])] + }).compileComponents(); + + fixture = TestBed.createComponent(ClassResponseTeacherComponent); + component = fixture.componentInstance; + component.response = createResponse('Hello World'); + fixture.detectChanges(); + }); + + isHidden(); + visiblePost(); + hiddenPost(); + visibleReply(); + hiddenReply(); + hiddenParentReply(); + hidePost(); + showPost(); +}); + +function createResponse(text: string = '', replies: any[] = []): any { + return { + workgroupId: 1, + usernames: 'Student A', + serverSaveTime: Date.now(), + studentData: { response: text, responseTextHTML: text, attachments: [] }, + replies, + latestInappropriateFlagAnnotation: null + }; +} + +function createReply(text: string = ''): any { + return { + workgroupId: 2, + usernames: 'Student B', + serverSaveTime: Date.now(), + studentData: { response: text, responseHTML: text }, + latestInappropriateFlagAnnotation: null + }; +} + +function hiddenAnnotation(): any { + return { data: { action: 'Delete' } }; +} + +function queryText(selector: string): string { + return fixture.nativeElement.querySelector(selector)?.textContent?.trim() ?? ''; +} + +function query(selector: string): HTMLElement | null { + return fixture.nativeElement.querySelector(selector); +} + +function isHidden() { + describe('isHidden', () => { + it('returns false when latestInappropriateFlagAnnotation is null', () => { + const post = createResponse(); + expect(component['isHidden'](post)).toBe(false); + }); + + it('returns false when latestInappropriateFlagAnnotation data is null', () => { + const post = createResponse(); + post.latestInappropriateFlagAnnotation = { data: null }; + expect(component['isHidden'](post)).toBe(false); + }); + + it('returns false when action is not Delete', () => { + const post = createResponse(); + post.latestInappropriateFlagAnnotation = { data: { action: 'Flag' } }; + expect(component['isHidden'](post)).toBe(false); + }); + + it('returns true when latestInappropriateFlagAnnotation action is Delete', () => { + const post = createResponse(); + post.latestInappropriateFlagAnnotation = hiddenAnnotation(); + expect(component['isHidden'](post)).toBe(true); + }); + }); +} + +function visiblePost() { + describe('visible post', () => { + it('renders post content', () => { + expect(queryText('.post')).toBe('Hello World'); + }); + + it('includes a hide button', () => { + const btn = query('button[mattooltip="Hide from students"]'); + expect(btn).not.toBeNull(); + }); + + it('does not wrap the post in a
element', () => { + expect(query('details')).toBeNull(); + }); + }); +} + +function hiddenPost() { + describe('hidden post', () => { + beforeEach(() => { + component.response = createResponse('Hidden content'); + component.response.latestInappropriateFlagAnnotation = hiddenAnnotation(); + fixture.detectChanges(); + }); + + it('wraps the post in a
element', () => { + expect(query('details')).not.toBeNull(); + }); + + it('shows hidden post message', () => { + expect(queryText('summary')).toContain('This post is hidden from students.'); + }); + + it('includes a show button', () => { + const btn = query('button[mattooltip="Show to students"]'); + expect(btn).not.toBeNull(); + }); + }); +} + +function visibleReply() { + describe('visible reply on a visible post', () => { + let reply: any; + + beforeEach(() => { + reply = createReply('Reply text'); + component.response.replies = [reply]; + component['repliesToShow'] = [reply]; + fixture.detectChanges(); + }); + + it('shows reply content without a
wrapper', () => { + expect(query('.comment details')).toBeNull(); + }); + + it('renders the reply content', () => { + expect(queryText('.comment')).toContain('Reply text'); + }); + + it('includes a hide button', () => { + const btn = query('.comment button[mattooltip="Hide from students"]'); + expect(btn).not.toBeNull(); + }); + }); +} + +function hiddenReply() { + describe('hidden reply on a visible post', () => { + let reply: any; + + beforeEach(() => { + reply = createReply('Hidden reply'); + reply.latestInappropriateFlagAnnotation = hiddenAnnotation(); + component.response.replies = [reply]; + component['repliesToShow'] = [reply]; + fixture.detectChanges(); + }); + + it('wraps the reply in a
element', () => { + expect(query('.comment details')).not.toBeNull(); + }); + + it('shows comment hidden message', () => { + expect(queryText('.comment summary')).toContain('This comment is hidden from students.'); + }); + + it('includes a show button', () => { + const btn = query('.comment button[mattooltip="Show to students"]'); + expect(btn).not.toBeNull(); + }); + }); +} + +function hiddenParentReply() { + describe('hidden parent post', () => { + let reply: any; + + beforeEach(() => { + reply = createReply('Reply on hidden post'); + component.response = createResponse('Hidden post content', [reply]); + component.response.latestInappropriateFlagAnnotation = hiddenAnnotation(); + component['repliesToShow'] = [reply]; + fixture.detectChanges(); + }); + + it('wraps replies in a
element', () => { + const detailsEls = fixture.nativeElement.querySelectorAll('.comment details'); + expect(detailsEls.length).toBeGreaterThan(0); + }); + + it('shows parent hidden message on replies', () => { + expect(queryText('.comment summary')).toContain( + 'Comment hidden because parent post is hidden.' + ); + }); + + it('does not include a show button on replies', () => { + const btn = query('.comment button[mattooltip="Show to students"]'); + expect(btn).toBeNull(); + }); + }); +} + +function hidePost() { + describe('hidePost', () => { + it('emits hidePostEvent after confirmation', () => { + spyOn(window, 'confirm').and.returnValue(true); + const spy = spyOn(component.hidePostEvent, 'emit'); + component['hidePost'](component.response); + expect(spy).toHaveBeenCalledWith(component.response); + }); + + it('does not emit hidePostEvent when confirmation is cancelled', () => { + spyOn(window, 'confirm').and.returnValue(false); + const spy = spyOn(component.hidePostEvent, 'emit'); + component['hidePost'](component.response); + expect(spy).not.toHaveBeenCalled(); + }); + }); +} + +function showPost() { + describe('showPost', () => { + it('emits showPostEvent after confirmation', () => { + spyOn(window, 'confirm').and.returnValue(true); + const spy = spyOn(component.showPostEvent, 'emit'); + component['showPost'](component.response); + expect(spy).toHaveBeenCalledWith(component.response); + }); + + it('does not emit showPostEvent when confirmation is cancelled', () => { + spyOn(window, 'confirm').and.returnValue(false); + const spy = spyOn(component.showPostEvent, 'emit'); + component['showPost'](component.response); + expect(spy).not.toHaveBeenCalled(); + }); + }); +} diff --git a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts index 425ced725d7..09a372b8125 100644 --- a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts +++ b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts @@ -29,6 +29,35 @@ import { ClassResponse } from '../class-response/class-response.component'; ], selector: 'class-response-teacher', styleUrl: '../class-response/class-response.component.scss', + styles: ` + @reference "tailwindcss"; + + details:open > summary { + .details-closed { + @apply hidden; + } + + .details-open { + @apply flex; + } + } + + .reply-details:open { + @apply px-2; + + > summary { + @apply -mx-2 mb-1 rounded-b-none; + } + } + + summary { + @apply italic cursor-pointer px-2 flex items-center gap-2; + + &::-webkit-details-marker { + display: none; + } + } + `, templateUrl: './class-response-teacher.component.html' }) export class ClassResponseTeacherComponent extends ClassResponse { @@ -49,6 +78,10 @@ export class ClassResponseTeacherComponent extends ClassResponse { } } + protected isHidden(post: any): boolean { + return post.latestInappropriateFlagAnnotation?.data?.action === 'Delete'; + } + protected showPost(componentState: any): void { if (confirm($localize`Are you sure you want to show this content?`)) { this.showPostEvent.emit(componentState); diff --git a/src/assets/wise5/components/discussion/class-response/class-response.component.ts b/src/assets/wise5/components/discussion/class-response/class-response.component.ts index d407a21fe7e..8018b6e66d3 100644 --- a/src/assets/wise5/components/discussion/class-response/class-response.component.ts +++ b/src/assets/wise5/components/discussion/class-response/class-response.component.ts @@ -82,6 +82,7 @@ export class ClassResponse { } private injectLinks(response: string): string { + if (response == null) return ''; return response.replace(this.urlMatcher, (match) => { let matchUrl = match; if (!match.startsWith('http')) { From b722fa8af0cd52b5c638c57c484ce319469042e6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 20:54:40 +0000 Subject: [PATCH 2/3] Updated messages --- src/messages.xlf | 64 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/messages.xlf b/src/messages.xlf index 33e39bf173f..c5898133e97 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -19336,33 +19336,62 @@ Category Name: 20 - - Hide from students + + This post is hidden from students. src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 29,32 + 5,7 + + + + (tap to view) + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 6,7 + + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 96,97 + + + + (tap to hide) + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 7,10 src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 100,102 + 97,99 Show to students src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 42,45 + 11,14 + + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 102,104 + + + + Hide from students + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 51,54 src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 125,127 + 149,153 Post attachment src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 52,55 + 61,66 src/assets/wise5/components/discussion/class-response/class-response.component.html @@ -19373,7 +19402,7 @@ Category Name: Comments () src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 62,63 + 72,73 src/assets/wise5/components/discussion/class-response/class-response.component.html @@ -19384,32 +19413,39 @@ Category Name: Comments () src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 66,67 + 76,77 src/assets/wise5/components/discussion/class-response/class-response.component.html 37,38 - - Parent post is hidden, so this comment is also hidden + + Comment hidden because parent post is hidden. + + src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html + 91,94 + + + + This comment is hidden from students. src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html - 113,115 + 94,96 Are you sure you want to hide this content? src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts - 47 + 76 Are you sure you want to show this content? src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.ts - 53 + 86 From 2519990c466324ed2804b34fb06e9b9f2344d0aa Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Thu, 30 Apr 2026 13:57:23 -0700 Subject: [PATCH 3/3] Remove unnecessary ng-container context --- .../class-response-teacher.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html index 30c61ddab9c..eda46ac1ac5 100644 --- a/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html +++ b/src/assets/wise5/components/discussion/class-response-teacher/class-response-teacher.component.html @@ -20,7 +20,7 @@
} @else { - + }