提交 6be86190 编写于 作者: T Tomas Vik

Merge branch '215-fix-double-comments' into 'main'

fix: don't double send message from issue detail

See merge request gitlab-org/gitlab-vscode-extension!125
...@@ -9,7 +9,10 @@ ...@@ -9,7 +9,10 @@
"runtimeExecutable": "${execPath}", "runtimeExecutable": "${execPath}",
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceRoot}"], "args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceRoot}"],
"stopOnEntry": false, "stopOnEntry": false,
"preLaunchTask": "${defaultBuildTask}" "preLaunchTask": "${defaultBuildTask}",
"env": {
"NODE_ENV": "development"
}
}, },
{ {
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script nonce="{{nonce}}" src="{{devScriptUri}}"></script> <script nonce="{{nonce}}" src="{{vendorUri}}"></script>
<script nonce="{{nonce}}" src="{{appScriptUri}}"></script>
</body> </body>
</html> </html>
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto');
const vscode = require('vscode'); const vscode = require('vscode');
const gitLabService = require('./gitlab_service'); const gitLabService = require('./gitlab_service');
...@@ -9,19 +10,6 @@ const addDeps = ctx => { ...@@ -9,19 +10,6 @@ const addDeps = ctx => {
context = ctx; context = ctx;
}; };
const getNonce = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// Temporarily disable eslint to be able to start enforcing stricter rules
// eslint-disable-next-line no-plusplus
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
const getResources = panel => { const getResources = panel => {
const paths = { const paths = {
appScriptUri: 'src/webview/dist/js/app.js', appScriptUri: 'src/webview/dist/js/app.js',
...@@ -40,17 +28,18 @@ const getResources = panel => { ...@@ -40,17 +28,18 @@ const getResources = panel => {
}; };
const getIndexPath = () => { const getIndexPath = () => {
const isDev = !fs.existsSync(path.join(context.extensionPath, 'src/webview/dist/js/app.js')); const isDev = process.env.NODE_ENV === 'development';
return isDev ? 'src/webview/public/dev.html' : 'src/webview/public/index.html'; return isDev ? 'src/webview/public/dev.html' : 'src/webview/public/index.html';
}; };
const replaceResources = panel => { const replaceResources = panel => {
const { appScriptUri, vendorUri, styleUri, devScriptUri } = getResources(panel); const { appScriptUri, vendorUri, styleUri, devScriptUri } = getResources(panel);
const nonce = crypto.randomBytes(20).toString('hex');
return fs return fs
.readFileSync(path.join(context.extensionPath, getIndexPath()), 'UTF-8') .readFileSync(path.join(context.extensionPath, getIndexPath()), 'UTF-8')
.replace(/{{nonce}}/gm, getNonce()) .replace(/{{nonce}}/gm, nonce)
.replace('{{styleUri}}', styleUri) .replace('{{styleUri}}', styleUri)
.replace('{{vendorUri}}', vendorUri) .replace('{{vendorUri}}', vendorUri)
.replace('{{appScriptUri}}', appScriptUri) .replace('{{appScriptUri}}', appScriptUri)
...@@ -66,101 +55,96 @@ const createPanel = issuable => { ...@@ -66,101 +55,96 @@ const createPanel = issuable => {
}); });
}; };
function sendIssuableAndDiscussions(panel, issuable, discussions, appIsReady) { const createMessageHandler = (panel, issuable, workspaceFolder) => async message => {
if (!discussions || !appIsReady) return; if (message.command === 'renderMarkdown') {
panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions }); const alteredMarkdown = message.markdown.replace(
} /\(\/.*(\/-)?\/merge_requests\//,
'(/-/merge_requests/',
);
let rendered = await gitLabService.renderMarkdown(alteredMarkdown, workspaceFolder);
rendered = (rendered || '')
.replace(/ src=".*" alt/gim, ' alt')
.replace(/" data-src/gim, '" src')
.replace(/ href="\//gim, ` href="${vscode.workspace.getConfiguration('gitlab').instanceUrl}/`)
.replace(/\/master\/-\/merge_requests\//gim, '/-/merge_requests/');
panel.webview.postMessage({
type: 'markdownRendered',
ref: message.ref,
object: message.object,
markdown: rendered,
});
}
async function handleCreate(panel, issuable, workspaceFolder) { if (message.command === 'saveNote') {
let discussions = false; const response = await gitLabService.saveNote({
let labelEvents = false; issuable: message.issuable,
let appIsReady = false; note: message.note,
panel.webview.onDidReceiveMessage(async message => { noteType: message.noteType,
if (message.command === 'appReady') { });
appIsReady = true;
sendIssuableAndDiscussions(panel, issuable, discussions, appIsReady); if (response.success !== false) {
const newDiscussions = await gitLabService.fetchDiscussions(issuable);
panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions: newDiscussions });
panel.webview.postMessage({ type: 'noteSaved' });
} else {
panel.webview.postMessage({ type: 'noteSaved', status: false });
} }
}
};
if (message.command === 'renderMarkdown') { async function handleChangeViewState(panel, issuable) {
// Temporarily disable eslint to be able to start enforcing stricter rules if (!panel.active) return;
// eslint-disable-next-line no-param-reassign
message.markdown = message.markdown.replace(
/\(\/.*(\/-)?\/merge_requests\//,
'(/-/merge_requests/',
);
let rendered = await gitLabService.renderMarkdown(message.markdown, workspaceFolder);
rendered = (rendered || '')
.replace(/ src=".*" alt/gim, ' alt')
.replace(/" data-src/gim, '" src')
.replace(
/ href="\//gim,
` href="${vscode.workspace.getConfiguration('gitlab').instanceUrl}/`,
)
.replace(/\/master\/-\/merge_requests\//gim, '/-/merge_requests/');
panel.webview.postMessage({
type: 'markdownRendered',
ref: message.ref,
object: message.object,
markdown: rendered,
});
}
if (message.command === 'saveNote') { const appReadyPromise = new Promise(resolve => {
const response = await gitLabService.saveNote({ const sub = panel.webview.onDidReceiveMessage(async message => {
issuable: message.issuable, if (message.command === 'appReady') {
note: message.note, sub.dispose();
noteType: message.noteType, resolve();
});
if (response.status !== false) {
const newDiscussions = await gitLabService.fetchDiscussions(issuable);
panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions: newDiscussions });
panel.webview.postMessage({ type: 'noteSaved' });
} else {
panel.webview.postMessage({ type: 'noteSaved', status: false });
} }
} });
}); });
// TODO: Call APIs in parallel const [discussions, labelEvents] = await Promise.all([
discussions = await gitLabService.fetchDiscussions(issuable); gitLabService.fetchDiscussions(issuable),
labelEvents = await gitLabService.fetchLabelEvents(issuable); gitLabService.fetchLabelEvents(issuable),
discussions = discussions.concat(labelEvents); ]);
discussions.sort((a, b) => {
const combinedEvents = discussions.concat(labelEvents);
combinedEvents.sort((a, b) => {
const aCreatedAt = a.label ? a.created_at : a.notes[0].created_at; const aCreatedAt = a.label ? a.created_at : a.notes[0].created_at;
const bCreatedAt = b.label ? b.created_at : b.notes[0].created_at; const bCreatedAt = b.label ? b.created_at : b.notes[0].created_at;
return aCreatedAt < bCreatedAt ? -1 : 1; return aCreatedAt < bCreatedAt ? -1 : 1;
}); });
sendIssuableAndDiscussions(panel, issuable, discussions, appIsReady); await appReadyPromise;
panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions: combinedEvents });
} }
const getIconPathForIssuable = issuable => {
const getIconUri = (shade, file) =>
vscode.Uri.file(path.join(context.extensionPath, 'src', 'assets', 'images', shade, file));
const lightIssueIcon = getIconUri('light', 'issues.svg');
const lightMrIcon = getIconUri('light', 'merge_requests.svg');
const darkIssueIcon = getIconUri('dark', 'issues.svg');
const darkMrIcon = getIconUri('dark', 'merge_requests.svg');
const isMr = issuable.squash_commit_sha !== undefined;
return isMr
? { light: lightMrIcon, dark: darkMrIcon }
: { light: lightIssueIcon, dark: darkIssueIcon };
};
async function create(issuable, workspaceFolder) { async function create(issuable, workspaceFolder) {
const panel = createPanel(issuable); const panel = createPanel(issuable);
const html = replaceResources(panel); const html = replaceResources(panel);
panel.webview.html = html; panel.webview.html = html;
panel.iconPath = getIconPathForIssuable(issuable);
let lightIconUri = vscode.Uri.file(
path.join(context.extensionPath, 'src', 'assets', 'images', 'light', 'issues.svg'),
);
let darkIconUri = vscode.Uri.file(
path.join(context.extensionPath, 'src', 'assets', 'images', 'dark', 'issues.svg'),
);
if (issuable.squash_commit_sha !== undefined) {
lightIconUri = vscode.Uri.file(
path.join(context.extensionPath, 'src', 'assets', 'images', 'light', 'merge_requests.svg'),
);
darkIconUri = vscode.Uri.file(
path.join(context.extensionPath, 'src', 'assets', 'images', 'dark', 'merge_requests.svg'),
);
}
panel.iconPath = { light: lightIconUri, dark: darkIconUri };
panel.onDidChangeViewState(() => { panel.onDidChangeViewState(() => {
handleCreate(panel, issuable, workspaceFolder); handleChangeViewState(panel, issuable);
}); });
handleCreate(panel, issuable, workspaceFolder); panel.webview.onDidReceiveMessage(createMessageHandler(panel, issuable, workspaceFolder));
return panel;
} }
exports.addDeps = addDeps; exports.addDeps = addDeps;
......
...@@ -24,6 +24,11 @@ const createTextEndpoint = (path, response) => ...@@ -24,6 +24,11 @@ const createTextEndpoint = (path, response) =>
return res(ctx.status(200), ctx.text(response)); return res(ctx.status(200), ctx.text(response));
}); });
const createPostEndpoint = (path, response) =>
rest.post(`${API_URL_PREFIX}${path}`, (req, res, ctx) => {
return res(ctx.status(201), ctx.json(response));
});
const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404))); const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => res(ctx.status(404)));
const getServer = (handlers = []) => { const getServer = (handlers = []) => {
...@@ -37,4 +42,10 @@ const getServer = (handlers = []) => { ...@@ -37,4 +42,10 @@ const getServer = (handlers = []) => {
return server; return server;
}; };
module.exports = { getServer, createJsonEndpoint, createQueryJsonEndpoint, createTextEndpoint }; module.exports = {
getServer,
createJsonEndpoint,
createQueryJsonEndpoint,
createTextEndpoint,
createPostEndpoint,
};
const assert = require('assert');
const vscode = require('vscode');
const sinon = require('sinon');
const EventEmitter = require('events');
const webviewController = require('../../src/webview_controller');
const { tokenService } = require('../../src/services/token_service');
const openIssueResponse = require('./fixtures/rest/open_issue.json');
const {
getServer,
createJsonEndpoint,
createPostEndpoint,
} = require('./test_infrastructure/mock_server');
const { GITLAB_URL } = require('./test_infrastructure/constants');
const waitForMessage = (panel, type) =>
new Promise(resolve => {
const sub = panel.webview.onDidReceiveMessage(message => {
if (message.type !== type) return;
sub.dispose();
resolve(message);
});
});
describe('GitLab webview', () => {
let server;
let webviewPanel;
const sandbox = sinon.createSandbox();
before(async () => {
server = getServer([
createJsonEndpoint(
`/projects/${openIssueResponse.project_id}/issues/${openIssueResponse.iid}/discussions`,
[],
),
createJsonEndpoint(
`/projects/${openIssueResponse.project_id}/issues/${openIssueResponse.iid}/resource_label_events`,
[],
),
createPostEndpoint(
`/projects/${openIssueResponse.project_id}/issues/${openIssueResponse.iid}/notes`,
{},
),
]);
await tokenService.setToken(GITLAB_URL, 'abcd-secret');
});
/*
This method replaces the mechanism that the Webview panel uses for sending messages between
the extension and the webview. This is necessary since we can't control the webview and so
we need to be able to simulate events triggered by the webview and see that the extension
handles them well.
*/
const replacePanelEventSystem = () => {
const { createWebviewPanel } = vscode.window;
sandbox.stub(vscode.window, 'createWebviewPanel').callsFake((viewType, title, column) => {
const panel = createWebviewPanel(viewType, title, column);
const eventEmitter = new EventEmitter();
sandbox
.stub(panel.webview, 'postMessage')
.callsFake(message => eventEmitter.emit('', message));
sandbox.stub(panel.webview, 'onDidReceiveMessage').callsFake(listener => {
eventEmitter.on('', listener);
return { dispose: () => {} };
});
return panel;
});
};
beforeEach(async () => {
server.resetHandlers();
replacePanelEventSystem();
webviewPanel = await webviewController.create(
openIssueResponse,
vscode.workspace.workspaceFolders[0],
);
});
afterEach(async () => {
sandbox.restore();
});
after(async () => {
server.close();
await tokenService.setToken(GITLAB_URL, undefined);
});
it('sends a message', async () => {
webviewPanel.webview.postMessage({
command: 'saveNote',
issuable: openIssueResponse,
note: 'Hello',
noteType: { path: 'issues' },
});
const sentMessage = await waitForMessage(webviewPanel, 'noteSaved');
assert.strictEqual(sentMessage.type, 'noteSaved');
assert.strictEqual(sentMessage.status, undefined);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册