Skip to content

Commit

Permalink
[Axon 97] Fix image rendering in Jira Cloud tickets' description and …
Browse files Browse the repository at this point in the history
…comments (#104)

* Fix images in Jira Cloud tickets

* fix linting

* rewrote comment to make it clearer
  • Loading branch information
marcomura authored Feb 11, 2025
1 parent cd0e638 commit c8b2dc7
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 16 deletions.
75 changes: 75 additions & 0 deletions src/util/html.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as html from './html';

describe('html util', () => {
describe('fix relative URLs in html', () => {
it('replaceRelativeURLsWithAbsolute correctly replaces the relative URL', () => {
const baseUrl = 'https://www.domain.com/path1/path2/path3';
const htmlText = '<p><label>/imgs/test.png</label><img src="/imgs/test.png" /></p>';

const expectedHtml =
'<p><label>/imgs/test.png</label><img src="https://www.domain.com/path1/path2/imgs/test.png" /></p>';

const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
expect(fixedHtml).toBe(expectedHtml);
});

it('replaceRelativeURLsWithAbsolute correctly replaces all relative URLs', () => {
const baseUrl = 'https://www.domain.com/path1/path2/path3';
const htmlText =
'<p>' +
'<label>/imgs/test1.png</label><img src="/imgs/test1.png" />' +
'<label>/imgs/test2.png</label><img src="/imgs/test2.png" />' +
"<label>/imgs/test3.png</label><img src='/imgs/test3.png' />" +
"<label>/imgs/test4.png</label><img src='/imgs/test4.png' />" +
'</p>';

const expectedHtml =
'<p>' +
'<label>/imgs/test1.png</label><img src="https://www.domain.com/path1/path2/imgs/test1.png" />' +
'<label>/imgs/test2.png</label><img src="https://www.domain.com/path1/path2/imgs/test2.png" />' +
"<label>/imgs/test3.png</label><img src='https://www.domain.com/path1/path2/imgs/test3.png' />" +
"<label>/imgs/test4.png</label><img src='https://www.domain.com/path1/path2/imgs/test4.png' />" +
'</p>';

const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
expect(fixedHtml).toBe(expectedHtml);
});

it("replaceRelativeURLsWithAbsolute doesn't replaces absolute URLs", () => {
const baseUrl = 'https://www.domain.com/path1/path2/path3';
const htmlText = '<p><label>/imgs/test.png</label><img src="https://www.domain.com/imgs/test.png" /></p>';

const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
expect(fixedHtml).toBe(htmlText);
});

it("if there aren't relative URLs, nothing changes", () => {
const baseUrl = 'https://www.domain.com/path1/path2/path3';
const htmlText = '<p><label>hello</label></p>';

const fixedHtml = html.replaceRelativeURLsWithAbsolute(htmlText, baseUrl);
expect(fixedHtml).toBe(htmlText);
});

it('last segment of the path is ignored unless it ends with a /', () => {
const htmlText = '<img src="/imgs/test.png" />';

expect(html.replaceRelativeURLsWithAbsolute(htmlText, 'https://www.domain.com/path1/path2')).toBe(
'<img src="https://www.domain.com/path1/imgs/test.png" />',
);
expect(html.replaceRelativeURLsWithAbsolute(htmlText, 'https://www.domain.com/path1/path2/')).toBe(
'<img src="https://www.domain.com/path1/path2/imgs/test.png" />',
);
});

it('nullables are correctly handled', () => {
expect(html.replaceRelativeURLsWithAbsolute('', 'https://www.domain.com/')).toBe('');
expect(html.replaceRelativeURLsWithAbsolute(null!, 'https://www.domain.com/')).toBe(null);
expect(html.replaceRelativeURLsWithAbsolute(undefined!, 'https://www.domain.com/')).toBe(undefined);

expect(html.replaceRelativeURLsWithAbsolute('<p></p>', '')).toBe('<p></p>');
expect(html.replaceRelativeURLsWithAbsolute('<p></p>', null!)).toBe('<p></p>');
expect(html.replaceRelativeURLsWithAbsolute('<p></p>', undefined!)).toBe('<p></p>');
});
});
});
15 changes: 15 additions & 0 deletions src/util/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const regex1 = / src="\/[^"]+/g;
const regex2 = / src='\/[^']+/g;

