Skip to content

Commit c8b2dc7

Browse files
authored
[Axon 97] Fix image rendering in Jira Cloud tickets' description and comments (#104)
* Fix images in Jira Cloud tickets * fix linting * rewrote comment to make it clearer
1 parent cd0e638 commit c8b2dc7

File tree

6 files changed

+145
-16
lines changed

6 files changed

+145
-16
lines changed

src/util/html.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as html from './html';
2+
3+
describe('html util', () => {
4+
describe('fix relative URLs in html', () => {
5+
it('replaceRelativeURLsWithAbsolute correctly replaces the relative URL', () => {
6+
const baseUrl = 'https://www.domain.com/path1/path2/path3';
7+
const htmlText = '<p><label>/imgs/test.png</label><img src="/imgs/test.png" /></p>';
8+
9+
const expectedHtml =
10+
'<p><label>/imgs/test.png</label><img src="https://www.domain.com/path1/path2/imgs/test.png" /></p>';
11+
12+
const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
13+
expect(fixedHtml).toBe(expectedHtml);
14+
});
15+
16+
it('replaceRelativeURLsWithAbsolute correctly replaces all relative URLs', () => {
17+
const baseUrl = 'https://www.domain.com/path1/path2/path3';
18+
const htmlText =
19+
'<p>' +
20+
'<label>/imgs/test1.png</label><img src="/imgs/test1.png" />' +
21+
'<label>/imgs/test2.png</label><img src="/imgs/test2.png" />' +
22+
"<label>/imgs/test3.png</label><img src='/imgs/test3.png' />" +
23+
"<label>/imgs/test4.png</label><img src='/imgs/test4.png' />" +
24+
'</p>';
25+
26+
const expectedHtml =
27+
'<p>' +
28+
'<label>/imgs/test1.png</label><img src="https://www.domain.com/path1/path2/imgs/test1.png" />' +
29+
'<label>/imgs/test2.png</label><img src="https://www.domain.com/path1/path2/imgs/test2.png" />' +
30+
"<label>/imgs/test3.png</label><img src='https://www.domain.com/path1/path2/imgs/test3.png' />" +
31+
"<label>/imgs/test4.png</label><img src='https://www.domain.com/path1/path2/imgs/test4.png' />" +
32+
'</p>';
33+
34+
const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
35+
expect(fixedHtml).toBe(expectedHtml);
36+
});
37+
38+
it("replaceRelativeURLsWithAbsolute doesn't replaces absolute URLs", () => {
39+
const baseUrl = 'https://www.domain.com/path1/path2/path3';
40+
const htmlText = '<p><label>/imgs/test.png</label><img src="https://www.domain.com/imgs/test.png" /></p>';
41+
42+
const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
43+
expect(fixedHtml).toBe(htmlText);
44+
});
45+
46+
it("if there aren't relative URLs, nothing changes", () => {
47+
const baseUrl = 'https://www.domain.com/path1/path2/path3';
48+
const htmlText = '<p><label>hello</label></p>';
49+
50+
const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
51+
expect(fixedHtml).toBe(htmlText);
52+
});
53+
54+
it('last segment of the path is ignored unless it ends with a /', () => {
55+
const htmlText = '<img src="/imgs/test.png" />';
56+
57+
expect(html.replaceRelativeURLsWithAbsolute(htmlText, 'https://www.domain.com/path1/path2')).toBe(
58+
'<img src="https://www.domain.com/path1/imgs/test.png" />',
59+
);
60+
expect(html.replaceRelativeURLsWithAbsolute(htmlText, 'https://www.domain.com/path1/path2/')).toBe(
61+
'<img src="https://www.domain.com/path1/path2/imgs/test.png" />',
62+
);
63+
});
64+
65+
it('nullables are correctly handled', () => {
66+
expect(html.replaceRelativeURLsWithAbsolute('', 'https://www.domain.com/')).toBe('');
67+
expect(html.replaceRelativeURLsWithAbsolute(null!, 'https://www.domain.com/')).toBe(null);
68+
expect(html.replaceRelativeURLsWithAbsolute(undefined!, 'https://www.domain.com/')).toBe(undefined);
69+
70+
expect(html.replaceRelativeURLsWithAbsolute('<p></p>', '')).toBe('<p></p>');
71+
expect(html.replaceRelativeURLsWithAbsolute('<p></p>', null!)).toBe('<p></p>');
72+
expect(html.replaceRelativeURLsWithAbsolute('<p></p>', undefined!)).toBe('<p></p>');
73+
});
74+
});
75+
});

