@goodandready/dsh-goal 0.2.1 → 0.2.2

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,33 +1,40 @@
1
1
  /**
2
- * Чистая логика состояния и валидации формы настроек dsh-goal.
3
- * Не зависит от DOM/React, полностью тестируема через node --test.
2
+ * Pure state and validation logic for dsh-goal settings form.
3
+ * Independent from DOM/React, 100% testable via node --test.
4
4
  */
5
5
 
6
6
  export const DEFAULT_SETTINGS = {
7
7
  maxIterations: 25,
8
8
  autoDrive: true,
9
9
  enableSound: true,
10
+ showQuickLaunchButton: true,
11
+ consecutiveToolFailureLimit: 3,
12
+ enableBrowserNotifications: true,
13
+ storagePath: '',
10
14
  };
11
15
 
12
16
  /**
13
- * Парсинг числового значения настроек:
14
- * - пустая строка / null / undefined -> undefined (сброс к базовому значению)
15
- * - валидное число >= 1 -> number
16
- * - иначе -> NaN (invalid)
17
+ * Parse numeric settings field:
18
+ * - empty string / null / undefined -> undefined (reset override)
19
+ * - valid integer >= min -> number
20
+ * - otherwise -> NaN (invalid)
21
+ * @param {any} raw
22
+ * @param {number} [min=1]
23
+ * @returns {number|undefined}
17
24
  */
18
- export function parseNumberField(raw) {
25
+ export function parseNumberField(raw, min = 1) {
19
26
  if (raw === '' || raw === null || raw === undefined) {
20
27
  return undefined;
21
28
  }
22
29
  const n = Number(raw);
23
- if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
30
+ if (!Number.isFinite(n) || n < min || !Number.isInteger(n)) {
24
31
  return NaN;
25
32
  }
26
33
  return n;
27
34
  }
28
35
 
29
36
  /**
30
- * Вычисление состояния конкретного поля:
37
+ * Compute status for a specific setting field
31
38
  * @param {string} field
32
39
  * @param {any} draftValue
33
40
  * @param {Object} snap - { value, base, user }
@@ -46,11 +53,10 @@ export function getFieldStatus(field, draftValue, snap) {
46
53
  if (field === 'maxIterations') {
47
54
  if (draftValue !== undefined) {
48
55
  if (draftValue === '') {
49
- // Пустая строка = сброс к базовому значению (не ошибка)
50
56
  invalid = false;
51
- isDirty = userVal !== undefined; // dirty если было переопределено
57
+ isDirty = userVal !== undefined;
52
58
  } else {
53
- const parsed = parseNumberField(draftValue);
59
+ const parsed = parseNumberField(draftValue, 1);
54
60
  if (Number.isNaN(parsed)) {
55
61
  invalid = true;
56
62
  isDirty = true;
@@ -59,10 +65,30 @@ export function getFieldStatus(field, draftValue, snap) {
59
65
  }
60
66
  }
61
67
  }
68
+ } else if (field === 'consecutiveToolFailureLimit') {
69
+ if (draftValue !== undefined) {
70
+ if (draftValue === '') {
71
+ invalid = false;
72
+ isDirty = userVal !== undefined;
73
+ } else {
74
+ const parsed = parseNumberField(draftValue, 0);
75
+ if (Number.isNaN(parsed)) {
76
+ invalid = true;
77
+ isDirty = true;
78
+ } else {
79
+ isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
80
+ }
81
+ }
82
+ }
83
+ } else if (field === 'storagePath') {
84
+ if (draftValue !== undefined) {
85
+ const cleanVal = typeof draftValue === 'string' ? draftValue.trim() : '';
86
+ isDirty = cleanVal !== (userVal !== undefined ? userVal : (baseVal || ''));
87
+ }
62
88
  } else {
63
- // boolean fields (autoDrive, enableSound)
89
+ // boolean fields (autoDrive, enableSound, showQuickLaunchButton, enableBrowserNotifications)
64
90
  if (draftValue !== undefined) {
65
- isDirty = draftValue !== (userVal !== undefined ? userVal : baseVal);
91
+ isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
66
92
  }
67
93
  }
68
94
 
@@ -77,7 +103,7 @@ export function getFieldStatus(field, draftValue, snap) {
77
103
  }
78
104
 
79
105
  /**
80
- * Вычисление списка операций записи (writes) для scope.set:
106
+ * Compute list of key-value writes for scope.set
81
107
  * @param {Object|null} draft
82
108
  * @param {Object} snap - { value, base, user }
83
109
  * @returns {Array<[string, any]>}
@@ -86,7 +112,15 @@ export function computeSavePlan(draft, snap) {
86
112
  if (!draft) return [];
87
113
 
88
114
  const writes = [];
89
- const fields = ['maxIterations', 'autoDrive', 'enableSound'];
115
+ const fields = [
116
+ 'maxIterations',
117
+ 'autoDrive',
118
+ 'enableSound',
119
+ 'showQuickLaunchButton',
120
+ 'consecutiveToolFailureLimit',
121
+ 'enableBrowserNotifications',
122
+ 'storagePath',
123
+ ];
90
124
 
91
125
  for (const f of fields) {
92
126
  if (draft[f] === undefined) continue;
@@ -95,11 +129,21 @@ export function computeSavePlan(draft, snap) {
95
129
  if (status.invalid) continue;
96
130
 
97
131
  if (status.isDirty) {
98
- if (f === 'maxIterations' && draft[f] === '') {
99
- // Сброс переопределения
100
- writes.push([f, undefined]);
101
- } else if (f === 'maxIterations') {
102
- writes.push([f, parseNumberField(draft[f])]);
132
+ if (f === 'maxIterations') {
133
+ if (draft[f] === '') {
134
+ writes.push([f, undefined]);
135
+ } else {
136
+ writes.push([f, parseNumberField(draft[f], 1)]);
137
+ }
138
+ } else if (f === 'consecutiveToolFailureLimit') {
139
+ if (draft[f] === '') {
140
+ writes.push([f, undefined]);
141
+ } else {
142
+ writes.push([f, parseNumberField(draft[f], 0)]);
143
+ }
144
+ } else if (f === 'storagePath') {
145
+ const clean = typeof draft[f] === 'string' ? draft[f].trim() : '';
146
+ writes.push([f, clean ? clean : undefined]);
103
147
  } else {
104
148
  writes.push([f, Boolean(draft[f])]);
105
149
  }