@kwirthmagnify/kwirth-sender-timed 0.1.0 → 0.1.3

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.
Files changed (3) hide show
  1. package/back.js +3 -37
  2. package/front.js +77 -111
  3. package/package.json +1 -1
package/back.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  var __defProp = Object.defineProperty;
3
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -116,20 +115,6 @@ function tzOffset(tz) {
116
115
  return "";
117
116
  }
118
117
  }
119
- function parseMinutes(hhmm) {
120
- const [h, m] = hhmm.split(":").map(Number);
121
- return (h ?? 0) * 60 + (m ?? 0);
122
- }
123
- function currentContext(timezone) {
124
- const now = timezone ? new Date((/* @__PURE__ */ new Date()).toLocaleString("en-US", { timeZone: timezone })) : /* @__PURE__ */ new Date();
125
- return { minutes: now.getHours() * 60 + now.getMinutes(), day: now.getDay() };
126
- }
127
- function matchesWindow(rule, minutes, day) {
128
- if (rule.days && rule.days.length > 0 && !rule.days.includes(day)) return false;
129
- const from = parseMinutes(rule.from);
130
- const to = parseMinutes(rule.to);
131
- return from <= to ? minutes >= from && minutes < to : minutes >= from || minutes < to;
132
- }
133
118
  var TimedSender = class {
134
119
  constructor() {
135
120
  this.id = "timed";
@@ -149,22 +134,7 @@ var TimedSender = class {
149
134
  getConfigNames() {
150
135
  return Array.from(this.configs.keys());
151
136
  }
152
- async send(configName, message) {
153
- const config = this.configs.get(configName);
154
- if (!config) throw new Error(`TimedSender: config '${configName}' not found`);
155
- if (!this.senderAccess) throw new Error(`TimedSender: senderAccess not initialized`);
156
- const { minutes, day } = currentContext(config.timezone);
157
- for (const rule of config.rules) {
158
- if (!matchesWindow(rule, minutes, day)) continue;
159
- if (rule.action === "send" && rule.senderId && rule.configName) {
160
- await this.senderAccess.send(rule.senderId, rule.configName, message);
161
- }
162
- return;
163
- }
164
- const defAction = config.defaultAction ?? "drop";
165
- if (defAction === "send" && config.defaultSenderId && config.defaultConfigName) {
166
- await this.senderAccess.send(config.defaultSenderId, config.defaultConfigName, message);
167
- }
137
+ async send(_configName, _message) {
168
138
  }
169
139
  getConfigSchema() {
170
140
  return [
@@ -174,16 +144,12 @@ var TimedSender = class {
174
144
  return off ? `${tz} (${off})` : tz;
175
145
  }) },
176
146
  { name: "rules", label: "Rules (JSON)", type: "json" },
177
- { name: "defaultAction", label: "Default action", type: "select", options: ["drop", "send"] },
178
- { name: "defaultSenderId", label: "Default sender ID" },
179
- { name: "defaultConfigName", label: "Default config name" }
147
+ { name: "defaultAction", label: "Default action", type: "select", options: ["drop", "send"] }
180
148
  ];
181
149
  }
182
- async startSender(senders) {
183
- this.senderAccess = senders;
150
+ async startSender(_senders) {
184
151
  }
185
152
  async stopSender() {
186
- this.senderAccess = void 0;
187
153
  }
188
154
  };
189
155
  var index_default = TimedSender;
package/front.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  (() => {
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
@@ -51,6 +50,7 @@
51
50
  var import_react = __toESM(require_react(), 1);
52
51
  var import_material = __toESM(require_material(), 1);
53
52
  var import_icons_material = __toESM(require_icons_material(), 1);
53
+ var import_icons_material2 = __toESM(require_icons_material(), 1);
54
54
  function authGet(accessString) {
55
55
  return { headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true" } };
56
56
  }
@@ -158,15 +158,10 @@
158
158
  function emptyConfig() {
159
159
  return { name: "", rules: [], defaultAction: "drop" };
160
160
  }
161
- var RuleRow = ({ rule, senders, onChange, onDelete }) => {
162
- const linkedSender = senders.find((s) => s.id === rule.senderId);
163
- const configNames = linkedSender?.configNames ?? [];
164
- const toggleDay = (day) => {
165
- const days = rule.days ?? [];
166
- const next = days.includes(day) ? days.filter((d) => d !== day) : [...days, day].sort();
167
- onChange({ ...rule, days: next.length === 0 ? void 0 : next });
168
- };
169
- return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { border: "1px solid", borderColor: "divider", borderRadius: 1, p: 1.5, mb: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react.default.createElement(
161
+ var RuleRow = ({ rule, onChange, onDelete }) => {
162
+ const allDays = [0, 1, 2, 3, 4, 5, 6];
163
+ const selectedDays = rule.days ?? allDays;
164
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { borderBottom: 1, borderColor: "divider", px: 1, py: 0.75, display: "flex", alignItems: "center", gap: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { flex: 1 } }, /* @__PURE__ */ import_react.default.createElement(
170
165
  import_material.TextField,
171
166
  {
172
167
  size: "small",
@@ -188,65 +183,34 @@
188
183
  onChange: (e) => onChange({ ...rule, to: e.target.value }),
189
184
  InputLabelProps: { shrink: true }
190
185
  }
191
- ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Action"), /* @__PURE__ */ import_react.default.createElement(
186
+ ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 110, flexShrink: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Action"), /* @__PURE__ */ import_react.default.createElement(
192
187
  import_material.Select,
193
188
  {
194
189
  label: "Action",
195
190
  value: rule.action,
196
- onChange: (e) => {
197
- const action = e.target.value;
198
- onChange({ ...rule, action, senderId: action === "drop" ? void 0 : rule.senderId, configName: action === "drop" ? void 0 : rule.configName });
199
- }
200
- },
201
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send"),
202
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop")
203
- )), rule.action === "send" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 130 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Sender"), /* @__PURE__ */ import_react.default.createElement(
204
- import_material.Select,
205
- {
206
- label: "Sender",
207
- value: rule.senderId ?? "",
208
- onChange: (e) => onChange({ ...rule, senderId: e.target.value || void 0, configName: void 0 })
191
+ onChange: (e) => onChange({ ...rule, action: e.target.value })
209
192
  },
210
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
211
- senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
212
- )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 130 }, disabled: !rule.senderId || configNames.length === 0 }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(
193
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { label: "drop", size: "small", color: "error", sx: { fontSize: 10, height: 18 } })),
194
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { label: "send", size: "small", color: "success", sx: { fontSize: 10, height: 18 } }))
195
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1, minWidth: 160 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Days"), /* @__PURE__ */ import_react.default.createElement(
213
196
  import_material.Select,
214
197
  {
215
- label: "Config",
216
- value: rule.configName ?? "",
217
- onChange: (e) => onChange({ ...rule, configName: e.target.value || void 0 })
218
- },
219
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
220
- configNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
221
- ))), /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: onDelete }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { fontSize: "small" }))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0, sx: { mt: 1 } }, DAY_LABELS.map((label, i) => /* @__PURE__ */ import_react.default.createElement(
222
- import_material.FormControlLabel,
223
- {
224
- key: i,
225
- control: /* @__PURE__ */ import_react.default.createElement(
226
- import_material.Checkbox,
227
- {
228
- size: "small",
229
- checked: !rule.days || rule.days.includes(i),
230
- onChange: () => toggleDay(i),
231
- sx: { p: 0.25 }
232
- }
233
- ),
234
- label: /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, label),
235
- sx: { mr: 0.5 }
236
- }
237
- )), rule.days && rule.days.length > 0 && /* @__PURE__ */ import_react.default.createElement(
238
- import_material.Button,
239
- {
240
- size: "small",
241
- sx: { fontSize: 10, p: 0.25, minWidth: 0 },
242
- onClick: () => onChange({ ...rule, days: void 0 })
198
+ multiple: true,
199
+ label: "Days",
200
+ value: selectedDays,
201
+ onChange: (e) => {
202
+ const val = e.target.value;
203
+ onChange({ ...rule, days: val.length === 0 || val.length === 7 ? void 0 : [...val].sort((a, b) => a - b) });
204
+ },
205
+ renderValue: (sel) => {
206
+ const days = sel;
207
+ return days.length === 7 ? "All days" : days.map((d) => DAY_LABELS[d]).join(", ");
208
+ }
243
209
  },
244
- "all"
245
- )));
210
+ DAY_LABELS.map((label, i) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: i, value: i, sx: { py: 0.25 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Checkbox, { checked: selectedDays.includes(i), size: "small", sx: { p: 0.25 } }), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2" }, label)))
211
+ ))), /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: onDelete, sx: { alignSelf: "center", flexShrink: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_icons_material2.Delete, { fontSize: "small" })));
246
212
  };
