I want to link an application to validate updates made in Excel. I am trying to use Glue42 interop methods. Does anyone have a code sample?
I have successfully linked two apps, but not excel
I change a cell in excel and my external app validates this can be changed.
I believe you missed the Glue for Office documentation. There are two sections you can go to from the home page: one for general programming and one for the Office connectors.
Anyways, up to the question, you need first to obtain a reference to a Glue4Office object with code, similar to the one below:
const config = {
// ...,
excel: true // enable Excel integration
}
Glue4Office(config)
.then(g4o => {
const excel = g4o.excel
// interact with Excel
})
.catch(console.error)
Then, once you open a sheet and get a reference to it, you can subscribe to its onChanged event, where you can call the errorCallback argument. Here is an example:
excel.openSheet(config)
.then(sheet => {
sheet.onChanged((data, errorCallback, doneCallback) => {
// process changes here
// errorCallback(...) - call if there are validation errors
// doneCallback() - call if not doing validations or everything's OK
})
})
sheet.onChanged((data, errorCallback, doneCallback) => {
// ...
const errors = []
data.reduce(
(errors, rowData, rowIndex) => {
if (!rowData['firstName']) {
errors.push({
row: rowIndex + 1,
column: 'firstName',
description: 'First name is mandatory'
})
}
if (Number(rowData['subscriptionMonths']) < 6) {
errors.push({
row: rowIndex + 1,
column: 1, // <- note column index this time
description: 'Subscription period must be at least 6 months',
text: '6' // <- replacing what the user typed
})
}
},
[])
// if during the validation pass we've accumulated any errors
// we need to call the errorCallback, otherwise the doneCallback
if (errors.length > 0) {
errorCallback(errors)
}
else {
doneCallback()
}
})
There are convenient declarative approaches as well.
Related
Context description
I have a Vue 3 project and using element plus, I created a form with async validation. The code of validation function (Laravel precognition):
const validatePrecognitive = async (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void) => {
return new Promise<void>((resolve, reject) => {
if (!rule.field) {
return;
}
precognitive.post('/events', computedFormData.value, {
validate: [rule.field],
onPrecognitionSuccess(response) {
if (response.status === 204) {
callback();
resolve();
}
},
onValidationError(response) {
callback(new Error(response.data.message));
reject();
},
});
});
};
and I define the rules like this:
const rules: FormRules = reactive({
} as Partial<Record<string, Arrayable<FormItemRule>>>);
// dynamically set the rules for each input, because of same value for all rules
Object.keys(getSubmitShape()).forEach(key => {
rules[key] = [{ asyncValidator: validatePrecognitiveDebounced, trigger: 'blur' }];
});
The getSubmitShape() funtion return Object with all form-item keys, that should be validated. So you can imagine it like:
const rules: FormRules = reactive({
rules['name'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
rules['surname'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
}
This works like a charm, when I write to inputs and switch between them. The errors would appear and dissappear correctly.
Submit
When the form is submitted, I am calling formEl.validate(...) function, which should validate all the inputs. It works, BUT:
for every rule item, one HTTP request is sent to server, because the validation method is called for each rule separately.
What I tried
I tried to implement debounce method, which would save the params from each previous function call and include that in next call, so in a last call, I would have all the inputs, rules and callbacks available. This works, when I try it while writing inputs to elements and switching them fast (triggering validation).
When on submit, it waits the debounce time for each rule. So the requests again go one by one with the delay of debounce time.
Any ideas, how I could make this in one request?
Thanks.
when we run the yeoman it is asking to add input one by one, is there anyway to give all input one go ? or programically?
for Example :
yo azuresfguest
it is asking to add 5 inputs, which i want to give one time, so i can run in CI/CD system
Thanks,
There isn't a general way, unfortunately. The specific generator would need to allow it.
I think it is deserving of a feature request on the Yeoman project, which I've logged here.
As a cumbersome workaround, you can create your own generator which re-uses an existing generator. The TypeScript code below gives an example; I'm using this approach to automate my CI process.
Add option to constructor:
constructor(args: string, opts: Generator.GeneratorOptions) {
super(args, opts);
...
this.option("prompts-json-file", {
type: String,
default: undefined,
description: "Skips prompting; uses file contents. Useful for automation",
});
}
Use the option:
async prompting() {
if (this.options["prompts-json-file"] !== undefined) {
this.answers = new Answers(JSON.parse(
fs.readFileSync(this.options["prompts-json-file"]).toString()
));
}
else {
this.answers = ...
}
}
Unfortunately this does bypass prompt validation so you'd need to separately ensure your file contains valid values.
Using it is relatively simple:
yo my-generator --prompts-json-file ./prompts.json
This should be accomplished using Yeomans storage API and a .yo-rc.json file:
https://yeoman.io/authoring/storage.html
I used to use a self defined option to make that optional similar to the approach from #BjornO
constructor(args, opts) {
super(args, opts);
// This method adds support for a `--yo-rc` flag
this.option('yo-rc', {
desc: 'Read and apply options from .yo-rc.json and skip prompting',
type: Boolean,
defaults: false
});
}
initializing() {
this.skipPrompts = false;
if (this.options['yo-rc']) {
const config = this.config.getAll();
this.log(
'Read and applied the following config from ' +
chalk.yellow('.yo-rc.json:\n')
);
this.log(config);
this.log('\n');
this.templateProps = {
projectName: config.projectName,
};
this.skipPrompts = true;
}
}
prompting() {
if (!this.skipPrompts) {
// Have Yeoman greet the user.
this.log(
yosay(
`Yo, welcome to the ${superb()} ${chalk.yellow(
'Baumeister'
)} generator!`
)
);
const prompts = [
{
type: 'input',
name: 'projectName',
message: 'What’s the name of your project?',
// Default to current folder name
default: _s.titleize(this.appname)
}
];
return this.prompt(prompts).then(props => {
this.templateProps = {
projectName: props.projectName
};
});
}
}
See https://github.com/micromata/generator-baumeister/blob/master/app/index.js#L20-L69 for the whole generator code and https://github.com/micromata/generator-baumeister/blob/master/\_\_tests__/yo-rc.json for the corresponding .yo-rc.json file.
My goal is to trigger a specific chat flow within the Microsoft Power Virtual Agent service based on the page the user is on. I haven't been able to find a way to customise the Microsoft service to dynamically start at a specific chat topic other than one fixed one using these instructions.
I want to use jQuery to pre-populate the dynamically generated text field:
$('.webchat__send-box-text-box__input').val('red');
The above code works where I see the word "red" appear in the text box very briefly but then it gets overwritten by the code that is generating the input field. If I run the above script manually from the browser console after everything has loading, it works fine.
Is there a way to customise the Microsoft webchat code to get the user to the start of a specific flow, or alternatively can I automate the insertion of the right words so that the user is automatically taken to the start of the relevant chat flow? It would be great if I could set a paramter in the webchat JS code that sets a topic right from the beginning, but I haven't found any instructions that suggests this is possible-just some basic styling parameters.
This is the code from Microsoft that generates a web chat interface:
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<script>
const styleOptions = {
// Add styleOptions to customize web chat canvas
hideUploadButton: true
};
// Add your BOT ID below
var BOT_ID = "[BOT ID VALUE REDACTED]";
var theURL = "https://powerva.microsoft.com/api/botmanagement/v1/directline/directlinetoken?botId=" + BOT_ID;
const store = window.WebChat.createStore(
{},
({ dispatch }) => next => action => {
if (action.type === "DIRECT_LINE/CONNECT_FULFILLED") {
dispatch({
meta: {
method: "keyboard",
},
payload: {
activity: {
channelData: {
postBack: true,
},
//Web Chat will show the 'Greeting' System Topic message which has a trigger-phrase 'hello'
name: 'startConversation',
type: "event"
},
},
type: "DIRECT_LINE/POST_ACTIVITY",
});
}
return next(action);
}
);
fetch(theURL)
.then(response => response.json())
.then(conversationInfo => {
window.WebChat.renderWebChat(
{
directLine: window.WebChat.createDirectLine({
token: conversationInfo.token,
}),
store: store,
styleOptions: styleOptions
},
document.getElementById('webchat');
);
})
.catch(err => console.error("An error occurred: " + err));
</script>
This is fairly easy to overcome. All you need to do is decide on how you want to identify the page being visited, filter on that in Web Chat's store, and post an activity that would trigger the appropriate topic.
In the example below, my page is title "PVA Test Page". When the page loads that has that title, using the DIRECT_LINE/CONNECT_FULFILLED action type and matching on document.title === 'PVA Test Page', then the DIRECT_LINE/POST_ACTIVITY action is dispatched. The activity I have setup is of type 'message' and sends a text value of 'store hours'. In my PVA bot I have a topic that is setup to return dialog detailing the store hours. The trigger phrases are configured to recognize that text value. So, providing all those conditions are met, the store hours are returned as a dialog when the page loads.
Si
<!DOCTYPE html>
<html>
<head>
<title>PVA Test Page</title>
[ ... ]
</head>
[ ... ]
</html>
const store = window.WebChat.createStore(
{},
( { dispatch } ) => next => action => {
if ( action.type === "DIRECT_LINE/CONNECT_FULFILLED" ) {
if (document.title === 'PVA Test Page') {
dispatch( {
meta: {
method: "keyboard",
},
payload: {
activity: {
channelData: {
postBack: true,
},
text: 'store hours',
type: "message"
},
},
type: "DIRECT_LINE/POST_ACTIVITY",
} );
}
}
return next( action );
}
);
After putting off testing for a while now due to Cypress not allowing visiting chrome:// urls, I decided to finally understand how to unit/integration test my extension - TabMerger. This comes after the many times that I had to manually test the ever growing functionality and in some cases forgot to check a thing or two. Having automated testing will certainly speed up the process and help me be more at peace when adding new functionality.
To do this, I chose Jest since my extension was made with React (CRA). I also used React Testing Library (#testing-library/react) to render all React components for testing.
As I recently made TabMerger open source, the full testing script can be found here
Here is the test case that I want to focus on for this question:
import React from "react";
import { render, fireEvent } from "#testing-library/react";
import * as TabFunc from "../src/Tab/Tab_functions";
import Tab from "../src/Tab/Tab";
var init_groups = {
"group-0": {
color: "#d6ffe0",
created: "11/12/2020 # 22:13:24",
tabs: [
{
title:
"Stack Overflow - Where Developers Learn, Share, & Build Careersaaaaaaaaaaaaaaaaaaaaaa",
url: "https://stackoverflow.com/",
},
{
title: "lichess.org • Free Online Chess",
url: "https://lichess.org/",
},
{
title: "Chess.com - Play Chess Online - Free Games",
url: "https://www.chess.com/",
},
],
title: "Chess",
},
"group-1": {
color: "#c7eeff",
created: "11/12/2020 # 22:15:11",
tabs: [
{
title: "Twitch",
url: "https://www.twitch.tv/",
},
{
title: "reddit: the front page of the internet",
url: "https://www.reddit.com/",
},
],
title: "Social",
},
};
describe("removeTab", () => {
it("correctly adjusts groups and counts when a tab is removed", () => {
var tabs = init_groups["group-0"].tabs;
const { container } = render(<Tab init_tabs={tabs} />);
expect(container.getElementsByClassName("draggable").length).toEqual(3);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
fireEvent.click(container.querySelector(".close-tab"));
expect(removeTabSpy).toHaveBeenCalledTimes(1);
expect(container.getElementsByClassName("draggable").length).toEqual(2); // fails (does not remove the tab for some reason)
});
});
I mocked the Chrome API according to my needs, but feel that something is missing. To mock the Chrome API I followed this post (along with many others, even for other test runners like Jasmine): testing chrome.storage.local.set with jest.
Even though the Chrome storage API is mocked, I think the issue lies in this function which gets called upon initial render. That is, I think the chrome.storage.local.get is not actually being executed, but am not sure why.
// ./src/Tab/Tab_functions.js
/**
* Sets the initial tabs based on Chrome's local storage upon initial render.
* If Chrome's local storage is empty, this is set to an empty array.
* #param {function} setTabs For re-rendering the group's tabs
* #param {string} id Used to get the correct group tabs
*/
export function setInitTabs(setTabs, id) {
chrome.storage.local.get("groups", (local) => {
var groups = local.groups;
setTabs((groups && groups[id] && groups[id].tabs) || []);
});
}
The reason I think the mocked Chrome storage API is not working properly is because when I manually set it in my tests, the number of tabs does not increase from 0. Which forced me to pass a prop (props.init_tabs) to my Tab component for testing purposes (https://github.com/lbragile/TabMerger/blob/f78a2694786d11e8270454521f92e679d182b577/src/Tab/Tab.js#L33-L35) - something I want to avoid if possible via setting local storage.
Can someone point me in the right direction? I would like to avoid using libraries like jest-chrome since they abstract too much and make it harder for me to understand what is going on in my tests.
I think I have a solution for this now, so I will share with others.
I made proper mocks for my chrome storage API to use localStorage:
// __mocks__/chromeMock.js
...
storage: {
local: {
...,
get: function (key, cb) {
const item = JSON.parse(localStorage.getItem(key));
cb({ [key]: item });
},
...,
set: function (obj, cb) {
const key = Object.keys(obj)[0];
localStorage.setItem(key, JSON.stringify(obj[key]));
cb();
},
},
...
},
...
Also, to simulate the tab settings on initial render, I have a beforeEach hook which sets my localStorage using the above mock:
// __tests__/Tab.spec.js
var init_ls_entry, init_tabs, mockSet;
beforeEach(() => {
chrome.storage.local.set({ groups: init_groups }, () => {});
init_ls_entry = JSON.parse(localStorage.getItem("groups"));
init_tabs = init_ls_entry["group-0"].tabs;
mockSet = jest.fn(); // mock for setState hooks
});
AND most importantly, when I render(<Tab/>), I noticed that I wasn't supplying the id prop which caused nothing to render (in terms of tabs from localStorage), so now I have this:
// __tests__/Tab.spec.js
describe("removeTab", () => {
it("correctly adjusts storage when a tab is removed", async () => {
const { container } = render(
<Tab id="group-0" setTabTotal={mockSet} setGroups={mockSet} />
);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
var chromeSetSpy = jest.spyOn(chrome.storage.local, "set");
fireEvent.click(container.querySelector(".close-tab"));
await waitFor(() => {
expect(chromeSetSpy).toHaveBeenCalled();
});
chrome.storage.local.get("groups", (local) => {
expect(init_tabs.length).toEqual(3);
expect(local.groups["group-0"].tabs.length).toEqual(2);
expect(removeTabSpy).toHaveBeenCalledTimes(1);
});
expect.assertions(4);
});
});
Which passes!!
Now on to drag and drop testing 😊
I'm building a simple Vuejs website in which you can write notes about meetings. Upon loading it takes the meeting notes from the server and displays them. When the user then writes something he can click the "Save" button, which saves the text to the server. When the notes are saved to the server the Save-button needs to be disabled and display a text saying "Saved". When the user then starts writing text again it should enable the button again and display "Save" again. This is a pretty basic functionality I would say, but I'm having trouble with it.
Here's my textarea and my save button:
<textarea v-model="selectedMeeting.content" ref="meetingContent"></textarea>
<button v-on:click="saveMeeting" v-bind:disabled="meetingSaved">
{{ saveMeetingButton.saveText }}
</button>
In my Vue app I first initiate my data:
data: {
selectedMeeting: {},
meetings: [],
meetingSaved: true,
saveMeetingButton: {saveText: 'Save Meeting', savedText: 'Saved', disabled: true},
},
Upon creation I get the meeting notes from the server:
created() {
axios.get('/ajax/meetings')
.then(response => {
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
});
},
I've got a method to save the notes:
methods: {
saveMeeting: function () {
axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting)
.then(function (response) {
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
});
},
},
And I've got a watcher in case something changes to the text which saves the text immediately (this saves with every letter I type, which I of course need to change, but this is just to get started.
watch: {
'selectedMeeting.content': function () {
this.meetingSaved = false;
console.log('Changed meeting ', new Date());
this.saveMeeting();
}
},
If I now type a letter I get this in the logs:
Changed meeting Tue Dec 04 2018 19:14:43 GMT+0100
Now setting meetingSaved to true
Done setting meetingSaved to true
The logs are as expected, but the button itself is never disabled. If I remove the watcher the button is always disabled however. Even though the watcher first sets this.meetingSaved to false, and then this.saveMeeting() sets it to true, adding the watcher somehow never disables the button.
What am I doing wrong here?
Edit
Here's a paste of the whole page: https://pastebin.com/x4VZvbr5
You've got a few things going on that could use some changing around.
Firstly the data attribute should be a function that returns an object:
data() {
return {
selectedMeeting: {
content: null
},
meetings: [],
meetingSaved: true,
saveMeetingButton: {
saveText: 'Save Meeting',
savedText: 'Saved',
disabled: true
},
};
}
This is so Vue can properly bind the properties to each instance.
Also, the content property of the selectedMeeting didn't exist on the initial render so Vue has not added the proper "wrappers" on the property to let things know it updated.
As an aside, this can be done with Vue.set
Next, I would suggest you use async/await for your promises as it makes it easier to follow.
async created() {
const response = await axios.get('/ajax/meetings');
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
},
For your method I would also write as async/await. You can also use Vue modifiers like once on click to only call the api if there is no previous request (think a fast double-click).
methods: {
async saveMeeting () {
const response = await axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting);
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
},
},
The rest of the code looks okay.
To summarize the main problem is that you didn't return an object in the data function and it didn't bind the properties reactively.
Going forward you are going to want to debounce the text input firing the api call and also throttle the calls.
this.meetingSaved = true;
this is referencing axios object. Make a reference to vue object outside your call and than use it. Same happens when you use jQuery.ajax().
created() {
var vm = this;
axios.get('/ajax/meetings')
.then(response => {
vm.meetings = response.data;
vm.selectedMeeting = vm.meetings[0];
vm.meetingSaved = true;
});
},