diff --git a/package-lock.json b/package-lock.json index d9aab94b..2f14df73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@atlassianlabs/jira-metaui-client": "^1.1.0", "@atlassianlabs/jira-metaui-transformer": "^1.1.0", "@atlassianlabs/jira-pi-client": "^1.1.0", + "@atlassianlabs/jira-pi-common-models": "^1.1.0", "@atlassianlabs/jira-pi-meta-models": "^1.1.0", "@atlassianlabs/pi-client-common": "^1.1.0", "@date-io/date-fns": "^3.0.0", @@ -56,6 +57,7 @@ "awesome-debounce-promise": "^2.1.0", "axios": "^1.7.4", "axios-curlirize": "^1.3.4", + "base64-arraybuffer-es6": "^3.1.0", "clsx": "^1.1.0", "date-fns": "^3.2.0", "dotenv": "^16.4.5", @@ -65,7 +67,7 @@ "fast-deep-equal": "^3.1.1", "filesize": "^4.1.2", "flatten-anything": "^2.0.1", - "form-data": "^2.5.1", + "form-data": "^2.5.2", "git-url-parse": "^15.0.0", "ini": "^1.3.8", "jwt-decode": "^3.1.2", @@ -154,7 +156,7 @@ "@types/terser-webpack-plugin": "^2.2.0", "@types/turndown": "^5.0.1", "@types/uuid": "^3.4.7", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.77.0", "@types/webpack": "^5.28.5", "@types/webpack-manifest-plugin": "^2.1.0", "@types/websocket": "^1.0.0", @@ -213,7 +215,7 @@ "webpack-node-externals": "^3.0.0" }, "engines": { - "vscode": "^1.93.0" + "vscode": "^1.77.0" } }, "../pi-clients/packages/jira-metaui-client": { @@ -8410,9 +8412,9 @@ } }, "node_modules/@vscode/vsce/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://packages.atlassian.com/api/npm/npm-remote/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { @@ -9539,9 +9541,10 @@ "license": "MIT" }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://packages.atlassian.com/api/npm/npm-remote/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -9948,6 +9951,15 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "license": "MIT" }, + "node_modules/base64-arraybuffer-es6": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-3.1.0.tgz", + "integrity": "sha512-QKKtftiSrKjilihGNLXxnrb9LJj7rnEdB1cYAqVpekFy0tisDklAf1RAgvpm0HsGYx9sv7FUbgpsrfwTyCPVLg==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -16231,14 +16243,15 @@ } }, "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" @@ -16254,6 +16267,26 @@ "node": ">= 18" } }, + "node_modules/form-data/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://packages.atlassian.com/api/npm/npm-remote/forwarded/-/forwarded-0.2.0.tgz", diff --git a/package.json b/package.json index b01b48a9..2249b006 100644 --- a/package.json +++ b/package.json @@ -1417,6 +1417,7 @@ "awesome-debounce-promise": "^2.1.0", "axios": "^1.7.4", "axios-curlirize": "^1.3.4", + "base64-arraybuffer-es6": "^3.1.0", "clsx": "^1.1.0", "date-fns": "^3.2.0", "dotenv": "^16.4.5", @@ -1426,7 +1427,7 @@ "fast-deep-equal": "^3.1.1", "filesize": "^4.1.2", "flatten-anything": "^2.0.1", - "form-data": "^2.5.1", + "form-data": "^2.5.2", "git-url-parse": "^15.0.0", "ini": "^1.3.8", "jwt-decode": "^3.1.2", diff --git a/src/util/files.ts b/src/util/files.ts new file mode 100644 index 00000000..655313eb --- /dev/null +++ b/src/util/files.ts @@ -0,0 +1,25 @@ +import { encode } from 'base64-arraybuffer-es6'; + +export interface FileWithContent extends File { + /** base64-encoded file content */ + fileContent: string | undefined; +} + +export function readFilesContentAsync(files: File[]): Promise { + const promise = new Promise((resolve) => { + let doneCount = 0; + for (let i = 0; i < files.length; ++i) { + const index = i; + const reader = new FileReader(); + reader.onloadend = (event) => { + (files[index] as FileWithContent).fileContent = encode(reader.result as ArrayBuffer); + if (++doneCount === files.length) { + resolve(files); + } + }; + reader.readAsArrayBuffer(files[index]); + } + }); + + return promise; +} diff --git a/src/webviews/components/issue/CreateIssuePage.tsx b/src/webviews/components/issue/CreateIssuePage.tsx index 5eef7731..a78c0c2a 100644 --- a/src/webviews/components/issue/CreateIssuePage.tsx +++ b/src/webviews/components/issue/CreateIssuePage.tsx @@ -28,6 +28,7 @@ import Spinner from '@atlaskit/spinner'; import { chain } from '../fieldValidators'; import { AtlascodeErrorBoundary } from 'src/react/atlascode/common/ErrorBoundary'; import { AnalyticsView } from 'src/analyticsTypes'; +import { readFilesContentAsync } from '../../../util/files'; type Emit = CommonEditorPageEmit; type Accept = CommonEditorPageAccept | CreateIssueData; @@ -64,6 +65,7 @@ const IconValue = (props: any) => ( export default class CreateIssuePage extends AbstractIssueEditorPage { private advancedFields: FieldUI[] = []; private commonFields: FieldUI[] = []; + private attachingInProgress: boolean; getProjectKey(): string { return this.state.fieldValues['project'].key; @@ -189,6 +191,33 @@ export default class CreateIssuePage extends AbstractIssueEditorPage { + if (this.attachingInProgress) { + return; + } + + if (Array.isArray(newValue) && newValue.length > 0) { + this.attachingInProgress = true; + readFilesContentAsync(newValue) + .then((filesWithContent) => { + const serFiles = filesWithContent.map((file) => { + return { + lastModified: file.lastModified, + lastModifiedDate: (file as any).lastModifiedDate, + name: file.name, + size: file.size, + type: file.type, + path: (file as any).path, + fileContent: file.fileContent, + }; + }); + + this.setState({ fieldValues: { ...this.state.fieldValues, ...{ [fieldkey]: serFiles } } }); + }) + .finally(() => (this.attachingInProgress = false)); + } + }; + protected handleInlineEdit = async (field: FieldUI, newValue: any) => { let typedVal = newValue; let fieldkey = field.key; @@ -214,20 +243,8 @@ export default class CreateIssuePage extends AbstractIssueEditorPage 0) { - const serFiles = newValue.map((file: any) => { - return { - lastModified: file.lastModified, - lastModifiedDate: file.lastModifiedDate, - name: file.name, - size: file.size, - type: file.type, - path: file.path, - }; - }); - - typedVal = serFiles; - } + await this.handleInlineAttachments(fieldkey, newValue); + return; } this.setState({ fieldValues: { ...this.state.fieldValues, ...{ [fieldkey]: typedVal } } }); diff --git a/src/webviews/components/issue/JiraIssuePage.tsx b/src/webviews/components/issue/JiraIssuePage.tsx index e5b90533..d9c5a38b 100644 --- a/src/webviews/components/issue/JiraIssuePage.tsx +++ b/src/webviews/components/issue/JiraIssuePage.tsx @@ -49,6 +49,7 @@ import WorklogForm from './WorklogForm'; import Worklogs from './Worklogs'; import { AtlascodeErrorBoundary } from 'src/react/atlascode/common/ErrorBoundary'; import { AnalyticsView } from 'src/analyticsTypes'; +import { readFilesContentAsync } from '../../../util/files'; type Emit = CommonEditorPageEmit | EditIssueAction | IssueCommentAction; type Accept = CommonEditorPageAccept | EditIssueData; @@ -68,6 +69,7 @@ const emptyState: ViewState = { export default class JiraIssuePage extends AbstractIssueEditorPage { private advancedSidebarFields: FieldUI[] = []; private advancedMainFields: FieldUI[] = []; + private attachingInProgress: boolean; constructor(props: any) { super(props); @@ -384,24 +386,34 @@ export default class JiraIssuePage extends AbstractIssueEditorPage { - this.setState({ currentInlineDialog: '', isSomethingLoading: false, loadingField: 'attachment' }); - const serFiles = files.map((file: any) => { - return { - lastModified: file.lastModified, - lastModifiedDate: file.lastModifiedDate, - name: file.name, - size: file.size, - type: file.type, - path: file.path, - }; - }); - this.postMessage({ - action: 'addAttachments', - site: this.state.siteDetails, - issueKey: this.state.key, - files: serFiles, - }); + handleAddAttachments = (files: File[]) => { + if (this.attachingInProgress) { + return; + } + this.attachingInProgress = true; + + readFilesContentAsync(files) + .then((filesWithContent) => { + this.setState({ currentInlineDialog: '', isSomethingLoading: false, loadingField: 'attachment' }); + const serFiles = filesWithContent.map((file) => { + return { + lastModified: file.lastModified, + lastModifiedDate: (file as any).lastModifiedDate, + name: file.name, + size: file.size, + type: file.type, + path: (file as any).path, + fileContent: file.fileContent, + }; + }); + this.postMessage({ + action: 'addAttachments', + site: this.state.siteDetails, + issueKey: this.state.key, + files: serFiles, + }); + }) + .finally(() => (this.attachingInProgress = false)); }; handleDeleteAttachment = (file: any) => { diff --git a/src/webviews/createIssueWebview.ts b/src/webviews/createIssueWebview.ts index 0af0692f..90dfb35e 100644 --- a/src/webviews/createIssueWebview.ts +++ b/src/webviews/createIssueWebview.ts @@ -1,5 +1,3 @@ -import * as fs from 'fs'; - import { Action, onlineStatus } from '../ipc/messaging'; import { CreateIssueAction, @@ -25,6 +23,7 @@ import { configuration } from '../config/configuration'; import { fetchCreateIssueUI } from '../jira/fetchIssue'; import { format } from 'date-fns'; import { issueCreatedEvent } from '../analytics'; +import { decode } from 'base64-arraybuffer-es6'; export interface PartialIssue { uri?: Uri; @@ -465,7 +464,10 @@ export class CreateIssueWebview if (attachments && attachments.length > 0) { let formData = new FormData(); attachments.forEach((file: any) => { - formData.append('file', fs.createReadStream(file.path), { + if (!file.fileContent) { + throw new Error(`Unable to read the file '${file.name}'`); + } + formData.append('file', Buffer.from(decode(file.fileContent)), { filename: file.name, contentType: file.type, }); diff --git a/src/webviews/jiraIssueWebview.ts b/src/webviews/jiraIssueWebview.ts index 71c63a44..1450ef4a 100644 --- a/src/webviews/jiraIssueWebview.ts +++ b/src/webviews/jiraIssueWebview.ts @@ -12,7 +12,6 @@ import { } from '@atlassianlabs/jira-pi-common-models'; import { FieldValues, ValueType } from '@atlassianlabs/jira-pi-meta-models'; import FormData from 'form-data'; -import * as fs from 'fs'; import { commands, env } from 'vscode'; import { issueCreatedEvent, issueUpdatedEvent, issueUrlCopiedEvent } from '../analytics'; import { DetailedSiteInfo, emptySiteInfo, Product, ProductJira } from '../atlclients/authInfo'; @@ -47,6 +46,7 @@ import { Logger } from '../logger'; import { iconSet, Resources } from '../resources'; import { AbstractIssueEditorWebview } from './abstractIssueEditorWebview'; import { InitializingWebview } from './abstractWebview'; +import { decode } from 'base64-arraybuffer-es6'; export class JiraIssueWebview extends AbstractIssueEditorWebview @@ -750,16 +750,18 @@ export class JiraIssueWebview if (isAddAttachmentsAction(msg)) { handled = true; try { - let client = await Container.clientManager.jiraClient(msg.site); - let formData = new FormData(); msg.files.forEach((file: any) => { - formData.append('file', fs.createReadStream(file.path), { + if (!file.fileContent) { + throw new Error(`Unable to read the file '${file.name}'`); + } + formData.append('file', Buffer.from(decode(file.fileContent)), { filename: file.name, contentType: file.type, }); }); + const client = await Container.clientManager.jiraClient(msg.site); const resp = await client.addAttachments(msg.issueKey, formData); if (