247
- var ConfigForm = ({ config, senders, onChange }) => {
248
- const defSender = senders.find((s) => s.id === config.defaultSenderId);
249
- const defConfigNames = defSender?.configNames ?? [];
213
+ var ConfigForm = ({ config, onChange }) => {
250
214
  const updateRule = (i, rule) => {
251
215
  const rules = [...config.rules];
252
216
  rules[i] = rule;
@@ -258,16 +222,16 @@
258
222
  const addRule = () => {
259
223
  onChange({ ...config, rules: [...config.rules, emptyRule()] });
260
224
  };
261
- return /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 2 }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1.5, flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react.default.createElement(
225
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 2 }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(
262
226
  import_material.TextField,
263
227
  {
264
228
  size: "small",
265
229
  label: "Name *",
266
230
  value: config.name,
267
231
  onChange: (e) => onChange({ ...config, name: e.target.value }),
268
- sx: { flex: 1, minWidth: 180 }
232
+ sx: { flex: 2 }
269
233
  }
270
- ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 240 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Timezone"), /* @__PURE__ */ import_react.default.createElement(
234
+ ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Timezone"), /* @__PURE__ */ import_react.default.createElement(
271
235
  import_material.Select,
272
236
  {
273
237
  label: "Timezone",
@@ -279,80 +243,71 @@
279
243
  const off = tzOffset(tz);
280
244
  return /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: tz, value: tz }, tz, off ? ` (${off})` : "");
281
245
  })
