view release on metacpan or search on metacpan
and label and size are configurable. Needs Text::QRCode, which is
loaded only when such a column is exported. CSV exports ignore the
option.
- CallBackery::GuiPlugin::AbstractTable used Mojo::Asset::Memory
without loading it, and only worked because Mojolicious happened to
have loaded it already.
0.59.0 2026-08-20 08:58:50 +0200 Tobias Oetiker <tobi@oetiker.ch>
- New optional actionCfg property "noBusyIndicator" for download and
display actions. When set, the modal busy indicator ("Preparing
Download ...") is not shown while the download is being prepared, so
the gui stays usable for actions which take a long time to produce
their data. Default behaviour is unchanged.
0.58.4 2026-07-31 16:15:51 +0200 Tobias Oetiker <tobi@oetiker.ch>
- Frontend ui.Login: a login could be lost outright. The credentials
were copied into the hidden iframe form that exists only to make the
browser offer to save the password, and that copy ran before the
login call, unguarded. The iframe reloads /login every time the login
window appears, so whenever that fetch had not completed yet -- a
busy server is enough -- getElementById returned null, the handler
threw on it, and the login request was never sent. The form had
already disabled itself, so it just sat there greyed out with nothing
on screen to explain it, and only a reload got the user out of it.
The password manager hand-off now happens after a successful login
and is fully guarded, so it can miss the save prompt but can no
longer come between the user and their session.
0.58.3 2026-07-30 16:11:29 +0200 Tobias Oetiker <tobi@oetiker.ch>
- Config: restoring a configuration blob replaced the configuration
0.55.2 2025-06-12 17:35:56 +0200 Tobias Oetiker <tobi@oetiker.ch>
- fix fake login page format
0.55.1 2025-06-12 17:28:21 +0200 Tobias Oetiker <tobi@oetiker.ch>
- properly close the popup after download if so configured
0.55.0 2025-06-12 15:24:25 +0200 Tobias Oetiker <tobi@oetiker.ch>
- download displays a busy message which can be configured using
busyMessage property
- upon download completing popup will close if closeAfterDownload => true,
is set
0.54.0 2025-06-12 14:52:06 +0200 Tobias Oetiker <tobi@oetiker.ch>
- upload actions are now busy during upload
use the busyMessage property to display a custom message during upload
0.53.1 2025-06-04 18:04:46 +0200 Tobias Oetiker <tobi@oetiker.ch>
- only log slow getters
- download cookies should be valid for 30s
0.53.0 2025-04-09 14:06:53 +0200 Tobias Oetiker <tobi@oetiker.ch>
- AbstractForm: add formData and formPhase subs.
Use signatures and cleanup parameter names.
| |\
| | * 29adfa0 - Fix pod error as reported by CPANTS. http://cpants.cpanauthors.org/dist/CallBackery (2 years, 3 months ago) <Mohammad S Anwar>
| |/
* | 2fe27fa - improve portability (2 years, 2 months ago) <Tobias Oetiker>
|/
* 4275dcc - better readme (2 years, 3 months ago) <Tobias Oetiker>
* 0c6b5e6 - better readme (2 years, 3 months ago) <Tobias Oetiker>
* b7b4b10 - this.tr is not available in server.js. fix for #29 (2 years, 3 months ago) <Tobias Oetiker>
* 25418af - (tag: v0.8.4) v0.8.4 (2 years, 3 months ago) <Tobias Oetiker>
* 847b0b1 - allow html in header (2 years, 3 months ago) <Tobias Oetiker>
* 02d43ea - translate busy message (2 years, 3 months ago) <Tobias Oetiker>
* 762bf8e - fix loading animation (2 years, 3 months ago) <Tobias Oetiker>
* 2cd7746 - fix args regression (2 years, 4 months ago) <Tobias Oetiker>
* 9ec77b6 - 0.8.2 (2 years, 4 months ago) <Tobias Oetiker>
* 55cfdca - args should alwais at least contain a hash ... this is only a stop gap fix ... we realy have to pass the partenArgs every time we interact with the plugin (2 years, 4 months ago) <Tobias Oetiker>
* c28ab41 - (tag: v0.8.1) 0.8.1 * allow escape to cancle popupClosed (2 years, 4 months ago) <Tobias Oetiker>
* 7c674ff - (tag: v0.7.1) fix readme issue (2 years, 4 months ago) <Tobias Oetiker>
* 80ef5e1 - cpanmin.us seems unstable (2 years, 4 months ago) <Tobias Oetiker>
* 061b82c - (tag: v0.7.0) added context menu and default action to table widget (2 years, 4 months ago) <Tobias Oetiker>
* a23d65a - handle validation on selectboxes without content (2 years, 5 months ago) <Tobias Oetiker>
* b485eb7 - be more careful with the server handler (2 years, 5 months ago) <Tobias Oetiker>
lib/CallBackery/Config.pm view on Meta::CPAN
logo_noscale => {
_doc => "don't scale logo on login window",
_re => '(yes|no|true|false)',
_re_error => 'pick yes or no OR true or false',
_sub => sub {
$_[0] = ($_[0] =~ /yes|true/) ? true : false;
return;
},
},
spinner => {
_doc => 'url for the busy animation spinner gif',
},
title => {
_doc => 'title string for the application'
},
initial_plugin => {
_doc => 'which tab should be active upon login ?'
},
hide_password => {
_doc => 'hide password field on login screen',
_re => '(yes|no|true|false)',
lib/CallBackery/Config.pm view on Meta::CPAN
no autodie;
unlink glob $staging.'*';
use autodie;
$builder->($staging);
chmod 0600, $staging;
my $dbh = DBI->connect("dbi:SQLite:dbname=$cfgDb",'','',{
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
# the backup runs the destination's busy handler while it waits for
# the write lock; on top of that we retry, because a backup that
# starts while another connection sits in a read transaction gives up
# rather than waiting
$dbh->sqlite_busy_timeout($RESTORE_BUSY_TIMEOUT_MS);
my $deadline = Time::HiRes::time() + $RESTORE_BUSY_TIMEOUT_MS / 1000;
my ($busy,$tries) = (undef,0);
while (1) {
$busy = undef;
last if eval { $dbh->sqlite_backup_from_file($staging) };
$busy = $@ || $dbh->errstr || 'unknown error';
last if Time::HiRes::time() >= $deadline;
# first attempt and then roughly every five seconds, so that a
# long wait is visible without a log line per retry
$self->log->warn("Config database busy, retrying restore: $busy")
if $tries++ % 50 == 0;
Time::HiRes::sleep(0.1);
}
$dbh->disconnect;
# giving up here is safe: the live database has not been touched, so
# this degrades to a clean error rather than a half restored config
die mkerror(3845,trm("Could not restore the configuration database: %1",$busy))
if $busy;
1;
} or $err = $@;
no autodie;
unlink glob $staging.'*';
die $err if $err;
return;
}
=head2 $cfg->restoreConfigBlob(configBlob)
lib/CallBackery/GuiPlugin/AbstractAction.pm view on Meta::CPAN
has screenOpts => sub {
{
}
};
=head2 actionCfg
Returns a list of action buttons to place at the top of the form.
For C<download> and C<display> actions the gui is blocked by a modal busy
indicator while the download is being prepared (for at most 3 seconds).
Actions which take a long time to produce their data can switch this off by
setting C<noBusyIndicator> to true, keeping the gui usable while they run:
{
label => trm('Create Report'),
action => 'download',
key => 'createReport',
noBusyIndicator => true,
actionHandler => sub { ... },
lib/CallBackery/Model/ConfigJsonSchema.pm view on Meta::CPAN
description: for the small logo brand the UI
type: string
format: uri
logo_noscale:
type: boolean
description: don't scale the login window logo
default: false
spinner:
type: string
format: uri
description: url for the busy animation spinner gif
title:
type: string
description: title string for the application
initial_plugin:
type: string
description: which tab should be active upon login ?
hide_password:
type: boolean
description: hide password field on login screen
hide_password_icon:
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/data/Server.js view on Meta::CPAN
origHandler(ret);
}
};
var newArgs = Array.prototype.slice.call(arguments);
newArgs[0] = superHandler;
this.callAsync.apply(this, newArgs);
},
callAsyncSmartBusy: function(handler, methodName) {
var origHandler = handler;
var busy = callbackery.ui.Busy.getInstance();
var superHandler = function(ret, exc, id) {
busy.vanish();
if (exc) {
callbackery.ui.MsgBox.getInstance().exc(exc);
} else {
origHandler(ret);
}
};
var newArgs = Array.prototype.slice.call(arguments);
newArgs[0] = superHandler;
busy.manifest('Running ' + methodName);
this.callAsync.apply(this, newArgs);
}
}
});
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/Busy.js view on Meta::CPAN
Utf8Check: äöü
************************************************************************ */
/**
@asset(callbackery/spinner.gif);
*/
/**
* singleton with two methods for blocking and unblocking the screen. while the screen
* is blocked, a busy icon is shown.
*
* <pre code='javascript'>
* var busy = callbackery.ui.Busy.getInstance();
* busy.manifest();busy.vanish();
* </pre>
*/
qx.Class.define("callbackery.ui.Busy", {
extend : qx.ui.basic.Atom,
type : "singleton",
construct : function() {
var img = 'callbackery/spinner.gif';
var cfg = callbackery.data.Config.getInstance().getBaseConfig();
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/Action.js view on Meta::CPAN
}
}, this);
}
else {
asyncCall();
}
break;
case 'download':
case 'display':
var formData = getFormData();
let busy = callbackery.ui.Busy.getInstance();
if (formData === false) {
callbackery.ui.MsgBox.getInstance().error(
this.tr("Validation Error"),
this.tr("The form can only be submitted when all data fields have valid content.")
);
return;
}
var key = btCfg.key;
// actions which take a long time to produce their
// data can opt out of the modal busy indicator so
// that the gui stays usable while they run
let showBusy = !btCfg.noBusyIndicator;
if (showBusy) {
if (btCfg.busyMessage) {
busy.manifest(this.xtr(btCfg.busyMessage));
} else {
busy.manifest(this.tr('Preparing Download ...'));
}
setTimeout(() => { busy.vanish(); }, 3 * 1000); // hide the activity indicator after 3 seconds anyway
}
callbackery.data.Server.getInstance().callAsyncSmart(function (cookie) {
let url = 'download'
+ '?name=' + cfg.name
+ '&key=' + key
+ '&xsc=' + encodeURIComponent(cookie)
+ '&formData=' + encodeURIComponent(qx.lang.Json.stringify(formData));
if (btCfg.action == 'display') {
window.open(url + '&display=1', '_blank');
return;
}
var iframe = new qx.ui.embed.Iframe().set({
width: 100,
height: 100
});
iframe.addListener('load', function (e) {
if (showBusy) {
busy.vanish();
}
var response = {
exception: {
message: String(that.tr("No Data")),
code: 9999
}
};
try {
// innerHTML is wrapped in `<pre>` tags, which we remove.
let innerHTML = iframe.getBody().innerHTML;
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/Action.js view on Meta::CPAN
var formData = getFormData();
if (formData && fileList) {
var form = new FormData();
form.append('name', name);
form.append('key', key);
form.append('file', fileList[0]);
form.append('formData', qx.lang.Json.stringify(formData));
var that = this;
serverCall.callAsyncSmart(function (cookie) {
form.append('xsc', cookie);
that._uploadForm(form,btCfg.busyMessage);
}, 'getSessionCookie');
} else {
callbackery.ui.MsgBox.getInstance().error(
this.tr("Upload Exception"),
this.tr("Make sure to select a file and properly fill the form")
);
}
}, this);
return button;
},
_uploadForm(form,busyMessage) {
var req = new qx.io.request.Xhr("upload", 'POST').set({
requestData: form
});
let busy = callbackery.ui.Busy.getInstance();
req.addListener('success', function (e) {
busy.vanish();
var response = req.getResponse();
if (response.exception) {
callbackery.ui.MsgBox.getInstance().error(
this.tr("Upload Exception"),
this.xtr(response.exception.message)
+ " (" + response.exception.code + ")"
);
} else {
this.fireDataEvent('actionResponse', response);
}
req.dispose();
}, this);
req.addListener('fail', function (e) {
var response = {};
busy.vanish();
try {
response = req.getResponse();
}
catch (e) {
response = {
exception: {
message: e.message,
code: 99999
}
};
}
callbackery.ui.MsgBox.getInstance().error(
this.tr("Upload Exception"),
this.xtr(response.exception.message)
+ " (" + response.exception.code + ")"
);
req.dispose();
});
if (busyMessage) {
busy.manifest(this.xtr(busyMessage));
} else {
busy.manifest(this.tr('Uploading File, please wait ...'));
}
req.send();
},
getTableContextMenu() {
return this._tableMenu;
},
getMobileMenu() {
return this._mobileMenu;
},
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/CardList.js view on Meta::CPAN
// called from form appear listener
_loadData : function() {
let that = this;
let rpc = callbackery.data.Server.getInstance();
let currentFormData = this._form.getData();
let parentFormData;
if (this._getParentFormData) {
parentFormData = this._getParentFormData();
}
let busy = callbackery.ui.Busy.getInstance();
busy.manifest(this.tr('Loading Card Data'));
this._loading++;
rpc.callAsync(function(data,exc){
if (!exc){
if (Array.isArray(data)){
// the data is an array of card data
that.setData(data,true);
that.setInfo(null);
}
else {
// the data is an object with card data and info
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/CardList.js view on Meta::CPAN
}
if (that._hasTrigger) {
that._reconfForm();
}
}
else {
if (exc.code != 2){ /* 2 is for aborted calls, this happens when the popup is closed */
callbackery.ui.MsgBox.getInstance().exc(exc);
}
}
busy.vanish();
that._loading--;
}, 'getPluginData', this._cfg.name, 'getAllCardData', parentFormData, { currentFormData: this._form.getData()});
},
// now special handling here
_loadDataReadOnly: function(){
this._loadData();
},
setData : function (data) {
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/Form.js view on Meta::CPAN
_loadData: function(mergeUrlData){
if (!this._form) return;
var that = this;
var rpc = callbackery.data.Server.getInstance();
this._loading++;
var parentFormData = {};
if (this._getParentFormData){
parentFormData = this._getParentFormData();
}
var busy = callbackery.ui.Busy.getInstance();
busy.manifest(this.tr('Loading Form Data'));
rpc.callAsync(function(data,exc){
if (!exc){
if (mergeUrlData) {
let urlData = that._getUrlData();
if (!data) {
data = urlData;
}
else {
Object.assign(data, urlData);
}
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/Form.js view on Meta::CPAN
if (that._hasTrigger) {
that._reconfForm();
}
}
}
else {
if (exc.code != 2){ /* 2 is for aborted calls, this happens when the popup is closed */
callbackery.ui.MsgBox.getInstance().exc(exc);
}
}
busy.vanish();
that._loading--;
// see _loadDataReadOnly: release reconfigurations parked
// while the form was loading (covers the error path too;
// on success _reconfForm above picks them up itself).
if (that._loading === 0) {
that._executeReconfPending();
}
},'getPluginData',this._cfg.name,'allFields',parentFormData,{ currentFormData: this._form.getData()});
}
},
lib/CallBackery/qooxdoo/callbackery/source/class/callbackery/ui/plugin/Html.js view on Meta::CPAN
return html;
},
_loadData: function(){
var html = this._html;
var cfg = this._cfg;
var rpc = callbackery.data.Server.getInstance();
var parentFormData = {};
if (this._getParentFormData){
parentFormData = this._getParentFormData();
}
var busy = callbackery.ui.Busy.getInstance();
busy.manifest(this.tr('Loading HTML Data'));
var that = this;
rpc.callAsync(function(data,exc){
if (!exc){
html.setHtml(data);
}
else {
if (exc.code != 2){ /* 2 is for aborted calls, this happens when the popup is closed */
callbackery.ui.MsgBox.getInstance().exc(exc);
}
}
busy.vanish();
},'getPluginData',cfg.name,parentFormData);
}
}
});