How to post message from VSCode extension to custom webview created with WebviewProvider? - webview

I am building a VSCode extension where I create a custom tab in the panel with a Webview Provider. I want to direct the output of an extension command to Webview and render in html script. The alternative is to execute vscode extension command inside html script of the webview. However I could not find an example that uses Webview Provider, instead they all have
currentPanel.webview.postMessage({ command: 'refactor' });
which is undefined in my case because I do not create a panel.
extension.js
let disposable = vscode.commands.registerCommand(
"this is where I want to send data to webview"
...
);
var thisProvider={
resolveWebviewView:function(thisWebview, thisWebviewContext, thisToke){
thisWebview.webview.options={enableScripts:true}
thisWebview.webview.html=`<!DOCTYPE html>
<html>
<body>
<div id="results" style="white-space: pre;" />
<script>
const resultsEl = document.getElementById("results");
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'results':
console.log(results);
break;
}
});
</script>
</body>
</html>`;
}
}
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("monitor.output", thisProvider)
);
package.json:
"contributes": {
"commands": [
{
"command": "monitor.listen",
"title": "Connect"
}
],
"menus": {
"view/title": [
{
"command": "monitor.listen",
"group": "navigation",
"when": "view == monitor.output"
}
]
},
"viewsContainers": {
"panel": [
{
"id": "monitor",
"title": "Monitor",
"icon": "resources/monitor.jpeg"
}
]
},
"views": {
"monitor": [
{
"type": "webview",
"id": "monitor.output",
"name": "Monitor"
}
]
}
}

