@kwirthmagnify/kwirth-sender-timed 0.1.2 → 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 -36
  2. package/front.js +77 -110
  3. package/package.json +1 -1
package/back.js CHANGED
@@ -115,20 +115,6 @@ function tzOffset(tz) {
115
115
  return "";
116
116
  }
117
117
  }
118
- function parseMinutes(hhmm) {
119
- const [h, m] = hhmm.split(":").map(Number);
120
- return (h ?? 0) * 60 + (m ?? 0);
121
- }
122
- function currentContext(timezone) {
123
- const now = timezone ? new Date((/* @__PURE__ */ new Date()).toLocaleString("en-US", { timeZone: timezone })) : /* @__PURE__ */ new Date();
124
- return { minutes: now.getHours() * 60 + now.getMinutes(), day: now.getDay() };
125
- }
126
- function matchesWindow(rule, minutes, day) {
127
- if (rule.days && rule.days.length > 0 && !rule.days.includes(day)) return false;
128
- const from = parseMinutes(rule.from);
129
- const to = parseMinutes(rule.to);
130
- return from <= to ? minutes >= from && minutes < to : minutes >= from || minutes < to;
131
- }
132
118
  var TimedSender = class {
133
119
  constructor() {
134
120
  this.id = "timed";
@@ -148,22 +134,7 @@ var TimedSender = class {
148
134
  getConfigNames() {
149
135
  return Array.from(this.configs.keys());
150
136
  }
151
- async send(configName, message) {
152
- const config = this.configs.get(configName);
153
- if (!config) throw new Error(`TimedSender: config '${configName}' not found`);
154
- if (!this.senderAccess) throw new Error(`TimedSender: senderAccess not initialized`);
155
- const { minutes, day } = currentContext(config.timezone);
156
- for (const rule of config.rules) {
157
- if (!matchesWindow(rule, minutes, day)) continue;
158
- if (rule.action === "send" && rule.senderId && rule.configName) {
159
- await this.senderAccess.send(rule.senderId, rule.configName, message);
160
- }
161
- return;
162
- }
163
- const defAction = config.defaultAction ?? "drop";
164
- if (defAction === "send" && config.defaultSenderId && config.defaultConfigName) {
165
- await this.senderAccess.send(config.defaultSenderId, config.defaultConfigName, message);
166
- }
137
+ async send(_configName, _message) {
167
138
  }
168
139
  getConfigSchema() {
169
140
  return [
@@ -173,16 +144,12 @@ var TimedSender = class {
173
144
  return off ? `${tz} (${off})` : tz;
174
145
  }) },
175
146
  { name: "rules", label: "Rules (JSON)", type: "json" },
176
- { name: "defaultAction", label: "Default action", type: "select", options: ["drop", "send"] },
177
- { name: "defaultSenderId", label: "Default sender ID" },
178
- { name: "defaultConfigName", label: "Default config name" }
147
+ { name: "defaultAction", label: "Default action", type: "select", options: ["drop", "send"] }
179
148
  ];
180
149
  }
181
- async startSender(senders) {
182
- this.senderAccess = senders;
150
+ async startSender(_senders) {
183
151
  }
184
152
  async stopSender() {
185
- this.senderAccess = void 0;
186
153
  }
187
154
  };
188
155
  var index_default = TimedSender;
package/front.js CHANGED
@@ -50,6 +50,7 @@
50
50
  var import_react = __toESM(require_react(), 1);
51
51
  var import_material = __toESM(require_material(), 1);
52
52
  var import_icons_material = __toESM(require_icons_material(), 1);
53
+ var import_icons_material2 = __toESM(require_icons_material(), 1);
53
54
  function authGet(accessString) {
54
55
  return { headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true" } };
55
56
  }
@@ -157,15 +158,10 @@
157
158
  function emptyConfig() {
158
159
  return { name: "", rules: [], defaultAction: "drop" };
159
160
  }
160
- var RuleRow = ({ rule, senders, onChange, onDelete }) => {
161
- const linkedSender = senders.find((s) => s.id === rule.senderId);
162
- const configNames = linkedSender?.configNames ?? [];
163
- const toggleDay = (day) => {
164
- const days = rule.days ?? [];
165
- const next = days.includes(day) ? days.filter((d) => d !== day) : [...days, day].sort();
166
- onChange({ ...rule, days: next.length === 0 ? void 0 : next });
167
- };
168
- 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(
169
165
  import_material.TextField,
170
166
  {
171
167
  size: "small",
@@ -187,65 +183,34 @@
187
183
  onChange: (e) => onChange({ ...rule, to: e.target.value }),
188
184
  InputLabelProps: { shrink: true }
189
185
  }
190
- ), /* @__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(
191
187
  import_material.Select,
192
188
  {
193
189
  label: "Action",
194
190
  value: rule.action,
195
- onChange: (e) => {
196
- const action = e.target.value;
197
- onChange({ ...rule, action, senderId: action === "drop" ? void 0 : rule.senderId, configName: action === "drop" ? void 0 : rule.configName });
198
- }
199
- },
200
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send"),
201
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop")
202
- )), 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(
203
- import_material.Select,
204
- {
205
- label: "Sender",
206
- value: rule.senderId ?? "",
207
- onChange: (e) => onChange({ ...rule, senderId: e.target.value || void 0, configName: void 0 })
191
+ onChange: (e) => onChange({ ...rule, action: e.target.value })
208
192
  },
209
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
210
- senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
211
- )), /* @__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(
212
196
  import_material.Select,
213
197
  {
214
- label: "Config",
215
- value: rule.configName ?? "",
216
- onChange: (e) => onChange({ ...rule, configName: e.target.value || void 0 })
217
- },
218
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
219
- configNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
220
- ))), /* @__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(
221
- import_material.FormControlLabel,
222
- {
223
- key: i,
224
- control: /* @__PURE__ */ import_react.default.createElement(
225
- import_material.Checkbox,
226
- {
227
- size: "small",
228
- checked: !rule.days || rule.days.includes(i),
229
- onChange: () => toggleDay(i),
230
- sx: { p: 0.25 }
231
- }
232
- ),
233
- label: /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, label),
234
- sx: { mr: 0.5 }
235
- }
236
- )), rule.days && rule.days.length > 0 && /* @__PURE__ */ import_react.default.createElement(
237
- import_material.Button,
238
- {
239
- size: "small",
240
- sx: { fontSize: 10, p: 0.25, minWidth: 0 },
241
- 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
+ }
242
209
  },
