Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,14 @@ export function mapCustomfieldToWidget(
break;

case "date":
// the container renders the label column itself - a label attr would show twice
// (filters re-set it after the switch, they have no label column)
delete attrs.label;
attrs.data_format = field.values?.format || "Y-m-d";
break;

case "date-time":
delete attrs.label;
attrs.data_format = field.values?.format || "Y-m-d H:i:s";
break;

Expand Down
88 changes: 86 additions & 2 deletions api/js/etemplate/Et2Customfields/Et2Customfields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ export class Et2Customfields extends Et2CustomfieldsBase

.customfields {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
/* cap the label column: max-content lets a long label claim the whole
width and squeeze the value to nothing in narrow panes - beyond the
cap the label wraps instead */
grid-template-columns: fit-content(45%) minmax(0, 1fr);
gap: var(--sl-spacing-2x-small, 0.25rem) var(--sl-spacing-small, 0.75rem);
align-items: start;
}

.customfields__label {
padding-top: var(--sl-spacing-2x-small, 0.25rem);
min-width: 0;
overflow-wrap: break-word;
}

.customfields__field {
Expand All @@ -56,6 +61,80 @@ export class Et2Customfields extends Et2CustomfieldsBase
return this;
}

private _dirtySnapshot : string | null = null;

/**
* Collect '#name' => value pairs from the rendered field widgets.
*
* Only the legacy DEFAULT_ID instance ('custom_fields') answers with the collected
* object - matching et2_customfields_list.getValue() - so an id-less instance in an
* edit dialog returns null and etemplate2.getValues() leaves submission to the
* individual '#name' child widgets.
*/
getValue() : Record<string, any> | null
{
return this.id === "custom_fields" ? this._collectValues() : null;
}

set_value(value : Record<string, any> | null)
{
this.value = value || {};
}

isDirty() : boolean
{
// no snapshot taken yet means nothing to compare against, not "everything changed"
return this._dirtySnapshot !== null && this._dirtySnapshot !== JSON.stringify(this._collectValues());
}

resetDirty()
{
// set_visible()/set_value() only queue a Lit re-render: snapshot after it settles,
// or the snapshot describes the previous entry's fields and every row click looks
// like unsaved changes. null in the meantime means "not dirty" to isDirty().
this._dirtySnapshot = null;
const token = ++this._dirtySnapshotToken;
this._renderSettled().then(() =>
{
if(token === this._dirtySnapshotToken)
{
this._dirtySnapshot = JSON.stringify(this._collectValues());
}
});
}

private _dirtySnapshotToken = 0;

private async _renderSettled()
{
await this.updateComplete;
await Promise.all(Array.from(this.querySelectorAll("[data-field] > *"))
.map((widget : any) => widget.updateComplete)
.filter(Boolean));
}

isValid() : boolean
{
return true;
}

private _collectValues() : Record<string, any>
{
const result : Record<string, any> = {};
const widgetValue = (widget : any) => (typeof widget?.getValue === "function" ? widget.getValue() : widget?.value) ?? "";
for(const wrapper of Array.from(this.querySelectorAll("[data-field]")))
{
const fieldName = wrapper.getAttribute("data-field");
const widget = wrapper.querySelector(":scope > :not(label)") as any;
if(!fieldName || !widget)
{
continue;
}
result[CUSTOMFIELD_PREFIX + fieldName] = widgetValue(widget);
}
return result;
}

private _fieldValue(fieldName : string)
{
return this.value?.[CUSTOMFIELD_PREFIX + fieldName] ?? this.value?.[fieldName] ?? "";
Expand Down Expand Up @@ -91,13 +170,18 @@ export class Et2Customfields extends Et2CustomfieldsBase

et2-customfields .customfields {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
/* cap the label column: max-content lets a long label claim the whole
width and squeeze the value to nothing in narrow panes - beyond the
cap the label wraps instead */
grid-template-columns: fit-content(45%) minmax(0, 1fr);
gap: var(--sl-spacing-2x-small, 0.25rem) var(--sl-spacing-small, 0.75rem);
align-items: start;
}

et2-customfields .customfields__label {
padding-top: var(--sl-spacing-2x-small, 0.25rem);
min-width: 0;
overflow-wrap: break-word;
}

et2-customfields .customfields__field {
Expand Down
18 changes: 18 additions & 0 deletions api/js/etemplate/et2_extension_customfields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,24 @@ export class et2_customfields_list extends et2_valueWidget implements et2_IDetac

switch(this.options.customfields[field_name].type)
{
case 'float':
case 'int':
case 'number': // _setup_float / _setup_int rewrite field.type to "number" on row creation
// et2-number renders null via parseFloat as the literal "NaN". Leave a
// never-set field alone instead of turning "not yet set" into "set as
// empty", but a reused widget still showing the previous entry's number
// (preview / CRM-view row clicks) must be cleared
if(value === null)
{
const widget : any = this.widgets[field_name];
const current = typeof widget.getValue === "function" ? widget.getValue() : widget.value;
if(current === "" || current === null || typeof current === "undefined")
{
continue;
}
value = "";
}
break;
case 'date':
// Date custom fields are always in Y-m-d, which seldom matches user's preference
// which fails when sent to date widget. This is only used for nm rows, when possible
Expand Down
Loading