提交 adfbdf13 编写于 作者: F Fatih Acet

Merge branch 'webview' into 'master'

Issue details and discussions in VSCode

Closes #52

See merge request fatihacet/gitlab-vscode-extension!25
{
// See https://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig"
]
"recommendations": ["dbaeumer.vscode-eslint", "editorconfig.editorconfig"]
}
{
"name": "gitlab-workflow",
"version": "1.9.2",
"version": "2.0.0-pre",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
......@@ -2,7 +2,7 @@
"name": "gitlab-workflow",
"displayName": "GitLab Workflow",
"description": "GitLab VSCode integration",
"version": "1.9.3",
"version": "2.0.0-rc1",
"publisher": "fatihacet",
"license": "MIT",
"repository": {
......@@ -219,17 +219,23 @@
"type": "boolean",
"default": true,
"description": "Enable the \"All Project Merge Requests\" sidebar pane"
},
"gitlab.enableExperimentalFeatures": {
"type": "boolean",
"default": false,
"description": "Enable experimental features including showing Issue details in VSCode"
}
}
}
},
"scripts": {
"preinstall": "cd src/webview ; yarn",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test",
"eslint": "eslint --max-warnings 0 --ext .js .",
"format": "prettier --write '**/*.{js,json}'",
"publish": "vsce publish",
"webview": "cd src/webview && yarn build --watch"
"webview": "cd src/webview ; yarn build --watch"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
......
......@@ -20,7 +20,7 @@ class DataProvider {
issues.forEach(issue => {
const title = `${this.issuableSign}${issue.iid} · ${issue.title}`;
items.push(new SidebarTreeItem(title, issue.web_url));
items.push(new SidebarTreeItem(title, issue));
});
} else {
items.push(new SidebarTreeItem(this.noItemText));
......
......@@ -7,6 +7,7 @@ const searchInput = require('./search_input');
const snippetInput = require('./snippet_input');
const sidebar = require('./sidebar');
const ciConfigValidator = require('./ci_config_validator');
const webviewController = require('./webview_controller');
const IssuableDataProvider = require('./data_providers/issuable').DataProvider;
const CurrentBranchDataProvider = require('./data_providers/current_branch').DataProvider;
......@@ -78,6 +79,7 @@ const registerCommands = () => {
'gl.createSnippet': snippetInput.show,
'gl.validateCIConfig': ciConfigValidator.validate,
'gl.refreshSidebar': sidebar.refresh,
'gl.showRichContent': webviewController.create,
};
Object.keys(commands).forEach(cmd => {
......@@ -88,6 +90,7 @@ const registerCommands = () => {
};
const init = () => {
webviewController.addDeps(context);
tokenService.init(context);
};
......
......@@ -66,7 +66,7 @@ const getInstancePath = () => {
};
const escapeForRegExp = str => {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
};
const parseGitRemote = remote => {
......@@ -89,7 +89,7 @@ const parseGitRemote = remote => {
return null;
}
const match = pathname.match(pathRegExp + '\/(.+)\/(.*?)(?:.git)?$');
const match = pathname.match(pathRegExp + '/(.+)/(.*?)(?:.git)?$');
if (!match) {
return null;
}
......
......@@ -374,6 +374,64 @@ async function validateCIConfig(content) {
return response;
}
async function fetchDiscussions(issuable) {
let discussions = [];
try {
const type = issuable.sha ? 'merge_requests' : 'issues';
discussions = await fetch(
`/projects/${issuable.project_id}/${type}/${issuable.iid}/discussions?sort=asc`,
);
} catch (e) {
vscode.window.showInformationMessage(
'GitLab Workflow: Failed to fetch discussions for this issuable.',
);
}
return discussions;
}
// TODO: Remove project fetch
async function renderMarkdown(markdown) {
let rendered = { html: markdown };
const [major] = version.split('.');
if (parseInt(major, 10) < 11) {
return markdown;
}
try {
const project = await fetchCurrentProject();
rendered = await fetch('/markdown', 'POST', {
text: markdown,
project: project.path_with_namespace,
gfm: 'true', // Needs to be a string for the API
});
} catch (e) {
return markdown;
}
return rendered.html;
}
async function saveNote({ issuable, note }) {
let response = {};
try {
const projectId = issuable.project_id;
const issueId = issuable.iid;
response = await fetch(`/projects/${projectId}/issues/${issueId}/notes`, 'POST', {
id: projectId,
issue_iid: issueId,
body: note,
});
} catch (e) {
response = { success: false };
}
return response;
}
exports.fetchUser = fetchUser;
exports.fetchIssuesAssignedToMe = fetchIssuesAssignedToMe;
exports.fetchIssuesCreatedByMe = fetchIssuesCreatedByMe;
......@@ -391,3 +449,6 @@ exports.fetchMRIssues = fetchMRIssues;
exports.createSnippet = createSnippet;
exports.validateCIConfig = validateCIConfig;
exports.fetchVersion = fetchVersion;
exports.fetchDiscussions = fetchDiscussions;
exports.renderMarkdown = renderMarkdown;
exports.saveNote = saveNote;
......@@ -79,8 +79,8 @@ const parseQuery = (query, noteableType) => {
}
// URL encode keys and values and return a new array to build actual query string.
const queryParams = Object.keys(params).map(
k => (params[k] ? `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}` : ''),
const queryParams = Object.keys(params).map(k =>
params[k] ? `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}` : '',
);
return queryParams.length ? `?${queryParams.join('&')}` : '';
......
const vscode = require('vscode');
class SidebarTreeItem extends vscode.TreeItem {
constructor(title, url) {
constructor(title, data) {
super(title);
if (url) {
const { enableExperimentalFeatures } = vscode.workspace.getConfiguration('gitlab');
if (data) {
let command = 'gl.showRichContent';
let arg = data;
if (data.sha || !enableExperimentalFeatures) {
command = 'vscode.open';
arg = vscode.Uri.parse(data.web_url);
}
this.command = {
command: 'vscode.open',
arguments: [vscode.Uri.parse(url)],
command,
arguments: [arg],
};
}
}
......
> 1%
last 2 versions
not ie <= 8
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# webview
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your unit tests
```
yarn run test:unit
```
module.exports = {
presets: ['@vue/app'],
};
{
"name": "webview",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"markdown-it": "^8.4.2",
"markdown-it-checkbox": "^1.1.0",
"vue": "^2.5.17"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.1",
"@vue/cli-plugin-unit-mocha": "^3.0.1",
"@vue/cli-service": "^3.0.1",
"@vue/test-utils": "^1.0.0-beta.20",
"chai": "^4.1.2",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.1",
"vue-template-compiler": "^2.5.17"
}
}
module.exports = {
plugins: {
autoprefixer: {},
},
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-{{nonce}}' 'unsafe-eval'; style-src 'unsafe-inline';">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>GitLab Workflow</title>
</head>
<body>
<div id="app"></div>
<script nonce="{{nonce}}" src="{{devScriptUri}}"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-{{nonce}}'; style-src 'nonce-{{nonce}}';">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>GitLab Workflow</title>
<link nonce="{{nonce}}" href="{{styleUri}}" rel="stylesheet" type="text/css">
</head>
<body>
<div id="app"></div>
<script nonce="{{nonce}}" src="{{vendorUri}}"></script>
<script nonce="{{nonce}}" src="{{appScriptUri}}"></script>
</body>
</html>
<script>
import IssuableDetails from './components/IssuableDetails';
import IssuableDiscussions from './components/IssuableDiscussions';
import CommentForm from './components/CommentForm';
const vscode = acquireVsCodeApi();
export default {
name: 'app',
data() {
return {
isLoading: false,
issuable: {},
discussions: [],
};
},
components: {
IssuableDetails,
IssuableDiscussions,
CommentForm,
},
computed: {
notesById() {
const notes = {}
this.discussions.forEach((d) => {
d.notes.forEach((n) => {
notes[n.id] = n;
});
});
notes[this.issuable.id] = this.issuable;
return notes;
},
},
created() {
window.vsCodeApi = vscode;
this.isLoading = true;
window.addEventListener('message', event => {
if (event.data.type === 'issuableFetch') {
this.issuable = event.data.issuable;
this.discussions = event.data.discussions;
this.isLoading = false;
} else if (event.data.type === 'markdownRendered') {
const { ref, key, markdown } = event.data;
const note = this.notesById[ref] || {};
note[key] = markdown;
note.markdownRenderedOnServer = true;
}
});
},
}
</script>
<template>
<div id="app">
<p v-if="isLoading" class="loading">
Fetching issuable details and discussions. This may take a while.
<br />
If it doesn't work, please
<a href="https://gitlab.com/fatihacet/gitlab-vscode-extension/issues/new">create an issue.</a>
</p>
<template v-else>
<issuable-details :issuable="issuable" />
<issuable-discussions :discussions="discussions" />
<comment-form :issuable="issuable" />
</template>
</div>
</template>
<style lang="scss">
body.vscode-light {
* {
border-color: #B3B7BE !important;
&::before {
border-color: #B3B7BE !important;
}
}
.idiff.deletion {
background: #fac5cd;
}
.idiff.addition {
background: #c7f0d2;
}
.issuable-details .state {
color: #fff;
}
}
.capitalize {
text-transform: capitalize;
}
code {
padding: 2px 4px;
color: #c0341d;
background-color: #fbe5e1;
border-radius: 4px;
}
.idiff.deletion {
background: #df818f;
}
.idiff.addition {
background: #7cba8d;
}
#app {
margin-bottom: 600px; // to give editor scroll past end effect
.loading {
text-align: center;
font-size: 14px;
line-height: 30px;
}
}
</style>
export default {
gitLabLogo:
'<svg aria-hidden="true" data-prefix="fab" data-icon="gitlab" class="svg-inline--fa fa-gitlab fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M29.782 199.732L256 493.714 8.074 309.699c-6.856-5.142-9.712-13.996-7.141-21.993l28.849-87.974zm75.405-174.806c-3.142-8.854-15.709-8.854-18.851 0L29.782 199.732h131.961L105.187 24.926zm56.556 174.806L256 493.714l94.257-293.982H161.743zm349.324 87.974l-28.849-87.974L256 493.714l247.926-184.015c6.855-5.142 9.711-13.996 7.141-21.993zm-85.404-262.78c-3.142-8.854-15.709-8.854-18.851 0l-56.555 174.806h131.961L425.663 24.926z"></path></svg>',
chevronDown:
'<svg aria-hidden="true" data-prefix="fas" data-icon="chevron-down" class="svg-inline--fa fa-chevron-down fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z"></path></svg>',
chevronRight:
'<svg aria-hidden="true" data-prefix="fas" data-icon="chevron-right" class="svg-inline--fa fa-chevron-right fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"></path></svg>',
};
<script>
export default {
props: {
issuable: {
type: Object,
required: true,
},
},
data() {
return {
note: '',
isSaving: false,
isFailed: false,
command: 'saveNote',
};
},
computed: {
buttonTitle() {
return this.isSaving ? 'Saving...' : 'Comment';
},
},
methods: {
addComment() {
const { issuable, note, command } = this;
this.isSaving = true;
this.isFailed = false;
window.vsCodeApi.postMessage({ command, issuable, note });
},
},
mounted() {
window.addEventListener('message', event => {
if (event.data.type === 'noteSaved') {
if (event.data.status !== false) {
this.note = '';
} else {
this.isFailed = true;
}
this.isSaving = false;
}
});
},
};
</script>
<template>
<div class="main-comment-form">
<textarea v-model="note" placeholder="Write a comment..."></textarea>
<button @click="addComment" :disabled="isSaving || !note.length">
{{ buttonTitle }}
</button>
<span v-if="isFailed">Failed to save your comment. Please try again.</span>
</div>
</template>
<style lang="scss">
.main-comment-form {
margin: 20px 0 30px 0;
textarea {
width: 100%;
min-height: 140px;
border-radius: 4px;
padding: 16px;
font-size: 13px;
box-sizing: border-box;
border: 1px solid #919191;
resize: vertical;
margin-bottom: 8px;
&:focus {
outline: 0;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
}
}
button {
background-color: #1aaa55;
border-color: #168f48;
color: #fff;
border-radius: 3px;
padding: 6px 10px;
font-size: 14px;
outline: 0;
margin-right: 10px;
cursor: pointer;
&:disabled {
opacity: .6;
cursor: default;
}
}
}
</style>
<script>
import Note from './Note';
import icons from '../assets/icons';
export default {
name: 'Discussion',
props: {
noteable: {
type: Object,
required: true,
},
},
data() {
return {
isRepliesVisible: true,
};
},
components: {
Note,
},
computed: {
initialDiscussion() {
return this.noteable.notes[0];
},
replies() {
return this.noteable.notes.slice(1);
},
hasReplies() {
return this.replies.length > 0;
},
toggleRepliesText() {
return this.isRepliesVisible ? 'Collapse replies' : 'Expand replies';
},
toggleRepliesIcon() {
return this.isRepliesVisible ? this.chevronDownSvg : this.chevronRightSvg;
},
},
methods: {
toggleReplies() {
this.isRepliesVisible = !this.isRepliesVisible;
},
},
created() {
this.chevronDownSvg = icons.chevronDown;
this.chevronRightSvg = icons.chevronRight;
},
};
</script>
<template>
<div
:class="{ collapsed: !isRepliesVisible }"
class="discussion"
>
<note :noteable="initialDiscussion" />
<div
v-if="hasReplies"
@click="toggleReplies"
class="toggle-widget"
>
<span
class="chevron"
v-html="toggleRepliesIcon"></span> {{ toggleRepliesText }}
</div>
<template v-if="isRepliesVisible">
<note
v-for="note in replies"
:key="note.id"
:noteable="note"
/>
</template>
</div>
</template>
<style lang="scss">
.discussion {
border: 1px solid #919191;
border-radius: 4px;
background: var(--background-color);
&.collapsed {
.toggle-widget {
border-radius: 0 0 4px 4px;
}
}
> .note {
border: none;
margin: 0;
}
.toggle-widget {
background: var(--vscode-activityBar-dropBackground);
padding: 8px 16px;
cursor: pointer;
user-select: none;
}
.chevron svg {
width: 10px;
height: 10px;
}
}
</style>
<script>
import UserAvatar from './UserAvatar';
const moment = require('moment');
const md = require('markdown-it')().use(require('markdown-it-checkbox'));
export default {
props: {
issuable: {
type: Object,
required: true,
},
},
components: {
UserAvatar,
},
computed: {
stateText() {
const states = {
opened: 'Open',
closed: 'Closed',
};
return states[this.issuable.state] || '';
},
description() {
if (this.issuable.markdownRenderedOnServer) {
return this.issuable.description;
}
const description = this.issuable.description || '';
const webUrl = this.issuable.web_url || '';
const path = `${webUrl.split('/issues/')[0]}/uploads/`;
const normalized = description.replace(/\/uploads/gm, path);
return md.render(normalized);
},
createdAgo() {
return moment(this.issuable.created_at).fromNow();
},
},
mounted() {
window.vsCodeApi.postMessage({
command: 'renderMarkdown',
markdown: this.issuable.description,
ref: this.issuable.id,
key: 'description',
});
},
};
</script>
<template>
<div class="issuable-details">
<div class="header">
<span
:class="{ [issuable.state]: true }"
class="state"
>{{ stateText }}</span>
<span class="capitalize"> opened</span>
{{ createdAgo }} by
<user-avatar
:user="issuable.author"
:show-handle="false"
/>
<a :href="issuable.web_url" class="view-link">
Open in GitLab
</a>
</div>
<div class="title">
<h2>{{ issuable.title }}</h2>
</div>
<div
class="description"
v-html="description"
></div>
</div>
</template>
<style lang="scss">
.issuable-details {
border-bottom: 1px solid #919191;
line-height: 21px;
.header {
padding: 10px 0 6px;
line-height: 36px;
margin-bottom: 8px;
border-bottom: 1px solid #919191;
position: relative;
.view-link {
position: absolute;
right: 0;
}
.state {
border-radius: 4px;
padding: 2px 9px;
margin-right: 5px;
font-size: 12px;
&.opened {
background-color: #2A9D3F;
}
&.closed {
background-color: #1D64C9;
}
}
}
.description {
margin-bottom: 16px;
}
}
</style>
<script>
import Note from './Note';
import Discussion from './Discussion';
import SystemNote from './SystemNote';
export default {
props: {
discussions: {
type: Array,
required: true,
},
},
components: {
Note,
Discussion,
SystemNote,
},
methods: {
getComponentName(discussion) {
if (discussion.individual_note) {
if (discussion.notes[0].system) {
return SystemNote;
}
return Note;
}
return Discussion;
},
getComponentData(discussion) {
return discussion.individual_note ? discussion.notes[0] : discussion;
},
},
};
</script>
<template>
<div class="issuable-discussions">
<component
v-for="discussion in discussions"
:key="discussion.id"
:is="getComponentName(discussion)"
:noteable="getComponentData(discussion)"
/>
</div>
</template>
<style lang="scss">
.issuable-discussions {
position: relative;
&::before {
content: '';
border-left: 2px solid #919191;
position: absolute;
left: 35px;
top: 16px;
bottom: 0;
z-index: -1;
}
}
</style>
<script>
import UserAvatar from './UserAvatar';
import NoteBody from './NoteBody'
const moment = require('moment');
export default {
name: 'Note',
props: {
noteable: {
type: Object,
required: true,
}
},
components: {
UserAvatar,
NoteBody,
},
computed: {
author() {
return this.noteable.author;
},
createdAgo() {
return moment(this.noteable.created_at).fromNow();
},
},
};
</script>
<template>
<div class="note">
<div class="note-header">
<user-avatar :user="author" :size="40" /> · {{ createdAgo }}
</div>
<note-body :note="noteable" />
</div>
</template>
<style lang="scss">
.note {
border: 1px solid #919191;
border-radius: 4px;
padding: 16px;
margin: 16px 0;
background: var(--background-color);
.avatar {
margin-right: 10px;
}
&:not(.system-note) {
.note-header {
position: relative;
top: -8px;
}
.avatar {
position: relative;
top: 8px;
}
.note-body {
margin-top: -12px;
}
}
}
</style>
<script>
const md = require('markdown-it')().use(require('markdown-it-checkbox'));
export default {
name: 'NoteBody',
props: {
note: {
type: Object,
required: true,
},
},
computed: {
renderedNoteBody() {
return this.note.markdownRenderedOnServer ? this.note.body : md.render(this.note.body);
},
},
mounted() {
window.vsCodeApi.postMessage({
command: 'renderMarkdown',
markdown: this.note.body,
ref: this.note.id,
key: 'body',
});
},
};
</script>
<template>
<div class="note-body">
<div class="body" v-html="renderedNoteBody"></div>
</div>
</template>
<style lang="scss">
.note-body {
margin-left: 56px;
line-height: 21px;
.badge {
padding: 0 8px;
line-height: 16px;
border-radius: 36px;
font-size: 12px;
display: inline-block;
}
.body p {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
</style>
<script>
import NoteBody from './NoteBody';
import UserAvatar from './UserAvatar';
import icons from '../assets/icons';
const moment = require('moment');
export default {
props: {
noteable: {
type: Object,
required: true,
},
},
components: {
NoteBody,
UserAvatar,
},
computed: {
author() {
return this.noteable.author;
},
createdAgo() {
return moment(this.noteable.created_at).fromNow();
},
},
created() {
this.gitLabLogoSvg = icons.gitLabLogo;
},
};
</script>
<template>
<div class="note system-note">
<span class="avatar" v-html="gitLabLogoSvg"></span>
<div class="note-body-wrapper">
<user-avatar :user="author" :show-avatar="false"/>
<note-body :note="noteable"/>
· {{ createdAgo }}
</div>
</div>
</template>
<style lang="scss">
.system-note {
border: none;
padding: 0 16px;
.avatar {
border: 1px solid #919191;
border-radius: 100%;
display: inline-block;
height: 30px;
width: 30px;
margin-left: 4px;
svg {
width: 16px;
height: 16px;
position: relative;
top: 7px;
left: 7px;
}
}
.note-body-wrapper {
display: inline-block;
position: relative;
top: 3px;
> * {
display: inline-block;
}
.note-body {
margin-left: 5px;
}
}
}
</style>
<script>
export default {
props: {
user: {
type: Object,
required: true,
},
size: {
type: Number,
required: false,
default: 24,
},
showAvatar: {
type: Boolean,
required: false,
default: true,
},
showHandle: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
sizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<span>
<img
v-if="showAvatar"
:src="user.avatar_url"
:class="sizeClass"
class="avatar"
/>
<span class="author">
{{ user.name }}
<a
v-if="showHandle"
:href="user.web_url"
target="_blank"
>@{{user.username}}</a>
</span>
</span>
</template>
<style lang="scss" scoped>
.avatar {
border-radius: 100%;
max-width: 64px;
max-height: 64px;
vertical-align: middle;
}
.s24 {
width: 24px;
height: 24px;
}
.s40 {
width: 40px;
height: 40px;
}
.capitalize {
text-transform: capitalize;
}
</style>
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
module.exports = {
filenameHashing: false,
};
此差异已折叠。
const fs = require('fs');
const path = require('path');
const vscode = require('vscode');
const gitLabService = require('./gitlab_service');
let context = null;
const addDeps = ctx => {
context = ctx;
};
const getNonce = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
const getResources = () => {
const paths = {
appScriptUri: 'src/webview/dist/js/app.js',
vendorUri: 'src/webview/dist/js/chunk-vendors.js',
styleUri: 'src/webview/dist/css/app.css',
devScriptUri: 'src/webview/dist/app.js',
};
Object.keys(paths).forEach(key => {
const uri = vscode.Uri.file(path.join(context.extensionPath, paths[key]));
paths[key] = uri.with({ scheme: 'vscode-resource' });
});
return paths;
};
const getIndexPath = () => {
const isDev = !fs.existsSync(path.join(context.extensionPath, 'src/webview/dist/js/app.js'));
return isDev ? 'src/webview/public/dev.html' : 'src/webview/public/index.html';
};
const replaceResources = () => {
const { appScriptUri, vendorUri, styleUri, devScriptUri } = getResources();
return fs
.readFileSync(path.join(context.extensionPath, getIndexPath()), 'UTF-8')
.replace(/{{nonce}}/gm, getNonce())
.replace('{{styleUri}}', styleUri)
.replace('{{vendorUri}}', vendorUri)
.replace('{{appScriptUri}}', appScriptUri)
.replace('{{devScriptUri}}', devScriptUri);
};
const createPanel = issuable => {
const title = `${issuable.title.slice(0, 20)}...`;
return vscode.window.createWebviewPanel('glWorkflow', title, vscode.ViewColumn.One, {
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'src'))],
});
};
async function handleCreate(panel, issuable) {
const discussions = await gitLabService.fetchDiscussions(issuable);
panel.webview.postMessage({ type: 'issuableFetch', issuable, discussions });
panel.webview.onDidReceiveMessage(async message => {
if (message.command === 'renderMarkdown') {
let rendered = await gitLabService.renderMarkdown(message.markdown);
rendered = (rendered || '')
.replace(/ src=".*" alt/gim, ' alt')
.replace(/" data-src/gim, '" src');
panel.webview.postMessage({
type: 'markdownRendered',
ref: message.ref,
key: message.key,
markdown: rendered,
});
}
if (message.command === 'saveNote') {
const response = await gitLabService.saveNote({
issuable: message.issuable,
note: message.note,
});
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 });
}
}
});
}
async function create(issuable) {
const panel = createPanel(issuable);
const html = replaceResources();
panel.webview.html = html;
panel.onDidChangeViewState(() => {
handleCreate(panel, issuable);
});
handleCreate(panel, issuable);
}
exports.addDeps = addDeps;
exports.create = create;
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册