Custom Bitbucket Merge Check - dynamic fields render twice after submitting the configuration - bitbucket

I'm creating a custom Merge Check for Bitbucket. I started by following this tutorial:
https://developer.atlassian.com/server/bitbucket/how-tos/hooks-merge-checks-guide/
I want the view to be dynamic, e.g. have button that creates multiple similar input fields (of specified IDs), which eventually get stored in the config.
First of all, I used soy for this - I created static template with call to one e.g. .textField. It worked okay, but I couldn't create new, similar fields on the fly (after pressing 'Add new' button).
So I use JavaScript to get data from soy's config. I rewrite the whole config to JS "map" and then render all the fields dynamically (by appending them to HTML code), filling them with values from configuration or creating new fields by pressing the button.
It works - I get all the data saved in config for keys like field_[id], e.g field_1, field_2 etc.
But there's a bug. When I press the "Save" button and view the pop-up for editing once again, I can see the JavaScript get executed twice: I get all my fields rendered two times - first time during first execution and second time during the second, appearing just a few seconds later. There's no such problem when I save the configuration, refresh the page and then view the pop-up once again.
Here's my merge check's configuration in atlassian-plugin.xml file:
<repository-merge-check key="isAdmin" class="com.bitbucket.plugin.MergeCheck" name="my check" configurable="true">
<config-form name="Simple Hook Config" key="simpleHook-config">
<view>hook.guide.example.hook.simple.myForm</view>
<directory location="/static/"/>
</config-form>
</repository-merge-check>
And my simplified .soy template code:
{namespace hook.guide.example.hook.simple}
/**
* #param config
* #param? errors
*/
{template .myForm}
<script type="text/javascript">
var configuration = new Object();
{foreach $key in keys($config)}
configuration["{$key}"] = "{$config[$key]}";
{/foreach}
var keys = Object.keys(configuration);
function addNewConfiguration() {lb}
var index = keys.length;
addNewItem(index);
keys.push("field_" + index);
{rb}
function addNewItem(id) {lb}
var html = `<label for="field_${lb}id{rb}">Field </label><input class="text" type="text" name="field_${lb}id{rb}" id="branch_${lb}id{rb}" value=${lb}configuration["field_" + id] || ""{rb}><br>`;
document.getElementById('items').insertAdjacentHTML('beforeend', html);
{rb}
keys.forEach(function(key) {lb}
var id = key.split("_")[1];
addNewItem(id);
{rb});
var button = `<button style="margin:auto;display:block" id="add_new_configuration_button">Add new</button>`;
document.getElementById('add_new').innerHTML = button;
document.getElementById('add_new_configuration_button').addEventListener("click", addNewConfiguration);
</script>
<div id="items"></div>
<div id="add_new"></div>
<div class="error" style="color:#FF0000">
{$errors ? $errors['errors'] : ''}
</div>
{/template}
Why does JavaScript get executed twice in this case? Is there any other way of creating such dynamic views?

The soy template will get loaded and executed again whenever you click to edit the configuration. Therefore the javascript will also get executed again. To prevent this you can create a javascript file and put it next to your simpleHook-config.soy template file with the same filename, so simpleHook-config.js. The javascript file will be loaded automatically with your soy template, but once. Therefore you can hold a global initialisation state within a new introduced js object.
Furthermore, even though you are adding fields dynamically, you can still and should build the saved configuration inside the soy template already and not building it with javascript.
For me this approach works quite good (I wrote the code more or less blindly, so maybe you need to adjust it a bit):
In .soy file:
{namespace hook.guide.example.hook.simple}
/**
* #param config
* #param? errors
*/
{template .myForm}
<div id="merge-check-config">
<div id="items">
{foreach $key in keys($config)}
{let $id: strSub($key, strIndexOf($key, "_") + 1) /}
{call .field}
{param id: $id /}
{param value: $config[$key] /}
{/foreach}
</div>
<div id="add_new">
<button style="margin:auto; display:block" id="add_new_configuration_button">Add new</button>
</div>
<div class="error" style="color:#FF0000">
{$errors ? $errors['errors'] : ''}
</div>
<script>
myPluginMergeCheck.init();
</script>
</div>
{/template}
/**
* #param id
* #param? value
*/
{template .field}
<div>
<label for="field_${id}">Field</label>
<input class="text" type="text" name="field_${id}" id="branch_${id}" value="${value}">
</div>
{/template}
In .js file:
myPluginMergeCheck = {
initialized: false,
init: function () {
if (this.initialized) {
return;
}
var me = this;
AJS.$(document).on("click", "div#merge-check-config button#add_new_configuration_button"), function (event) {
me.addNewItem();
}
this.initialized = true;
},
addNewItem: function() {
var itemsDiv = AJS.$("div#merge-check-config div#items");
var newId = itemsDiv.children("div").length;
var html = hook.guide.example.hook.simple.field({id: newId});
itemsDiv.append(html);
}
};

