[ACS-9535] open links inside strings in md format as separated browser tab (#4521)

* [ACS-9535] Opening embedded links in MD formatted answer as new tab

* [ACS-9535] Unit tests for marked options
This commit is contained in:
AleksanderSklorz 2025-04-18 08:03:20 +02:00 committed by GitHub
parent 6ce927e63c
commit f201760ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 6 deletions

View File

@ -79,7 +79,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro
import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component'; import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component';
import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component'; import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component';
import { SaveSearchSidenavComponent } from './components/search/search-save/sidenav/save-search-sidenav.component'; import { SaveSearchSidenavComponent } from './components/search/search-save/sidenav/save-search-sidenav.component';
import { MarkdownModule } from 'ngx-markdown';
@NgModule({ @NgModule({
imports: [ imports: [
@ -101,8 +100,7 @@ import { MarkdownModule } from 'ngx-markdown';
AcaFolderRulesModule, AcaFolderRulesModule,
CreateFromTemplateDialogComponent, CreateFromTemplateDialogComponent,
OpenInAppComponent, OpenInAppComponent,
UploadFilesDialogComponent, UploadFilesDialogComponent
MarkdownModule.forRoot()
], ],
providers: [ providers: [
{ provide: ContentVersionService, useClass: ContentUrlService }, { provide: ContentVersionService, useClass: ContentUrlService },

View File

@ -0,0 +1,61 @@
/*!
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { searchAiMarkedOptions } from './search-ai-marked-options';
describe('SearchAiMarkedOptions', () => {
let link = '';
beforeEach(() => {
link = searchAiMarkedOptions.renderer.link('https://example.com', 'Example', 'Example Link');
});
it('should return a element', () => {
expect(link).toContain('<a');
});
it('should returned link contain correct href', () => {
expect(link).toContain('href="https://example.com"');
});
it('should returned link contain correct target', () => {
expect(link).toContain('target="_blank"');
});
it('should returned link contain correct rel', () => {
expect(link).toContain('rel="noopener noreferrer"');
});
it('should returned link contain correct title', () => {
expect(link).toContain('title="Example"');
});
it('should returned link contain correct text', () => {
expect(link).toContain('>Example Link</a>');
});
it('should returned link contain correct title if title is null', () => {
expect(searchAiMarkedOptions.renderer.link('https://example.com', null, 'Example Link')).toContain('title=""');
});
});

View File

@ -0,0 +1,32 @@
/*!
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
const renderer = new MarkedRenderer();
renderer.link = (href: string, title: string, text: string): string =>
`<a href="${href}" target="_blank" rel="noopener noreferrer" title="${title || ''}">${text}</a>`;
export const searchAiMarkedOptions: MarkedOptions = {
renderer
};

View File

@ -41,7 +41,8 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { getAppSelection, getCurrentFolder, ViewNodeAction } from '@alfresco/aca-shared/store'; import { getAppSelection, getCurrentFolder, ViewNodeAction } from '@alfresco/aca-shared/store';
import { ViewerService } from '@alfresco/aca-content/viewer'; import { ViewerService } from '@alfresco/aca-content/viewer';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { MarkdownComponent, MarkdownModule } from 'ngx-markdown'; import { MarkdownComponent, MarkdownModule, MARKED_OPTIONS } from 'ngx-markdown';
import { searchAiMarkedOptions } from './search-ai-marked-options';
const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } }; const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } };
const getAiAnswerEntry = (noAnswer?: boolean): AiAnswerEntry => { const getAiAnswerEntry = (noAnswer?: boolean): AiAnswerEntry => {
@ -361,6 +362,10 @@ describe('SearchAiResultsComponent', () => {
answerEntry = getAiAnswerEntry(); answerEntry = getAiAnswerEntry();
}); });
it('should have correct marked options', () => {
expect(fixture.debugElement.injector.get(MARKED_OPTIONS)).toBe(searchAiMarkedOptions);
});
it('should be rendered when answer is loaded successfully', fakeAsync(() => { it('should be rendered when answer is loaded successfully', fakeAsync(() => {
getAnswerSpyAnd.returnValues( getAnswerSpyAnd.returnValues(
throwError(() => 'error'), throwError(() => 'error'),

View File

@ -43,7 +43,8 @@ import { ModalAiService } from '../../../../services/modal-ai.service';
import { ViewNodeAction } from '@alfresco/aca-shared/store'; import { ViewNodeAction } from '@alfresco/aca-shared/store';
import { ViewerService } from '@alfresco/aca-content/viewer'; import { ViewerService } from '@alfresco/aca-content/viewer';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MarkdownComponent } from 'ngx-markdown'; import { MarkdownModule, MARKED_OPTIONS, provideMarkdown } from 'ngx-markdown';
import { searchAiMarkedOptions } from './search-ai-marked-options';
@Component({ @Component({
standalone: true, standalone: true,
@ -58,7 +59,15 @@ import { MarkdownComponent } from 'ngx-markdown';
EmptyContentComponent, EmptyContentComponent,
MatCardModule, MatCardModule,
MatTooltipModule, MatTooltipModule,
MarkdownComponent MarkdownModule
],
providers: [
provideMarkdown({
markedOptions: {
provide: MARKED_OPTIONS,
useValue: searchAiMarkedOptions
}
})
], ],
selector: 'aca-search-ai-results', selector: 'aca-search-ai-results',
templateUrl: './search-ai-results.component.html', templateUrl: './search-ai-results.component.html',