ui/controllers/base_controller.js
const { Context } = require('../../core/context')
const electron = require('electron');
const Templates = require('../common/templates');
const url = require('url');
const { Util } = require('../../core/util');
const { Workflow } = require('../../core/workflow');
/**
* BaseController is the base class from which other view controllers
* derive. This class implements the basic controller actions such as
* displaying forms and lists, saving user-edited objects, etc.
*
* The BaseController and all its subclasses use standard HTTP query
* params to pass parameters. They also use DART Forms, which parse
* HTML form data using jQuery.
*
* @param {url.URLSearchParams} params - URL parameters from the UI.
* These usually come from a query string attached to the href attribute
* of a link or button.
*
* @param {string} navSection - The section of DART's top navigation bar
* that should be highlighted when this controller renders a page. For
* example, when rendering {@link AppSetting} pages, the "Settings"
* section of the top nav bar should be active.
*/
class BaseController {
constructor(params, navSection) {
/**
* A {@link url.URLSearchParams} object containing parameters that
* the controller will need to render the display. For forms, the
* only required param is usually "id", which is the UUID of the
* object to be edited. For lists, this typically includes
* limit, offset, orderBy, and sortDirection.
*
* @type {url.URLSearchParams}
*/
this.params = params || new url.URLSearchParams();
/**
* The typeMap describes how values in this.params should be converted,
* if they do need to be converted. Because {@link url.URLSearchParams}
* are always either strings or arrays of strings, we sometimes need
* to convert them to numbers or booleans before we can use them.
* For example, a controller that lists a number of objects needs the
* "limit" param to be converted from string "20" to number 20.
*
* typeMap key is the param name and value is the data type.
*
* The typeMap currently supports types "string", "boolean", and
* "number" (no dates yet). Each controller sets its own typeMap
* as necessary.
*
* @type {object.<string, string>}
*/
this.typeMap = {};
/**
* The name of the nav section to highlight when this controller
* renders a page. Valid values include any names you see on DART's
* top nav bar, including "Dashboard", "Settings", "Jobs" and "Help".
*
* @type {string}
*/
this.navSection = navSection;
/**
* Controllers may optionally set this before redirecting,
* to make an alert message appear on the top of the destination
* page. For example, after successfully submitting a form, a
* controller typically redirects to the list page with an alert
* message at the top saying the data was saved.
*
* @type {string}
*/
this.alertMessage = null;
/**
* Controllers may optionally set this before redirecting to control
* the appearance of the optional alertMessage. The value can be any
* valid Bootstrap 4 alert class. The most common values will be
* "alert-success" and "alert-danger".
*
* @type {string}
* @default "alert-success"
*/
this.alertCssClass = 'alert-success';
// The following are all set by child classes.
/**
* This is the model which the controller represents. For example,
* The AppSettingController renders forms and lists for the AppSetting
* model. This must be set by the child class.
*
* @type {PersistentObject|object}
*/
this.model;
/**
* This is the name of the form class that can render a form for
* this controller's model. For example, the AppSettingController
* will have formClass AppSettingForm. This must be set by the child
* class.
*
* @type {Form}
*/
this.formClass;
/**
* This is the template that renders this controller's form.
* Templates are properties of the {@link Template} object.
* This property must be set by the child class.
*
* @type {handlebars.Template}
*/
this.formTemplate;
/**
* This is the template that renders this controller's object list.
* Templates are properties of the {@link Template} object.
* This property must be set by the child class.
*
* @type {handlebars.Template}
*/
this.listTemplate;
/**
* The name property of this template's model. This is used when
* ordering lists of objects by name. For example, the nameProperty
* for model {@link BagItProfile} or {@link AppSetting} would be
* "name".
*
* @type {string}
*/
this.nameProperty;
/**
* This property will be set to true if the controller that was
* originally called redirected to a new controller.
*
* @type {boolean}
* @default false
*/
this.redirected = false;
}
/**
* Converts URLSearchParams to a simple hash with correct data types.
* The data types are specified in each controller's typeMap, which
* can specify that certain params be converted to numbers or booleans.
*
*/
paramsToHash() {
let data = {};
for(let [key, value] of this.params.entries()) {
let toType = this.typeMap[key] || 'string';
if (toType === 'string') {
data[key] = value;
} else {
data[key] = Util.cast(value, toType);
}
}
return data;
}
/**
* The sets the content of the main page container to the html
* you pass in.
*
* @param {string} html - HTML content to render in the page's
* main container.
*/
containerContent(html) {
let workflows = Workflow.list(null, {
orderBy: 'name',
sortDirection: 'asc'
});
return {
nav: Templates.nav({
section: this.navSection,
workflows: workflows
}, Templates.renderOptions),
container: html
}
}
/**
* This sets the title and body of the modal popup window.
*
* @param {string} title - The title of the modal popup.
*
* @param {string} body - The HTML content of the modal popup.
*/
modalContent(title, body) {
return {
modalTitle: title,
modalContent: body
}
}
/**
* Controller methods call this when they do not want to render
* any new content.
*/
noContent() {
return {};
}
/**
* This opens a URL in an external browser window, using the user's
* default web browser.
*
* The URL comes the value of the "url" param in the params object
* that was passed in to this object's constructor. Derived classes
* can also call this.params.set('url', '...value...') before calling
* this method.
*/
openExternal() {
electron.shell.openExternal(this.params.get('url'));
return this.noContent();
}
/**
* This redirects to a new URL that will call the function fnName
* on the controller controllerName with the specified params.
* Use this only when redirecting to an entirely new controller.
*
* This changes the page URL, causing the {@link RequestHandler}
* to parse and route the new request.
*
* @example
* this.redirect('JobFiles', 'show', this.params);
*
* @param {string} controllerName - The name of the controller you
* want to redirect to.
*
* @param {string} fnName - The name of the function on the new
* controller that should process the request.
*
* @param {URLSearchParams} params - The URL query parameters to
* pass into the constructor of the controller you are redirecting to.
*
*/
redirect(controllerName, fnName, params) {
this.redirected = true;
window.location.href = `#${controllerName}/${fnName}?${params.toString()}`;
return this.noContent();
}
/**
* This displays a blank form that allows a user to create a new
* instance of an object.
*/
new() {
let form = new this.formClass(new this.model());
let html = this.formTemplate({ form: form }, Templates.renderOptions);
return this.containerContent(html);
}
/**
* This displays a HTML form with pre-filled values, so a user
* can edit an existing object.
*/
edit() {
let obj = this.model.find(this.params.get('id'));
let form = new this.formClass(obj);
let html = this.formTemplate({ form: form });
return this.containerContent(html);
}
/**
* This handles the submission of a form, saving an object to the
* local database if it's valid, or highlighting errors on the form
* if the object is not valid.
*/
update() {
let obj = this.model.find(this.params.get('id')) || new this.model();
let form = new this.formClass(obj);
form.parseFromDOM();
if (!form.obj.validate()) {
form.setErrors();
let html = this.formTemplate({ form: form }, Templates.renderOptions);
return this.containerContent(html);
}
this.alertMessage = Context.y18n.__(
"ObjectSaved_message",
Util.camelToTitle(obj.constructor.name),
obj[this.nameProperty]);
obj.save();
return this.list();
}
/**
* This displays a list of all instances of an object type from the local
* database. It works with params limit, offset, orderBy, and sortDirection,
* which are passed into the constructor in the params object.
*/
list() {
let listParams = this.paramsToHash();
listParams.orderBy = listParams.sortBy || this.defaultOrderBy;
listParams.sortDirection = listParams.sortOrder || this.defaultSortDirection;
let items = this.model.list(null, listParams);
let data = {
alertMessage: this.alertMessage,
items: items
};
let html = this.listTemplate(data, Templates.renderOptions);
return this.containerContent(html);
}
/**
* This deletes an object from the database, after prompting the user
* to confirm they really want to delete it.
*/
destroy() {
let obj = this.model.find(this.params.get('id'));
let confirmDeletionMessage = Context.y18n.__(
"Confirm_deletion",
Util.camelToTitle(obj.constructor.name),
obj[this.nameProperty]);
if (confirm(confirmDeletionMessage)) {
this.alertMessage =Context.y18n.__(
"ObjectDeleted_message",
Util.camelToTitle(obj.constructor.name),
obj[this.nameProperty]);
obj.delete();
return this.list();
}
return this.noContent();
}
/**
* This is to be defined by derived classes as necessary.
* It does nothing in the base class, but derived classes
* can use it to attach event handlers to HTML elements
* that have just been rendered.
*
* @param {string} fnName - The name of the function that was
* called in the original request. The postRenderCallback may
* include logic to perform different actions based on what
* the user initially requested. For example, you may want to
* attach one set of event handlers after rendering
* Controller.new() and a different set after rendering
* Controller.update().
*
*/
postRenderCallback(fnName) {
}
}
module.exports.BaseController = BaseController;