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 needsJQuery; // unused *() {} *(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) { if (!withCmds) ret renderTable(false); try answer handleCommands(params); ret renderMsgs(params) + pUnlessEmpty(nav()) + renderTable(withCmds); } swappable S nav() { ret !actuallyAllowCreate() ? "" : ahref(newLink(), "New " + itemName()); } 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, "f_")); msgs.add(itemName() + " created (ID: " + id + ")"); } if (eqGet(params, "action", "update")) { if (!actuallyAllowEdit()) fail("Editing objects not allowed"); msgs.add(data.updateObject(params.get("id"), subMapStartingWith_dropPrefix(params, "f_"))); } 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 nempty(msgs) ? refreshWithMsgs(msgs) : ""; } 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 renderTable(bool withCmds, L l) { if (empty(l)) ret p("No entries"); //LS fields = data.fields(); //if (fields == null) fields = allKeysFromList_inOrder(); l = map(l, map -> { O id = itemID(map); map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields)); MapSO map2 = postProcessTableRow(map, mapToMap( (key, value) -> pair(encodeField(key), renderValue(key, value)), map)); if (singleton) map2.remove(data.fieldNameToHTML(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; }); ret hpostform( htmlTable2_noHtmlEncode(l, tableParams()) + (!withCmds || !showCheckBoxes ? "" : "\n" + pUnlessEmpty(renderBulkCmds())), action := baseLink); } 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) { map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields)); LLS matrix = map(map, (field, value) -> { S help = data.fieldHelp(field); ret ll( encodeField(field), renderInput(field, value) + (empty(help) ? "" : p(small(help), style := "text-align: right")) ); }); 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 = "f_" + 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.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 hselect_list(r.entries, r.valueToEntry(value), +name); 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 leeway = 5, n = l(list)+leeway; 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()); } S renderNewForm(MapSO map1) { //printStruct("renderNewForm", map1); Map map = mapWithoutKey(map1, data.idField()); //printStruct("renderNewForm", map); ret hpostform( hhidden("action", "create") + renderForm(map) + p(hsubmit("Create")), paramsPlus(formParameters(), action := baseLink)); } swappable O[] formParameters() { null; } S renderEditForm(S id) { Map map = mapWithoutKey(data.getObject(id), data.idField()); if (map == null) ret htmlEncode2("Entry " + id + " not found"); ret hpostform( hhidden("action", "update") + hhidden(+id) + p("Object ID: " + htmlEncode2(id)) + renderForm(map) + p(hsubmit("Save changes")), paramsPlus(formParameters(), action := baseLink)); } S renderPage(SS params) { if (eqGet(params, "cmd", "new")) ret frame("New " + itemName(), renderNewForm()); if (nempty(params.get("edit"))) ret frame("Edit " + itemName(), renderEditForm(params.get("edit"))); if (nempty(params.get("duplicate"))) ret frame("New " + itemName(), renderNewForm(data.getObject(params.get("duplicate")))); 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"), !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"), !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) { } }