282
- ))), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, "Rules (evaluated in order \u2014 first match wins)")), config.rules.length === 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No rules yet. Add one below."), config.rules.map((rule, i) => /* @__PURE__ */ import_react.default.createElement(
246
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 110, flexShrink: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default"), /* @__PURE__ */ import_react.default.createElement(
247
+ import_material.Select,
248
+ {
249
+ label: "Default",
250
+ value: config.defaultAction ?? "drop",
251
+ onChange: (e) => onChange({ ...config, defaultAction: e.target.value })
252
+ },
253
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { label: "drop", size: "small", color: "error", sx: { fontSize: 10, height: 18 } })),
254
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, /* @__PURE__ */ import_react.default.createElement(import_material.Chip, { label: "send", size: "small", color: "success", sx: { fontSize: 10, height: 18 } }))
255
+ ))), /* @__PURE__ */ import_react.default.createElement(
256
+ import_material.TextField,
257
+ {
258
+ size: "small",
259
+ label: "Description",
260
+ value: config.description ?? "",
261
+ onChange: (e) => onChange({ ...config, description: e.target.value || void 0 }),
262
+ fullWidth: true,
263
+ multiline: true,
264
+ maxRows: 2
265
+ }
266
+ ), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, "Rules (evaluated in order \u2014 first match wins)")), config.rules.length === 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No rules yet. Add one below."), config.rules.map((rule, i) => /* @__PURE__ */ import_react.default.createElement(
283
267
  RuleRow,
