sclass HCRUD_Concepts extends HCRUD_Data { Concepts cc = db_mainConcepts(); Class cClass; new L> onCreateOrUpdate; IVF2 afterUpdate; // parameter 2: old values MapSO filters; // fields to filter by/add to new objects ValueConverterForField valueConverter; bool referencesBlockDeletion; bool trimAllSingleLineValues; Set fieldsToHideInCreationForm; bool lockDB; // lock DB while updating object bool verbose; bool dropEmptyListValues = true; bool lsMagic; bool useDynamicComboBoxes; // dynamically load concepts combo box entries *(Class *cClass) {} // XXX - breaking change from just shortName() swappable S itemName() { ret humanizeShortName(cClass); } swappable S itemNamePlural() { ret super.itemNamePlural(); } //LS fields() { ret conceptFields(cClass); } L itemsForListing() { ret defaultSort(asList(listConcepts())); } @Override L list() { ret lambdaMap itemToMapForList(itemsForListing()); } // more efficient version - convert items only after taking subList // TODO: this is never called by HCRUD; make lazy list instead? @Override L list(IntRange range) { ret lambdaMap itemToMapForList(subListOrFull(itemsForListing(), range)); } Cl listConcepts() { ret conceptsWhere(cc, cClass, mapToParams(filters)); } L defaultSort(L l) { ret l; } swappable MapSO emptyObject() { A c = unlisted(cClass); ret mapMinusKeys(fieldsToHideInCreationForm, itemToMap(c)); } MapSO itemToMap(A c) { if (c == null) null; ret putKeysFirst(getFieldOrder(c), conceptToMap_gen_withNullValues(c)); } MapSO itemToMapForList(A c) { if (c == null) null; MapSO map = itemToMap(c); massageItemMapForList(c, map); ret map; } swappable void massageItemMapForList(A c, MapSO map) { } swappable void massageItemMapForUpdate(A c, MapSO map) { // Concepts.RefL magic for (Field f : nonStaticNonTransientFieldObjectsOfType(Concept.RefL.class, cClass)) { new TreeMap values; new Matches m; for (S key, O value : cloneMap(map)) if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { long conceptID = parseFirstLong((S) value); Concept concept = getConcept(cc, conceptID); printVars_str("RefL magic", +key, +conceptID, +concept); if (concept != null || !dropEmptyListValues) values.put(parseInt(m.rest()), concept); map.remove(key); } if (!dropEmptyListValues) while (nempty(values) && lastValue(values) == null) { if (verbose) print("Dropping value " + lastEntry(values)); removeLastKey(values); } map.put(f.getName(), valuesAsList(values)); } // Concepts.Ref magic (look up concept) for (Field f : nonStaticNonTransientFieldObjectsOfType(Concept.Ref.class, cClass)) { O value = map.get(f.getName()); if (value cast S) { long conceptID = parseFirstLong(value); Concept concept = getConcept(cc, conceptID); map.put(f.getName(), concept); } } if (trimAllSingleLineValues) for (Map.Entry e : map.entrySet()) { S val = optCastString(e.getValue()); if (val != null && isSingleLine(val) && isUntrimmed(val)) e.setValue(trim(val)); } // LS magic if (lsMagic) for (Field f : nonStaticNonTransientFieldObjectsOfType(L.class, cClass)) { if (eqOneOf(f.getName(), "refs", "backRefs")) continue; new TreeMap values; new Matches m; for (S key, O value : cloneMap(map)) { if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { if (!dropEmptyListValues || nempty((S) value)) { if (verbose) print("Adding value " + m.rest() + " / " + value); mapPut(values, parseInt(m.rest()), (S) value); } map.remove(key); } } if (!dropEmptyListValues) while (nempty(values) && empty(lastValue(values))) { if (verbose) print("Dropping value " + lastEntry(values)); removeLastKey(values); } map.put(f.getName(), valuesAsList(values)); } // don't set SecretValue fields for (Field f : nonStaticNonTransientFieldObjectsOfType(SecretValue, cClass)) map.remove(f.getName()); } swappable MapSO getObject(O id) { ret itemToMap(conceptForID(id)); } O createObject(SS map) { A c = cnew(cc, cClass); // make sure filters override setValues(c, mapMinusKeys(map, filteredFields()), true); cset(c, mapToParams(filters)); pcallFAll(onCreateOrUpdate, c); ret c.id; } void setValues(A c, SS map, bool creating) { lock lockDB && !creating ? dbLock(cc) : null; MapSO map2 = (Map) cloneMap(map); massageItemMapForUpdate(c, map2); if (verbose) { print("setValues " + map); print("backRefs: " + c.backRefs); } MapSO oldValues = !creating && afterUpdate != null ? cgetAll_cloneLists(c, keys(map2)) : null; if (valueConverter == null) cSmartSet(c, mapToParams(map2)); else cSmartSet_withConverter(verbose, valueConverter, c, mapToParams(map2)); if (oldValues != null) callF(afterUpdate, c, oldValues); if (verbose) print("backRefs: " + c.backRefs); } A conceptForID(O id) { ret getConcept(cc, cClass, toLong(id)); } S updateObject(O id, SS map) { A c = conceptForID(id); if (c == null) ret "Object " + id + " not found"; try answer checkFilters(c); setValues(c, map, false); pcallFAll(onCreateOrUpdate, c); ret "Object " + id + " updated"; } S deleteObject(O id) { A c = conceptForID(id); if (c == null) ret "Object " + id + " not found"; try answer checkFilters(c); actuallyDeleteConcept(c); ret "Object " + id + " deleted"; } swappable void actuallyDeleteConcept(A c) { deleteConcept(c); } S checkFilters(A c) { ret checkConceptFields(c, mapToParams(filters)) ? "" : "Object " + c.id + " not in view"; } HCRUD_Concepts addFilters(MapSO map) { fOr (S field, O value : map) addFilter(field, value); this; } HCRUD_Concepts addFilter(S field, O value) { filters = orderedMapPutOrCreate(filters, field, value); this; } Renderer getRenderer(S field) { Class type = fieldType(cClass, field); if (eq(type, bool.class)) ret new CheckBox; if (eq(type, Bool.class)) ret new ComboBox(ll("", "yes", "no"), b -> trueFalseNull((Bool) b, "yes", "no", "")); // show a Ref<> field as a combo box if (eq(type, Concept.Ref.class)) { Class c = getTypeArgumentAsClass(genericFieldType(cClass, field)); ret makeConceptsComboBox(field, c); } // show a RefL<> field as a list of combo boxes if (eq(type, Concept.RefL.class)) { Class c = getTypeArgumentAsClass(genericFieldType(cClass, field)); ret new FlexibleLengthList(makeConceptsComboBox(field, c)); } ret super.getRenderer(field); } ComboBox makeConceptsComboBox(S field, Class c) { if (useDynamicComboBoxes) { DynamicComboBox cb = new(field); cb.valueToEntry = value -> { value = deref(value); //print("ComboBox: value type=" + _getClass(value); long id = 0; if (value cast Concept) id = conceptID(value); else if (value instanceof S && isInteger((S) value)) id = parseLong(value); if (id != 0) ret comboBoxItem(getConcept(cc, id)); null; }; ret cb; } LS entries = comboBoxItemsForConceptClass(c); ComboBox cb = new(entries); cb.valueToEntry = value -> { value = deref(value); //print("ComboBox: value type=" + _getClass(value); long id = 0; if (value cast Concept) id = conceptID(value); else if (value instanceof S && isInteger((S) value)) id = parseLong(value); if (id != 0) { S entry = firstWhereFirstLongIs(entries, id); //print("combobox selected: " + id + " / " + entry); ret entry; } null; }; ret cb; } // TODO: filters! LS comboBoxItemsForConceptClass(Class c) { ret comboBoxItems(cc.list(c)); } LS comboBoxItems(Cl l) { ret itemPlus("", lmap comboBoxItem(l)); } S comboBoxItem(Concept val) { ret val == null ? null : shorten(val.id + ": " + val); } bool objectCanBeDeleted(O id) { ret !referencesBlockDeletion || !hasBackRefs(conceptForID(id)); } Set filteredFields() { ret keys(filters); } LS comboBoxSearch(S info, S query) { // info is the field name (Ref or RefL). get concept class S field = info; Class c = getTypeArgumentAsClass(genericFieldType(cClass, field)); if (c == null) null; LS items = comboBoxItemsForConceptClass(c); ret takeFirst(10, scoredSearch(query, items)); } }