src/util/html.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const regex1 = / src="\/[^"]+/g;
2+
const regex2 = / src='\/[^']+/g;
3+
4+
export function replaceRelativeURLsWithAbsolute(renderedHtml: string, baseApiUrl: string): string | undefined {
5+
if (!renderedHtml || !baseApiUrl) {
6+
return renderedHtml;
7+
}
8+
9+
// The regex is searching for anything starting with ' src="/' which is 7 chars long,
10+
// and we need to get the relative URL without including its first /, so anything after those 7 characters.
11+
// Therefore, substring(7).
12+
return renderedHtml
13+
.replace(regex1, (x) => ` src=\"${new URL(x.substring(7), baseApiUrl).href}`)
14+
.replace(regex2, (x) => ` src=\'${new URL(x.substring(7), baseApiUrl).href}`);
15+
}

src/webviews/components/issue/AbstractIssueEditorPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from '../../../ipc/issueMessaging';
4141
import { Action, HostErrorMessage, Message } from '../../../ipc/messaging';
4242
import { ConnectionTimeout } from '../../../util/time';
43+
import { replaceRelativeURLsWithAbsolute } from '../../../util/html';
4344
import { colorToLozengeAppearanceMap } from '../colors';
4445
import * as FieldValidators from '../fieldValidators';
4546
import * as SelectFieldHelper from '../selectFieldHelper';
@@ -371,7 +372,7 @@ export abstract class AbstractIssueEditorPage<
371372
}
372373
}, 100);
373374