I was looking for this too, but there is really no example showing how this could be added.
You can get and save the reference to the current webView inside resolveWebView function in your provider.
Store the view in a private instance variable private _view?: vscode.WebviewView; and use it in a public method public postMessageToWebview(message: any)
The provider code:
import * as vscode from 'vscode';
function getNonce() {
let text = "";
const possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export class WebViewProvider
implements vscode.WebviewViewProvider
{
public static readonly viewType = 'myExtension.controlsView';
private _view?: vscode.WebviewView;
constructor(private readonly _extensionUri: vscode.Uri) {
}
public postMessageToWebview(message: any) {
this._view?.webview.postMessage(message);
}
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView; // needed so we can use it in postMessageToWebview later
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
localResourceRoots: [this._extensionUri],
};
webviewView.webview.html = this._getHtmlForWebview(
webviewView.webview,
);
webviewView.webview.onDidReceiveMessage((data) => {
switch (data.type) {
// other message types ...
case 'onYourEvent': {
console.log(data.value); // see below webview to extension communication snippet
break;
}
case 'onInfo': {
if (!data.value) {
return;
}
vscode.window.showInformationMessage(data.value);
break;
}
case 'onError': {
if (!data.value) {
return;
}
vscode.window.showErrorMessage(data.value);
break;
}
}
});
}
private _getHtmlForWebview(webview: vscode.Webview) {
// // And the uri we use to load this script in the webview
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(
this._extensionUri,
'out',
'svelte-app/bundle.js',
),
);
// const scriptUri = webview.asWebviewUri(
// vscode.Uri.joinPath(this._extensionUri, "media", "main.js")
// );
// Local path to css styles
const styleResetPath = vscode.Uri.joinPath(
this._extensionUri,
'media',
'reset.css',
);
const stylesPathMainPath = vscode.Uri.joinPath(
this._extensionUri,
'media',
'vscode.css',
);
// Uri to load styles into webview
const stylesResetUri = webview.asWebviewUri(styleResetPath);
const stylesMainUri = webview.asWebviewUri(stylesPathMainPath);
const cssUri = webview.asWebviewUri(
vscode.Uri.joinPath(
this._extensionUri,
'out/svelte-app',
'bundle.css',
),
// vscode.Uri.joinPath(this._extensionUri, "media", "main.css")
);
// Use a nonce to only allow specific scripts to be run
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--
Use a content security policy to only allow loading images from https or from our extension directory,
and only allow scripts that have a specific nonce.
-->
<meta http-equiv="Content-Security-Policy" content="img-src https: data:; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${stylesResetUri}" rel="stylesheet">
<link href="${stylesMainUri}" rel="stylesheet">
<link href="${cssUri}" rel="stylesheet">
<script nonce="${nonce}">
const tsvscode = acquireVsCodeApi();
</script>
</head>
<body>
</body>
<script nonce="${nonce}" src="${scriptUri}"></script>
</html>`;
}
}
Note:
To get started remove the svelte bundle stuff (this was just part of my code base) and add a script file media/main.js with the code from the webview snippet below.
The CSS files reset.css and vscode.css can be downloaded from a VS Code example
To see your console.log messages you can open your dev. tools in your extension host VS code instance by hitting ctrl+shif+p and type Open webview dev - maybe you have to open/close your webview to generate new console logs.
You can use it in your extension.ts:
import * as vscode from 'vscode';
import { WebViewProvider } from './WebViewProvider';
export function activate(context: vscode.ExtensionContext) {
const provider = new WebViewProvider(context.extensionUri);
context.subscriptions.push(
vscode.commands.registerCommand('myExtension.sayHello', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
// vscode.window.showInformationMessage(output);
provider.postMessageToWebview({
type: 'greeting',
message: 'HelloWorld',
});
}),
);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
WebViewProvider.viewType,
provider,
),
);
}
Add it to the package.json so the command & view will be available:
{
"other-config": "...",
"activationEvents": [
"onView:myExtension.controlsView",
"onCommand:myExtension.sayHello"
],
"contributes": {
"views": {
"explorer": [
{
"type": "webview",
"id": "myExtension.controlsView",
"name": "MyExtension"
}
],
},
"commands": [
{
"command": "myExtension.sayHello",
"category": "myExtension",
"title": "SayHello"
},
]
}
}
Webviews can be added at multiple locations in this code it will be added to the explorer view.
Use the following code in your webview script to get the message from the extension:
// main.js code
const handleExtensionMessages = (event) => {
const { message, type }= event.data;
switch (message.type) {
case 'greeting':
console.log("received", message);
break;
}
}
window.addEventListener("message", handleExtensionMessages);
For the other "direction" web view to extension communication.
You can use in your webview content script:
tsvscode.postMessage({
type: "onYourEvent",
value: "anything you like to return",
});
The global tsvscode variable is generated in _getHtmlForWebview with acquireVsCodeApi().
To fix the typing for the global tsvscode. Install #types/vscode-webview with npm as devDependency and add a global.d.ts to your webview script with this content:
import type WebViewApi from '#types/vscode-webview';
global {
declare const tsvscode: WebViewApi<unknown>;
}
Or if you don't like the global variable tsvscode and the typing above. You could also create a VS Code API wrapper like in the following repository.
Just to get this code running, create an extension as mentioned in the Getting Started Guide with Yeoman generator by running yo code in your terminal.
You can also find the snippets in the following Github gist.

Related

How to allow location when using headless browser of cypress testing on event on click?

What I want to do
I want to integrate husky with the Cypress test when trying to commit some changes. So in my package.json file, there are two tests script
"scripts": {
"test:run": "./node_modules/.bin/cypress run",
"test:open": "./node_modules/.bin/cypress open"
},
The test:open is SUCCESS
But The test:run is FAIL
It says that User has not allowed access to system location
This is the function when Button "Godkänn" clicked in "Onboarding" page
const getDevicePermission = async () => {
await getDeviceLocation().then(() =>
getDeviceOrientation().then(() => Router.push(routes.ONBOARDING))
);
};
What I've tried
Add configuration of browser permission in cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
env: {
browserPermissions: {
notifications: "allow",
geolocation: "allow",
},
},
chromeWebSecurity: false,
e2e: {
setupNodeEvents(_on, _config) {
// implement node event listeners here
},
},
});
The result is still FAIL with the same error
I've been following this issue's solution but still got the same error and no typescript support
Add package cypress-browser-permissions
npm i cypress-browser-permissions --save-dev
//or
yarn install cypress-browser-permissions --save-dev
Update your cypress.config.js
const { defineConfig } = require('cypress')
const { cypressBrowserPermissionsPlugin } = require('cypress-browser-permissions')
module.exports = defineConfig({
env: {
browserPermissions: {
notifications: "allow",
geolocation: "allow",
},
},
chromeWebSecurity: false,
e2e: {
setupNodeEvents(on, config) {
config = cypressBrowserPermissionsPlugin(on, config)
return config
},
},
})
Testing
To test, I used an app which essentially does this
navigator.geolocation.getCurrentPosition(showPosition, showError);
With configuration browserPermissions: { geolocation: "ask" } the test throws up a permissions request dialog.
With configuration browserPermissions: { geolocation: "allow" } the test bypasses the dialog and the page displays the lat & long coordinates.
With configuration browserPermissions: { geolocation: "block" } the page shows "User denied the request for Geolocation." and the Cypress log shows the error
(uncaught exception) undefined: User denied Geolocation
App
<html lang="en">
<body>
<div id="demo">Demo</div>
<script>
var demoDiv = document.getElementById("demo");
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showPosition, showError);
}
else { demoDiv.innerHTML = "Geolocation is not supported by this browser."; }
}
function showPosition(position) {
var lat = position.coords.latitude;
var lon = position.coords.longitude;
demoDiv.innerText = `${lat} : ${lon}`
}
function showError(error) {
demoDiv.innerHTML = "User denied the request for Geolocation."
throw error
}
getLocation()
</script>
</body>
</html>
It's because when you are running the tests on the test runner, you are running them on Chrome, And when running it on CLI you are running them on Electron browser. You can add the chrome browser for the run mode.
"test:run": "./node_modules/.bin/cypress run --browser chrome"
To ignore exceptions generated from the application globally, you can add this in cypress/support/e2e.js
Cypress.on('uncaught:exception', (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false
})

Webpacker, babel, uglifyjs-webpack-plugin - not transforming arrow functions, but only in Vue files

Running webpacker 3.5.5 (both the gem and package). This is mostly working, but in IE11 the app is broken because arrow functions do not appear to be transformed. However, inspecting the minified code it seems like the only place arrow functions aren't transformed are inside my vue components.
I think this is because my babel class properties plugin is not applying to my Vue loader somehow, but I haven't been able to come up with a solution.
Here's my .babelrc
{
"presets": [
[
"env",
{
"modules": false,
"targets": {
"browsers": [
"> 1%",
"IE 11"
],
"uglify": true
},
"useBuiltIns": true
}
]
],
"plugins": [
"syntax-dynamic-import",
"transform-object-rest-spread",
[
"transform-class-properties",
{
"spec": true
}
]
],
"env": {
"test": {
"presets": ["es2015"]
}
}
}
And here's the entirety of my environment.js file that modifies the webpack environment that webpacker ships with (vue loader is at the bottom).
const { environment } = require('#rails/webpacker');
environment.loaders.append('expose', {
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: 'jQuery'
}]
});
const webpack = require('webpack');
// append some global plugins
environment.plugins.append('Provide', new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
axios: 'axios',
moment: 'moment-timezone',
_: 'lodash'
}));
// Necesary configuration for vue-loader v15
const VueLoaderPlugin = require('vue-loader/lib/plugin');
environment.plugins.append(
'VueLoaderPlugin',
new VueLoaderPlugin()
);
environment.loaders.append('vue', {
test: /\.vue$/,
loader: 'vue-loader'
});
module.exports = environment;
Edit for more info: Here is the entry point to my pack called 'wrestling'
import 'babel-polyfill';
import 'wrestling';
Then in wrestling.js...
import './styles/wrestling'
import Rails from 'rails-ujs'
Rails.start();
import wrestlingSetup from './wrestlingSetup'
wrestlingSetup();
WrestlingSetup contains the actual references to the vue files. I've cut down the file to show what a single vue import looks like within the file. All the rest are essentially the same.
import Vue from 'vue/dist/vue.esm'
// Redacted a bunch of imports, but they all look like this oen
import WrestlerCreate from './vue/wrestler_create.vue'
export default function() {
document.addEventListener('DOMContentLoaded', () => {
axiosSetup();
const app = new Vue({
el: '#app',
components: {
// Other vue components here that I've removed for simplicity
WrestlerCreate,
}
})
});
}
Here's an actual example of the Vue component
<template>
<div role="form">
<!-- other form elements -->
</div>
</template>
<script>
export default {
name: 'wrestler-create',
props: [
],
// This does not get transformed by babel
data() {
return {
loading: false,
error: false,
errorMessage: "Error, please try again later or contact support.",
first_name: '',
last_name: '',
weight_class: '',
academic_class: ''
}
},
methods: {
// removed for simplicity
}
}
</script>
For clarify sake:
Please use function() for data. I find function() gives me less trouble than arrow functions.
export default {
data: function() {
return {
message: "Hello something!",
secondMessage: "Hello world!"
};
}
}
If you really wish to use arrow function, you can write:
export default {
data: () => {
return {
message: "Hello something!",
secondMessage: "Hello world!"
};
}
}

clear google spreadsheet data using http

I have try to clear spreadsheet using http request.
Below is my http request
https://sheets.googleapis.com/v4/spreadsheets/spresheetId/values/B2:B10?key=APIKEY
but json return below error
{
"error": {
"code": 403,
"message": "Requests from referer \u003cempty\u003e are blocked.",
"status": "PERMISSION_DENIED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developer console API key",
"url": "https://console.developers.google.com/project/562822880211/apiui/credential"
}
]
}
]
}
}
so how can i clear my spreadsheet using http
When you deploy as a webapp you'll get a url that looks something like this: https://script.google.com/macros/s/.../exec add a querystring to it so it looks like this: https://script.google.com/macros/s/.../exec?ssid=spreadsheetid only replace spreadsheetid for the real spreadsheet id.
This is the gs file:
function clearSpreadSheet()
{
var ssid=PropertiesService.getScriptProperties().getProperty('SSID');
var ss=SpreadsheetApp.openById(ssid);
var allSheets=ss.getSheets();
for(var i=0;i<allSheets.length;i++)
{
allSheets[i].clear();//clear all
//allSheets[i].getRange(2,1,sheet.getLastRow(),sheet.getLastColumn()).clear();//everything but first row.
}
return{'clearMessage':'Spreadsheet has been cleared.','ssid':ssid}
}
function doGet(e)
{
PropertiesService.getScriptProperties().setProperty('SSID', e.parameter.ssid)
var html = HtmlService.createHtmlOutputFromFile('clearss');
return html.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
}
This is the clearss.html file:
<!DOCTYPE html>
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
google.script.run
.withSuccessHandler(dispCleared)
.clearSpreadSheet();//runs when DOM is loaded
});
function dispCleared(data)//runs after spreadsheet is cleared via the SuccessHandler
{
$('#clrd').text(data.clearMessage);
$('#ssid').text('SpreadSheet ID is ' + data.ssid);
}
console.log('My Code');
</script>
</head>
<body>
<h1 id="clrd"></h1>
<h2 id="ssid"></h2>
</body>
</html>
This is a contained web app. So load these files into the script editor of a spreadsheet.

Custom menu in TFS web access (TFS 15)

I tried these steps and i created a sample extension similar to this site https://www.visualstudio.com/en-us/docs/integrate/extensions/get-started/node .
But i need to pass parameter to the site which i am opening using this new extension. Is it possible ?
Parameters like : project name , environment(TFS URL) where the TFS is running.
And i am facing an error when trying to open a page in an MVC application hosted on IIS internally, the application uses Durandal ,Knockout and HTML 5 for the UI.
Not able attach the screen shot pasting the part of new extension script
"icons": {
},
"contributions": [
{
"id": "Fabrikam.HelloWorld",
"type": "ms.vss-web.hub",
"description": "Adds a 'Hello' hub to the Work hub group.",
"targets": [
"ms.vss-work-web.work-hub-group"
],
"properties": {
"name": "Hello Testing",
"order": 99,
"uri": "http://test-server/AdminConsole2015Beta/#/tfsreports/boc_projects/ALM/alm-beta-app1/0"
}
}
],
"scopes": [
"vso.work"
],
"files": [
{
"path": "tfsReports.html", "addressable": true
},
{
"path": "scripts", "addressable": true
},
{
"path": "sdk/scripts", "addressable": true
}
]
}
In the above script i want to open the URL in the URI property which opens the page tfsreports.html after routing using durandal. Is that possible here? See screen shot for refrence
Update your extension to below:
In the manifest file, update the "uri" of the "contributions" to "tfsReports.html".
"properties": {
"name": "Hello Testing",
"order": 99,
"uri": "tfsReports.html"
}
Add followings content in "tfsReports.html":
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Hello World</title>
<script src="sdk/scripts/VSS.SDK.js"></script>
</head>
<body>
<script type="text/javascript">VSS.init();</script>
<h1>Hello World</h1>
<script type="text/javascript">
VSS.init();
VSS.ready(function () {
var webContext = VSS.getWebContext();
var projectname = webContext.project.name;
var TFSUrl = webContext.collection.uri;
var reportFrame = document.getElementById("reportFrame");
var finalurl = "http://test-server/AdminConsole2015Beta/#/tfsreports/boc_projects/ALM/alm-beta-app1/0/" + projectname + "/" + TFSUrl;
reportFrame.src = finalurl;
VSS.notifyLoadSucceeded();
});
</script>
<div id="tfsreport">
<iframe id="reportFrame" style="width:100%" frameborder="0"></iframe>
</div>
</body>
</html>

Work with the simple-prefs module and export the values to a script stored in the data folder

I'm currently trying to add some preferences to a Firefox add-on. To do so, I'm playing with the new "simple-prefs" module. (Simple-Prefs on the Mozilla Blog)
The documentation is not very detailed and I face some problems understanding how I can retrieve the value attached to an option and export it to a JS script present in my data folder.
Let's say that I have only one optional setting in my addon, a boolean one, then my packages.json will look like this:
{
"name": "test",
...
"preferences": [{
"type": "bool",
"name": "option1",
"value": true,
"title": "Desc Option 1"
}]
}
Here is my main.js file [UPDATED]:
var pageMod = require("page-mod");
const data = require("self").data;
const prefSet = require("simple-prefs"); //Simple-prefs module
var option1 = prefSet.prefs.option1; //get the value for option1
function onPrefChange(prefName) { //Listen for changes
var prefName = prefSet.prefs[prefName];
}
prefSet.on("option1", onPrefChange);
exports.main = function() {
pageMod.PageMod({
include: ["https://mail.google.com/*","http://mail.google.com/*"],
contentScriptWhen: 'ready',
contentScriptFile: [data.url("jquery.js"),data.url("script.js")],
onAttach: function(worker)
{
worker.postMessage( option1 );
}
});
}
How can I retrieve the value attached to "option1" and export it in order to call it in my "script.js" file?
As usually, content scripts don't have access to the API - they can only receive messages from your extension's scripts. Here you would do:
pageMod.PageMod({
include: ["https://mail.google.com/*","http://mail.google.com/*"],
contentScriptWhen: 'ready',
contentScriptFile: [data.url("jquery.js"),data.url("script.js")],
onAttach: function(worker)
{
worker.postMessage(backtop);
}
});
And in the content script you would have the following code:
self.on("message", function(data)
{
alert("Received option value: " + data);
});
This message arrives asynchronously meaning that your content script won't know the option value initially - but that's how content scripts work.

Resources