284
268
  {
285
269
  key: i,
286
270
  rule,
287
- senders,
288
271
  onChange: (r) => updateRule(i, r),
289
272
  onDelete: () => deleteRule(i)
290
273
  }
291
- )), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null), onClick: addRule, sx: { alignSelf: "flex-start" } }, "Add rule"), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, "Default (when no rule matches)")), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1.5, flexWrap: "wrap", useFlexGap: true, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 110 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default action"), /* @__PURE__ */ import_react.default.createElement(
292
- import_material.Select,
293
- {
294
- label: "Default action",
295
- value: config.defaultAction ?? "drop",
296
- onChange: (e) => {
297
- const action = e.target.value;
298
- onChange({ ...config, defaultAction: action, defaultSenderId: action === "drop" ? void 0 : config.defaultSenderId, defaultConfigName: action === "drop" ? void 0 : config.defaultConfigName });
299
- }
300
- },
301
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop"),
302
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send")
303
- )), config.defaultAction === "send" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 160 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default sender"), /* @__PURE__ */ import_react.default.createElement(
304
- import_material.Select,
305
- {
306
- label: "Default sender",
307
- value: config.defaultSenderId ?? "",
308
- onChange: (e) => onChange({ ...config, defaultSenderId: e.target.value || void 0, defaultConfigName: void 0 })
309
- },
310
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
311
- senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
312
- )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 160 }, disabled: !config.defaultSenderId || defConfigNames.length === 0 }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default config"), /* @__PURE__ */ import_react.default.createElement(
313
- import_material.Select,
314
- {
315
- label: "Default config",
316
- value: config.defaultConfigName ?? "",
317
- onChange: (e) => onChange({ ...config, defaultConfigName: e.target.value || void 0 })
318
- },
319
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
320
- defConfigNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
321
- )))));
274
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material2.Add, null), onClick: addRule, sx: { alignSelf: "flex-start" } }, "Add rule"));
322
275
  };
323
276
  var TimedConfigDialog = ({ onClose, backendUrl, accessString }) => {
324
277
  const [configs, setConfigs] = (0, import_react.useState)([]);
325
- const [senders, setSenders] = (0, import_react.useState)([]);
326
278
  const [loading, setLoading] = (0, import_react.useState)(true);
327
279
  const [saving, setSaving] = (0, import_react.useState)(false);
328
280
  const [deletingName, setDeletingName] = (0, import_react.useState)();
329
281
  const [showForm, setShowForm] = (0, import_react.useState)(false);
330
282
  const [formConfig, setFormConfig] = (0, import_react.useState)(emptyConfig());
283
+ const [originalFormName, setOriginalFormName] = (0, import_react.useState)();
331
284
  const [error, setError] = (0, import_react.useState)();
332
285
  (0, import_react.useEffect)(() => {
333
- Promise.all([
334
- fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString)).then((r) => r.json()).then(setConfigs).catch(() => {
335
- }),
336
- fetch(`${backendUrl}/senders`, authGet(accessString)).then((r) => r.json()).then(setSenders).catch(() => {
337
- })
338
- ]).finally(() => setLoading(false));
286
+ fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString)).then((r) => r.json()).then(setConfigs).catch(() => {
287
+ }).finally(() => setLoading(false));
339
288
  }, []);