Related

Retrieving input form data Trello power-up

I have created a custom power up that requires settings. I use the show-settings capability for this.
The settings form is shown successfully:
But I don't know how to retrieve data from that form. Getting and setting is for saving en retrieving data, as I understand, but not for getting http params.
I've tried to get parameters like:
function get(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function(item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
Then calling get('webhookUrl') in different places;
outside of window.TrelloPowerUp.initialize({});
inside 'show-settings': function(t, options) {
inside callback in t.popup(..)
Anything I log there, it doesn't seem to work.
app.js:
...
'show-settings': function(t, options) {
// when a user clicks the gear icon by your Power-Up in the Power-Ups menu
// what should Trello show. We highly recommend the popup in this case as
// it is the least disruptive, and fits in well with the rest of Trello's UX
return t.popup({
title: 'Custom Fields Settings',
url: 'settings.html?v=' + timeStamp,
height: 184, // we can always resize later
callback: function(t) {
console.log(t);
}
});
},
In the settings.html
<body>
<form id="settings">
<input name="webhookUrl" placeholder="https://app.buddy.works/user-name/projects-name/pipelines/pipeline .."></input>
<button type="submit" class="mod-primary">Save</button>
</form>
</body>
Btw, i find it hard to debug things in the browser, in Chrome Developers Tools I check Javascript Context, every time I scroll to the right iframe, it is tedious.

vaadin-grid-filter for an Array of Strings not working

I am using the vaadin-grid-filter with Polymer 2.x and I am facing the following problem.
I have a vaadin-grid-column as following,
<vaadin-grid-column width="15em">
<template class="header">
<vaadin-grid-filter aria-label="Inventory" path="" value="[[_filterItems]]">
<vaadin-text-field slot="filter" placeholder="Item" value="{{_filterItems}}" focus-target></vaadin-text-field>
</vaadin-grid-filter>
</template>
<template>[[item]]</template>
</vaadin-grid-column>
[[item]] is an array of strings and the filtering doesn't work when the path is set to an empty string.
If I put each string inside a Json object and access it as in the documentation, then it works fine.
But I am wondering whether there is a way I can filter this as it is.
Thank you in advance.
I did some digging into the vaadin-grid source code and found some less than ideal answers. Unfortunately there doesn't seem to be any "magic" placeholders for the path property value that will get you what you want in the context of using the "x-array-data-provider" component for [[item]]. Here are a couple values I tried and their results:
"" : the filtering mechanism attempts to retrieve the item[""] property for string comparison. Unfortunately item[""] will be undefined and the comparison will fail to match.
null/undefined : there is a check for this very early on in the logic that will abort any filtering process as a whole.
So unfortunately due to the lack of any self-referencing object property (that I'm aware of) available in the default JS objects, I was unable to circumvent the property accessor in a way to get what you want.
I did however find a potential workaround in the context of using a server data provider. Using the vaadin-grid-filter example (from vaadin) here, it appears as though the filtering request can be serialized and sent to the server. If you have full control over the internals of the remote server data provider code then you could write your own custom filtering mechanism there to accomplish what you want. Here is the code from the example (in case of removal):
<x-remote-filtering-example></x-remote-filtering-example>
<dom-module id="x-remote-filtering-example">
<template>
<vaadin-grid aria-label="Filtering with Data Provider Example" id="grid">
<vaadin-grid-column width="50px" flex-grow="0">
<template class="header">#</template>
<template>[[index]]</template>
</vaadin-grid-column>
<vaadin-grid-column>
<template class="header">
<vaadin-grid-filter aria-label="Fist Name" path="firstName" value="[[_filterFirstName]]">
<input placeholder="First Name" value="{{_filterFirstName::input}}" focus-target>
</vaadin-grid-filter>
</template>
<template>[[item.firstName]]</template>
</vaadin-grid-column>
<vaadin-grid-column>
<template class="header">
<vaadin-grid-filter aria-label="Last Name" path="lastName" value="[[_filterLastName]]">
<input placeholder="Last Name" value="{{_filterLastName::input}}" focus-target>
</vaadin-grid-filter>
</template>
<template>[[item.lastName]]</template>
</vaadin-grid-column>
</vaadin-grid>
</template>
<script>
addEventListener('WebComponentsReady', function() {
Polymer({
is: 'x-remote-filtering-example',
ready: function() {
var grid = this.$.grid;
grid.size = 200;
grid.dataProvider = function(params, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var response = JSON.parse(xhr.responseText);
// Number of items changes after filtering. We need
// to update the grid size based on server response.
grid.size = response.size;
callback(response.result);
};
var index = params.page * params.pageSize;
var url = 'https://demo.vaadin.com/demo-data/1.0/people?index=' + index + '&count=' + params.pageSize;
// `params.filters` format: [{path: 'lastName', direction: 'asc'}, ...];
params.filters.forEach(function(filter) {
url += '&filters[' + filter.path + ']=' + encodeURIComponent(filter.value);
});
xhr.open('GET', url, true);
xhr.send();
};
}
});
});
</script>
</dom-module>

Best practice for html tags in vue-i18n translations?

I am using vue-i18n and I need to translate a sentence with an anchor tag in the middle. Obviously I want to keep the html specific markup out of my translations, but how best to handle this?
Consider the following example:
This is a test sentence which cannot
be split
or it will not make sense
The only solution I can come up with is:
{
"en": {
"example": "This is a test sentence which cannot {linkOpen}be split{linkClose} or it will not make sense"
}
}
and then in the component template
<p v-html="$t('example', {
'linkOpen': `<a href="https://example/com" class="test-class test-another-class">`,
'linkClose: '</a>'
})
"></p>
Not exactly elegant however...
Edit: I've tested this and it doesn't actually work (can't put html in params) so now I'm really out of ideas!
You can come up with some simple markup for links and write a small transformation function, for example:
//In this example links are structured as follows [[url | text]]
var text = `This is a test sentence which
cannot [[https://example.com | be split]] or it will not make sense`
var linkExpr = /\[\[(.*?)\]\]/gi;
var linkValueExpr = /(\s+\|\s+)/;
var transformLinks = (string) => {
return text.replace(linkExpr, (expr, value) => {
var parts = value.split(linkValueExpr);
var link = `${parts[2]}`;
return link;
});
}
alert(transformLinks(text));
JSFiddle: https://jsfiddle.net/ru5smdy3/
With vue-i18n it will look like this (which of course you can simplify):
<p v-html="transformLinks($t('example'))"></p>
You can put the HTML into an element that is not part of the displayed DOM and then extract its textContent. This may not work for what you're actually trying to do, though. I can't tell.
new Vue({
el: '#app',
data: {
html: `This is a test sentence which cannot
be split
or it will not make sense`,
utilityEl: document.createElement('div')
},
methods: {
htmlToText: function (html) {
this.utilityEl.innerHTML = html;
return this.utilityEl.textContent;
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<div id="app">
<p v-html="html"></p>
<p>{{htmlToText(html)}}</p>
</div>
I have found myself in a similar situation, and I propose using Vue-i18n slots.
I have a JSON i18n file which had error messages that were html. These rendered fine but they will not be compiled as vue templates, and cannot have bindings. I want to call an onclick function when users click the link in a given error message.
In my example I have a cake-state json with some status messages:
// cake_state.json, where I want links in error messages to call a function when clicked
{
"state":{
"stage": {
"mixing": "Cake is being mixed. The current time is {time}",
"resting": "Cake is resting. The current time is {time}",
"oven": "Cake is cooking. The current time is {time}"
},
"error": {
"ovenIssue": "Oven of brand is malfunctioning. Click {email_support_link} to get help",
"chefIssue": "Chef is down. Click {email_support_link} to get help",
"leakIssue": "There is a leak"
},
}
}
Now if we have some Vue SFC, with the template as such:
<template>
<div>
<i18n :path="getMessage">
<!-- enter various i18n slots -->
<template #time>
<span>{{ getTime }}</span>
</template>
<template #email_support_link>
<!-- binding now works because it is not v-html -->
<a href="" #click.prevent="getRightSupportDepartment">here</span>
</template>
</i18n>
</div>
</template>
...
// js
computed: {
getTime(): string { //implementation ...},
getRightSupportDepartment(): string { //implementation ...},
//return strings that are keys to cake_state.json messages
getMessage(): string {
...
switch (this.cakeState) {
case Failure.Overheat:
return "state.error.ovenIssue";
case Failure.ChefIdle:
return "state.error.chefIssue";
case Failure.LeakSensor:
return "state.error.leakIssue";
So what we see here is:
the getMessage function provides us the key to the message in the i18n JSON. This is passed into i18n component
the <template #XXX> slots in the i18n component's scope are supplied with this key from the function, which gets the corresponding message, and then
if the relevant message has any of the keywords, it gets put in from the corresponding template.
To re-iterate, it helps to provide a means to have vue bindings to html elements which would otherwise be served from the i18n json as raw html.
For example now we might see "Oven of brand is malfunctioning. Click here to get help", and we can run an onclick function when user clicks 'here'.

Alfresco: linking directly to workflow

I would like to start a workflow from the site links dashlet on my Alfresco site. Using Firebug to examine the POST gives me a URL that works, but it only displays the form without any UI:
http://localhost:8081/share/service/components/form?htmlid=template_x002e_start-workflow_x002e_start-workflow_x0023_default-startWorkflowForm-alf-id1&itemKind=workflow&itemId=activiti%24orpWorkflow&mode=create&submitType=json&showCaption=true&formUI=true&showCancelButton=true&destination=
Is this possible? And if so, how can I format the link to include the UI?
If not, are there custom dashlets out there designed for starting workflows?
When you select workflow from dropdown it will generate url based on selected workflow and redirect you to that.
Ex. For ParallelGroupReview workflow URL is.
http://localhost:8080/share/service/components/form?htmlid=template_x002e_start-workflow_x002e_start-workflow_x0023_default-startWorkflowForm-alf-id1&itemKind=workflow&itemId=activiti%24activitiParallelGroupReview&mode=create&submitType=json&showCaption=true&formUI=true&showCancelButton=true&destination=
Now if you use this url directly in browser you will be able to see same form but header and footer part will be missing, because those global components will not be avilable outside of share context.
If you see start-workflow.ftl you will be able to see header and footer components are inserted which are responsible for rest of the UI.
<#include "include/alfresco-template.ftl" />
<#templateHeader />
<#templateBody>
<#markup id="alf-hd">
<div id="alf-hd">
<#region scope="global" id="share-header" chromeless="true"/>
</div>
</#>
<#markup id="bd">
<div id="bd">
<div class="share-form">
<#region id="start-workflow" scope="template"/>
</div>
</div>
</#>
</#>
<#templateFooter>
<#markup id="al-ft">
<div id="alf-ft">
<#region id="footer" scope="global"/>
</div>
</#>
</#>
You can reuse same component just need to make sure header and footer are initialized properly.
I created an extension module which has the following target:
<targetPackageRoot>org.alfresco.components.workflow</targetPackageRoot>
I included the following piece in my extended start-workflow.get.html.ftl:
<#markup id="start-workflow-js" target="js" action="after">
<#script src="${url.context}/res/components/workflow/initiate-workflow.js" group="workflow"/>
</#>
to extend the default start-workflow.js of my own.
You'll need to change the following methods:
onReady: so it reads your param from the url to know which workflowdefinition to start and fire onWorkflowSelectChange
onWorkflowSelectChange: So it reads the workflowdefintion to load the form
You can make a little customization for Share.
For example, if you need to open the start form of any business process, you can find its index in the popup and add an additional parameter to the URL (let's say, openFormParam).
In start-workflow.js:
onReady: function StartWorkflow_onReady() {
// skipped ...
// get the additional parameter from the URL here
// var openFormParam = ...
if(openFormParam !== null) {
var p_aArgs = [];
var index = {index: 0}; // for the first workflow in the popup
p_aArgs.push(0, index);
this.onWorkflowSelectChange(null, p_aArgs);
}
return Alfresco.component.StartWorkflow.superclass.onReady.call(this);
},
// OOTB
onWorkflowSelectChange: function StartWorkflow_onWorkflowSelectChange(p_sType, p_aArgs) {
var i = p_aArgs[1].index;
if (i >= 0) {
// Update label of workflow menu button
var workflowDefinition = this.options.workflowDefinitions[i];
this.widgets.workflowDefinitionMenuButton.set("label", workflowDefinition.title + " " + Alfresco.constants.MENU_ARROW_SYMBOL);
this.widgets.workflowDefinitionMenuButton.set("title", workflowDefinition.description);
// Load the form for the specific workflow
Alfresco.util.Ajax.request({
url: Alfresco.constants.URL_SERVICECONTEXT + "components/form",
dataObj: {
htmlid: this.id + "-startWorkflowForm-" + Alfresco.util.generateDomId(),
itemKind: "workflow",
itemId: workflowDefinition.name,
mode: "create",
submitType: "json",
showCaption: true,
formUI: true,
showCancelButton: true,
destination: this.options.destination
},
successCallback: {
fn: this.onWorkflowFormLoaded,
scope: this
},
failureMessage: this.msg("message.failure"),
scope: this,
execScripts: true
});
}
},

datetimepicker and Backbone.View Interaction

This is the code:
var MyModel = Backbone.Model.extend({
defaults: {std:"",
pod:""
}
});
var MyView = Backbone.View.extend({
tagName:'ul',
events: {
'change input' : 'changed', // When input changes, call changed.
'hover .std' : 'timepick', //
'mouseout .std': 'doesntwork'
},
template:_.template($('#mytemplate').html()),
initialize: function() {
this.model.bind('change:pod',this.render,this); // When pod changes, re-render
},
timepick: function(e) {
$('.std').each(function(){
$.datepicker.setDefaults({dateFormat:'mm-dd'});
$(this).datetimepicker({timeFormat:'hh:mm',ampm:false});
});
},
doesntwork: function() {
// Would this.model.set here but mouseout happens when you select date/time values with mouse
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
changed: function(e) {
var value = $(e.currentTarget).val(); // Get Change value
var cls = $(e.currentTarget).attr('class'); //Get Class of changed input
var obj = {};
obj[cls] = value; // The model variables match class names
this.model.set(obj); // this.model.set({'std':value})
}
});
I have a datetimepicker in the UI I'm working on, and having difficulties assigning the value that is selected from the datetimepicker to MyModel.
It appears from using console.log output that 'change input' is triggered when clicking on the DTP and assigns the default value of (MM-DD 00:00). Even when you select a different date/time value than the default, the 'change input' is not triggered again, unless you click on the input box (for a second time), and then the correct value is assigned. Not very user-friendly.
So I had the idea that I would just assign the value on mouseout, which didn't work since mouseout happens when you start to select date/time values. I also tried blur, and that didn't work either.
Where am I going wrong?
Edit: Here is a jsfiddle.net link that illustrates my problem http://jsfiddle.net/9gSUe/1/
Looks like you're getting tripped up by jQuery UI's CSS. When you bind a datepicker to an <input>, jQuery UI will add a hasDatepicker class to the <input>. Then you do this:
var cls = $(e.currentTarget).attr('class');
on the <input> and get 'std hasDatepicker' in cls.
There are too many things that will mess around with class so you're better off using something else to identify the property you want to change. You could use an id if there is only one std:
<!-- HTML -->
<input id="std" class="std" ...>
// JavaScript
var cls = e.currentTarget.id;
or the name attribute:
<!-- HTML -->
<input name="std" class="std" ...>
// JavaScript
var cls = $(e.currentTarget).attr('name');
or perhaps even a HTML5 data attribute:
<!-- HTML -->
<input class="std" data-attr="std" ...>
// JavaScript
var cls = $(e.currentTarget).data('attr');
I think the name attribute would be the most natural: http://jsfiddle.net/ambiguous/RRKVJ/
And a couple side issues:
Your fiddle was including multiple versions of jQuery and that's generally a bad idea.
You don't have to build an object for set, you can say just m.set(attr, value) if you're just setting one attribute.
You don't have to $(this.el) in your views, newer Backbones have this.$el already cached.
console.log can handle multiple arguments so you can say console.log('Std: ', attrs.std, ' Pod: ', attrs.pod, ' Poa: ', attrs.poa); instead of console.log('Std: ' + attrs.std + ' Pod: ' + attrs.pod + ' Poa: ' + attrs.poa); if you don't want + stringify things behind your back.

Resources