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
});
}
},
Related
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.
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);
}
};
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'.
I have a template that I am loading from a route like so:
this.route('formEdit', {
path: '/admin/form/:_id',
data: function() { return Forms.findOne({_id: this.params._id}); },
onBeforeAction: function() { AccountUtils.authenticationRequired(this, ['ADMIN']); }
});
In which I have a template defined like:
<template name="formEdit">
<div id="formContainer">
...
{{#each header_fields}}
<div class="sortable">
{{> headerFieldViewRow }}
</div>
{{/each}}
</div>
</template>
And:
<template name="headerFieldViewRow">
{{#with header_field}}
...
{{/with}}
</template>
I then make the container around all the header fields sortable using jQuery UI Sortable:
Template.formEdit.rendered = function() {
$('.sortable').sortable({
axis: "y",
stop: function(event, ui) {
var form = Blaze.getData($('#formContainer')[0]);
var newFormHeaders = [];
$('#headerFieldsTable div.headerField').each(function(idx, headerFieldDiv) {
var header = Blaze.getData(headerFieldDiv);
header.sequence = idx;
Meteor.call('saveHeaderField', header);
newFormHeaders.push({header_field_id: header._id});
});
form.header_fields = newFormHeaders;
Meteor.call('saveForm', form);
}
});
}
Basically, when sorting stops, loop through all the headers, getting the data for each and updating the sequence number, then re-build the array in Forms and save them back. In the server code I have printouts for the two save calls, and the do properly print out the correct order of both the headers and the form.
The problem I am running into is that, after sorting, the visual display of the form and it's headers "snaps" back to the pre-sorted state, even though the data in the DB is correct. If I simply reload the form, either by hitting enter in the Address bar or by simply re-loading it from the menu, everything is displayed correctly. It's as if the reactive piece isn't working.
I have noted that I am getting an error when I update the client code in my server log that reads:
=> Client modified -- refreshing
I20141010-18:25:47.017(-4)? Failed to receive keepalive! Exiting.
=> Exited with code: 1
I don't think this is related as I was getting that error prior to adding this sorting code.
Update: Adding code for saveForm and saveHeader
saveForm:
// Saves the Form to the DB
saveForm: function(params) {
// Structure the data to send
var formEntry = {
title: params.title,
client_id: params.client_id,
header_fields: params.header_fields,
form_fields: params.form_fields,
created_by: Meteor.userId(),
created_on: new Date()
};
if (params._id == null) {
console.log("Saving new Form entry: %j", formEntry);
formEntry._id = Forms.insert(formEntry);
} else {
formEntry._id = params._id;
console.log("Updating Form entry: %j", formEntry);
Forms.update({_id: formEntry._id}, formEntry);
}
return formEntry;
}
saveHeader:
// Saves the HeaderField to the DB
saveHeaderField: function(params) {
// Structure the data to send
var headerFieldEntry = {
label: params.label,
field_type: params.field_type,
field_options: params.field_options,
form_id: params.form_id,
required: params.required,
allows_pictures: params.allows_pictures,
record_geo: params.record_geo
};
if (params._id == null) {
console.log("Saving new HeaderField entry: %j", headerFieldEntry);
headerFieldEntry._id = HeaderFields.insert(headerFieldEntry);
} else {
headerFieldEntry._id = params._id;
console.log("Updating HeaderField entry: %j", headerFieldEntry);
HeaderFields.update({_id: headerFieldEntry._id}, headerFieldEntry);
}
return headerFieldEntry;
}
I think the issue here is that Meteor.call will run on the server - you either need to use a callback or invalidate your template, if you want to return a value Meteor.call. From the docs:
On the client, if you do not pass a callback and you are not inside a stub, call will return undefined, and you will have no way to get the return value of the method. That is because the client doesn't have fibers, so there is not actually any way it can block on the remote execution of a method.
There is more info in this answer and this answer and the Meteor.call docs.
Hope that helps!
Im trying to create an ajax (post) event that will populate a table in a div on button click.
I have a list of groups, when you click on a group, I would like the table to "disappear" and the members that belong to that group to "appear".
My problem comes up when using jQuery's .ajax...
When I click on the button, it is looking for a controller that doesnt exist, and a controller that is NOT referenced. I am, however, using AREAS (MVC2), and the area is named Member_Select where the controller is named MemberSelect. When I click on the button, I get a 404 stating it cannot find the controller Member_Select. I have examined the link button and it is set to Member_Select when clicked on, but here's the ajax call:
$.ajax({
type: "POST",
url: '/MemberSelect/GetMembersFromGroup',
success: function(html) { $("#groupResults").html(html); }
});
I havent been able to find any examples/help online.
Any thoughts/suggestions/hints would be greatly appreciated.
Thanks!
Have you tried navigating to /MemberSelect/GetMembersFromGroup to see what you get? - if it's 404'ing it's because the route can't be matched to a controller/ action.
I've not used the new areas functionality, but I'm not sure that the URL you've got is correct...I would have thought it would have been /AREANAME/MemberSelect/GetMembersFromGroup...but I could be wrong..!
When I did this, it worked fine. I didn't use POST and I don't know what AREAS means.
$("#item").autocomplete({
source: function(req, responseFn) {
addMessage("search on: '" + req.term + "'<br/>", true);
$.ajax({
url : ajaxUrlBase1 + "GetMatchedCities/" + req.term,
cache : false,
type : "GET", // http method
success : function(msg){
// ajax call has returned
var result = msg;
var a = [];
if (result !== null){
for(var i=0; i < result.length; i++) {
a.push({label: result[i].prop1, id: result[i].prop2});
}
}
responseFn(a);
}
});
}
});
Use:
area_name/controller_name/action_name
Instead of doing $.ajax I would use jQuery Form Plugin.
and have my form set as:
Html.BeginForm("Index","AdminArea/Admin",FormMethod.Post,
new { id="form-user", name="form-user"})
To use jQuery Form Plugin have a look here:
http://arturito.net/2010/12/02/asp-net-mvc2-jquery-form-post-tutorial/
You cold save your url in a Hidden Form element in (Html.HiddenForm()) and use the #id javascript operator to retrieve it. Just found this out today.