340
289
  const saveConfig = async () => {
341
- if (!formConfig.name.trim()) {
290
+ const trimmed = formConfig.name.trim();
291
+ if (!trimmed) {
342
292
  setError("Name is required");
343
293
  return;
344
294
  }
345
295
  setSaving(true);
346
296
  setError(void 0);
347
297
  try {
348
- const res = await fetch(`${backendUrl}/senders/timed/configs`, authPost(accessString, JSON.stringify(formConfig)));
298
+ const payload = { ...formConfig, name: trimmed };
299
+ const res = await fetch(`${backendUrl}/senders/timed/configs`, authPost(accessString, JSON.stringify(payload)));
349
300
  if (!res.ok) throw new Error((await res.json()).error ?? `HTTP ${res.status}`);
301
+ if (originalFormName && originalFormName !== trimmed) {
302
+ await fetch(`${backendUrl}/senders/timed/configs/${encodeURIComponent(originalFormName)}`, authDelete(accessString));
303
+ }
350
304
  setConfigs((prev) => {
351
- const idx = prev.findIndex((c) => c.name === formConfig.name);
352
- return idx >= 0 ? prev.map((c, i) => i === idx ? formConfig : c) : [...prev, formConfig];
305
+ const withoutOld = originalFormName && originalFormName !== trimmed ? prev.filter((c) => c.name !== originalFormName) : prev;
306
+ const idx = withoutOld.findIndex((c) => c.name === trimmed);
307
+ return idx >= 0 ? withoutOld.map((c, i) => i === idx ? payload : c) : [...withoutOld, payload];
353
308
  });
354
- setShowForm(false);
355
- setFormConfig(emptyConfig());
309
+ setOriginalFormName(trimmed);
310
+ setFormConfig(payload);
356
311
  } catch (err) {
357
312
  setError(`Save failed: ${err}`);
358
313
  } finally {
@@ -373,11 +328,19 @@
373
328
  };
374
329
  const startAdd = () => {
375
330
  setFormConfig(emptyConfig());
331
+ setOriginalFormName(void 0);
376
332
  setError(void 0);
377
333
  setShowForm(true);
378
334
  };
379
335
  const startEdit = (config) => {
380
336
  setFormConfig({ ...config });
337
+ setOriginalFormName(config.name);
338
+ setError(void 0);
339
+ setShowForm(true);
340
+ };
341
+ const startClone = (config) => {
342
+ setFormConfig({ ...config, name: `${config.name} (copy)` });
343
+ setOriginalFormName(void 0);
381
344
  setError(void 0);
382
345
  setShowForm(true);
383
346
  };
@@ -388,10 +351,13 @@
388
351
  parts.push(`default: ${c.defaultAction ?? "drop"}`);
389
352
  return parts.join(" \xB7 ");
390
353
  };
391
- return /* @__PURE__ */ import_react.default.createElement(import_material.Dialog, { open: true, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "760px" } } }, /* @__PURE__ */ import_react.default.createElement(import_material.DialogTitle, null, "Configure: Timed Sender"), /* @__PURE__ */ import_react.default.createElement(import_material.DialogContent, null, loading ? /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { display: "flex", justifyContent: "center", py: 4 } }, /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, null)) : /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 1.5, sx: { mt: 1 } }, configs.length === 0 && !showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No configs yet."), configs.map((cfg) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: cfg.name, sx: { display: "flex", alignItems: "center", justifyContent: "space-between", px: 1.5, py: 0.75, border: "1px solid", borderColor: "divider", borderRadius: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", fontWeight: "bold" }, cfg.name), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary" }, configSummary(cfg))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0.5 }, /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Edit" }, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", onClick: () => startEdit(cfg) }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, { fontSize: "small", sx: { transform: "rotate(45deg)" } }))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Delete" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", disabled: deletingName === cfg.name, onClick: () => deleteConfig(cfg.name) }, deletingName === cfg.name ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { fontSize: "small" }))))))), !showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null), onClick: startAdd, sx: { alignSelf: "flex-start" } }, "Add config"), showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { p: 2, border: "1px solid", borderColor: "divider", borderRadius: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "subtitle2", sx: { mb: 2 } }, formConfig.name && configs.some((c) => c.name === formConfig.name) ? `Edit: ${formConfig.name}` : "New config"), /* @__PURE__ */ import_react.default.createElement(ConfigForm, { config: formConfig, senders, onChange: setFormConfig }), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", spacing: 1, sx: { mt: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", onClick: () => {
354
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Dialog, { open: true, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "900px", height: "640px" } } }, /* @__PURE__ */ import_react.default.createElement(import_material.DialogTitle, null, "Configure: Timed Sender"), /* @__PURE__ */ import_react.default.createElement(import_material.DialogContent, { sx: { display: "flex", gap: 2, p: "16px !important", overflow: "hidden", height: "100%" } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { width: 190, display: "flex", flexDirection: "column", gap: 1, flexShrink: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, "Configs"), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, border: 1, borderColor: "divider", borderRadius: 1, overflowY: "auto" } }, loading ? /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { p: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 16 })) : configs.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.disabled", sx: { p: 1, display: "block" } }, "No configs yet.") : configs.map((cfg) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: cfg.name, sx: { display: "flex", alignItems: "center", px: 1, py: 0.5, borderBottom: 1, borderColor: "divider", borderLeft: formConfig.name === cfg.name && showForm ? 3 : 0, borderLeftColor: "primary.main", bgcolor: formConfig.name === cfg.name && showForm ? "action.selected" : "transparent" } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, minWidth: 0, overflow: "hidden", cursor: "pointer" }, onClick: () => startEdit(cfg) }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", fontWeight: "bold", noWrap: true }, cfg.name), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary", noWrap: true, display: "block" }, configSummary(cfg))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Delete" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", disabled: deletingName === cfg.name, onClick: () => deleteConfig(cfg.name) }, deletingName === cfg.name ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 12 }) : /* @__PURE__ */ import_react.default.createElement(import_icons_material2.Delete, { sx: { fontSize: 14 } }))))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0.5 }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material2.Add, null), onClick: startAdd, sx: { flex: 1 } }, "New"), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.ContentCopy, null), onClick: () => {
355
+ const c = configs.find((x) => x.name === formConfig.name && showForm);
356
+ if (c) startClone(c);
357
+ }, disabled: !showForm || !originalFormName, sx: { flex: 1 } }, "Clone"))), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, { orientation: "vertical", flexItem: true }), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, display: "flex", flexDirection: "column", gap: 1.5, minWidth: 0, overflow: "hidden" } }, showForm ? /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, originalFormName ? `Editing: ${originalFormName}` : "New config"), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, overflowY: "auto", pt: 1 } }, /* @__PURE__ */ import_react.default.createElement(ConfigForm, { config: formConfig, onChange: setFormConfig })), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", alignItems: "center", spacing: 1 }, error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error", sx: { flex: 1 } }, error), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", onClick: () => {
392
358
  setShowForm(false);
393
359
  setError(void 0);
394
- } }, "Cancel"), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !formConfig.name.trim(), onClick: saveConfig }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : "Save"))), error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error" }, error))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, { sx: { px: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { onClick: onClose }, "Close")));
360
+ } }, "Cancel"), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !formConfig.name.trim(), onClick: saveConfig }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : originalFormName ? "Update" : "Add"))) : /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { m: "auto", color: "text.disabled" } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2" }, "Select a config to edit or click New.")))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, { sx: { px: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { onClick: onClose }, "Close")));
395
361
  };
396
362
  var TimedConfigDialog_default = TimedConfigDialog;
397
363
 
package/package.json CHANGED
@@ -2,6 +2,6 @@
2
2
  "id": "timed",
3
3
  "name": "@kwirthmagnify/kwirth-sender-timed",
4
4
  "displayName": "Timed Sender",
5
- "version": "0.1.0",
5
+ "version": "0.1.3",
6
6
  "description": "Timed sender for Kwirth — routes or drops messages based on time-of-day windows and day-of-week rules"
7
7
  }