@stonecrop/desktop 0.13.14 → 0.15.0

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.
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@stonecrop/desktop",
3
- "version": "0.13.14",
4
- "license": "MIT",
5
- "type": "module",
6
- "author": {
7
- "name": "Tyler Matteson",
8
- "email": "tyler@agritheory.com"
3
+ "version": "0.15.0",
4
+ "description": "Desktop-specific components for Stonecrop UI",
5
+ "bugs": {
6
+ "url": "https://github.com/agritheory/stonecrop/issues"
9
7
  },
10
8
  "repository": {
11
9
  "type": "git",
12
10
  "url": "https://github.com/agritheory/stonecrop",
13
11
  "directory": "desktop"
14
12
  },
15
- "bugs": {
16
- "url": "https://github.com/agritheory/stonecrop/issues"
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Tyler Matteson",
16
+ "email": "tyler@agritheory.com"
17
17
  },
18
- "types": "./dist/desktop.d.ts",
18
+ "sideEffects": [
19
+ "**/*.css"
20
+ ],
21
+ "type": "module",
19
22
  "exports": {
20
23
  ".": {
21
24
  "types": "./dist/desktop.d.ts",
@@ -24,22 +27,17 @@
24
27
  "./styles": "./dist/desktop.css",
25
28
  "./types": "./dist/src/types/index.d.ts"
26
29
  },
30
+ "types": "./dist/desktop.d.ts",
27
31
  "files": [
28
32
  "dist/*",
29
33
  "src/*"
30
34
  ],
31
- "sideEffects": [
32
- "**/*.css"
33
- ],
34
35
  "dependencies": {
35
- "@stonecrop/aform": "0.13.14",
36
- "@stonecrop/schema": "0.13.14",
37
- "@stonecrop/atable": "0.13.14",
38
- "@stonecrop/stonecrop": "0.13.14",
39
- "@stonecrop/themes": "0.13.14"
40
- },
41
- "peerDependencies": {
42
- "vue": "^3.5.33"
36
+ "@stonecrop/aform": "0.15.0",
37
+ "@stonecrop/atable": "0.15.0",
38
+ "@stonecrop/stonecrop": "0.15.0",
39
+ "@stonecrop/schema": "0.15.0",
40
+ "@stonecrop/themes": "0.15.0"
43
41
  },
44
42
  "devDependencies": {
45
43
  "@microsoft/api-documenter": "^7.30.5",
@@ -59,15 +57,19 @@
59
57
  "vitest": "^4.1.5",
60
58
  "vue": "^3.5.33",
61
59
  "vue-router": "^5.0.6",
60
+ "vue-tsc": "^3.3.6",
62
61
  "stonecrop-rig": "0.7.0"
63
62
  },
64
- "description": "Desktop-specific components for Stonecrop UI",
65
- "publishConfig": {
66
- "access": "public"
63
+ "peerDependencies": {
64
+ "pinia": "^3.0.4",
65
+ "vue": "^3.5.33"
67
66
  },
68
67
  "engines": {
69
68
  "node": ">=24.0.0"
70
69
  },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
71
73
  "scripts": {
72
74
  "_phase:build": "heft build && vite build && rushx docs",
73
75
  "build": "heft build && vite build && rushx docs",
@@ -75,7 +77,8 @@
75
77
  "lint": "oxlint",
76
78
  "lint:fix": "oxlint --fix",
77
79
  "test": "vitest run --coverage.enabled false",
78
- "test:watch": "vitest watch",
79
- "test:coverage": "vitest run --coverage"
80
+ "test:coverage": "vitest run --coverage",
81
+ "test:types": "vue-tsc --noEmit",
82
+ "test:watch": "vitest watch"
80
83
  }
81
84
  }
@@ -52,7 +52,7 @@ const emit = defineEmits<{
52
52
  const dropdownStates = ref<Record<number, boolean>>({})
53
53
 
54
54
  const isOpen = ref(true)
55
- const dropdownOpen = ref([])
55
+ const dropdownOpen = ref<boolean[]>([])
56
56
 
57
57
  onMounted(() => {
58
58
  closeDropdowns()
@@ -4,7 +4,11 @@
4
4
  <ActionSet :elements="actionElements" @action-click="handleActionClick" />
5
5
 
6
6
  <!-- Main content using AForm -->
7
- <AForm v-if="currentViewSchema.length > 0" v-model:data="currentViewData" :schema="currentViewSchema" />
7
+ <AForm
8
+ v-if="currentViewSchema.length > 0"
9
+ v-model:data="currentViewData"
10
+ :schema="currentViewSchema"
11
+ :errors="fieldErrors" />
8
12
  <div v-else-if="!stonecrop" class="loading"><p>Initializing Stonecrop...</p></div>
9
13
  <div v-else class="loading">
10
14
  <p>Loading {{ currentView }} data...</p>
@@ -31,7 +35,7 @@
31
35
  </template>
32
36
 
33
37
  <script setup lang="ts">
34
- import { useStonecrop } from '@stonecrop/stonecrop'
38
+ import { useStonecrop, useValidationStore } from '@stonecrop/stonecrop'
35
39
  import { AForm, type AFormLinkNavigator, type ResolvedField, type ResolvedTable } from '@stonecrop/aform'
36
40
  import type { ColumnSchema } from '@stonecrop/schema'
37
41
  import { computed, onMounted, onUnmounted, provide, ref, unref, watch } from 'vue'
@@ -105,6 +109,24 @@ const emit = defineEmits<{
105
109
 
106
110
  const { stonecrop } = useStonecrop()
107
111
 
112
+ // Field-validation store (advisory, client-side). Pinia is a declared peerDependency, kept external
113
+ // in this package's build (rollupOptions), so `useValidationStore()` resolves the host app's single
114
+ // active Pinia directly — the old `getCurrentInstance().$pinia` workaround for the bundled-Pinia bug
115
+ // is no longer needed. Still guarded: validation is optional and Desktop predates it, so a host that
116
+ // mounts Desktop without Pinia disables validation gracefully instead of crashing on mount.
117
+ let validationStore: ReturnType<typeof useValidationStore> | null = null
118
+ try {
119
+ validationStore = useValidationStore()
120
+ } catch {
121
+ validationStore = null
122
+ }
123
+
124
+ // Inline field errors handed to AForm. Only surfaced in the record form view (currentView is
125
+ // defined below); empty elsewhere so a previous record's errors never bleed into a list view.
126
+ const fieldErrors = computed<Record<string, string[]>>(() =>
127
+ currentView.value === 'record' ? (validationStore?.errorsByField ?? {}) : {}
128
+ )
129
+
108
130
  // State
109
131
  const loading = ref(false)
110
132
  const commandPaletteOpen = ref(false)
@@ -214,20 +236,46 @@ const currentViewData = computed<Record<string, any>>({
214
236
  // schema fields absent from the record as undefined; writing them would silently
215
237
  // clear values that exist in HST. Explicit null is allowed (intentional clear).
216
238
  const hstStore = stonecrop.value.getStore()
239
+ const changedFields: string[] = []
217
240
  for (const [fieldname, value] of Object.entries(flatData)) {
218
241
  if (value === undefined) continue
219
242
  const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${fieldname}`
220
243
  const currentValue = hstStore.has(fieldPath) ? hstStore.get(fieldPath) : undefined
221
244
  if (currentValue !== value) {
222
245
  hstStore.set(fieldPath, value)
246
+ changedFields.push(fieldname)
223
247
  }
224
248
  }
249
+
250
+ // Advisory field-validation runs on the fields that actually changed (honors each
251
+ // trigger's `on` set). Driven here, after the HST writes, so HST stays value-only.
252
+ if (changedFields.length > 0) {
253
+ driveFieldValidation(changedFields)
254
+ }
225
255
  } catch (error) {
226
256
  console.warn('HST update failed:', error)
227
257
  }
228
258
  },
229
259
  })
230
260
 
261
+ // Advisory field-validation: run the doctype's triggers for the fields that changed on this edit.
262
+ // Snapshots the post-edit record as a plain, flat object so validators read siblings read-only
263
+ // (the core store freezes it). Fire-and-forget — the reactive error store repaints AForm on resolve.
264
+ function driveFieldValidation(changedFields: string[]) {
265
+ if (!validationStore || !stonecrop.value || !currentDoctype.value || !currentRecordId.value) return
266
+
267
+ const doctype = stonecrop.value.registry.getDoctype(currentDoctype.value)
268
+ const triggers = doctype?.getTriggers()
269
+ if (!triggers || Object.keys(triggers).length === 0) return
270
+
271
+ const node = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
272
+ const record = { ...(node?.get('') as Record<string, unknown>) }
273
+
274
+ for (const field of changedFields) {
275
+ void validationStore.validateField(triggers, field, record)
276
+ }
277
+ }
278
+
231
279
  // Computed properties for current route context.
232
280
  // When a routeAdapter is provided it takes full precedence over the registry's internal router.
233
281
  const route = computed(() => (routeAdapter ? null : unref(stonecrop.value?.registry.router?.currentRoute)))
@@ -353,8 +401,10 @@ const getAvailableTransitions = () => {
353
401
 
354
402
  // Each transition emits an 'action' event. The host app decides what to do
355
403
  // (call the server, trigger an FSM actor, update HST, etc.).
356
- return transitions.map(({ name, targetState }) => ({
357
- label: `${name} (→ ${targetState})`,
404
+ return transitions.map(({ name }) => ({
405
+ // Prefer the workflow action's human-readable label (WorkflowMeta format);
406
+ // fall back to the raw transition name for XState workflows with no action meta.
407
+ label: doctype.getActionMeta(name)?.label ?? name,
358
408
  action: () => {
359
409
  emit('action', {
360
410
  name,
@@ -370,6 +420,39 @@ const getAvailableTransitions = () => {
370
420
  }
371
421
  }
372
422
 
423
+ // Helper: stateless Commands available for the current record — side-effect actions that
424
+ // change no workflow state. Surfaced in the same Actions dropdown as transitions; each emits
425
+ // the same 'action' event, so the host's handler runs a Command's clientHandler identically.
426
+ const getAvailableCommands = () => {
427
+ if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
428
+ return []
429
+ }
430
+
431
+ try {
432
+ const doctype = stonecrop.value.registry.getDoctype(currentDoctype.value)
433
+ if (!doctype?.workflow) return []
434
+
435
+ const currentState = stonecrop.value.getRecordState(currentDoctype.value, currentRecordId.value)
436
+ const commands = doctype.getAvailableCommands(currentState)
437
+ const recordData = currentViewData.value || {}
438
+
439
+ return commands.map(({ name }) => ({
440
+ label: doctype.getActionMeta(name)?.label ?? name,
441
+ action: () => {
442
+ emit('action', {
443
+ name,
444
+ doctype: currentDoctype.value,
445
+ recordId: currentRecordId.value,
446
+ data: recordData,
447
+ })
448
+ },
449
+ }))
450
+ } catch (error) {
451
+ console.warn('Error getting available commands:', error)
452
+ return []
453
+ }
454
+ }
455
+
373
456
  const actionElements = computed(() => {
374
457
  const elements: ActionElements[] = []
375
458
 
@@ -382,14 +465,14 @@ const actionElements = computed(() => {
382
465
  })
383
466
  break
384
467
  case 'record': {
385
- // Populate the Actions dropdown with every FSM transition available in the
386
- // record's current state. Clicking a transition emits 'action'.
387
- const transitionActions = getAvailableTransitions()
388
- if (transitionActions.length > 0) {
468
+ // Populate the Actions dropdown with every FSM transition AND stateless Command
469
+ // available in the record's current state. Clicking either emits 'action'.
470
+ const recordActions = [...getAvailableTransitions(), ...getAvailableCommands()]
471
+ if (recordActions.length > 0) {
389
472
  elements.push({
390
473
  type: 'dropdown',
391
474
  label: 'Actions',
392
- actions: transitionActions,
475
+ actions: recordActions,
393
476
  })
394
477
  }
395
478
  break
@@ -556,7 +639,7 @@ const getDoctypesSchema = (): ResolvedField[] => {
556
639
  {
557
640
  fieldname: 'doctype',
558
641
  label: 'Doctype',
559
- fieldtype: 'Data',
642
+ component: 'ATextInput',
560
643
  align: 'left' as const,
561
644
  edit: false,
562
645
  width: '20ch',
@@ -564,7 +647,7 @@ const getDoctypesSchema = (): ResolvedField[] => {
564
647
  {
565
648
  fieldname: 'display_name',
566
649
  label: 'Name',
567
- fieldtype: 'Data',
650
+ component: 'ATextInput',
568
651
  align: 'left' as const,
569
652
  edit: false,
570
653
  width: '30ch',
@@ -572,7 +655,7 @@ const getDoctypesSchema = (): ResolvedField[] => {
572
655
  {
573
656
  fieldname: 'record_count',
574
657
  label: 'Records',
575
- fieldtype: 'Int',
658
+ component: 'ANumericInput',
576
659
  align: 'center' as const,
577
660
  edit: false,
578
661
  width: '15ch',
@@ -580,7 +663,7 @@ const getDoctypesSchema = (): ResolvedField[] => {
580
663
  {
581
664
  fieldname: 'actions',
582
665
  label: 'Actions',
583
- fieldtype: 'Data',
666
+ component: 'ATextInput',
584
667
  align: 'center' as const,
585
668
  edit: false,
586
669
  width: '20ch',
@@ -612,7 +695,7 @@ const getRecordsSchema = (): ResolvedField[] => {
612
695
  component: 'ATable',
613
696
  schema: [
614
697
  ...(flattenFieldsets(schema) as ColumnSchema[]),
615
- { fieldname: 'actions', label: 'Actions', fieldtype: 'Data' },
698
+ { fieldname: 'actions', label: 'Actions', component: 'ATextInput' },
616
699
  ],
617
700
  config: { view: 'list' as const, fullWidth: true },
618
701
  } satisfies ResolvedTable,
@@ -787,6 +870,12 @@ watch(
787
870
  { immediate: true }
788
871
  )
789
872
 
873
+ // Clear advisory validation errors when the target record changes, so errors from a previously
874
+ // edited record never bleed into a newly opened one.
875
+ watch([currentDoctype, currentRecordId], () => {
876
+ validationStore?.clearAll()
877
+ })
878
+
790
879
  // Stonecrop reactive computed properties update automatically when the instance
791
880
  // becomes available — no manual watcher needed.
792
881
 
@@ -1,137 +0,0 @@
1
- import { L as o, I as c } from "./index-C3nXTrEd.js";
2
- import { reactive as g } from "vue";
3
- const l = g([]);
4
- let f = 0;
5
- function a(i, e = "info") {
6
- const t = {
7
- id: `notif-${++f}`,
8
- message: i,
9
- type: e,
10
- timestamp: /* @__PURE__ */ new Date()
11
- };
12
- l.push(t);
13
- const n = ["warning", "error"].includes(e) ? 8e3 : 5e3;
14
- setTimeout(() => {
15
- const r = l.findIndex((d) => d.id === t.id);
16
- r > -1 && l.splice(r, 1);
17
- }, n);
18
- }
19
- function p(i) {
20
- const e = l.findIndex((t) => t.id === i);
21
- e > -1 && l.splice(e, 1);
22
- }
23
- o("validateName", (i) => {
24
- const { fieldname: e, afterValue: t } = i;
25
- if (!t || typeof t != "string") {
26
- a(`${e} cannot be empty`, "warning");
27
- return;
28
- }
29
- t.length < 2 ? a(`${e} must be at least 2 characters long`, "warning") : t.length > 50 ? a(`${e} is too long (max 50 characters)`, "warning") : a(`${e} validated successfully`, "success");
30
- });
31
- o("updateFullName", (i) => {
32
- const { path: e, afterValue: t } = i;
33
- e.split("."), a(`Full name updated due to ${i.fieldname} change to "${t}"`, "info");
34
- });
35
- o("validatePhoneFormat", (i) => {
36
- const { afterValue: e } = i;
37
- if (!e) return;
38
- /^\+?[\d\s\-\(\)]+$/.test(e) ? e.length < 10 ? a("Phone number seems too short", "warning") : a("Phone number format is valid", "success") : a("Phone number format is invalid", "error");
39
- });
40
- o("notifyPhoneChange", (i) => {
41
- const { beforeValue: e, afterValue: t } = i;
42
- e !== t && a(`Phone number changed from "${e || "empty"}" to "${t || "empty"}"`, "info");
43
- });
44
- o("validateEmailFormat", (i) => {
45
- const { afterValue: e } = i;
46
- if (!e) return;
47
- /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e) ? a("Email format is valid", "success") : a("Email format is invalid", "error");
48
- });
49
- o("validateNotes", (i) => {
50
- const { afterValue: e } = i;
51
- e && typeof e == "string" && (e.length > 500 ? a("Notes are quite long. Consider keeping them concise.", "warning") : e.length > 50 && a("Good notes provided", "success"));
52
- });
53
- o("validateSubject", (i) => {
54
- const { afterValue: e } = i;
55
- if (!e || typeof e != "string") {
56
- a("Issue subject is required", "error");
57
- return;
58
- }
59
- e.length < 5 ? a("Issue subject should be more descriptive (min 5 characters)", "warning") : e.length > 100 ? a("Issue subject is too long (max 100 characters)", "warning") : a("Issue subject looks good", "success");
60
- });
61
- o("onStatusChange", (i) => {
62
- const { beforeValue: e, afterValue: t } = i;
63
- e !== t && (a(`Issue status changed from "${e || "none"}" to "${t}"`, "info"), t === "Resolved" ? a("🎉 Issue marked as resolved! Consider adding resolution notes.", "success") : t === "Closed" ? a("📁 Issue closed. Thank you for your work!", "info") : t === "In Progress" && a("⚡ Issue is now in progress. Good luck!", "info"));
64
- });
65
- o("onPriorityChange", (i) => {
66
- const { beforeValue: e, afterValue: t } = i;
67
- e !== t && (a(`Priority changed from "${e || "none"}" to "${t}"`, "info"), t === "Critical" ? a("🚨 CRITICAL priority set! This needs immediate attention.", "warning") : t === "High" && a("⚠️ High priority issue - please address soon.", "warning"));
68
- });
69
- o("validatePriorityStatus", (i) => {
70
- const { afterValue: e } = i;
71
- e === "Critical" && a("Critical priority issues should have immediate attention", "info");
72
- });
73
- o("validateDescription", (i) => {
74
- const { afterValue: e } = i;
75
- e && typeof e == "string" && (e.length > 1e3 ? a("Description is quite long. Consider breaking it into smaller sections.", "info") : e.length > 50 && a("Good detailed description provided", "success"));
76
- });
77
- o("validateFutureDate", (i) => {
78
- const { afterValue: e } = i;
79
- if (e) {
80
- const t = new Date(e), n = /* @__PURE__ */ new Date();
81
- n.setHours(0, 0, 0, 0), t < n ? a("⚠️ Selected date is in the past", "warning") : t.getTime() === n.getTime() ? a("📅 Due date is today", "info") : a("📅 Future date selected", "success");
82
- }
83
- });
84
- o("updateTimestamp", (i) => {
85
- a(`Record timestamp updated due to ${i.fieldname} change`, "info");
86
- });
87
- o("logFieldChange", (i) => {
88
- const { path: e, fieldname: t, beforeValue: n, afterValue: r, operation: d, timestamp: u } = i;
89
- console.log("🔄 Field Change Log:", {
90
- path: e,
91
- field: t,
92
- from: n,
93
- to: r,
94
- operation: d,
95
- timestamp: u.toISOString()
96
- }), a(`Field "${t}" changed`, "info");
97
- });
98
- const s = c();
99
- s && (s.registerDoctypeActions(
100
- "todo-form",
101
- /* @__PURE__ */ new Map([
102
- ["first_name", ["validateName", "updateFullName", "logFieldChange"]],
103
- ["last_name", ["validateName", "updateFullName", "logFieldChange"]],
104
- ["phone", ["validatePhoneFormat", "notifyPhoneChange", "logFieldChange"]],
105
- ["email", ["validateEmailFormat", "logFieldChange"]],
106
- ["notes", ["validateNotes", "logFieldChange"]]
107
- ])
108
- ), s.registerDoctypeActions(
109
- "todo-list",
110
- /* @__PURE__ */ new Map([
111
- ["first_name", ["validateName", "updateFullName", "logFieldChange"]],
112
- ["last_name", ["validateName", "updateFullName", "logFieldChange"]],
113
- ["phone", ["validatePhoneFormat", "notifyPhoneChange", "logFieldChange"]]
114
- ])
115
- ), s.registerDoctypeActions(
116
- "issue-form",
117
- /* @__PURE__ */ new Map([
118
- ["subject", ["validateSubject", "logFieldChange"]],
119
- ["status", ["onStatusChange", "validatePriorityStatus", "updateTimestamp", "logFieldChange"]],
120
- ["priority", ["onPriorityChange", "validatePriorityStatus", "logFieldChange"]],
121
- ["description", ["validateDescription", "logFieldChange"]],
122
- ["due_date", ["validateFutureDate", "updateTimestamp", "logFieldChange"]]
123
- ])
124
- ), s.registerDoctypeActions(
125
- "issue-list",
126
- /* @__PURE__ */ new Map([
127
- ["subject", ["validateSubject", "logFieldChange"]],
128
- ["status", ["onStatusChange", "logFieldChange"]],
129
- ["priority", ["onPriorityChange", "logFieldChange"]]
130
- ])
131
- ));
132
- export {
133
- a as addNotification,
134
- l as notifications,
135
- p as removeNotification
136
- };
137
- //# sourceMappingURL=actions-Bg0Mh5s7.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"actions-Bg0Mh5s7.js","sources":["../../examples/desktop/actions.ts"],"sourcesContent":["/**\n * Field trigger action functions for the Desktop example\n * This demonstrates the field trigger system in action\n */\nimport { registerGlobalAction, getGlobalTriggerEngine, type FieldChangeContext } from '@stonecrop/stonecrop'\nimport { reactive } from 'vue'\n\n// ============= DEBUGGING AND NOTIFICATIONS =============\n\n// Notification interface\ninterface Notification {\n\tid: string\n\tmessage: string\n\ttype: 'info' | 'success' | 'warning' | 'error'\n\ttimestamp: Date\n}\n\n// Reactive notifications array for UI\nexport const notifications = reactive<Notification[]>([])\n\nlet notificationId = 0\n\nexport function addNotification(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {\n\tconst notification = {\n\t\tid: `notif-${++notificationId}`,\n\t\tmessage,\n\t\ttype,\n\t\ttimestamp: new Date(),\n\t}\n\tnotifications.push(notification)\n\n\t// Auto-remove after 5 seconds for info/success, 8 seconds for warnings/errors\n\tconst timeout = ['warning', 'error'].includes(type) ? 8000 : 5000\n\tsetTimeout(() => {\n\t\tconst index = notifications.findIndex(n => n.id === notification.id)\n\t\tif (index > -1) {\n\t\t\tnotifications.splice(index, 1)\n\t\t}\n\t}, timeout)\n}\n\nexport function removeNotification(id: string) {\n\tconst index = notifications.findIndex(n => n.id === id)\n\tif (index > -1) {\n\t\tnotifications.splice(index, 1)\n\t}\n}\n\n// ============= NAME VALIDATION ACTIONS =============\n\nregisterGlobalAction('validateName', (context: FieldChangeContext) => {\n\tconst { fieldname, afterValue } = context\n\n\tif (!afterValue || typeof afterValue !== 'string') {\n\t\taddNotification(`${fieldname} cannot be empty`, 'warning')\n\t\treturn\n\t}\n\n\tif (afterValue.length < 2) {\n\t\taddNotification(`${fieldname} must be at least 2 characters long`, 'warning')\n\t} else if (afterValue.length > 50) {\n\t\taddNotification(`${fieldname} is too long (max 50 characters)`, 'warning')\n\t} else {\n\t\taddNotification(`${fieldname} validated successfully`, 'success')\n\t}\n})\n\nregisterGlobalAction('updateFullName', (context: FieldChangeContext) => {\n\tconst { path, afterValue } = context\n\tconst pathParts = path.split('.')\n\n\t// This would typically update a computed full_name field\n\t// For demo purposes, we'll just show a notification\n\taddNotification(`Full name updated due to ${context.fieldname} change to \"${afterValue}\"`, 'info')\n})\n\n// ============= PHONE VALIDATION ACTIONS =============\n\nregisterGlobalAction('validatePhoneFormat', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (!afterValue) return\n\n\t// Simple phone validation - you could use a more sophisticated library\n\tconst phoneRegex = /^\\+?[\\d\\s\\-\\(\\)]+$/\n\n\tif (!phoneRegex.test(afterValue)) {\n\t\taddNotification('Phone number format is invalid', 'error')\n\t} else if (afterValue.length < 10) {\n\t\taddNotification('Phone number seems too short', 'warning')\n\t} else {\n\t\taddNotification('Phone number format is valid', 'success')\n\t}\n})\n\nregisterGlobalAction('notifyPhoneChange', (context: FieldChangeContext) => {\n\tconst { beforeValue, afterValue } = context\n\n\tif (beforeValue !== afterValue) {\n\t\taddNotification(`Phone number changed from \"${beforeValue || 'empty'}\" to \"${afterValue || 'empty'}\"`, 'info')\n\t}\n})\n\n// ============= EMAIL VALIDATION ACTIONS =============\n\nregisterGlobalAction('validateEmailFormat', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (!afterValue) return\n\n\t// Simple email validation\n\tconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n\tif (!emailRegex.test(afterValue)) {\n\t\taddNotification('Email format is invalid', 'error')\n\t} else {\n\t\taddNotification('Email format is valid', 'success')\n\t}\n})\n\n// ============= NOTES VALIDATION ACTIONS =============\n\nregisterGlobalAction('validateNotes', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (afterValue && typeof afterValue === 'string') {\n\t\tif (afterValue.length > 500) {\n\t\t\taddNotification('Notes are quite long. Consider keeping them concise.', 'warning')\n\t\t} else if (afterValue.length > 50) {\n\t\t\taddNotification('Good notes provided', 'success')\n\t\t}\n\t}\n})\n\n// ============= ISSUE-SPECIFIC ACTIONS =============\n\nregisterGlobalAction('validateSubject', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (!afterValue || typeof afterValue !== 'string') {\n\t\taddNotification('Issue subject is required', 'error')\n\t\treturn\n\t}\n\n\tif (afterValue.length < 5) {\n\t\taddNotification('Issue subject should be more descriptive (min 5 characters)', 'warning')\n\t} else if (afterValue.length > 100) {\n\t\taddNotification('Issue subject is too long (max 100 characters)', 'warning')\n\t} else {\n\t\taddNotification('Issue subject looks good', 'success')\n\t}\n})\n\nregisterGlobalAction('onStatusChange', (context: FieldChangeContext) => {\n\tconst { beforeValue, afterValue } = context\n\n\tif (beforeValue !== afterValue) {\n\t\taddNotification(`Issue status changed from \"${beforeValue || 'none'}\" to \"${afterValue}\"`, 'info')\n\n\t\t// Demo: Automatic actions based on status\n\t\tif (afterValue === 'Resolved') {\n\t\t\taddNotification('🎉 Issue marked as resolved! Consider adding resolution notes.', 'success')\n\t\t} else if (afterValue === 'Closed') {\n\t\t\taddNotification('📁 Issue closed. Thank you for your work!', 'info')\n\t\t} else if (afterValue === 'In Progress') {\n\t\t\taddNotification('⚡ Issue is now in progress. Good luck!', 'info')\n\t\t}\n\t}\n})\n\nregisterGlobalAction('onPriorityChange', (context: FieldChangeContext) => {\n\tconst { beforeValue, afterValue } = context\n\n\tif (beforeValue !== afterValue) {\n\t\taddNotification(`Priority changed from \"${beforeValue || 'none'}\" to \"${afterValue}\"`, 'info')\n\n\t\tif (afterValue === 'Critical') {\n\t\t\taddNotification('🚨 CRITICAL priority set! This needs immediate attention.', 'warning')\n\t\t} else if (afterValue === 'High') {\n\t\t\taddNotification('⚠️ High priority issue - please address soon.', 'warning')\n\t\t}\n\t}\n})\n\nregisterGlobalAction('validatePriorityStatus', (context: FieldChangeContext) => {\n\t// This would typically check other fields in the record\n\t// For demo, we'll just validate the priority itself\n\tconst { afterValue } = context\n\n\tif (afterValue === 'Critical') {\n\t\t// In a real app, you might check if status is appropriate for critical priority\n\t\taddNotification('Critical priority issues should have immediate attention', 'info')\n\t}\n})\n\nregisterGlobalAction('validateDescription', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (afterValue && typeof afterValue === 'string') {\n\t\tif (afterValue.length > 1000) {\n\t\t\taddNotification('Description is quite long. Consider breaking it into smaller sections.', 'info')\n\t\t} else if (afterValue.length > 50) {\n\t\t\taddNotification('Good detailed description provided', 'success')\n\t\t}\n\t}\n})\n\nregisterGlobalAction('validateFutureDate', (context: FieldChangeContext) => {\n\tconst { afterValue } = context\n\n\tif (afterValue) {\n\t\tconst selectedDate = new Date(afterValue)\n\t\tconst today = new Date()\n\t\ttoday.setHours(0, 0, 0, 0) // Reset time for date comparison\n\n\t\tif (selectedDate < today) {\n\t\t\taddNotification('⚠️ Selected date is in the past', 'warning')\n\t\t} else if (selectedDate.getTime() === today.getTime()) {\n\t\t\taddNotification('📅 Due date is today', 'info')\n\t\t} else {\n\t\t\taddNotification('📅 Future date selected', 'success')\n\t\t}\n\t}\n})\n\n// ============= GENERIC ACTIONS =============\n\nregisterGlobalAction('updateTimestamp', (context: FieldChangeContext) => {\n\t// This would typically update a \"modified\" or \"last_updated\" field\n\taddNotification(`Record timestamp updated due to ${context.fieldname} change`, 'info')\n})\n\nregisterGlobalAction('logFieldChange', (context: FieldChangeContext) => {\n\tconst { path, fieldname, beforeValue, afterValue, operation, timestamp } = context\n\n\tconsole.log('🔄 Field Change Log:', {\n\t\tpath,\n\t\tfield: fieldname,\n\t\tfrom: beforeValue,\n\t\tto: afterValue,\n\t\toperation,\n\t\ttimestamp: timestamp.toISOString(),\n\t})\n\n\t// Show a subtle notification for field changes\n\taddNotification(`Field \"${fieldname}\" changed`, 'info')\n})\n\n// ============= DOCTYPE FIELD TRIGGER REGISTRATION =============\n// Register field triggers for actual doctype names that HST will use\n// HST should generate paths like \"todo-form.123.first_name\" using doctype.slug\n\nconst engine = getGlobalTriggerEngine()\nif (engine) {\n\t// Register field triggers for actual doctype names from the server\n\t// These match the doctype names defined in server.ts\n\n\t// Todo form field triggers (doctype: \"todo-form\")\n\tengine.registerDoctypeActions(\n\t\t'todo-form',\n\t\tnew Map([\n\t\t\t['first_name', ['validateName', 'updateFullName', 'logFieldChange']],\n\t\t\t['last_name', ['validateName', 'updateFullName', 'logFieldChange']],\n\t\t\t['phone', ['validatePhoneFormat', 'notifyPhoneChange', 'logFieldChange']],\n\t\t\t['email', ['validateEmailFormat', 'logFieldChange']],\n\t\t\t['notes', ['validateNotes', 'logFieldChange']],\n\t\t])\n\t)\n\n\t// Todo list field triggers (doctype: \"todo-list\")\n\tengine.registerDoctypeActions(\n\t\t'todo-list',\n\t\tnew Map([\n\t\t\t['first_name', ['validateName', 'updateFullName', 'logFieldChange']],\n\t\t\t['last_name', ['validateName', 'updateFullName', 'logFieldChange']],\n\t\t\t['phone', ['validatePhoneFormat', 'notifyPhoneChange', 'logFieldChange']],\n\t\t])\n\t)\n\n\t// Issue form field triggers (doctype: \"issue-form\")\n\tengine.registerDoctypeActions(\n\t\t'issue-form',\n\t\tnew Map([\n\t\t\t['subject', ['validateSubject', 'logFieldChange']],\n\t\t\t['status', ['onStatusChange', 'validatePriorityStatus', 'updateTimestamp', 'logFieldChange']],\n\t\t\t['priority', ['onPriorityChange', 'validatePriorityStatus', 'logFieldChange']],\n\t\t\t['description', ['validateDescription', 'logFieldChange']],\n\t\t\t['due_date', ['validateFutureDate', 'updateTimestamp', 'logFieldChange']],\n\t\t])\n\t)\n\n\t// Issue list field triggers (doctype: \"issue-list\")\n\tengine.registerDoctypeActions(\n\t\t'issue-list',\n\t\tnew Map([\n\t\t\t['subject', ['validateSubject', 'logFieldChange']],\n\t\t\t['status', ['onStatusChange', 'logFieldChange']],\n\t\t\t['priority', ['onPriorityChange', 'logFieldChange']],\n\t\t])\n\t)\n}\n"],"names":["notifications","reactive","notificationId","addNotification","message","type","notification","timeout","index","n","removeNotification","id","registerGlobalAction","context","fieldname","afterValue","path","beforeValue","selectedDate","today","operation","timestamp","engine","getGlobalTriggerEngine"],"mappings":"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"}