374-
protected getInputMarkup(field: FieldUI, editmode: boolean = false): any {
375+
protected getInputMarkup(field: FieldUI, baseApiUrl: string, editmode: boolean = false): any {
375376
switch (field.uiType) {
376377
case UIType.Input: {
377378
let validateFunc = this.getValidateFunction(field, editmode);
@@ -401,10 +402,12 @@ export abstract class AbstractIssueEditorPage<
401402
let markup: React.ReactNode = <p></p>;
402403

403404
if ((field as InputFieldUI).isMultiline) {
405+
const html = this.state.fieldValues[`${field.key}.rendered`] || undefined;
406+
const fixedHtml = replaceRelativeURLsWithAbsolute(html, baseApiUrl);
404407
markup = (
405408
<EditRenderedTextArea
406409
text={this.state.fieldValues[`${field.key}`]}
407-
renderedText={this.state.fieldValues[`${field.key}.rendered`]}
410+
renderedText={fixedHtml}
408411
fetchUsers={async (input: string) =>
409412
(await this.fetchUsers(input)).map((user) => ({
410413
displayName: user.displayName,

src/webviews/components/issue/CommentComponent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import React, { useState } from 'react';
1212
import { DetailedSiteInfo } from '../../../atlclients/authInfo';
1313
import { RenderedContent } from '../RenderedContent';
1414
import { TextAreaEditor } from './TextAreaEditor';
15+
import { replaceRelativeURLsWithAbsolute } from '../../../util/html';
1516

1617
type Props = {
1718
siteDetails: DetailedSiteInfo;
1819
comment: JiraComment;
1920
isServiceDeskProject: boolean;
21+
baseApiUrl: string;
2022
fetchUsers: (input: string) => Promise<any[]>;
2123
onSave: (commentBody: string, commentId?: string, restriction?: CommentVisibility) => void;
2224
onDelete: (commentId: string) => void;
@@ -27,6 +29,7 @@ export const CommentComponent: React.FC<Props> = ({
2729
siteDetails,
2830
comment,
2931
isServiceDeskProject,
32+
baseApiUrl,
3033
fetchUsers,
3134
onSave,
3235
onDelete,
@@ -37,7 +40,7 @@ export const CommentComponent: React.FC<Props> = ({
3740
const [isSaving, setIsSaving] = useState(false);
3841

3942
const prettyCreated = `${formatDistanceToNow(parseISO(comment.created))} ago`;
40-
const body = comment.renderedBody ? comment.renderedBody : comment.body;
43+
const body = replaceRelativeURLsWithAbsolute(comment.renderedBody!, baseApiUrl) || comment.body;
4144
const type = isServiceDeskProject ? (comment.jsdPublic ? 'external' : 'internal') : undefined;
4245

4346
if (editing && !isSaving) {

src/webviews/components/issue/CreateIssuePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,11 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
278278
};
279279

280280
getCommonFieldMarkup(): any {
281-
return this.commonFields.map((field) => this.getInputMarkup(field));
281+
return this.commonFields.map((field) => this.getInputMarkup(field, this.state.siteDetails.baseApiUrl));
282282
}
283283

284284
getAdvancedFieldMarkup(): any {
285-
return this.advancedFields.map((field) => this.getInputMarkup(field));
285+
return this.advancedFields.map((field) => this.getInputMarkup(field, this.state.siteDetails.baseApiUrl));
286286
}
287287

288288
public render() {

src/webviews/components/issue/JiraIssuePage.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
7979
// TODO: proper error handling in webviews :'(
8080
// This is a temporary workaround to hopefully troubleshoot
8181
// https://github.com/atlassian/atlascode/issues/46
82-
override getInputMarkup(field: FieldUI, editmode?: boolean, context?: String) {
82+
override getInputMarkup(field: FieldUI, baseApiUrl: string, editmode?: boolean, context?: String) {
8383
if (!field) {
8484
console.warn(`Field error - no field when trying to render ${context}`);
8585
return null;
8686
}
87-
return super.getInputMarkup(field, editmode);
87+
return super.getInputMarkup(field, baseApiUrl, editmode);
8888
}
8989

9090
getProjectKey = (): string => {
@@ -501,7 +501,14 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
501501
/>
502502
</Tooltip>
503503
</div>
504-
<h2>{this.getInputMarkup(this.state.fields['summary'], true, 'summary')}</h2>
504+
<h2>
505+
{this.getInputMarkup(
506+
this.state.fields['summary'],
507+
this.state.siteDetails.baseApiUrl,
508+
true,
509+
'summary',
510+
)}
511+
</h2>
505512
</div>
506513
{this.state.isErrorBannerOpen && (
507514
<ErrorBanner onDismissError={this.handleDismissError} errorDetails={this.state.errorDetails} />
@@ -516,7 +523,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
516523
{this.state.fields['description'] && (
517524
<div className="ac-vpadding">
518525
<label className="ac-field-label">{this.state.fields['description'].name}</label>
519-
{this.getInputMarkup(this.state.fields['description'], true, 'description')}
526+
{this.getInputMarkup(
527+
this.state.fields['description'],
528+
this.state.siteDetails.baseApiUrl,
529+
true,
530+
'description',
531+
)}
520532
</div>
521533
)}
522534
{this.state.fields['attachment'] &&
@@ -552,7 +564,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
552564
this.state.fieldValues['environment'].trim() !== '' && (
553565
<div className="ac-vpadding">
554566
<label className="ac-field-label">{this.state.fields['environment'].name}</label>
555-
{this.getInputMarkup(this.state.fields['environment'], true, 'environment')}
567+
{this.getInputMarkup(
568+
this.state.fields['environment'],
569+
this.state.siteDetails.baseApiUrl,
570+
true,
571+
'environment',
572+
)}
556573
</div>
557574
)}
558575

@@ -568,7 +585,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
568585
!this.state.isEpic &&
569586
!this.state.fieldValues['issuetype'].subtask && (
570587
<div className="ac-vpadding">
571-
{this.getInputMarkup(this.state.fields['subtasks'], true, 'subtasks')}
588+
{this.getInputMarkup(
589+
this.state.fields['subtasks'],
590+
this.state.siteDetails.baseApiUrl,
591+
true,
592+
'subtasks',
593+
)}
572594
<IssueList
573595
issues={this.state.fieldValues['subtasks']}
574596
onIssueClick={this.handleOpenIssue}
@@ -577,7 +599,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
577599
)}
578600
{this.state.fields['issuelinks'] && (
579601
<div className="ac-vpadding">
580-
{this.getInputMarkup(this.state.fields['issuelinks'], true, 'issuelinks')}
602+
{this.getInputMarkup(
603+
this.state.fields['issuelinks'],
604+
this.state.siteDetails.baseApiUrl,
605+
true,
606+
'issuelinks',
607+
)}
581608
<LinkedIssues
582609
issuelinks={this.state.fieldValues['issuelinks']}
583610
onIssueClick={this.handleOpenIssue}
@@ -604,6 +631,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
604631
this.state.fieldValues['project'].projectTypeKey === 'service_desk'
605632
}
606633
comment={comment}
634+
baseApiUrl={this.state.siteDetails.baseApiUrl}
607635
fetchUsers={this.fetchUsers}
608636
onSave={this.handleUpdateComment}
609637
onDelete={this.handleDeleteComment}
@@ -624,7 +652,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
624652
}}
625653
/>
626654
))}
627-
{this.getInputMarkup(this.state.fields['comment'], true, 'comment')}
655+
{this.getInputMarkup(
656+
this.state.fields['comment'],
657+
this.state.siteDetails.baseApiUrl,
658+
true,
659+
'comment',
660+
)}
628661
</div>
629662
)}
630663
</div>
@@ -814,7 +847,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
814847
field && (
815848
<div className="ac-vpadding" onClick={onClick}>
816849
<label className="ac-field-label">{field.name}</label>
817-
{this.getInputMarkup(field, true, key)}
850+
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, key)}
818851
</div>
819852
)
820853
);
@@ -831,7 +864,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
831864
markups.push(
832865
<div className="ac-vpadding">
833866
<label className="ac-field-label">{field.name}</label>
834-
{this.getInputMarkup(field, true, `Advanced sidebar`)}
867+
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, `Advanced sidebar`)}
835868
</div>,
836869
);
837870
}
@@ -873,7 +906,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
873906
markups.push(
874907
<div className="ac-vpadding">
875908
<label className="ac-field-label">{field.name}</label>
876-
{this.getInputMarkup(field, true, `Advanced main`)}
909+
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, `Advanced main`)}
877910
</div>,
878911
);
879912
}

0 commit comments

Comments
 (0)