CallBackery

 view release on metacpan or  search on metacpan

CHANGES  view on Meta::CPAN

   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

CHANGES  view on Meta::CPAN

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.

CHANGES  view on Meta::CPAN

| |\  
| | * 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);
        }
    }
});



( run in 4.073 seconds using v1.01-cache-2.11-cpan-302cb4679cc )