Warning: session_start(): open(/var/lib/php/sessions/sess_siq30jet4kou7lscf9hbofsfks, O_RDWR) failed: No space left on device (28) in /var/www/tb-usercake/models/config.php on line 51
Warning: session_start(): Failed to read session data: files (path: /var/lib/php/sessions) in /var/www/tb-usercake/models/config.php on line 51
// one instance should only be used for one page at a time
sclass HCRUD extends HAbstractRenderable {
HCRUD_Data data;
int defaultTextFieldCols = 80;
// yeah these fields are a mess...
bool mutationRights = true;
bool allowCreateOrDelete = true;
bool allowCreate = true;
bool allowEdit = true;
bool singleton;
bool cmdsLeft; // put commands on the left instead of the right
S tableClass; // CSS class for table
S formTableClass; // CSS class for form
Set unshownFields; // not shown in table or form
Set uneditableFields; // not shown in form
Set unlistedFields; // not shown in table
bool showCheckBoxes;
bool haveJQuery, haveSelectizeJS;
bool needsJQuery; // unused
bool paginate;
bool sortable;
bool buttonsOnTop, buttonsOnBottom = true;
S formID = "crudForm";
bool showQuickSaveButton; // needs hnotificationPopups()
SS params;
new HTMLPaginator paginator;
// sort options
S sortByField;
S sortParameter = "sort";
bool descending;
int flexibleLengthListLeeway = 5; // how many empty rows to display at the end of each flexible length list
// set internally after update/create or from "selectObj" param to highlight an item in the list (or show only that item)
O objectIDToHighlight;
S fieldPrefix = "f_", listFieldPrefix = "fList_";
*() {}
*(HCRUD_Data *data) {}
*(S *baseLink, HCRUD_Data *data) {}
S newLink() { ret appendQueryToURL(baseLink, cmd := "new"); }
S deleteLink(O id) { ret appendQueryToURL(baseLink, "delete_" + id, 1); }
S editLink(O id) { ret appendQueryToURL(baseLink, edit := id); }
S duplicateLink(O id) { ret appendQueryToURL(baseLink, duplicate := id); }
// also handles commands if withCmds=true
// you probably want to call renderPage() instead to handle all commands
S render(bool withCmds, SS params) {
this.params = params;
if (objectIDToHighlight == null) objectIDToHighlight = params.get("selectObj");
if (!withCmds) ret renderTable(false);
try answer handleCommands(params);
ret renderMsgs(params)
+ pUnlessEmpty(nav())
+ renderTable(withCmds);
}
swappable S nav() {
new LS l;
if (actuallyAllowCreate())
l.add(ahref(newLink(), "New " + itemName()));
ret joinWithVBar(l);
}
S handleCommands(SS params) {
new LS msgs;
if (eqGet(params, "action", "create")) {
if (!actuallyAllowCreate()) fail("Creating objects not allowed");
O id = data.createObject(subMapStartingWith_dropPrefix(params, fieldPrefix));
msgs.add(itemName() + " created (ID: " + id + ")");
objectIDToHighlight = id;
}
if (eqGet(params, "action", "update")) {
if (!actuallyAllowEdit()) fail("Editing objects not allowed");
S id = params.get("id");
msgs.add(data.updateObject(id,
subMapStartingWith_dropPrefix(params, fieldPrefix)));
objectIDToHighlight = id;
}
LS toDeleteList = keysDeprefixNemptyValue(params, "delete_");
if (eq(params.get("bulkAction"), "deleteSelected"))
toDeleteList.addAll(keysDeprefixNemptyValue(params, "obj_"));
for (S toDelete : toDeleteList) {
if (!actuallyAllowDelete()) fail("Deleting objects not allowed");
msgs.add(data.deleteObject(toDelete));
}
ret empty(msgs) ? "" : refreshAfterCommand(params, msgs);
}
swappable S refreshAfterCommand(SS params, LS msgs) {
ret refreshWithMsgs(msgs,
anchor := objectIDToHighlight == null ? null : "obj" + objectIDToHighlight,
params := objectIDToHighlight == null ? null : litmap(selectObj := objectIDToHighlight));
}
S encodeField(S s) {
ret or(data.fieldNameToHTML(s), s);
}
// in table
swappable S renderValue(S field, O value) {
if (value cast HTML) ret value.html;
value = deref(value);
if (value cast SecretValue)
ret hhiddenStuff(renderValue_inner(value!));
ret renderValue_inner(value);
}
swappable S renderValue_inner(O value) {
if (value cast Bool)
ret yesNo_short(value);
ret htmlEncode_nlToBr_withIndents(strOrEmpty(value));
}
S renderTable(bool withCmds) {
ret renderTable(withCmds, data.list());
}
S valueToSortable(O value) {
if (value cast HTML)
ret value!;
ret strOrNull(value);
}
// l = list of maps as it comes from data object
S renderTable(bool withCmds, L l) {
if (empty(l)) ret p("No entries");
if (nempty(sortByField)) l = sortByTransformedMapKey_alphaNum valueToSortable(l, sortByField);
if (descending) l = reversed(l);
// l2 is the rendered map. use keyEncoding to get from l keys to l2 keys
new SS keyEncoding;
L l2 = map(l, map -> {
O id = itemID(map);
map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields));
MapSO map2 = postProcessTableRow(map, mapToMap(
(key, value) -> pair(mapPut_returnValue(keyEncoding, key, encodeField(key)), renderValue(key, value)),
map));
if (singleton)
map2.remove(encodeField(data.idField())); // don't show ID in table in singleton mode
if (withCmds)
map2 = addCmdsToTableRow(map, map2);
// add anchor to row
map2.put(firstKey(map2), aname("obj" + id, firstValue(map2)));
ret map2;
});
new LS out;
if (paginate) {
paginator.processParams(params);
paginator.baseLink = baseLink;
paginator.max = l(l2);
out.add(pUnlessEmpty(paginator.renderNav()));
L l3 = subListOrFull(l2, paginator.visibleRange());
// if highlighted object is not on page, show only this object
if (objectIDToHighlight != null && !any isHighlighted(l3))
l3 = llNonNulls(firstThat isHighlighted(l2));
l2 = l3;
}
new SS replaceHeaders;
if (sortable && !singleton)
for (S key, html : keyEncoding) {
bool sortedByField = eq(sortByField, key);
bool showDescendingLink = sortedByField && !descending;
S htmlOld = html;
if (sortedByField) {
S title = showDescendingLink ? "Click here to sort descending" : "Click here to sort ascending";
S titleSorted = "Sorted by this field ("
+ (descending ? "descending" : "ascending") + ")";
title = titleSorted + ". " + title;
html = span_title(title, unicode_downOrUpPointingTriangle(descending)) + " " + html;
}
S sortLink = appendQueryToURL(baseLink, sortParameter, showDescendingLink ? "-" + key : key);
replaceHeaders.put(htmlOld,
/*html + " " + ahref(sortLink,
unicode_smallDownOrUpPointingTriangle(showDescendingLink), +title)*/
ahref(sortLink, html));
}
out.add(hpostform(
htmlTable2_noHtmlEncode(l2, paramsPlus(tableParams(), +replaceHeaders))
+ (!withCmds || !showCheckBoxes ? "" : "\n" + pUnlessEmpty(renderBulkCmds())),
action := baseLink));
ret lines_rtrim(out);
}
// item is after encodeField
bool isHighlighted(MapSO item) {
O id = item.get(encodeField(idField()));
//print("isHighlighted ID: " + toStringWithClass(id) + " / " + toStringWithClass(objectIDToHighlight));
ret eq(id, objectIDToHighlight);
}
swappable S renderBulkCmds() {
ret "Bulk action: " + hselect("bulkAction", litorderedmap("" := "", "deleteSelected" := "Delete selected"))
+ " " + hsubmit("OK", onclick := "return confirm('Are you sure?')");
}
MapSO addCmdsToTableRow(MapSO map, MapSO map2) {
if (showCheckBoxes) {
O id = itemID(map);
map2.put(checkBoxKey(), hcheckbox("obj_" + id, false, title := "Select this object for a bulk action"));
map2 = putKeysFirst(map2, checkBoxKey());
}
map2.put(cmdsKey(), renderCmds(map));
if (cmdsLeft)
map2 = putKeysFirst(map2, cmdsKey());
ret map2;
}
/*swappable*/ O[] tableParams() {
ret litparams(
tdParams := litparams(valign := "top"),
tableParams := litparams(class := tableClass));
}
S renderForm(MapSO map) {
temp tempSetTL(htmlencode_forParams_useV2, true);
//print("renderForm: filteredFields=" + data.filteredFields());
map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields, data.filteredFields()));
MapSO mapWithoutID = mapWithoutKey(map, data.idField());
LLS matrix = map(mapWithoutID, (field, value) -> {
S help = data.fieldHelp(field);
ret ll(
encodeField(field),
addHelpText(help, renderInput(field, value))
);
});
massageFormMatrix(map, matrix);
ret htableRaw_valignTop(matrix
, empty(formTableClass) ? litparams(border := 1, cellpadding := 4) : litparams(class := formTableClass));
}
S renderInput(S field, O value) {
S name = fieldPrefix + field;
ret renderInput(name, data.getRenderer(field), value);
}
S renderInput(S name, HCRUD_Data.Renderer r, O value) {
//print("Renderer for " + name + ": " + r);
if (r != null) value = r.preprocessValue(value);
// switch by renderer type
if (r cast HCRUD_Data.AceEditor)
ret hAceEditor(strOrEmpty(value), style := "width: " + r.cols + "ch; height: " + r.rows + "em", +name);
if (r cast HCRUD_Data.TextArea)
ret htextarea(strOrEmpty(value), +name, cols := r.cols, rows := r.rows);
if (r cast HCRUD_Data.TextField)
ret htextfield(name, strOrEmpty(value), size := r.cols, style := "font-family: monospace");
if (r cast HCRUD_Data.ComboBox)
ret renderComboBox(name, r.valueToEntry(value), r.entries, r.editable);
if (r cast HCRUD_Data.DynamicComboBox)
ret renderDynamicComboBox(name, r.valueToEntry(value), r.info, r.editable);
if (r cast HCRUD_Data.CheckBox)
//ret hcheckbox(name, isTrue(value));
ret htrickcheckboxWithText(name, "", isTrue(value));
if (r cast HCRUD_Data.FlexibleLengthList) {
L list = cast value;
new LS rows;
int n = l(list)+flexibleLengthListLeeway;
for i to n: {
O item = _get(list, i);
//print("Item: " + item);
rows.add(tr(td(i+1 + ".", align := "right")
+ td(renderInput(name + "_" + i, r.itemRenderer, item))));
}
ret htag table(lines(rows));
}
ret renderInput_default(name, value);
}
S renderInput_default(S name, O value) {
ret htextfield(name, strOrEmpty(value), size := defaultTextFieldCols);
}
S renderNewForm() {
ret renderNewForm(data.emptyObject());
}
// pre-populate fields from request parameters
S renderNewFormWithParams(SS params) {
SS filteredMap = subMapStartingWith_dropPrefix(params, fieldPrefix);
MapSO map = joinMaps(data.emptyObject(), filteredMap);
// pre-populate list fields
for (S key, value : filteredMap) {
LS l = splitAt(key, "_");
if (l(l) == 2) {
S field = first(l);
int idx = parseInt(second(l));
LS list = cast map.get(field);
if (!list instanceof ArrayList) map.put(field, list = new L);
listPut(list, idx, value);
}
}
ret renderNewForm(map);
}
S renderNewForm(MapSO map) {
//printStruct("renderNewForm", map);
S buttons = p(hsubmit("Create"));
ret hpostform(
hhidden("action", "create")
+ stringIf(buttonsOnTop, buttons)
+ renderForm(map)
+ stringIf(buttonsOnBottom, buttons),
paramsPlus(formParameters(), action := baseLink));
}
swappable O[] formParameters() { ret litparams(id := formID); }
S idField() { ret data.idField(); }
S renderEditForm(S id) {
MapSO map = data.getObject(id);
if (map == null) ret htmlEncode2("Entry " + id + " not found");
S onlyFields = mapGet(params, "onlyFields");
if (nempty(onlyFields))
map = onlyKeys(map, itemPlus(idField(), tok_identifiersOnly(onlyFields)));
S buttons = p_vbar(
hsubmit("Save changes"),
!showQuickSaveButton ? "" :
hbuttonOnClick_noSubmit("Save & keep editing", [[
$.ajax({
type: 'POST',
url: $('#crudForm').attr('action'),
data: $('#crudForm').serialize(),
success: function(response) { successNotification("Saved"); },
}).error(function() { errorNotification("Couldn't save"); });
]]),
deleteObjectHTML(id));
ret hpostform(
hhidden("action", "update") +
hhidden(+id) +
p("Object ID: " + htmlEncode2(id)) +
+ stringIf(buttonsOnTop, buttons)
+ renderForm(map)
+ stringIf(buttonsOnBottom, buttons),
paramsPlus(formParameters(), action := baseLink + "#obj" + id));
}
S renderPage(SS params) {
this.params = params;
try answer handleComboSearch(params);
if (eqGet(params, "cmd", "new"))
ret frame("New " + itemName(), renderNewFormWithParams(params));
if (nempty(params.get("edit")))
ret frame("Edit " + itemName(), renderEditForm(params.get("edit")));
if (nempty(params.get("duplicate")))
ret frame("New " + itemName(), renderNewForm(data.getObjectForDuplication(params.get("duplicate"))));
// handle commands, render list
ret frame(ahref(baseLink, firstToUpper(singleton ? data.itemName() : data.itemNamePlural())), render(mutationRights, params));
}
HCRUD makeFrame(MakeFrame makeFrame) { super.makeFrame(makeFrame); this; }
S cmdsKey() { ret ""; }
S checkBoxKey() { ret ""; }
S itemName() { ret data.itemName(); }
swappable MapSO postProcessTableRow(MapSO data, MapSO rendered) { ret rendered; }
O itemID(MapSO item) { ret mapGet(item, data.idField()); }
swappable S renderCmds(MapSO item) {
O id = itemID(item);
ret joinNemptiesWithVBar(
!actuallyAllowEdit() ? null : ahref(editLink(id), "EDIT"),
deleteObjectHTML(id),
!actuallyAllowCreate() ? null : ahref(duplicateLink(id), "dup", title := "duplicate")
);
}
bool actuallyAllowCreate() {
ret !singleton && allowCreateOrDelete && allowCreate;
}
bool actuallyAllowEdit() {
ret allowCreateOrDelete && allowEdit;
}
bool actuallyAllowDelete() {
ret !singleton && allowCreateOrDelete;
}
// e.g. for adding rows to edit/create form
swappable void massageFormMatrix(MapSO map, LLS matrix) {
}
S renderComboBox(S name, S value, LS entries, bool editable) {
if (haveSelectizeJS) {
// coolest option - use selectize.js
S id = aGlobalID();
ret hselect_list(entries, value, +name, +id)
+ hjs("$('#" + id + "').selectize" + [[
({
searchField: 'text',
openOnFocus: true,
dropdownParent: 'body',
create: ]] + jsBool(editable) + [[
/*allowEmptyOption: true*/
]] + moreSelectizeOptions(name) + [[
});
]])
// dirty CSS quick-fix (TODO: send only once)
+ hcss(".selectize-input { min-width: 300px }");
}
if (haveJQuery) {
// make searchable list if JQuery is available
// (functional, but looks quite poor on Firefox)
// TODO: this seems to always be editable
S id = aGlobalID();
ret tag datalist(mapToLines hoption(entries), +id)
+ tag input("", +name, list := id);
}
// standard non-searchable list
if (editable)
ret hinputfield(name, value); // no editable combo box possible without selectize.js
ret hselect_list(entries, value, +name);
}
// dev.
S renderDynamicComboBox(S name, S value, S info, bool editable) {
assertTrue(+haveSelectizeJS);
S id = aGlobalID();
ret hselect_list(llNempties(value), value, +name, +id)
+ hjs("$('#" + id + "').selectize" + [[
({
searchField: 'text',
valueField: 'text',
labelField: 'text',
openOnFocus: true,
dropdownParent: 'body',
create: ]] + jsBool(editable) + [[,
load: function(query, callback) {
if (!query.length) return callback();
console.log("Loading " + ]] + jsQuote(baseLink) + [[);
$.ajax({
url: ]] + jsQuote(baseLink) + [[,
type: 'GET',
dataType: 'json',
data: {
comboSearchInfo: ]] + jsQuote(info) + [[,
comboSearchQuery: query
},
error: function() {
console.log("Got error");
callback();
},
success: function(res) {
//console.log("Got data: " + res);
var converted = res.map(x => { return {text: x}; });
//console.log("Converted: " + converted);
callback(converted);
}
});
}
/*allowEmptyOption: true*/
]] + moreSelectizeOptions(name) + [[
});
]])
// dirty CSS quick-fix (TODO: send only once)
+ hcss(".selectize-input { min-width: 300px }");
}
void processSortParameter(SS params) {
S sort = mapGet(params, sortParameter);
sortByField = null;
descending = false;
if (nempty(sort))
if (startsWith(sort, "-")) {
descending = true;
sortByField = substring(sort, 1);
} else
sortByField = sort;
}
S deleteObjectHTML(O id) {
ret !actuallyAllowDelete() ? null :
!data.objectCanBeDeleted(id) ?
span_title("Object can't be deleted, please delete references first", htmlEncode2(unicode_DEL()))
: ahrefWithConfirm(
"Really delete item " + id + "?", deleteLink(id), htmlEncode2(unicode_DEL()), title := "delete");
}
// helpText is also HTML
S addHelpText(S helpText, S html) {
ret empty(helpText) ? html : html + p(small(helpText), style := "text-align: right");
}
swappable S moreSelectizeOptions(S name) { ret ""; }
// deliver content for dynamic search in combo boxes
S handleComboSearch(SS params) {
S query = params.get("comboSearchQuery");
if (nempty(query)) {
S info = params.get("comboSearchInfo");
ret jsonEncode_shallowLineBreaks(data.comboBoxSearch(info, query));
}
null;
}
}