phoenix_duskmoon 9.16.13 → 9.16.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,10 @@
1
- export function initialState(conversationId, messages = []) {
1
+ export function initialState(conversationId, messages = [], application = {}) {
2
2
  return {
3
3
  conversationId,
4
4
  messages,
5
+ metadata: application.metadata || {},
6
+ runtime: application.runtime || {},
7
+ admission: application.admission || {},
5
8
  sequences: {},
6
9
  error: null,
7
10
  needsSync: false,
@@ -12,7 +15,7 @@ export function reduceChat(state, event) {
12
15
  if (event?.conversation_id !== state.conversationId) return state;
13
16
  if (["chat.snapshot", "chat.reset"].includes(event.type)) {
14
17
  return {
15
- ...initialState(state.conversationId, event.messages || []),
18
+ ...initialState(state.conversationId, event.messages || [], event),
16
19
  sequences: event.sequences || {},
17
20
  };
18
21
  }
@@ -79,6 +82,9 @@ export function reduceChat(state, event) {
79
82
  return {
80
83
  ...state,
81
84
  messages,
85
+ metadata: event.metadata ?? state.metadata,
86
+ runtime: event.runtime ?? state.runtime,
87
+ admission: event.admission ?? state.admission,
82
88
  sequences: { ...state.sequences, [event.message_id]: event.sequence },
83
89
  };
84
90
  }
@@ -10,30 +10,41 @@ const h = React.createElement;
10
10
  export function ReactField({ config, value, errors, disabled, onChange }) {
11
11
  const errorId = useId();
12
12
  const label = config.label || (Array.isArray(config.name) ? config.name.join(".") : config.name);
13
- const common = { disabled, "aria-label": label, "aria-invalid": errors?.length ? "true" : undefined, "aria-describedby": errors?.length ? errorId : undefined };
13
+ const common = { disabled: Boolean(disabled || config.disabled), "aria-label": label, "aria-invalid": errors?.length ? "true" : undefined, "aria-describedby": errors?.length ? errorId : undefined };
14
+ const textAttributes = { placeholder: config.placeholder ?? undefined, autoComplete: config.autocomplete ?? undefined, required: config.required, readOnly: config.readonly };
14
15
  let control;
15
16
 
16
17
  switch (config.type) {
17
18
  case "checkbox":
18
- control = h(Checkbox, { ...common, checked: Boolean(value), onChange: (event) => onChange(event.target.checked) });
19
+ control = h(Checkbox, { ...common, required: config.required, checked: Boolean(value), onChange: (event) => onChange(event.target.checked) });
19
20
  break;
20
21
  case "number":
21
- control = h(InputNumber, { ...common, value: value ?? null, onChange });
22
+ // TODO(upstream): duskmoon-dev/duskmoon-react#82
23
+ // WORKAROUND(upstream): duskmoon-dev/duskmoon-react#82 — readOnly does not guard upstream stepping.
24
+ control = h(InputNumber, { ...common, ...textAttributes, min: config.min ?? undefined, max: config.max ?? undefined, step: config.step ?? undefined,
25
+ controls: config.readonly ? false : undefined, keyboard: config.readonly ? false : undefined, value: value ?? null, onChange });
22
26
  break;
23
27
  case "select":
24
28
  case "multiselect":
25
- control = h(Select, { ...common, value: value ?? undefined, mode: config.type === "multiselect" ? "multiple" : undefined,
29
+ // TODO(upstream): duskmoon-dev/duskmoon-react#83
30
+ // WORKAROUND(upstream): duskmoon-dev/duskmoon-react#83 — only the mounted trigger can receive required ARIA in 0.5.0.
31
+ control = h(Select, { ...common, placeholder: config.placeholder ?? undefined, ref: (container) => {
32
+ const trigger = container?.querySelector('button[aria-haspopup="listbox"]');
33
+ if (config.required) trigger?.setAttribute("aria-required", "true");
34
+ else trigger?.removeAttribute("aria-required");
35
+ }, value: value ?? undefined, mode: config.type === "multiselect" ? "multiple" : undefined,
26
36
  options: config.options || [], showSearch: true, allowClear: true, onChange: (next) => onChange(next ?? null) });
27
37
  break;
28
38
  case "textarea":
29
- control = h("textarea", { ...common, className: "textarea", value: value ?? "", onChange: (event) => onChange(event.target.value) });
39
+ control = h("textarea", { ...common, ...textAttributes, rows: config.rows ?? undefined, className: "textarea", value: value ?? "", onChange: (event) => onChange(event.target.value) });
30
40
  break;
31
41
  default:
32
- control = h(Input, { ...common, type: config.type, value: value ?? "", onChange: (event) => onChange(event.target.value) });
42
+ control = h(Input, { ...common, ...textAttributes, type: config.type, value: value ?? "", onChange: (event) => onChange(event.target.value) });
33
43
  }
34
44
 
35
45
  return h(Form.Item, {
36
46
  label,
47
+ required: config.required,
37
48
  className: errors?.length ? "form-item-error" : undefined,
38
49
  help: errors?.length ? h(Form.ErrorList, { id: errorId, errors, role: "alert" }) : undefined,
39
50
  }, control);
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
3
3
  import { ReactField } from "./component.js";
4
4
  import { compileForm } from "@duskmoon-dev/components/json-schema-form";
5
5
  import { ReactSchemaForm, changedPath, schemaErrors } from "./schema.js";
6
- import { collectValues, fieldKey, getAt, pathOf, setAt } from "./values.js";
6
+ import { collectValues, fieldKey, getAt, nativeValue, pathOf, setAt, setNativeValue } from "./values.js";
7
7
  function parseJSON(value, fallback = {}) { try { return JSON.parse(value); } catch { return fallback; } }
8
8
  function flattenErrors(errors, prefix = [], result = {}) {
9
9
  Object.entries(errors || {}).forEach(([key, value]) => {
@@ -26,10 +26,10 @@ export const DuskmoonReactForm = {
26
26
  },
27
27
  mountForm() {
28
28
  this.form = this.el.querySelector("[data-dm-react-form]");
29
- const nativeValues = collectValues([...this.form.querySelectorAll("[name]")]);
29
+ this.fields = [...this.el.querySelectorAll("[data-dm-react-field]")];
30
+ const nativeValues = collectValues(this.nativeControls());
30
31
  this.values = { ...nativeValues, ...parseJSON(this.el.dataset.initialValues) };
31
32
  this.initialValues = structuredClone(this.values);
32
- this.fields = [...this.el.querySelectorAll("[data-dm-react-field]")];
33
33
  const fieldKeys = this.fields.map((element) => fieldKey(parseJSON(element.dataset.dmReactField).name));
34
34
  if (new Set(fieldKeys).size !== fieldKeys.length) throw new Error("React form field names must be unique");
35
35
  this.mountFields(); this.bindNative(); this.bindForm();
@@ -42,6 +42,7 @@ export const DuskmoonReactForm = {
42
42
  this.renderSchema();
43
43
  },
44
44
  renderSchema() {
45
+ // TODO(upstream): duskmoon-dev/duskmoon-react#85 - JsonSchemaForm has no native form class prop.
45
46
  this.schemaRoot.render(React.createElement(ReactSchemaForm, {
46
47
  key: this.schemaRevision, compiled: this.compiledSchema, value: this.values,
47
48
  defaultValue: this.initialValues, errors: this.errors, disabled: this.disconnectedState,
@@ -68,18 +69,23 @@ export const DuskmoonReactForm = {
68
69
  onChange: (value) => this.change(config.name, value),
69
70
  }));
70
71
  },
71
- bindNative() {
72
- this.native = [...this.form.querySelectorAll("[name]")].filter((element) => !element.closest("[data-dm-react-field]"));
73
- this.native.forEach((element) => {
74
- const handler = () => this.change(element.name, this.nativeValue(element));
75
- element.addEventListener("input", handler); element.addEventListener("change", handler); element.__dmReactHandler = handler;
72
+ nativeControls() {
73
+ const managedPaths = this.fields.map((element) => pathOf(parseJSON(element.dataset.dmReactField).name).map(String));
74
+ return [...this.form.querySelectorAll("input[name], select[name], textarea[name]")].filter((element) => {
75
+ if (element.closest("[data-dm-react-field]")) return false;
76
+ const path = pathOf(element.name).map(String);
77
+ return !managedPaths.some((managed) => managed.slice(0, Math.min(path.length, managed.length)).every((key, index) => key === path[index]));
76
78
  });
77
79
  },
80
+ bindNative() {
81
+ this.nativeHandler = (event) => {
82
+ const controls = this.nativeControls(); const element = event.target;
83
+ if (controls.includes(element)) this.change(element.name, nativeValue(element, controls));
84
+ };
85
+ this.form.addEventListener("input", this.nativeHandler); this.form.addEventListener("change", this.nativeHandler);
86
+ },
78
87
  nativeValue(element) {
79
- if (element.type === "checkbox") return element.checked;
80
- if (element.type === "number") return element.value === "" ? null : Number(element.value);
81
- if (element.multiple) return [...element.selectedOptions].map((option) => option.value);
82
- return element.value;
88
+ return nativeValue(element, this.nativeControls());
83
89
  },
84
90
  bindForm() {
85
91
  this.submitHandler = (event) => { event.preventDefault(); this.submit(); };
@@ -94,7 +100,15 @@ export const DuskmoonReactForm = {
94
100
  const event = this.el.getAttribute("phx-change"); if (!event || this.disconnectedState) return;
95
101
  clearTimeout(this.changeTimer); this.changeTimer = setTimeout(() => this.send(event, name), Number(this.el.getAttribute("phx-debounce") || 0));
96
102
  },
97
- submit() { clearTimeout(this.changeTimer); const event = this.el.getAttribute("phx-submit"); if (event && !this.disconnectedState) this.send(event); },
103
+ submit() {
104
+ clearTimeout(this.changeTimer); const event = this.el.getAttribute("phx-submit"); if (!event || this.disconnectedState) return;
105
+ if (this.form) {
106
+ if (this.form.noValidate === false && !this.form.reportValidity()) return;
107
+ const controls = this.nativeControls();
108
+ controls.forEach((element) => { this.values = setAt(this.values, element.name, nativeValue(element, controls)); });
109
+ }
110
+ this.send(event);
111
+ },
98
112
  send(event, changed) {
99
113
  const revision = ++this.revision; const values = this.values;
100
114
  const payload = { id: this.el.id, values, revision, ...(changed ? { changed: pathOf(changed) } : {}) };
@@ -109,9 +123,9 @@ export const DuskmoonReactForm = {
109
123
  clearTimeout(this.changeTimer);
110
124
  this.values = structuredClone(values); this.errors = {}; this.revision++;
111
125
  if (this.schemaRoot) { this.schemaRevision++; this.renderAll(); return; }
112
- this.native.forEach((element) => { if (element.type === "checkbox") element.checked = Boolean(getAt(this.values, element.name)); else element.value = getAt(this.values, element.name) ?? ""; }); this.renderAll();
126
+ this.nativeControls().forEach((element) => setNativeValue(element, getAt(this.values, element.name))); this.renderAll();
113
127
  },
114
128
  disconnected() { clearTimeout(this.changeTimer); this.disconnectedState = true; this.renderAll(); },
115
129
  reconnected() { this.disconnectedState = false; this.renderAll(); },
116
- destroyed() { this.destroyedState = true; clearTimeout(this.changeTimer); this.schemaRoot?.unmount(); this.form?.removeEventListener("submit", this.submitHandler); this.form?.removeEventListener("reset", this.resetHandler); this.native?.forEach((element) => { element.removeEventListener("input", element.__dmReactHandler); element.removeEventListener("change", element.__dmReactHandler); }); this.roots.forEach(({ root }) => root.unmount()); },
130
+ destroyed() { this.destroyedState = true; clearTimeout(this.changeTimer); this.schemaRoot?.unmount(); this.form?.removeEventListener("submit", this.submitHandler); this.form?.removeEventListener("reset", this.resetHandler); this.form?.removeEventListener("input", this.nativeHandler); this.form?.removeEventListener("change", this.nativeHandler); this.roots.forEach(({ root }) => root.unmount()); },
117
131
  };
@@ -17,13 +17,27 @@ export function setAt(values, name, value) {
17
17
  return result;
18
18
  }
19
19
 
20
+ function isCheckboxGroup(element) {
21
+ return element.type === "checkbox" && element.name.endsWith("[]");
22
+ }
23
+
24
+ export function nativeValue(element, elements = [element]) {
25
+ if (isCheckboxGroup(element)) {
26
+ return elements.filter((control) => control.type === "checkbox" && control.name === element.name && control.checked)
27
+ .map((control) => control.value);
28
+ }
29
+ if (element.type === "checkbox") return Boolean(element.checked);
30
+ if (element.type === "number") return element.value === "" ? null : Number(element.value);
31
+ if (element.multiple) return [...element.selectedOptions].map((option) => option.value);
32
+ return element.value;
33
+ }
34
+
35
+ export function setNativeValue(element, value) {
36
+ if (element.type === "checkbox") {
37
+ element.checked = isCheckboxGroup(element) ? Array.isArray(value) && value.includes(element.value) : Boolean(value);
38
+ } else element.value = value ?? "";
39
+ }
40
+
20
41
  export function collectValues(elements) {
21
- return elements.reduce((values, element) => {
22
- let value;
23
- if (element.type === "checkbox") value = Boolean(element.checked);
24
- else if (element.type === "number") value = element.value === "" ? null : Number(element.value);
25
- else if (element.multiple) value = [...element.selectedOptions].map((option) => option.value);
26
- else value = element.value;
27
- return setAt(values, element.name, value);
28
- }, {});
42
+ return elements.reduce((values, element) => setAt(values, element.name, nativeValue(element, elements)), {});
29
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_duskmoon",
3
- "version": "9.16.13",
3
+ "version": "9.16.15",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./priv/static/assets/js/phoenix_duskmoon.js",
@@ -37,18 +37,18 @@
37
37
  "registry": "https://registry.npmjs.org/"
38
38
  },
39
39
  "dependencies": {
40
- "@duskmoon-dev/art-elements": "1.9.0",
41
- "@duskmoon-dev/components": "0.4.1",
42
- "@duskmoon-dev/core": "1.20.3",
43
- "@duskmoon-dev/css-art": "1.20.3",
44
- "@duskmoon-dev/elements": "1.9.0",
40
+ "@duskmoon-dev/art-elements": "1.9.1",
41
+ "@duskmoon-dev/components": "0.5.0",
42
+ "@duskmoon-dev/core": "1.20.4",
43
+ "@duskmoon-dev/css-art": "1.20.4",
44
+ "@duskmoon-dev/elements": "1.9.1",
45
45
  "react": "19.3.0",
46
46
  "react-dom": "19.3.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@mdi/svg": "7.4.47",
50
- "bootstrap-icons": "1.13.1",
51
- "postcss": "8.5.28"
50
+ "bootstrap-icons": "1.13.2",
51
+ "postcss": "8.5.29"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "tailwindcss": "^4.3.2"