243
- "all"
244
- )));
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" })));
245
212
  };
246
- var ConfigForm = ({ config, senders, onChange }) => {
247
- const defSender = senders.find((s) => s.id === config.defaultSenderId);
248
- const defConfigNames = defSender?.configNames ?? [];
213
+ var ConfigForm = ({ config, onChange }) => {
249
214
  const updateRule = (i, rule) => {
250
215
  const rules = [...config.rules];
251
216
  rules[i] = rule;
@@ -257,16 +222,16 @@
257
222
  const addRule = () => {
258
223
  onChange({ ...config, rules: [...config.rules, emptyRule()] });
259
224
  };
260
- 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(
261
226
  import_material.TextField,
262
227
  {
263
228
  size: "small",
264
229
  label: "Name *",
265
230
  value: config.name,
266
231
  onChange: (e) => onChange({ ...config, name: e.target.value }),
267
- sx: { flex: 1, minWidth: 180 }
232
+ sx: { flex: 2 }
268
233
  }
269
- ), /* @__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(
270
235
  import_material.Select,
271
236
  {
272
237
  label: "Timezone",
@@ -278,80 +243,71 @@
278
243
  const off = tzOffset(tz);
279
244
  return /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: tz, value: tz }, tz, off ? ` (${off})` : "");
280
245
  })
281
- ))), /* @__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(
282
267
  RuleRow,
283
268
  {
284
269
  key: i,
285
270
  rule,
286
- senders,
287
271
  onChange: (r) => updateRule(i, r),
288
272
  onDelete: () => deleteRule(i)
289
273
  }
290
- )), /* @__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(
291
- import_material.Select,
292
- {
293
- label: "Default action",
294
- value: config.defaultAction ?? "drop",
295
- onChange: (e) => {
296
- const action = e.target.value;
297
- onChange({ ...config, defaultAction: action, defaultSenderId: action === "drop" ? void 0 : config.defaultSenderId, defaultConfigName: action === "drop" ? void 0 : config.defaultConfigName });
298
- }
299
- },
300
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop"),
301
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send")
302
- )), 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(
303
- import_material.Select,
304
- {
305
- label: "Default sender",
306
- value: config.defaultSenderId ?? "",
307
- onChange: (e) => onChange({ ...config, defaultSenderId: e.target.value || void 0, defaultConfigName: void 0 })
308
- },
309
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
310
- senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
311
- )), /* @__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(
312
- import_material.Select,
313
- {
314
- label: "Default config",
315
- value: config.defaultConfigName ?? "",
316
- onChange: (e) => onChange({ ...config, defaultConfigName: e.target.value || void 0 })
317
- },
318
- /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
319
- defConfigNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
320
- )))));
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"));
321
275
  };
322
276
  var TimedConfigDialog = ({ onClose, backendUrl, accessString }) => {
323
277
  const [configs, setConfigs] = (0, import_react.useState)([]);
324
- const [senders, setSenders] = (0, import_react.useState)([]);
325
278
  const [loading, setLoading] = (0, import_react.useState)(true);
326
279
  const [saving, setSaving] = (0, import_react.useState)(false);
327
280
  const [deletingName, setDeletingName] = (0, import_react.useState)();
328
281
  const [showForm, setShowForm] = (0, import_react.useState)(false);
329
282
  const [formConfig, setFormConfig] = (0, import_react.useState)(emptyConfig());
283
+ const [originalFormName, setOriginalFormName] = (0, import_react.useState)();
330
284
  const [error, setError] = (0, import_react.useState)();
331
285
  (0, import_react.useEffect)(() => {
332
- Promise.all([
333
- fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString)).then((r) => r.json()).then(setConfigs).catch(() => {
334
- }),
335
- fetch(`${backendUrl}/senders`, authGet(accessString)).then((r) => r.json()).then(setSenders).catch(() => {
336
- })
337
- ]).finally(() => setLoading(false));
286
+ fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString)).then((r) => r.json()).then(setConfigs).catch(() => {
287
+ }).finally(() => setLoading(false));
338
288
  }, []);
339
289
  const saveConfig = async () => {
340
- if (!formConfig.name.trim()) {
290
+ const trimmed = formConfig.name.trim();
291
+ if (!trimmed) {
341
292
  setError("Name is required");
342
293
  return;
343
294
  }
344
295
  setSaving(true);
345
296
  setError(void 0);
346
297
  try {
347
- 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)));
348
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
+ }
349
304
  setConfigs((prev) => {
350
- const idx = prev.findIndex((c) => c.name === formConfig.name);
351
- 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];
352
308
  });
353
- setShowForm(false);
354
- setFormConfig(emptyConfig());
309
+ setOriginalFormName(trimmed);
310
+ setFormConfig(payload);
355
311
  } catch (err) {
356
312
  setError(`Save failed: ${err}`);
357
313
  } finally {
@@ -372,11 +328,19 @@
372
328
  };
373
329
  const startAdd = () => {
374
330
  setFormConfig(emptyConfig());
331
+ setOriginalFormName(void 0);
375
332
  setError(void 0);
376
333
  setShowForm(true);
377
334
  };
378
335
  const startEdit = (config) => {
379
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);
380
344
  setError(void 0);
381
345
  setShowForm(true);
382
346
  };
@@ -387,10 +351,13 @@
387
351
  parts.push(`default: ${c.defaultAction ?? "drop"}`);
388
352
  return parts.join(" \xB7 ");
389
353
  };
390
- 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: () => {
391
358
  setShowForm(false);
392
359
  setError(void 0);
393
- } }, "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")));
394
361
  };
395
362
  var TimedConfigDialog_default = TimedConfigDialog;
396
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.2",
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
  }