imean-service-engine-htmx-plugin 2.1.1 → 2.2.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/dist/index.js CHANGED
@@ -295,144 +295,350 @@ function getFieldValue(field, initialData) {
295
295
  if (value === null || value === void 0) {
296
296
  return "";
297
297
  }
298
+ if (typeof value === "object") {
299
+ if (value instanceof Date) {
300
+ return value.toISOString();
301
+ }
302
+ try {
303
+ return JSON.stringify(value);
304
+ } catch (e) {
305
+ return "";
306
+ }
307
+ }
298
308
  return String(value);
299
309
  }
300
310
  return "";
301
311
  }
312
+ function isJsonString(value) {
313
+ if (!value || typeof value !== "string") {
314
+ return false;
315
+ }
316
+ const trimmed = value.trim();
317
+ return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
318
+ }
319
+ function formatJsonString(value) {
320
+ if (!value || typeof value !== "string") {
321
+ return value;
322
+ }
323
+ try {
324
+ const parsed = JSON.parse(value);
325
+ return JSON.stringify(parsed, null, 2);
326
+ } catch (e) {
327
+ return value;
328
+ }
329
+ }
330
+ function renderFormField(field, initialData, formFieldRenderers) {
331
+ const value = getFieldValue(field, initialData);
332
+ const customRenderer = formFieldRenderers?.[field.name];
333
+ if (customRenderer) {
334
+ let parsedValue = null;
335
+ if (value && isJsonString(value)) {
336
+ try {
337
+ parsedValue = JSON.parse(value);
338
+ } catch (e) {
339
+ parsedValue = null;
340
+ }
341
+ } else if (value) {
342
+ parsedValue = value;
343
+ }
344
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
345
+ /* @__PURE__ */ jsxRuntime.jsxs(
346
+ "label",
347
+ {
348
+ htmlFor: field.name,
349
+ className: "block text-sm font-semibold text-gray-700",
350
+ "data-testid": `label-${field.name}`,
351
+ children: [
352
+ field.label,
353
+ field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
354
+ ]
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: customRenderer({
358
+ field,
359
+ value: parsedValue,
360
+ initialData,
361
+ fieldName: field.name
362
+ }) }),
363
+ field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
364
+ ] }, field.name);
365
+ }
366
+ const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
367
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
368
+ /* @__PURE__ */ jsxRuntime.jsxs(
369
+ "label",
370
+ {
371
+ htmlFor: field.name,
372
+ className: "block text-sm font-semibold text-gray-700",
373
+ "data-testid": `label-${field.name}`,
374
+ children: [
375
+ field.label,
376
+ field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
377
+ ]
378
+ }
379
+ ),
380
+ shouldUseTextarea ? /* @__PURE__ */ jsxRuntime.jsx(
381
+ "textarea",
382
+ {
383
+ id: field.name,
384
+ name: field.name,
385
+ required: field.required,
386
+ placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
387
+ rows: isJsonString(value) ? 10 : 4,
388
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
389
+ "data-testid": `input-${field.name}`,
390
+ children: isJsonString(value) ? formatJsonString(value) : value
391
+ },
392
+ `${field.name}-${value}`
393
+ ) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
394
+ "select",
395
+ {
396
+ id: field.name,
397
+ name: field.name,
398
+ required: field.required,
399
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
400
+ "data-testid": `select-${field.name}`,
401
+ children: [
402
+ !field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
403
+ field.options && field.options.length > 0 ? field.options.map((option) => {
404
+ const optionValue = String(option.value);
405
+ const isSelected = value === optionValue;
406
+ return /* @__PURE__ */ jsxRuntime.jsx(
407
+ "option",
408
+ {
409
+ value: optionValue,
410
+ selected: isSelected,
411
+ children: option.label
412
+ },
413
+ optionValue
414
+ );
415
+ }) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
416
+ ]
417
+ },
418
+ `${field.name}-${value || ""}`
419
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
420
+ "input",
421
+ {
422
+ type: field.type || "text",
423
+ id: field.name,
424
+ name: field.name,
425
+ required: field.required,
426
+ placeholder: field.placeholder,
427
+ step: field.type === "number" ? field.step ?? void 0 : void 0,
428
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
429
+ value,
430
+ "data-testid": `input-${field.name}`
431
+ },
432
+ `${field.name}-${value}`
433
+ ),
434
+ field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
435
+ ] }, field.name);
436
+ }
302
437
  function FormPage(props) {
303
- const { fields, submitUrl, method = "post", initialData, formId } = props;
438
+ const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
304
439
  const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
305
440
  if (process.env.NODE_ENV === "development" && initialData) {
441
+ const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
306
442
  console.log(
307
- `[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fields.map((f) => f.name).join(", ")}`
443
+ `[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
308
444
  );
309
445
  }
310
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full", "data-testid": "form-container", children: /* @__PURE__ */ jsxRuntime.jsxs(
311
- "form",
312
- {
313
- id: finalFormId,
314
- method: method === "put" ? "post" : method,
315
- action: submitUrl,
316
- "hx-boost": "true",
317
- "hx-method": method === "put" ? "put" : void 0,
318
- "hx-indicator": "#form-loading-indicator",
319
- className: "space-y-6",
320
- "data-testid": "form",
321
- children: [
322
- /* @__PURE__ */ jsxRuntime.jsxs(
323
- "div",
324
- {
325
- id: "form-loading-indicator",
326
- className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
327
- children: [
328
- /* @__PURE__ */ jsxRuntime.jsxs(
329
- "svg",
330
- {
331
- className: "animate-spin h-4 w-4",
332
- xmlns: "http://www.w3.org/2000/svg",
333
- fill: "none",
334
- viewBox: "0 0 24 24",
335
- children: [
336
- /* @__PURE__ */ jsxRuntime.jsx(
337
- "circle",
338
- {
339
- className: "opacity-25",
340
- cx: "12",
341
- cy: "12",
342
- r: "10",
343
- stroke: "currentColor",
344
- strokeWidth: "4"
345
- }
346
- ),
347
- /* @__PURE__ */ jsxRuntime.jsx(
348
- "path",
349
- {
350
- className: "opacity-75",
351
- fill: "currentColor",
352
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
353
- }
354
- )
355
- ]
356
- }
357
- ),
358
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
359
- ]
446
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
447
+ /* @__PURE__ */ jsxRuntime.jsxs(
448
+ "div",
449
+ {
450
+ id: "form-loading-indicator",
451
+ className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
452
+ children: [
453
+ /* @__PURE__ */ jsxRuntime.jsxs(
454
+ "svg",
455
+ {
456
+ className: "animate-spin h-4 w-4",
457
+ xmlns: "http://www.w3.org/2000/svg",
458
+ fill: "none",
459
+ viewBox: "0 0 24 24",
460
+ children: [
461
+ /* @__PURE__ */ jsxRuntime.jsx(
462
+ "circle",
463
+ {
464
+ className: "opacity-25",
465
+ cx: "12",
466
+ cy: "12",
467
+ r: "10",
468
+ stroke: "currentColor",
469
+ strokeWidth: "4"
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsxRuntime.jsx(
473
+ "path",
474
+ {
475
+ className: "opacity-75",
476
+ fill: "currentColor",
477
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
478
+ }
479
+ )
480
+ ]
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
484
+ ]
485
+ }
486
+ ),
487
+ groups && groups.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
488
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ "x-on:click": `activeTab = ${index}`,
493
+ "x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
494
+ className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
495
+ "data-testid": `form-tab-${index}`,
496
+ children: group.label
497
+ },
498
+ index
499
+ )) }) }),
500
+ /* @__PURE__ */ jsxRuntime.jsx(
501
+ "form",
502
+ {
503
+ id: finalFormId,
504
+ method: method === "put" ? "post" : method,
505
+ action: submitUrl,
506
+ "hx-boost": "true",
507
+ ...method === "put" ? { "hx-put": submitUrl } : {},
508
+ "hx-indicator": "#form-loading-indicator",
509
+ "data-testid": "form",
510
+ className: isDialog ? "p-6" : "mt-6",
511
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
512
+ "div",
513
+ {
514
+ "x-show": `activeTab === ${index}`,
515
+ className: "space-y-6",
516
+ "data-testid": `form-tab-content-${index}`,
517
+ children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
518
+ },
519
+ index
520
+ )) }) })
521
+ }
522
+ )
523
+ ] }) : (
524
+ /* 平铺模式(向后兼容) */
525
+ /* @__PURE__ */ jsxRuntime.jsx(
526
+ "form",
527
+ {
528
+ id: finalFormId,
529
+ method: method === "put" ? "post" : method,
530
+ action: submitUrl,
531
+ "hx-boost": "true",
532
+ ...method === "put" ? { "hx-put": submitUrl } : {},
533
+ "hx-indicator": "#form-loading-indicator",
534
+ className: "space-y-6",
535
+ "data-testid": "form",
536
+ children: fields && fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
537
+ }
538
+ )
539
+ )
540
+ ] });
541
+ }
542
+
543
+ // src/utils/form-data-processor.ts
544
+ function preprocessFormData(data, zodSchema) {
545
+ if (!zodSchema) {
546
+ return data;
547
+ }
548
+ const processed = { ...data };
549
+ const def = zodSchema._def;
550
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
551
+ if (!shape || typeof shape !== "object") {
552
+ return processed;
553
+ }
554
+ for (const [fieldName, fieldSchema] of Object.entries(shape)) {
555
+ const value = processed[fieldName];
556
+ if (value === void 0) {
557
+ continue;
558
+ }
559
+ if (value === null) {
560
+ processed[fieldName] = void 0;
561
+ continue;
562
+ }
563
+ if (value === "") {
564
+ processed[fieldName] = void 0;
565
+ continue;
566
+ }
567
+ const fieldDef = fieldSchema._def;
568
+ let typeName = fieldDef?.type || fieldDef?.typeName;
569
+ if (typeName === "optional" || typeName === "ZodOptional") {
570
+ const innerType = fieldDef.innerType;
571
+ if (innerType) {
572
+ const innerDef = innerType._def;
573
+ typeName = innerDef?.type || innerDef?.typeName;
574
+ }
575
+ }
576
+ if (typeName === "number" || typeName === "ZodNumber") {
577
+ if (typeof value === "string") {
578
+ const trimmed = value.trim();
579
+ if (trimmed !== "") {
580
+ const numValue = Number(trimmed);
581
+ if (!isNaN(numValue)) {
582
+ processed[fieldName] = numValue;
360
583
  }
361
- ),
362
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => {
363
- const value = getFieldValue(field, initialData);
364
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
365
- /* @__PURE__ */ jsxRuntime.jsxs(
366
- "label",
367
- {
368
- htmlFor: field.name,
369
- className: "block text-sm font-semibold text-gray-700",
370
- "data-testid": `label-${field.name}`,
371
- children: [
372
- field.label,
373
- field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
374
- ]
375
- }
376
- ),
377
- field.type === "textarea" ? /* @__PURE__ */ jsxRuntime.jsx(
378
- "textarea",
379
- {
380
- id: field.name,
381
- name: field.name,
382
- required: field.required,
383
- placeholder: field.placeholder,
384
- rows: 4,
385
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y",
386
- value,
387
- "data-testid": `input-${field.name}`
388
- },
389
- `${field.name}-${value}`
390
- ) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
391
- "select",
392
- {
393
- id: field.name,
394
- name: field.name,
395
- required: field.required,
396
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
397
- "data-testid": `select-${field.name}`,
398
- children: [
399
- !field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
400
- field.options && field.options.length > 0 ? field.options.map((option) => {
401
- const optionValue = String(option.value);
402
- const isSelected = value === optionValue;
403
- return /* @__PURE__ */ jsxRuntime.jsx(
404
- "option",
405
- {
406
- value: optionValue,
407
- selected: isSelected,
408
- children: option.label
409
- },
410
- optionValue
411
- );
412
- }) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
413
- ]
414
- },
415
- `${field.name}-${value || ""}`
416
- ) : /* @__PURE__ */ jsxRuntime.jsx(
417
- "input",
418
- {
419
- type: field.type || "text",
420
- id: field.name,
421
- name: field.name,
422
- required: field.required,
423
- placeholder: field.placeholder,
424
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
425
- value,
426
- "data-testid": `input-${field.name}`
427
- },
428
- `${field.name}-${value}`
429
- ),
430
- field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
431
- ] }, field.name);
432
- }) }) })
433
- ]
584
+ }
585
+ } else if (typeof value === "number") {
586
+ processed[fieldName] = value;
587
+ }
434
588
  }
435
- ) });
589
+ if (typeName === "boolean" || typeName === "ZodBoolean") {
590
+ if (typeof value === "string") {
591
+ const trimmed = value.trim().toLowerCase();
592
+ if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
593
+ processed[fieldName] = true;
594
+ } else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
595
+ processed[fieldName] = false;
596
+ }
597
+ } else if (typeof value === "boolean") {
598
+ processed[fieldName] = value;
599
+ }
600
+ }
601
+ if (typeName === "array" || typeName === "ZodArray") {
602
+ if (typeof value === "string") {
603
+ const trimmed = value.trim();
604
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
605
+ try {
606
+ const parsed = JSON.parse(trimmed);
607
+ processed[fieldName] = parsed;
608
+ } catch (e) {
609
+ }
610
+ } else {
611
+ const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
612
+ processed[fieldName] = parts;
613
+ }
614
+ }
615
+ }
616
+ if (typeName === "object" || typeName === "ZodObject") {
617
+ if (typeof value === "string" && value.trim() !== "") {
618
+ try {
619
+ const trimmed = value.trim();
620
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
621
+ const parsed = JSON.parse(trimmed);
622
+ processed[fieldName] = parsed;
623
+ }
624
+ } catch (e) {
625
+ }
626
+ }
627
+ }
628
+ if (typeName === "any" || typeName === "ZodAny") {
629
+ if (typeof value === "string" && value.trim() !== "") {
630
+ try {
631
+ const trimmed = value.trim();
632
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
633
+ const parsed = JSON.parse(trimmed);
634
+ processed[fieldName] = parsed;
635
+ }
636
+ } catch (e) {
637
+ }
638
+ }
639
+ }
640
+ }
641
+ return processed;
436
642
  }
437
643
 
438
644
  // src/utils/schema-utils.ts
@@ -459,13 +665,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
459
665
  return null;
460
666
  }
461
667
  const label = getFieldDescription(fieldSchema) || fieldName;
462
- const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
668
+ const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
463
669
  return {
464
670
  name: fieldName,
465
671
  label,
466
672
  type,
467
673
  required,
468
674
  options,
675
+ step,
469
676
  schema: innerSchema || fieldSchema
470
677
  };
471
678
  }
@@ -522,10 +729,20 @@ function analyzeFieldType(schema) {
522
729
  let fieldType = "text";
523
730
  if (def?.checks) {
524
731
  const hasEmailCheck = def.checks.some(
525
- (check) => check.kind === "email"
732
+ (check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
526
733
  );
527
734
  if (hasEmailCheck) {
528
735
  fieldType = "email";
736
+ } else {
737
+ const maxLengthCheck = def.checks.find(
738
+ (check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
739
+ );
740
+ if (maxLengthCheck) {
741
+ const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
742
+ if (maxLength !== void 0 && maxLength > 50) {
743
+ fieldType = "textarea";
744
+ }
745
+ }
529
746
  }
530
747
  }
531
748
  return {
@@ -535,10 +752,22 @@ function analyzeFieldType(schema) {
535
752
  };
536
753
  }
537
754
  if (typeName === "number" || typeName === "ZodNumber") {
755
+ let step = void 0;
756
+ if (def?.checks) {
757
+ const hasIntCheck = def.checks.some(
758
+ (check) => check.isInt === true || check.format === "safeint" || check.def?.format === "safeint" || check.kind === "int" || check.constructor?.name === "ZodInt" || check._zod?.def?.check === "int" || check._zod?.def?.kind === "int"
759
+ );
760
+ if (!hasIntCheck) {
761
+ step = "any";
762
+ }
763
+ } else {
764
+ step = "any";
765
+ }
538
766
  return {
539
767
  type: "number",
540
768
  required: true,
541
- innerSchema: schema
769
+ innerSchema: schema,
770
+ step
542
771
  };
543
772
  }
544
773
  if (typeName === "date" || typeName === "ZodDate") {
@@ -603,7 +832,8 @@ function modelFieldsToFormFields(fields) {
603
832
  type: field.type,
604
833
  label: field.label,
605
834
  required: field.required,
606
- options: field.options
835
+ options: field.options,
836
+ step: field.step
607
837
  }));
608
838
  }
609
839
  function getFieldNamesFromFields(fields) {
@@ -621,6 +851,8 @@ var BaseFormFeature = class extends BaseFeature {
621
851
  descriptionGetter;
622
852
  /** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
623
853
  currentFormId;
854
+ /** 自定义表单字段渲染器 */
855
+ formFieldRenderers;
624
856
  /**
625
857
  * 获取或生成表单 ID(确保在同一个请求中保持一致)
626
858
  */
@@ -687,59 +919,7 @@ var BaseFormFeature = class extends BaseFeature {
687
919
  if (!this.schema) {
688
920
  return data;
689
921
  }
690
- const processed = { ...data };
691
- for (const key of Object.keys(processed)) {
692
- if (processed[key] === "") {
693
- processed[key] = void 0;
694
- }
695
- }
696
- const def = this.schema._def;
697
- const shape = typeof def.shape === "function" ? def.shape() : def.shape;
698
- if (!shape || typeof shape !== "object") {
699
- return data;
700
- }
701
- for (const [fieldName, fieldSchema] of Object.entries(shape)) {
702
- const value = processed[fieldName];
703
- if (value === void 0 || value === null) {
704
- continue;
705
- }
706
- const fieldDef = fieldSchema._def;
707
- let typeName = fieldDef?.typeName;
708
- let isOptional = false;
709
- if (typeName === "ZodOptional") {
710
- isOptional = true;
711
- const innerType = fieldDef.innerType?._def;
712
- typeName = innerType?.typeName;
713
- }
714
- if (isOptional && typeof value === "string" && value.trim() === "") {
715
- if (fieldName === "authorId") {
716
- imeanServiceEngine.logger.info(`[BaseFormFeature] authorId empty optional -> undefined`);
717
- }
718
- processed[fieldName] = void 0;
719
- continue;
720
- }
721
- if (typeName === "ZodNumber") {
722
- if (typeof value === "string") {
723
- const numValue = Number(value);
724
- if (fieldName === "authorId") {
725
- imeanServiceEngine.logger.info(
726
- `[BaseFormFeature] authorId raw value="${value}", numValue=${numValue}, isOptional=${isOptional}`
727
- );
728
- }
729
- if (!isNaN(numValue)) {
730
- processed[fieldName] = numValue;
731
- }
732
- }
733
- }
734
- if (typeName === "ZodArray") {
735
- if (typeof value === "string") {
736
- const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
737
- processed[fieldName] = parts;
738
- }
739
- }
740
- }
741
- imeanServiceEngine.logger.info(`[BaseFormFeature] processed form data: ${JSON.stringify(processed)}`);
742
- return processed;
922
+ return preprocessFormData(data, this.schema);
743
923
  }
744
924
  /**
745
925
  * 处理请求
@@ -749,12 +929,23 @@ var BaseFormFeature = class extends BaseFeature {
749
929
  this.currentFormId = void 0;
750
930
  if (ctx.req.method === "GET") {
751
931
  return this.render(context);
752
- } else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
932
+ } else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
933
+ const methodOverride = ctx.req.header("X-HTTP-Method-Override");
934
+ const actualMethod = methodOverride || ctx.req.method;
935
+ const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
936
+ if (actualMethod.toUpperCase() !== expectedMethod) {
937
+ imeanServiceEngine.logger.warn(
938
+ `[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
939
+ );
940
+ }
753
941
  const originalData = { ...context.body };
754
942
  imeanServiceEngine.logger.info(
755
943
  `[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
756
944
  );
757
945
  let data = this.preprocessFormData(context.body);
946
+ imeanServiceEngine.logger.info(
947
+ `[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
948
+ );
758
949
  if (!this.schema) {
759
950
  throw new Error("Schema is required for form validation");
760
951
  }
@@ -771,7 +962,10 @@ var BaseFormFeature = class extends BaseFeature {
771
962
  );
772
963
  return this.render(context, originalData);
773
964
  }
774
- const item = await this.handleSubmit(context, parseResult.data);
965
+ const item = await this.handleSubmit(
966
+ context,
967
+ parseResult.data
968
+ );
775
969
  if (!item) {
776
970
  context.sendError(
777
971
  this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
@@ -789,6 +983,9 @@ var BaseFormFeature = class extends BaseFeature {
789
983
  `[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
790
984
  );
791
985
  context.setRefresh(true);
986
+ if (context.redirectUrl) {
987
+ context.redirectUrl = void 0;
988
+ }
792
989
  return null;
793
990
  } else {
794
991
  const redirectUrl = this.getSuccessRedirectUrl(context, item);
@@ -801,12 +998,11 @@ var BaseFormFeature = class extends BaseFeature {
801
998
  }
802
999
  }
803
1000
  formFieldNames;
1001
+ groups;
804
1002
  /**
805
1003
  * 渲染表单页面
806
1004
  */
807
1005
  async render(context, initialData) {
808
- const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
809
- const fields = modelFieldsToFormFields(filteredFields);
810
1006
  let formData;
811
1007
  if (this.getFormAction() === "edit") {
812
1008
  if (initialData) {
@@ -828,6 +1024,50 @@ var BaseFormFeature = class extends BaseFeature {
828
1024
  }
829
1025
  const method = this.getFormAction() === "create" ? "post" : "put";
830
1026
  const formId = this.getFormId(context);
1027
+ if (this.groups && this.groups.length > 0) {
1028
+ if (!this.schema) {
1029
+ throw new Error("Schema is required when using groups");
1030
+ }
1031
+ const schema = this.schema;
1032
+ const groupSchemas = this.groups.map((group) => {
1033
+ const pickObject = group.fields.reduce(
1034
+ (acc, fieldName) => {
1035
+ acc[fieldName] = true;
1036
+ return acc;
1037
+ },
1038
+ {}
1039
+ );
1040
+ return {
1041
+ label: group.label,
1042
+ schema: schema.pick(pickObject),
1043
+ fields: group.fields
1044
+ };
1045
+ });
1046
+ const groupFields = groupSchemas.map(
1047
+ ({ label, schema: schema2, fields: fieldNames }) => {
1048
+ const groupFields2 = parseSchemaToFields(schema2);
1049
+ const formFields = modelFieldsToFormFields(groupFields2);
1050
+ return {
1051
+ label,
1052
+ fields: formFields
1053
+ };
1054
+ }
1055
+ );
1056
+ return /* @__PURE__ */ jsxRuntime.jsx(
1057
+ FormPage,
1058
+ {
1059
+ groups: groupFields,
1060
+ submitUrl,
1061
+ method,
1062
+ initialData: formData,
1063
+ formId,
1064
+ isDialog: context.isDialog,
1065
+ formFieldRenderers: this.formFieldRenderers
1066
+ }
1067
+ );
1068
+ }
1069
+ const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
1070
+ const fields = modelFieldsToFormFields(filteredFields);
831
1071
  return /* @__PURE__ */ jsxRuntime.jsx(
832
1072
  FormPage,
833
1073
  {
@@ -835,7 +1075,9 @@ var BaseFormFeature = class extends BaseFeature {
835
1075
  submitUrl,
836
1076
  method,
837
1077
  initialData: formData,
838
- formId
1078
+ formId,
1079
+ isDialog: context.isDialog,
1080
+ formFieldRenderers: this.formFieldRenderers
839
1081
  }
840
1082
  );
841
1083
  }
@@ -944,6 +1186,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
944
1186
  this.fields = parseSchemaToFields(options.schema);
945
1187
  this.createItem = options.createItem;
946
1188
  this.formFieldNames = options.formFieldNames;
1189
+ this.groups = options.groups;
1190
+ this.formFieldRenderers = options.formFieldRenderers;
947
1191
  }
948
1192
  getFormAction() {
949
1193
  return "create";
@@ -1003,12 +1247,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
1003
1247
  }
1004
1248
  }
1005
1249
  };
1250
+ function Card(props) {
1251
+ const {
1252
+ children,
1253
+ title,
1254
+ className = "",
1255
+ shadow = true,
1256
+ bordered = false,
1257
+ noPadding = false
1258
+ } = props;
1259
+ const baseClasses = "bg-white rounded-lg";
1260
+ const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
1261
+ const borderClass = bordered ? "border border-gray-200" : "";
1262
+ const paddingClass = noPadding ? "" : "p-6";
1263
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1264
+ "div",
1265
+ {
1266
+ className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
1267
+ children: [
1268
+ title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
1269
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
1270
+ ]
1271
+ }
1272
+ );
1273
+ }
1274
+ function renderDefaultValue(value) {
1275
+ if (value === null || value === void 0) {
1276
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
1277
+ }
1278
+ if (Array.isArray(value)) {
1279
+ if (value.length === 0) {
1280
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
1281
+ }
1282
+ if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
1283
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600", children: [
1284
+ "\u5305\u542B ",
1285
+ value.length,
1286
+ " \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
1287
+ ] });
1288
+ }
1289
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1290
+ "span",
1291
+ {
1292
+ className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
1293
+ children: String(item)
1294
+ },
1295
+ index
1296
+ )) });
1297
+ }
1298
+ if (typeof value === "object") {
1299
+ if (value instanceof Date) {
1300
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
1301
+ }
1302
+ return /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
1303
+ }
1304
+ if (typeof value === "boolean") {
1305
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
1306
+ }
1307
+ if (typeof value === "number") {
1308
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
1309
+ }
1310
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(value) });
1311
+ }
1312
+ function renderField(field, value, item) {
1313
+ let content;
1314
+ if (field.render) {
1315
+ const rendered = field.render(value, item);
1316
+ if (rendered === null || rendered === void 0) {
1317
+ content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
1318
+ } else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
1319
+ content = /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(rendered) });
1320
+ } else {
1321
+ content = rendered;
1322
+ }
1323
+ } else {
1324
+ content = renderDefaultValue(value);
1325
+ }
1326
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1327
+ "div",
1328
+ {
1329
+ className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
1330
+ children: [
1331
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
1332
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content }) })
1333
+ ]
1334
+ },
1335
+ field.key
1336
+ );
1337
+ }
1006
1338
  function DetailPage(props) {
1007
- const { item, fields } = props;
1008
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-300 rounded p-4", children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "grid grid-cols-1 gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1009
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "font-semibold text-gray-700", children: field.label }),
1010
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "mt-1 text-gray-900", children: String(item[field.key] ?? "-") })
1011
- ] }, field.key)) }) });
1339
+ const { item, fields, groups } = props;
1340
+ if (groups && groups.length > 0) {
1341
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsxRuntime.jsx(
1342
+ Card,
1343
+ {
1344
+ title: group.label,
1345
+ shadow: true,
1346
+ bordered: true,
1347
+ noPadding: true,
1348
+ children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
1349
+ const value = group.values[field.key];
1350
+ return renderField(field, value, item);
1351
+ }) })
1352
+ },
1353
+ groupIndex
1354
+ )) });
1355
+ }
1356
+ if (fields && fields.length > 0) {
1357
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: fields.map((field) => {
1358
+ const value = item[field.key];
1359
+ return renderField(field, value, item);
1360
+ }) }) });
1361
+ }
1362
+ return null;
1012
1363
  }
1013
1364
  var DefaultDetailFeature = class extends BaseFeature {
1014
1365
  getItem;
@@ -1016,6 +1367,8 @@ var DefaultDetailFeature = class extends BaseFeature {
1016
1367
  titleGetter;
1017
1368
  descriptionGetter;
1018
1369
  detailFieldNames;
1370
+ fieldRenderers;
1371
+ groups;
1019
1372
  constructor(options) {
1020
1373
  super({
1021
1374
  name: "detail",
@@ -1031,6 +1384,8 @@ var DefaultDetailFeature = class extends BaseFeature {
1031
1384
  this.titleGetter = options.getTitle;
1032
1385
  this.descriptionGetter = options.getDescription;
1033
1386
  this.detailFieldNames = options.detailFieldNames;
1387
+ this.fieldRenderers = options.fieldRenderers;
1388
+ this.groups = options.groups;
1034
1389
  }
1035
1390
  async getTitle(context) {
1036
1391
  if (this.titleGetter) {
@@ -1061,6 +1416,40 @@ var DefaultDetailFeature = class extends BaseFeature {
1061
1416
  if (!item) {
1062
1417
  return context.ctx.json({ error: "Not found" }, 404);
1063
1418
  }
1419
+ if (this.groups && this.groups.length > 0) {
1420
+ if (!this.schema) {
1421
+ throw new Error("Schema is required when using groups");
1422
+ }
1423
+ const schema = this.schema;
1424
+ const groupSchemas = this.groups.map((group) => {
1425
+ const pickObject = group.fields.reduce((acc, fieldName) => {
1426
+ acc[fieldName] = true;
1427
+ return acc;
1428
+ }, {});
1429
+ return {
1430
+ label: group.label,
1431
+ schema: schema.pick(pickObject),
1432
+ fields: group.fields
1433
+ };
1434
+ });
1435
+ const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
1436
+ const groupFields2 = parseSchemaToFields(schema2);
1437
+ const detailFields2 = groupFields2.map((field) => ({
1438
+ key: field.name,
1439
+ label: field.label,
1440
+ render: this.fieldRenderers?.[field.name]
1441
+ }));
1442
+ return {
1443
+ label,
1444
+ fields: detailFields2,
1445
+ values: fieldNames.reduce((acc, fieldName) => {
1446
+ acc[fieldName] = item[fieldName];
1447
+ return acc;
1448
+ }, {})
1449
+ };
1450
+ });
1451
+ return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, groups: groupFields });
1452
+ }
1064
1453
  const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
1065
1454
  if (this.detailFieldNames) {
1066
1455
  const systemFields = ["id", "createdAt", "updatedAt"];
@@ -1079,7 +1468,9 @@ var DefaultDetailFeature = class extends BaseFeature {
1079
1468
  const detailFieldNames = getFieldNamesFromFields(detailFields);
1080
1469
  const fields = detailFieldNames.map((fieldName) => ({
1081
1470
  key: fieldName,
1082
- label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
1471
+ label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
1472
+ render: this.fieldRenderers?.[fieldName]
1473
+ // 如果有自定义渲染函数则使用
1083
1474
  }));
1084
1475
  return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, fields });
1085
1476
  }
@@ -1157,6 +1548,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
1157
1548
  this.getItem = options.getItem;
1158
1549
  this.updateItem = options.updateItem;
1159
1550
  this.formFieldNames = options.formFieldNames;
1551
+ this.groups = options.groups;
1552
+ this.formFieldRenderers = options.formFieldRenderers;
1160
1553
  }
1161
1554
  getFormAction() {
1162
1555
  return "edit";
@@ -1392,30 +1785,6 @@ function Button(props) {
1392
1785
  }
1393
1786
  );
1394
1787
  }
1395
- function Card(props) {
1396
- const {
1397
- children,
1398
- title,
1399
- className = "",
1400
- shadow = true,
1401
- bordered = false,
1402
- noPadding = false
1403
- } = props;
1404
- const baseClasses = "bg-white rounded-lg";
1405
- const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
1406
- const borderClass = bordered ? "border border-gray-200" : "";
1407
- const paddingClass = noPadding ? "" : "p-6";
1408
- return /* @__PURE__ */ jsxRuntime.jsxs(
1409
- "div",
1410
- {
1411
- className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
1412
- children: [
1413
- title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
1414
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
1415
- ]
1416
- }
1417
- );
1418
- }
1419
1788
  function EmptyState(props) {
1420
1789
  const { message = "\u6682\u65E0\u6570\u636E", children } = props;
1421
1790
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
@@ -2307,7 +2676,8 @@ function Dialog(props) {
2307
2676
  className = "",
2308
2677
  size = "lg",
2309
2678
  closeOnBackdropClick = true,
2310
- actions = []
2679
+ actions = [],
2680
+ fixedContentHeight = false
2311
2681
  } = props;
2312
2682
  const sizeClasses = {
2313
2683
  sm: "max-w-md",
@@ -2366,7 +2736,13 @@ function Dialog(props) {
2366
2736
  }
2367
2737
  )
2368
2738
  ] }),
2369
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children }),
2739
+ /* @__PURE__ */ jsxRuntime.jsx(
2740
+ "div",
2741
+ {
2742
+ className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
2743
+ children
2744
+ }
2745
+ ),
2370
2746
  actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
2371
2747
  ]
2372
2748
  }
@@ -2627,6 +3003,13 @@ function BaseLayout(props) {
2627
3003
  {
2628
3004
  dangerouslySetInnerHTML: {
2629
3005
  __html: `
3006
+ /* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
3007
+ @supports (container-type: inline-size) {
3008
+ .\\@container {
3009
+ container-type: inline-size;
3010
+ }
3011
+ }
3012
+
2630
3013
  @keyframes fadeIn {
2631
3014
  from { opacity: 0;}
2632
3015
  to { opacity: 1;}
@@ -3149,9 +3532,9 @@ async function renderResult(ctx, context, result, renderOptions) {
3149
3532
  headers
3150
3533
  );
3151
3534
  }
3152
- if (context.redirectUrl) {
3535
+ if (context.redirectUrl && !context.refresh) {
3153
3536
  imeanServiceEngine.logger.info(
3154
- `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
3537
+ `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
3155
3538
  );
3156
3539
  if (context.isHtmxRequest) {
3157
3540
  return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
@@ -3160,7 +3543,11 @@ async function renderResult(ctx, context, result, renderOptions) {
3160
3543
  } else {
3161
3544
  return ctx.redirect(context.redirectUrl);
3162
3545
  }
3163
- } else {
3546
+ } else if (context.redirectUrl && context.refresh) {
3547
+ imeanServiceEngine.logger.info(
3548
+ `[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
3549
+ );
3550
+ } else if (!context.redirectUrl) {
3164
3551
  imeanServiceEngine.logger.info(
3165
3552
  `[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
3166
3553
  );
@@ -3211,6 +3598,8 @@ async function renderResult(ctx, context, result, renderOptions) {
3211
3598
  if (context.isDialog) {
3212
3599
  const dialogSize = renderOptions.feature?.dialogSize || "lg";
3213
3600
  const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
3601
+ const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
3602
+ const fixedContentHeight = isFormFeature;
3214
3603
  return ctx.html(
3215
3604
  /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3216
3605
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3220,6 +3609,7 @@ async function renderResult(ctx, context, result, renderOptions) {
3220
3609
  size: dialogSize,
3221
3610
  closeOnBackdropClick,
3222
3611
  actions,
3612
+ fixedContentHeight,
3223
3613
  children: result
3224
3614
  }
3225
3615
  ),
@@ -3480,6 +3870,28 @@ function registerPageRoutes(page, options) {
3480
3870
  });
3481
3871
  };
3482
3872
  options.hono[route.method](fullPath, handler);
3873
+ if (route.method === "put" || route.method === "delete") {
3874
+ const postHandler = async (ctx) => {
3875
+ const methodOverride = ctx.req.header("X-HTTP-Method-Override");
3876
+ const expectedMethod = route.method.toUpperCase();
3877
+ if (methodOverride === expectedMethod) {
3878
+ imeanServiceEngine.logger.info(
3879
+ `[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
3880
+ );
3881
+ return handleRequest(ctx, page, feature, {
3882
+ options: options.options
3883
+ });
3884
+ }
3885
+ imeanServiceEngine.logger.warn(
3886
+ `[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
3887
+ );
3888
+ return ctx.text("Method Not Allowed", 405);
3889
+ };
3890
+ imeanServiceEngine.logger.info(
3891
+ `[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
3892
+ );
3893
+ options.hono.post(fullPath, postHandler);
3894
+ }
3483
3895
  }
3484
3896
  }
3485
3897
  }
@@ -3569,6 +3981,81 @@ var HtmxAdminPlugin = class {
3569
3981
  registerHomeRedirect(this.pages, routeOptions);
3570
3982
  }
3571
3983
  };
3984
+ function createFormFieldXData(options) {
3985
+ const {
3986
+ fieldName,
3987
+ dataKey,
3988
+ defaultValue = [],
3989
+ customData = {},
3990
+ customMethods = {}
3991
+ } = options;
3992
+ const dataEntries = [];
3993
+ dataEntries.push(`${dataKey}: ${JSON.stringify(defaultValue)}`);
3994
+ for (const [key, value] of Object.entries(customData)) {
3995
+ dataEntries.push(`${key}: ${JSON.stringify(value)}`);
3996
+ }
3997
+ const methodEntries = [];
3998
+ methodEntries.push(`init() {
3999
+ const dataAttr = this.$el.getAttribute('data-initial-value');
4000
+ if (dataAttr) {
4001
+ try {
4002
+ this.${dataKey} = JSON.parse(dataAttr);
4003
+ } catch (e) {
4004
+ console.error('Failed to parse initial value:', e);
4005
+ this.${dataKey} = ${JSON.stringify(defaultValue)};
4006
+ }
4007
+ }
4008
+ this.updateHiddenField();
4009
+ }`);
4010
+ methodEntries.push(`updateHiddenField() {
4011
+ const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4012
+ if (hiddenInput) {
4013
+ hiddenInput.value = JSON.stringify(this.${dataKey});
4014
+ }
4015
+ }`);
4016
+ for (const [methodName, methodBody] of Object.entries(customMethods)) {
4017
+ methodEntries.push(`${methodName}${methodBody}`);
4018
+ }
4019
+ const dataStr = dataEntries.join(",\n ");
4020
+ const methodsStr = methodEntries.join(",\n ");
4021
+ return `{
4022
+ ${dataStr},
4023
+ ${methodsStr}
4024
+ }`;
4025
+ }
4026
+ function FormFieldWrapper(props) {
4027
+ const {
4028
+ fieldName,
4029
+ initialValue,
4030
+ xData,
4031
+ autoSync = false,
4032
+ children,
4033
+ className = "space-y-4"
4034
+ } = props;
4035
+ const initialValueJson = JSON.stringify(initialValue);
4036
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4037
+ "div",
4038
+ {
4039
+ "x-data": xData,
4040
+ "data-initial-value": initialValueJson,
4041
+ "x-init": "init()",
4042
+ ...autoSync ? { "x-effect": "updateHiddenField()" } : {},
4043
+ className,
4044
+ children: [
4045
+ /* @__PURE__ */ jsxRuntime.jsx(
4046
+ "input",
4047
+ {
4048
+ type: "hidden",
4049
+ name: fieldName,
4050
+ value: "",
4051
+ "data-testid": `hidden-${fieldName}`
4052
+ }
4053
+ ),
4054
+ children
4055
+ ]
4056
+ }
4057
+ );
4058
+ }
3572
4059
 
3573
4060
  exports.BaseFeature = BaseFeature;
3574
4061
  exports.CustomFeature = CustomFeature;
@@ -3579,10 +4066,12 @@ exports.DefaultEditFeature = DefaultEditFeature;
3579
4066
  exports.DefaultListFeature = DefaultListFeature;
3580
4067
  exports.Dialog = Dialog;
3581
4068
  exports.ErrorAlert = ErrorAlert;
4069
+ exports.FormFieldWrapper = FormFieldWrapper;
3582
4070
  exports.HtmxAdminPlugin = HtmxAdminPlugin;
3583
4071
  exports.LoadingBar = LoadingBar;
3584
4072
  exports.PageModel = PageModel;
3585
4073
  exports.checkUserPermission = checkUserPermission;
4074
+ exports.createFormFieldXData = createFormFieldXData;
3586
4075
  exports.getUserInfo = getUserInfo;
3587
4076
  exports.modelNameToPath = modelNameToPath;
3588
4077
  exports.parseListParams = parseListParams;