export function replaceRelativeURLsWithAbsolute(renderedHtml: string, baseApiUrl: string): string | undefined {
if (!renderedHtml || !baseApiUrl) {
return renderedHtml;
}

// The regex is searching for anything starting with ' src="/' which is 7 chars long,
// and we need to get the relative URL without including its first /, so anything after those 7 characters.
// Therefore, substring(7).
return renderedHtml
.replace(regex1, (x) => ` src=\"${new URL(x.substring(7), baseApiUrl).href}`)
.replace(regex2, (x) => ` src=\'${new URL(x.substring(7), baseApiUrl).href}`);
}
7 changes: 5 additions & 2 deletions src/webviews/components/issue/AbstractIssueEditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from '../../../ipc/issueMessaging';
import { Action, HostErrorMessage, Message } from '../../../ipc/messaging';
import { ConnectionTimeout } from '../../../util/time';
import { replaceRelativeURLsWithAbsolute } from '../../../util/html';
import { colorToLozengeAppearanceMap } from '../colors';
import * as FieldValidators from '../fieldValidators';
import * as SelectFieldHelper from '../selectFieldHelper';
Expand Down Expand Up @@ -371,7 +372,7 @@ export abstract class AbstractIssueEditorPage<
}
}, 100);

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

if ((field as InputFieldUI).isMultiline) {
const html = this.state.fieldValues[`${field.key}.rendered`] || undefined;
const fixedHtml = replaceRelativeURLsWithAbsolute(html, baseApiUrl);
markup = (
<EditRenderedTextArea
text={this.state.fieldValues[`${field.key}`]}
renderedText={this.state.fieldValues[`${field.key}.rendered`]}
renderedText={fixedHtml}
fetchUsers={async (input: string) =>
(await this.fetchUsers(input)).map((user) => ({
displayName: user.displayName,
Expand Down
5 changes: 4 additions & 1 deletion src/webviews/components/issue/CommentComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import React, { useState } from 'react';
import { DetailedSiteInfo } from '../../../atlclients/authInfo';
import { RenderedContent } from '../RenderedContent';
import { TextAreaEditor } from './TextAreaEditor';
import { replaceRelativeURLsWithAbsolute } from '../../../util/html';

type Props = {
siteDetails: DetailedSiteInfo;
comment: JiraComment;
isServiceDeskProject: boolean;
baseApiUrl: string;
fetchUsers: (input: string) => Promise<any[]>;
onSave: (commentBody: string, commentId?: string, restriction?: CommentVisibility) => void;
onDelete: (commentId: string) => void;
Expand All @@ -27,6 +29,7 @@ export const CommentComponent: React.FC<Props> = ({
siteDetails,
comment,
isServiceDeskProject,
baseApiUrl,
fetchUsers,
onSave,
onDelete,
Expand All @@ -37,7 +40,7 @@ export const CommentComponent: React.FC<Props> = ({
const [isSaving, setIsSaving] = useState(false);

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

if (editing && !isSaving) {
Expand Down
4 changes: 2 additions & 2 deletions src/webviews/components/issue/CreateIssuePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,11 @@ export default class CreateIssuePage extends AbstractIssueEditorPage<Emit, Accep
};

getCommonFieldMarkup(): any {
return this.commonFields.map((field) => this.getInputMarkup(field));
return this.commonFields.map((field) => this.getInputMarkup(field, this.state.siteDetails.baseApiUrl));
}

getAdvancedFieldMarkup(): any {
return this.advancedFields.map((field) => this.getInputMarkup(field));
return this.advancedFields.map((field) => this.getInputMarkup(field, this.state.siteDetails.baseApiUrl));
}

public render() {
Expand Down
55 changes: 44 additions & 11 deletions src/webviews/components/issue/JiraIssuePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
// TODO: proper error handling in webviews :'(
// This is a temporary workaround to hopefully troubleshoot
// https://github.com/atlassian/atlascode/issues/46
override getInputMarkup(field: FieldUI, editmode?: boolean, context?: String) {
override getInputMarkup(field: FieldUI, baseApiUrl: string, editmode?: boolean, context?: String) {
if (!field) {
console.warn(`Field error - no field when trying to render ${context}`);
return null;
}
return super.getInputMarkup(field, editmode);
return super.getInputMarkup(field, baseApiUrl, editmode);
}

getProjectKey = (): string => {
Expand Down Expand Up @@ -501,7 +501,14 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
/>
</Tooltip>
</div>
<h2>{this.getInputMarkup(this.state.fields['summary'], true, 'summary')}</h2>
<h2>
{this.getInputMarkup(
this.state.fields['summary'],
this.state.siteDetails.baseApiUrl,
true,
'summary',
)}
</h2>
</div>
{this.state.isErrorBannerOpen && (
<ErrorBanner onDismissError={this.handleDismissError} errorDetails={this.state.errorDetails} />
Expand All @@ -516,7 +523,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
{this.state.fields['description'] && (
<div className="ac-vpadding">
<label className="ac-field-label">{this.state.fields['description'].name}</label>
{this.getInputMarkup(this.state.fields['description'], true, 'description')}
{this.getInputMarkup(
this.state.fields['description'],
this.state.siteDetails.baseApiUrl,
true,
'description',
)}
</div>
)}
{this.state.fields['attachment'] &&
Expand Down Expand Up @@ -552,7 +564,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
this.state.fieldValues['environment'].trim() !== '' && (
<div className="ac-vpadding">
<label className="ac-field-label">{this.state.fields['environment'].name}</label>
{this.getInputMarkup(this.state.fields['environment'], true, 'environment')}
{this.getInputMarkup(
this.state.fields['environment'],
this.state.siteDetails.baseApiUrl,
true,
'environment',
)}
</div>
)}

Expand All @@ -568,7 +585,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
!this.state.isEpic &&
!this.state.fieldValues['issuetype'].subtask && (
<div className="ac-vpadding">
{this.getInputMarkup(this.state.fields['subtasks'], true, 'subtasks')}
{this.getInputMarkup(
this.state.fields['subtasks'],
this.state.siteDetails.baseApiUrl,
true,
'subtasks',
)}
<IssueList
issues={this.state.fieldValues['subtasks']}
onIssueClick={this.handleOpenIssue}
Expand All @@ -577,7 +599,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
)}
{this.state.fields['issuelinks'] && (
<div className="ac-vpadding">
{this.getInputMarkup(this.state.fields['issuelinks'], true, 'issuelinks')}
{this.getInputMarkup(
this.state.fields['issuelinks'],
this.state.siteDetails.baseApiUrl,
true,
'issuelinks',
)}
<LinkedIssues
issuelinks={this.state.fieldValues['issuelinks']}
onIssueClick={this.handleOpenIssue}
Expand All @@ -604,6 +631,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
this.state.fieldValues['project'].projectTypeKey === 'service_desk'
}
comment={comment}
baseApiUrl={this.state.siteDetails.baseApiUrl}
fetchUsers={this.fetchUsers}
onSave={this.handleUpdateComment}
onDelete={this.handleDeleteComment}
Expand All @@ -624,7 +652,12 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
}}
/>
))}
{this.getInputMarkup(this.state.fields['comment'], true, 'comment')}
{this.getInputMarkup(
this.state.fields['comment'],
this.state.siteDetails.baseApiUrl,
true,
'comment',
)}
</div>
)}
</div>
Expand Down Expand Up @@ -814,7 +847,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
field && (
<div className="ac-vpadding" onClick={onClick}>
<label className="ac-field-label">{field.name}</label>
{this.getInputMarkup(field, true, key)}
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, key)}
</div>
)
);
Expand All @@ -831,7 +864,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
markups.push(
<div className="ac-vpadding">
<label className="ac-field-label">{field.name}</label>
{this.getInputMarkup(field, true, `Advanced sidebar`)}
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, `Advanced sidebar`)}
</div>,
);
}
Expand Down Expand Up @@ -873,7 +906,7 @@ export default class JiraIssuePage extends AbstractIssueEditorPage<Emit, Accept,
markups.push(
<div className="ac-vpadding">
<label className="ac-field-label">{field.name}</label>
{this.getInputMarkup(field, true, `Advanced main`)}
{this.getInputMarkup(field, this.state.siteDetails.baseApiUrl, true, `Advanced main`)}
</div>,
);
}
Expand Down

0 comments on commit c8b2dc7

Please sign in to comment.