create-middag-ui 0.24.0 → 0.25.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/README.md +1 -1
- package/cli.js +11 -6
- package/lib/scaffold.js +364 -321
- package/lib/scaffoldPRO.js +2 -2
- package/lib/templates/pro/mock-navigation.ts +8 -16
- package/lib/templates/pro/mock-routes.tsx +13 -10
- package/lib/templates/shared/demo-page.tsx +2 -0
- package/lib/templates/shared/moodle-admin-shell.tsx +4 -2
- package/lib/templates/shared/page-resolver.tsx +7 -10
- package/lib/templates/shared/register-free.ts +7 -4
- package/package.json +1 -1
package/README.md
CHANGED
package/cli.js
CHANGED
|
@@ -257,8 +257,12 @@ if (hostKey === "moodle" && moodleComponent) {
|
|
|
257
257
|
scaffoldMoodlePlugin(targetDir, moodleComponent);
|
|
258
258
|
scaffoldMoodleTailwind(targetDir);
|
|
259
259
|
scaffoldMoodleAdapters(targetDir, moodleComponent);
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
// AdminShell uses Pro-only chrome (SidebarNav/PageHeader from @middag-io/react-pro);
|
|
261
|
+
// only scaffold it for PRO. FREE Moodle uses BasicShell via registerDefaults().
|
|
262
|
+
if (isPro) {
|
|
263
|
+
scaffoldMoodleAdminShell(targetDir);
|
|
264
|
+
}
|
|
265
|
+
success(`Moodle: AMD plugin + Tailwind + adapters${isPro ? " + AdminShell" : ""} for ${ moodleComponent }`);
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
// ── Step 9: npm install ──────────────────────────────────────────────────
|
|
@@ -296,11 +300,12 @@ if (installOk) {
|
|
|
296
300
|
|
|
297
301
|
blank();
|
|
298
302
|
console.log(" Your scaffold includes:");
|
|
299
|
-
console.log(" src/pages/
|
|
300
|
-
console.log(" src/pages/
|
|
301
|
-
console.log(" src/pages/
|
|
303
|
+
console.log(" src/pages/tasks.ts \u2190 list: metric_card + dense_table + row actions");
|
|
304
|
+
console.log(" src/pages/task-form.ts \u2190 create/edit: schema-driven form_panel");
|
|
305
|
+
console.log(" src/pages/task-detail.ts \u2190 detail: field/value via dense_table");
|
|
306
|
+
console.log(" src/pages/login.ts \u2190 login: basic shell + form_panel");
|
|
302
307
|
console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
|
|
303
|
-
console.log(" src/app.tsx \u2190
|
|
308
|
+
console.log(" src/app.tsx \u2190 page router (mirrors middag-php-demo-standalone)");
|
|
304
309
|
|
|
305
310
|
blank();
|
|
306
311
|
console.log(` Production build for ${host.name}:`);
|
package/lib/scaffold.js
CHANGED
|
@@ -198,7 +198,7 @@ export function scaffoldTsconfig(targetDir) {
|
|
|
198
198
|
noUnusedLocals: true,
|
|
199
199
|
noUnusedParameters: true,
|
|
200
200
|
skipLibCheck: true,
|
|
201
|
-
paths: { "@/*": ["./src/*"] },
|
|
201
|
+
paths: { "@/*": ["./src/*"], "@mock/*": ["./mock/*"] },
|
|
202
202
|
baseUrl: ".",
|
|
203
203
|
},
|
|
204
204
|
include: ["src", "mock"],
|
|
@@ -634,100 +634,132 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
634
634
|
// ── Page contract examples ──────────────────────────────────────────────
|
|
635
635
|
|
|
636
636
|
/**
|
|
637
|
-
* Scaffold
|
|
637
|
+
* Scaffold the Tasks CRUD page contracts in src/pages/.
|
|
638
|
+
*
|
|
639
|
+
* These mirror the PageContracts emitted by the middag-php-demo-standalone
|
|
640
|
+
* backend (a contract-driven Tasks CRUD): login, list, create/edit form and
|
|
641
|
+
* detail. The mock dev server renders them with the free engine; a real host
|
|
642
|
+
* sends the same contracts over Inertia.
|
|
638
643
|
*/
|
|
639
644
|
export function scaffoldPageExamples(targetDir) {
|
|
640
645
|
ensureDir(join(targetDir, "src", "pages"));
|
|
641
646
|
|
|
642
|
-
// ──
|
|
643
|
-
const
|
|
644
|
-
if (!skipIfExists(
|
|
647
|
+
// ── login.ts (basic shell + form_panel) ─────────────────────────────
|
|
648
|
+
const loginPath = join(targetDir, "src", "pages", "login.ts");
|
|
649
|
+
if (!skipIfExists(loginPath, "src/pages/login.ts")) {
|
|
645
650
|
writeFile(
|
|
646
|
-
|
|
651
|
+
loginPath,
|
|
647
652
|
`/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* Demonstrates the "dashboard" layout with two block types:
|
|
651
|
-
* - metric_card (KPI indicators in the metrics region)
|
|
652
|
-
* - dense_table (data table in the content region)
|
|
653
|
+
* Login page contract.
|
|
653
654
|
*
|
|
654
|
-
*
|
|
655
|
+
* Uses the "basic" shell and a form_panel block. form_panel ships in the free
|
|
656
|
+
* @middag-io/react engine; its heavy deps (react-hook-form + zod) load on demand.
|
|
655
657
|
*/
|
|
656
658
|
import type { PageContract } from "@middag-io/react";
|
|
657
659
|
|
|
658
|
-
export const
|
|
660
|
+
export const loginContract: PageContract = {
|
|
659
661
|
version: "1",
|
|
660
|
-
shell: "
|
|
661
|
-
page: {
|
|
662
|
-
key: "dashboard",
|
|
663
|
-
title: "Dashboard",
|
|
664
|
-
breadcrumbs: [{ label: "Home", href: "#/" }],
|
|
665
|
-
},
|
|
662
|
+
shell: "basic",
|
|
663
|
+
page: { key: "demo.login", title: "Sign in", subtitle: "Use any credentials, this is a mock." },
|
|
666
664
|
layout: {
|
|
667
|
-
template: "
|
|
665
|
+
template: "stack",
|
|
668
666
|
regions: {
|
|
669
|
-
|
|
670
|
-
{
|
|
671
|
-
key: "total_users",
|
|
672
|
-
type: "metric_card",
|
|
673
|
-
data: {
|
|
674
|
-
label: "Total Users",
|
|
675
|
-
value: "1,284",
|
|
676
|
-
delta: "+12%",
|
|
677
|
-
deltaDirection: "positive",
|
|
678
|
-
icon: "users",
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
key: "active_sessions",
|
|
683
|
-
type: "metric_card",
|
|
684
|
-
data: {
|
|
685
|
-
label: "Active Sessions",
|
|
686
|
-
value: "342",
|
|
687
|
-
delta: "+5%",
|
|
688
|
-
deltaDirection: "positive",
|
|
689
|
-
icon: "activity",
|
|
690
|
-
},
|
|
691
|
-
},
|
|
667
|
+
content: [
|
|
692
668
|
{
|
|
693
|
-
key: "
|
|
694
|
-
type: "
|
|
669
|
+
key: "login",
|
|
670
|
+
type: "form_panel",
|
|
695
671
|
data: {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
672
|
+
action: "/login",
|
|
673
|
+
method: "post",
|
|
674
|
+
schema: [
|
|
675
|
+
{ kind: "field", key: "email", component: "email", props: { label: "Email", required: true, placeholder: "you@example.com" } },
|
|
676
|
+
{ kind: "field", key: "password", component: "password", props: { label: "Password", required: true } },
|
|
677
|
+
],
|
|
678
|
+
values: {},
|
|
679
|
+
errors: {},
|
|
680
|
+
meta: { submitLabel: "Sign in" },
|
|
701
681
|
},
|
|
702
682
|
},
|
|
703
683
|
],
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
`,
|
|
688
|
+
"src/pages/login.ts",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── tasks.ts (metric_card + dense_table with row actions) ────────────
|
|
693
|
+
const tasksPath = join(targetDir, "src", "pages", "tasks.ts");
|
|
694
|
+
if (!skipIfExists(tasksPath, "src/pages/tasks.ts")) {
|
|
695
|
+
writeFile(
|
|
696
|
+
tasksPath,
|
|
697
|
+
`/**
|
|
698
|
+
* Tasks list page contract, the dashboard after login.
|
|
699
|
+
*
|
|
700
|
+
* Demonstrates two free blocks plus row-level actions:
|
|
701
|
+
* - metric_card (task count)
|
|
702
|
+
* - dense_table (rows, rowHref for navigation, rowActions for edit/delete)
|
|
703
|
+
*/
|
|
704
|
+
import type { PageContract } from "@middag-io/react";
|
|
705
|
+
|
|
706
|
+
export const tasksContract: PageContract = {
|
|
707
|
+
version: "1",
|
|
708
|
+
shell: "product",
|
|
709
|
+
page: {
|
|
710
|
+
key: "demo.tasks",
|
|
711
|
+
title: "Tasks",
|
|
712
|
+
subtitle: "A contract-driven CRUD, mirroring the middag-php-demo-standalone backend.",
|
|
713
|
+
actions: [
|
|
714
|
+
{ id: "new", label: "New task", intent: "primary", icon: "plus", target: { kind: "link", href: "/tasks/new" } },
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
layout: {
|
|
718
|
+
template: "stack",
|
|
719
|
+
regions: {
|
|
704
720
|
content: [
|
|
705
721
|
{
|
|
706
|
-
key: "
|
|
707
|
-
type: "
|
|
708
|
-
data: {
|
|
722
|
+
key: "task_count",
|
|
723
|
+
type: "metric_card",
|
|
724
|
+
data: { label: "Tasks", value: 3, icon: "list-check" },
|
|
709
725
|
},
|
|
710
726
|
{
|
|
711
|
-
key: "
|
|
727
|
+
key: "tasks",
|
|
712
728
|
type: "dense_table",
|
|
713
|
-
title: "
|
|
729
|
+
title: "All tasks",
|
|
714
730
|
data: {
|
|
715
731
|
columns: [
|
|
716
|
-
{ key: "
|
|
717
|
-
{ key: "action", label: "Action" },
|
|
718
|
-
{ key: "date", label: "Date", sortable: true },
|
|
732
|
+
{ key: "title", label: "Title" },
|
|
719
733
|
{ key: "status", label: "Status", variant: "badge" },
|
|
734
|
+
{ key: "priority", label: "Priority" },
|
|
735
|
+
{ key: "created", label: "Created", variant: "timestamp" },
|
|
720
736
|
],
|
|
721
737
|
rows: [
|
|
722
|
-
{ id: 1,
|
|
723
|
-
{ id: 2,
|
|
724
|
-
{ id: 3,
|
|
725
|
-
{ id: 4, user: "Dave Wilson", action: "Enrolled", date: "2024-01-14", status: "Active" },
|
|
726
|
-
{ id: 5, user: "Eve Brown", action: "Completed course", date: "2024-01-13", status: "Completed" },
|
|
738
|
+
{ id: 1, title: "Write onboarding docs", status: "open", priority: "high", created: "2024-05-30" },
|
|
739
|
+
{ id: 2, title: "Review pull request", status: "open", priority: "normal", created: "2024-05-29" },
|
|
740
|
+
{ id: 3, title: "Ship the release", status: "done", priority: "low", created: "2024-05-28" },
|
|
727
741
|
],
|
|
728
|
-
pagination: { page: 1, perPage: 10, total:
|
|
729
|
-
sort: { column: "
|
|
742
|
+
pagination: { page: 1, perPage: 10, total: 3, lastPage: 1 },
|
|
743
|
+
sort: { column: "created", direction: "desc" },
|
|
730
744
|
filters: { available: [], applied: {} },
|
|
745
|
+
rowHref: "/tasks/{id}",
|
|
746
|
+
rowActions: [
|
|
747
|
+
{ id: "edit", label: "Edit", intent: "secondary", icon: "pencil", target: { kind: "link", href: "/tasks/{id}/edit" } },
|
|
748
|
+
{
|
|
749
|
+
id: "delete",
|
|
750
|
+
label: "Delete",
|
|
751
|
+
intent: "danger",
|
|
752
|
+
icon: "trash",
|
|
753
|
+
target: { kind: "request", endpoint: "/tasks/{id}", method: "delete" },
|
|
754
|
+
confirmation: {
|
|
755
|
+
title: "Delete task",
|
|
756
|
+
message: "Remove this task? This cannot be undone.",
|
|
757
|
+
variant: "danger",
|
|
758
|
+
confirmLabel: "Delete",
|
|
759
|
+
cancelLabel: "Cancel",
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
],
|
|
731
763
|
},
|
|
732
764
|
},
|
|
733
765
|
],
|
|
@@ -735,99 +767,133 @@ export const dashboardContract: PageContract = {
|
|
|
735
767
|
},
|
|
736
768
|
};
|
|
737
769
|
`,
|
|
738
|
-
"src/pages/
|
|
770
|
+
"src/pages/tasks.ts",
|
|
739
771
|
);
|
|
740
772
|
}
|
|
741
773
|
|
|
742
|
-
// ──
|
|
743
|
-
const
|
|
744
|
-
if (!skipIfExists(
|
|
774
|
+
// ── task-form.ts (form_panel, create + edit share one schema) ────────
|
|
775
|
+
const taskFormPath = join(targetDir, "src", "pages", "task-form.ts");
|
|
776
|
+
if (!skipIfExists(taskFormPath, "src/pages/task-form.ts")) {
|
|
745
777
|
writeFile(
|
|
746
|
-
|
|
778
|
+
taskFormPath,
|
|
747
779
|
`/**
|
|
748
|
-
*
|
|
780
|
+
* Task create/edit page contracts, schema-driven form_panel.
|
|
749
781
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
* - detail_panel (metadata in the aside)
|
|
754
|
-
*
|
|
755
|
-
* Layout regions used: main, aside
|
|
782
|
+
* One field schema drives both create (empty values, POST) and edit (prefilled
|
|
783
|
+
* values, PUT). Shows text, textarea, select, radio, date, int, switch,
|
|
784
|
+
* entity_picker and a conditional field (done_reason appears when status = done).
|
|
756
785
|
*/
|
|
757
|
-
import type { PageContract } from "@middag-io/react";
|
|
786
|
+
import type { PageContract, FormSchemaNode } from "@middag-io/react";
|
|
758
787
|
|
|
759
|
-
|
|
788
|
+
const taskFormSchema: FormSchemaNode[] = [
|
|
789
|
+
{ kind: "field", key: "title", component: "text", props: { label: "Title", required: true, placeholder: "What needs doing?" } },
|
|
790
|
+
{ kind: "field", key: "notes", component: "textarea", props: { label: "Notes", rows: 4 } },
|
|
791
|
+
{
|
|
792
|
+
kind: "field",
|
|
793
|
+
key: "priority",
|
|
794
|
+
component: "select",
|
|
795
|
+
props: {
|
|
796
|
+
label: "Priority",
|
|
797
|
+
required: true,
|
|
798
|
+
options: [
|
|
799
|
+
{ value: "low", label: "Low" },
|
|
800
|
+
{ value: "normal", label: "Normal" },
|
|
801
|
+
{ value: "high", label: "High" },
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
kind: "field",
|
|
807
|
+
key: "status",
|
|
808
|
+
component: "radio",
|
|
809
|
+
props: {
|
|
810
|
+
label: "Status",
|
|
811
|
+
required: true,
|
|
812
|
+
options: [
|
|
813
|
+
{ value: "open", label: "Open" },
|
|
814
|
+
{ value: "done", label: "Done" },
|
|
815
|
+
],
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
kind: "field",
|
|
820
|
+
key: "done_reason",
|
|
821
|
+
component: "text",
|
|
822
|
+
props: {
|
|
823
|
+
label: "Done reason",
|
|
824
|
+
helpText: "Shown only when status is Done.",
|
|
825
|
+
visible_when: { field: "status", operator: "equals", value: "done" },
|
|
826
|
+
required_when: { field: "status", operator: "equals", value: "done" },
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
{ kind: "field", key: "due_on", component: "date", props: { label: "Due date" } },
|
|
830
|
+
{ kind: "field", key: "estimate_minutes", component: "int", props: { label: "Estimate (minutes)", min: 0, max: 100000 } },
|
|
831
|
+
{
|
|
832
|
+
kind: "field",
|
|
833
|
+
key: "parent_task",
|
|
834
|
+
component: "entity_picker",
|
|
835
|
+
props: {
|
|
836
|
+
label: "Parent task",
|
|
837
|
+
options: [
|
|
838
|
+
{ value: "1", label: "Write onboarding docs" },
|
|
839
|
+
{ value: "2", label: "Review pull request" },
|
|
840
|
+
],
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
{ kind: "field", key: "notify", component: "switch", props: { label: "Notify on change" } },
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
export const newTaskContract: PageContract = {
|
|
760
847
|
version: "1",
|
|
761
848
|
shell: "product",
|
|
762
849
|
page: {
|
|
763
|
-
key: "
|
|
764
|
-
title: "
|
|
765
|
-
breadcrumbs: [
|
|
766
|
-
{ label: "Home", href: "#/" },
|
|
767
|
-
{ label: "Connectors" },
|
|
768
|
-
],
|
|
769
|
-
actions: [
|
|
770
|
-
{ id: "add", label: "Add Connector", intent: "primary", icon: "plus" },
|
|
771
|
-
],
|
|
850
|
+
key: "demo.tasks.create",
|
|
851
|
+
title: "New task",
|
|
852
|
+
breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "New task" }],
|
|
772
853
|
},
|
|
773
854
|
layout: {
|
|
774
|
-
template: "
|
|
855
|
+
template: "stack",
|
|
775
856
|
regions: {
|
|
776
|
-
|
|
857
|
+
content: [
|
|
777
858
|
{
|
|
778
|
-
key: "
|
|
779
|
-
type: "
|
|
780
|
-
title: "Available Connectors",
|
|
859
|
+
key: "task_form",
|
|
860
|
+
type: "form_panel",
|
|
781
861
|
data: {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
rows: [
|
|
789
|
-
{ id: 1, name: "Moodle LMS", type: "LMS", status: "Active", icon: "graduation-cap", href: "#/connectors" },
|
|
790
|
-
{ id: 2, name: "Google Workspace", type: "SSO", status: "Active", icon: "shield", href: "#/connectors" },
|
|
791
|
-
{ id: 3, name: "Stripe", type: "Payment", status: "Inactive", icon: "credit-card", href: "#/connectors" },
|
|
792
|
-
{ id: 4, name: "Mailchimp", type: "Email", status: "Active", icon: "mail", href: "#/connectors" },
|
|
793
|
-
],
|
|
862
|
+
action: "/tasks",
|
|
863
|
+
method: "post",
|
|
864
|
+
schema: taskFormSchema,
|
|
865
|
+
values: { priority: "normal", status: "open", notify: true },
|
|
866
|
+
errors: {},
|
|
867
|
+
meta: { submitLabel: "Create task", cancelHref: "/" },
|
|
794
868
|
},
|
|
795
869
|
},
|
|
796
870
|
],
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
export const editTaskContract: PageContract = {
|
|
876
|
+
version: "1",
|
|
877
|
+
shell: "product",
|
|
878
|
+
page: {
|
|
879
|
+
key: "demo.tasks.edit",
|
|
880
|
+
title: "Edit task",
|
|
881
|
+
breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "Edit task" }],
|
|
882
|
+
},
|
|
883
|
+
layout: {
|
|
884
|
+
template: "stack",
|
|
885
|
+
regions: {
|
|
886
|
+
content: [
|
|
813
887
|
{
|
|
814
|
-
key: "
|
|
815
|
-
type: "
|
|
816
|
-
title: "Selected Connector",
|
|
888
|
+
key: "task_form",
|
|
889
|
+
type: "form_panel",
|
|
817
890
|
data: {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
{ key: "version", label: "API Version", value: "4.3.2" },
|
|
825
|
-
{ key: "connected", label: "Connected Since", value: "2024-01-01", kind: "timestamp" },
|
|
826
|
-
{ key: "status", label: "Status", value: "Active", kind: "status" },
|
|
827
|
-
{ key: "endpoint", label: "Endpoint", value: "https://moodle.example.com/webservice/rest", kind: "code", copyable: true },
|
|
828
|
-
],
|
|
829
|
-
},
|
|
830
|
-
],
|
|
891
|
+
action: "/tasks/1",
|
|
892
|
+
method: "put",
|
|
893
|
+
schema: taskFormSchema,
|
|
894
|
+
values: { title: "Write onboarding docs", priority: "high", status: "open", notify: true },
|
|
895
|
+
errors: {},
|
|
896
|
+
meta: { submitLabel: "Save changes", cancelHref: "/" },
|
|
831
897
|
},
|
|
832
898
|
},
|
|
833
899
|
],
|
|
@@ -835,36 +901,32 @@ export const connectorsContract: PageContract = {
|
|
|
835
901
|
},
|
|
836
902
|
};
|
|
837
903
|
`,
|
|
838
|
-
"src/pages/
|
|
904
|
+
"src/pages/task-form.ts",
|
|
839
905
|
);
|
|
840
906
|
}
|
|
841
907
|
|
|
842
|
-
// ──
|
|
843
|
-
const
|
|
844
|
-
if (!skipIfExists(
|
|
908
|
+
// ── task-detail.ts (dense_table as a field/value detail view) ────────
|
|
909
|
+
const taskDetailPath = join(targetDir, "src", "pages", "task-detail.ts");
|
|
910
|
+
if (!skipIfExists(taskDetailPath, "src/pages/task-detail.ts")) {
|
|
845
911
|
writeFile(
|
|
846
|
-
|
|
912
|
+
taskDetailPath,
|
|
847
913
|
`/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* Demonstrates the "stack" layout with nested block types:
|
|
851
|
-
* - tabbed_panel (tabs that contain other blocks)
|
|
852
|
-
* - form_panel (schema-driven form inside a tab)
|
|
853
|
-
* - link_list (navigation links inside a tab)
|
|
914
|
+
* Task detail page contract, a read-only field/value view.
|
|
854
915
|
*
|
|
855
|
-
*
|
|
916
|
+
* Reuses dense_table as a two-column key/value display (the demo backend does
|
|
917
|
+
* the same), plus an Edit action in the page header.
|
|
856
918
|
*/
|
|
857
919
|
import type { PageContract } from "@middag-io/react";
|
|
858
920
|
|
|
859
|
-
export const
|
|
921
|
+
export const taskDetailContract: PageContract = {
|
|
860
922
|
version: "1",
|
|
861
923
|
shell: "product",
|
|
862
924
|
page: {
|
|
863
|
-
key: "
|
|
864
|
-
title: "
|
|
865
|
-
breadcrumbs: [
|
|
866
|
-
|
|
867
|
-
{ label: "
|
|
925
|
+
key: "demo.tasks.show",
|
|
926
|
+
title: "Task detail",
|
|
927
|
+
breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "Task detail" }],
|
|
928
|
+
actions: [
|
|
929
|
+
{ id: "edit", label: "Edit", intent: "secondary", icon: "pencil", target: { kind: "link", href: "/tasks/1/edit" } },
|
|
868
930
|
],
|
|
869
931
|
},
|
|
870
932
|
layout: {
|
|
@@ -872,115 +934,23 @@ export const settingsContract: PageContract = {
|
|
|
872
934
|
regions: {
|
|
873
935
|
content: [
|
|
874
936
|
{
|
|
875
|
-
key: "
|
|
876
|
-
type: "
|
|
937
|
+
key: "task",
|
|
938
|
+
type: "dense_table",
|
|
939
|
+
title: "Write onboarding docs",
|
|
877
940
|
data: {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
{
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
type: "form_panel",
|
|
888
|
-
data: {
|
|
889
|
-
action: "/api/settings/general",
|
|
890
|
-
method: "put",
|
|
891
|
-
schema: [
|
|
892
|
-
{
|
|
893
|
-
kind: "section",
|
|
894
|
-
id: "site",
|
|
895
|
-
label: "Site Settings",
|
|
896
|
-
children: [
|
|
897
|
-
{
|
|
898
|
-
kind: "field",
|
|
899
|
-
key: "site_name",
|
|
900
|
-
component: "text",
|
|
901
|
-
props: { label: "Site Name", placeholder: "My Platform", required: true },
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
kind: "field",
|
|
905
|
-
key: "site_url",
|
|
906
|
-
component: "url",
|
|
907
|
-
props: { label: "Site URL", placeholder: "https://example.com" },
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
kind: "field",
|
|
911
|
-
key: "timezone",
|
|
912
|
-
component: "select",
|
|
913
|
-
props: {
|
|
914
|
-
label: "Timezone",
|
|
915
|
-
options: [
|
|
916
|
-
{ value: "UTC", label: "UTC" },
|
|
917
|
-
{ value: "America/Sao_Paulo", label: "S\\u00e3o Paulo (BRT)" },
|
|
918
|
-
{ value: "America/New_York", label: "New York (EST)" },
|
|
919
|
-
{ value: "Europe/London", label: "London (GMT)" },
|
|
920
|
-
],
|
|
921
|
-
},
|
|
922
|
-
},
|
|
923
|
-
],
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
kind: "section",
|
|
927
|
-
id: "features",
|
|
928
|
-
label: "Features",
|
|
929
|
-
children: [
|
|
930
|
-
{
|
|
931
|
-
kind: "field",
|
|
932
|
-
key: "enable_notifications",
|
|
933
|
-
component: "switch",
|
|
934
|
-
props: { label: "Enable Notifications", helpText: "Send email notifications for important events" },
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
kind: "field",
|
|
938
|
-
key: "enable_analytics",
|
|
939
|
-
component: "switch",
|
|
940
|
-
props: { label: "Enable Analytics", helpText: "Track user activity and generate reports" },
|
|
941
|
-
},
|
|
942
|
-
{
|
|
943
|
-
kind: "field",
|
|
944
|
-
key: "maintenance_mode",
|
|
945
|
-
component: "switch",
|
|
946
|
-
props: { label: "Maintenance Mode", helpText: "Show maintenance page to non-admin users" },
|
|
947
|
-
},
|
|
948
|
-
],
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
values: {
|
|
952
|
-
site_name: "My Platform",
|
|
953
|
-
site_url: "https://example.com",
|
|
954
|
-
timezone: "UTC",
|
|
955
|
-
enable_notifications: true,
|
|
956
|
-
enable_analytics: true,
|
|
957
|
-
maintenance_mode: false,
|
|
958
|
-
},
|
|
959
|
-
errors: {},
|
|
960
|
-
meta: { submitLabel: "Save Changes", cancelHref: "#/" },
|
|
961
|
-
},
|
|
962
|
-
},
|
|
963
|
-
],
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
key: "notifications",
|
|
967
|
-
label: "Notifications",
|
|
968
|
-
icon: "bell",
|
|
969
|
-
blocks: [
|
|
970
|
-
{
|
|
971
|
-
key: "notification_links",
|
|
972
|
-
type: "link_list",
|
|
973
|
-
data: {
|
|
974
|
-
items: [
|
|
975
|
-
{ label: "Email Templates", href: "#/settings", icon: "mail", description: "Customize email notification templates" },
|
|
976
|
-
{ label: "Webhook Endpoints", href: "#/settings", icon: "webhook", description: "Configure webhook delivery endpoints" },
|
|
977
|
-
{ label: "Notification Rules", href: "#/settings", icon: "filter", description: "Set up conditional notification routing" },
|
|
978
|
-
],
|
|
979
|
-
},
|
|
980
|
-
},
|
|
981
|
-
],
|
|
982
|
-
},
|
|
941
|
+
columns: [
|
|
942
|
+
{ key: "field", label: "Field" },
|
|
943
|
+
{ key: "value", label: "Value" },
|
|
944
|
+
],
|
|
945
|
+
rows: [
|
|
946
|
+
{ id: "title", field: "Title", value: "Write onboarding docs" },
|
|
947
|
+
{ id: "status", field: "Status", value: "open" },
|
|
948
|
+
{ id: "priority", field: "Priority", value: "high" },
|
|
949
|
+
{ id: "due_on", field: "Due date", value: "2024-06-10" },
|
|
983
950
|
],
|
|
951
|
+
pagination: { page: 1, perPage: 10, total: 4, lastPage: 1 },
|
|
952
|
+
sort: { column: null, direction: null },
|
|
953
|
+
filters: { available: [], applied: {} },
|
|
984
954
|
},
|
|
985
955
|
},
|
|
986
956
|
],
|
|
@@ -988,7 +958,7 @@ export const settingsContract: PageContract = {
|
|
|
988
958
|
},
|
|
989
959
|
};
|
|
990
960
|
`,
|
|
991
|
-
"src/pages/
|
|
961
|
+
"src/pages/task-detail.ts",
|
|
992
962
|
);
|
|
993
963
|
}
|
|
994
964
|
}
|
|
@@ -1009,15 +979,33 @@ let _navigate: ((to: string) => void) | null = null;
|
|
|
1009
979
|
|
|
1010
980
|
export function setMockNavigate(fn: (to: string) => void) { _navigate = fn; }
|
|
1011
981
|
|
|
982
|
+
type MockOpts = {
|
|
983
|
+
only?: string[];
|
|
984
|
+
preserveState?: boolean;
|
|
985
|
+
preserveScroll?: boolean;
|
|
986
|
+
onSuccess?: () => void;
|
|
987
|
+
onFinish?: () => void;
|
|
988
|
+
onError?: (errors: Record<string, string>) => void;
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// A synchronous mock resolves instantly — fire the lifecycle callbacks the
|
|
992
|
+
// engine expects (e.g. useLazyTabs marks a tab loaded in onSuccess; blocks
|
|
993
|
+
// reset loading flags in onFinish), else those UI states stick forever.
|
|
994
|
+
const settle = (o?: MockOpts) => { o?.onSuccess?.(); o?.onFinish?.(); };
|
|
995
|
+
|
|
1012
996
|
export const router = {
|
|
1013
|
-
get: (url: string
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
997
|
+
get: (url: string, _params?: Record<string, unknown>, opts?: MockOpts) => {
|
|
998
|
+
_navigate ? _navigate(url) : console.log("[mock] GET", url);
|
|
999
|
+
settle(opts);
|
|
1000
|
+
},
|
|
1001
|
+
post: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] POST", url); settle(opts); },
|
|
1002
|
+
put: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PUT", url); settle(opts); },
|
|
1003
|
+
patch: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PATCH", url); settle(opts); },
|
|
1004
|
+
delete: (url: string, opts?: MockOpts) => { console.log("[mock] DELETE", url); settle(opts); },
|
|
1005
|
+
reload: (opts?: MockOpts) => { settle(opts); },
|
|
1019
1006
|
visit: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] VISIT", url); },
|
|
1020
|
-
on: () => () => {},
|
|
1007
|
+
on: (_event?: string, _cb?: (event: unknown) => void) => () => {},
|
|
1008
|
+
poll: (_intervalMs?: number, _reload?: unknown, _opts?: unknown) => ({ stop: () => {}, start: () => {}, destroy: () => {} }),
|
|
1021
1009
|
};
|
|
1022
1010
|
`, "mock/adapters/inertia-core.ts");
|
|
1023
1011
|
}
|
|
@@ -1067,6 +1055,35 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
|
|
|
1067
1055
|
return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
1068
1056
|
});
|
|
1069
1057
|
|
|
1058
|
+
// ── Inertia v3 primitives the engine imports ──────────────────────────
|
|
1059
|
+
// @middag-io/react imports Deferred/WhenVisible (lazy blocks) and useRemember
|
|
1060
|
+
// (DataTable view-prefs) from @inertiajs/react. In the no-server mock the data
|
|
1061
|
+
// is already present, so render children immediately; useRemember = useState.
|
|
1062
|
+
|
|
1063
|
+
export function Deferred({ children }: {
|
|
1064
|
+
data: string | string[];
|
|
1065
|
+
fallback?: React.ReactNode;
|
|
1066
|
+
rescue?: React.ReactNode;
|
|
1067
|
+
children: React.ReactNode;
|
|
1068
|
+
}) {
|
|
1069
|
+
return children;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function WhenVisible({ children }: {
|
|
1073
|
+
data?: string | string[];
|
|
1074
|
+
buffer?: number;
|
|
1075
|
+
as?: string;
|
|
1076
|
+
always?: boolean;
|
|
1077
|
+
fallback?: React.ReactNode;
|
|
1078
|
+
children: React.ReactNode;
|
|
1079
|
+
}) {
|
|
1080
|
+
return children;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export function useRemember<T>(initialState: T, _key?: string): [T, React.Dispatch<React.SetStateAction<T>>] {
|
|
1084
|
+
return React.useState(initialState);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1070
1087
|
export { router };
|
|
1071
1088
|
`, "mock/adapters/inertia-react.ts");
|
|
1072
1089
|
}
|
|
@@ -1182,9 +1199,10 @@ import { ContractPage, I18nProvider } from "@middag-io/react";
|
|
|
1182
1199
|
import type { PageContract } from "@middag-io/react";
|
|
1183
1200
|
import { PageProvider } from "@mock/adapters/inertia-react";
|
|
1184
1201
|
import { setMockNavigate } from "@mock/adapters/inertia-core";
|
|
1185
|
-
import {
|
|
1186
|
-
import {
|
|
1187
|
-
import {
|
|
1202
|
+
import { loginContract } from "./pages/login";
|
|
1203
|
+
import { tasksContract } from "./pages/tasks";
|
|
1204
|
+
import { newTaskContract, editTaskContract } from "./pages/task-form";
|
|
1205
|
+
import { taskDetailContract } from "./pages/task-detail";
|
|
1188
1206
|
|
|
1189
1207
|
function NavigateBridge() {
|
|
1190
1208
|
const navigate = useNavigate();
|
|
@@ -1204,12 +1222,10 @@ const sharedProps = {
|
|
|
1204
1222
|
function buildNavigation(activeKey: string) {
|
|
1205
1223
|
return {
|
|
1206
1224
|
tree: [
|
|
1207
|
-
{ key: "
|
|
1208
|
-
{ key: "
|
|
1209
|
-
],
|
|
1210
|
-
footer: [
|
|
1211
|
-
{ key: "system.settings", label: "Settings", icon: "settings", href: "/settings", children: [] },
|
|
1225
|
+
{ key: "tasks", label: "Tasks", icon: "list-check", href: "/", children: [] },
|
|
1226
|
+
{ key: "tasks.new", label: "New task", icon: "plus", href: "/tasks/new", children: [] },
|
|
1212
1227
|
],
|
|
1228
|
+
footer: [],
|
|
1213
1229
|
activeKey,
|
|
1214
1230
|
};
|
|
1215
1231
|
}
|
|
@@ -1229,9 +1245,11 @@ export function App() {
|
|
|
1229
1245
|
<BrowserRouter>
|
|
1230
1246
|
<NavigateBridge />
|
|
1231
1247
|
<Routes>
|
|
1232
|
-
<Route path="/" element={<MockRoute contract={
|
|
1233
|
-
<Route path="/
|
|
1234
|
-
<Route path="/
|
|
1248
|
+
<Route path="/login" element={<MockRoute contract={loginContract} activeKey="" />} />
|
|
1249
|
+
<Route path="/" element={<MockRoute contract={tasksContract} activeKey="tasks" />} />
|
|
1250
|
+
<Route path="/tasks/new" element={<MockRoute contract={newTaskContract} activeKey="tasks.new" />} />
|
|
1251
|
+
<Route path="/tasks/:id/edit" element={<MockRoute contract={editTaskContract} activeKey="tasks" />} />
|
|
1252
|
+
<Route path="/tasks/:id" element={<MockRoute contract={taskDetailContract} activeKey="tasks" />} />
|
|
1235
1253
|
</Routes>
|
|
1236
1254
|
</BrowserRouter>
|
|
1237
1255
|
</I18nProvider>
|
|
@@ -1759,9 +1777,10 @@ createRoot(document.getElementById("root")!).render(
|
|
|
1759
1777
|
`import { useState, useEffect } from "react";
|
|
1760
1778
|
import { ContractPage, I18nProvider } from "@middag-io/react";
|
|
1761
1779
|
import type { PageContract } from "@middag-io/react";
|
|
1762
|
-
import {
|
|
1763
|
-
import {
|
|
1764
|
-
import {
|
|
1780
|
+
import { loginContract } from "./pages/login";
|
|
1781
|
+
import { tasksContract } from "./pages/tasks";
|
|
1782
|
+
import { newTaskContract, editTaskContract } from "./pages/task-form";
|
|
1783
|
+
import { taskDetailContract } from "./pages/task-detail";
|
|
1765
1784
|
|
|
1766
1785
|
/**
|
|
1767
1786
|
* Route map \u2014 hash fragment to PageContract.
|
|
@@ -1771,9 +1790,11 @@ import { settingsContract } from "./pages/settings";
|
|
|
1771
1790
|
* you build them.
|
|
1772
1791
|
*/
|
|
1773
1792
|
const routes: Record<string, PageContract> = {
|
|
1774
|
-
"/":
|
|
1775
|
-
"/
|
|
1776
|
-
"/
|
|
1793
|
+
"/": tasksContract,
|
|
1794
|
+
"/login": loginContract,
|
|
1795
|
+
"/tasks/new": newTaskContract,
|
|
1796
|
+
"/tasks/edit": editTaskContract,
|
|
1797
|
+
"/tasks/show": taskDetailContract,
|
|
1777
1798
|
};
|
|
1778
1799
|
|
|
1779
1800
|
function getRoute(): string {
|
|
@@ -1789,7 +1810,7 @@ export function App() {
|
|
|
1789
1810
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
1790
1811
|
}, []);
|
|
1791
1812
|
|
|
1792
|
-
const contract = routes[route] ||
|
|
1813
|
+
const contract = routes[route] || tasksContract;
|
|
1793
1814
|
|
|
1794
1815
|
// Expose contract for usePage() mock adapter
|
|
1795
1816
|
(window as any).__MIDDAG_MOCK_CONTRACT__ = contract;
|
|
@@ -1824,41 +1845,23 @@ import { router } from "./inertia-core";
|
|
|
1824
1845
|
function getActiveKey(): string {
|
|
1825
1846
|
const hash = window.location.hash.replace("#", "") || "/";
|
|
1826
1847
|
const map: Record<string, string> = {
|
|
1827
|
-
"/": "
|
|
1828
|
-
"/
|
|
1829
|
-
"/settings": "system.settings",
|
|
1848
|
+
"/": "tasks",
|
|
1849
|
+
"/tasks/new": "tasks.new",
|
|
1830
1850
|
};
|
|
1831
|
-
return map[hash] || "
|
|
1851
|
+
return map[hash] || "tasks";
|
|
1832
1852
|
}
|
|
1833
1853
|
|
|
1834
1854
|
function buildNavigation() {
|
|
1835
1855
|
const activeKey = getActiveKey();
|
|
1836
1856
|
const sections = [
|
|
1837
1857
|
{
|
|
1838
|
-
key: "
|
|
1839
|
-
label: "
|
|
1840
|
-
icon: "
|
|
1858
|
+
key: "tasks",
|
|
1859
|
+
label: "Tasks",
|
|
1860
|
+
icon: "list-check",
|
|
1841
1861
|
group: "main" as const,
|
|
1842
1862
|
items: [
|
|
1843
|
-
{ key: "
|
|
1844
|
-
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
key: "integration",
|
|
1848
|
-
label: "Integration",
|
|
1849
|
-
icon: "plug",
|
|
1850
|
-
group: "main" as const,
|
|
1851
|
-
items: [
|
|
1852
|
-
{ key: "integration.connectors", label: "Connectors", href: "#/connectors", active: activeKey === "integration.connectors", children: [] },
|
|
1853
|
-
],
|
|
1854
|
-
},
|
|
1855
|
-
{
|
|
1856
|
-
key: "system",
|
|
1857
|
-
label: "System",
|
|
1858
|
-
icon: "settings",
|
|
1859
|
-
group: "system" as const,
|
|
1860
|
-
items: [
|
|
1861
|
-
{ key: "system.settings", label: "Settings", href: "#/settings", active: activeKey === "system.settings", children: [] },
|
|
1863
|
+
{ key: "tasks", label: "Tasks", href: "#/", active: activeKey === "tasks", children: [] },
|
|
1864
|
+
{ key: "tasks.new", label: "New task", href: "#/tasks/new", active: activeKey === "tasks.new", children: [] },
|
|
1862
1865
|
],
|
|
1863
1866
|
},
|
|
1864
1867
|
];
|
|
@@ -1916,6 +1919,34 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
|
|
|
1916
1919
|
return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
1917
1920
|
});
|
|
1918
1921
|
|
|
1922
|
+
// ── Inertia v3 primitives the engine imports ──────────────────────────
|
|
1923
|
+
// @middag-io/react imports Deferred/WhenVisible + useRemember from
|
|
1924
|
+
// @inertiajs/react; mock renders children eagerly, useRemember = useState.
|
|
1925
|
+
|
|
1926
|
+
export function Deferred({ children }: {
|
|
1927
|
+
data: string | string[];
|
|
1928
|
+
fallback?: React.ReactNode;
|
|
1929
|
+
rescue?: React.ReactNode;
|
|
1930
|
+
children: React.ReactNode;
|
|
1931
|
+
}) {
|
|
1932
|
+
return children;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
export function WhenVisible({ children }: {
|
|
1936
|
+
data?: string | string[];
|
|
1937
|
+
buffer?: number;
|
|
1938
|
+
as?: string;
|
|
1939
|
+
always?: boolean;
|
|
1940
|
+
fallback?: React.ReactNode;
|
|
1941
|
+
children: React.ReactNode;
|
|
1942
|
+
}) {
|
|
1943
|
+
return children;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
export function useRemember<T>(initialState: T, _key?: string): [T, React.Dispatch<React.SetStateAction<T>>] {
|
|
1947
|
+
return React.useState(initialState);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1919
1950
|
export { router };
|
|
1920
1951
|
`,
|
|
1921
1952
|
"mock/adapters/inertia-react.ts",
|
|
@@ -1933,15 +1964,27 @@ export { router };
|
|
|
1933
1964
|
* Vite alias redirects @inertiajs/core imports here.
|
|
1934
1965
|
* In production, the real Inertia package handles this.
|
|
1935
1966
|
*/
|
|
1967
|
+
type MockOpts = {
|
|
1968
|
+
only?: string[];
|
|
1969
|
+
preserveState?: boolean;
|
|
1970
|
+
preserveScroll?: boolean;
|
|
1971
|
+
onSuccess?: () => void;
|
|
1972
|
+
onFinish?: () => void;
|
|
1973
|
+
onError?: (errors: Record<string, string>) => void;
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1976
|
+
const settle = (o?: MockOpts) => { o?.onSuccess?.(); o?.onFinish?.(); };
|
|
1977
|
+
|
|
1936
1978
|
export const router = {
|
|
1937
|
-
get: (url: string) => { window.location.hash = url; },
|
|
1938
|
-
post: (url: string) => { console.log("[mock] POST", url); },
|
|
1939
|
-
put: (url: string) => { console.log("[mock] PUT", url); },
|
|
1940
|
-
patch: (url: string) => { console.log("[mock] PATCH", url); },
|
|
1941
|
-
delete: (url: string) => { console.log("[mock] DELETE", url); },
|
|
1942
|
-
reload: () => {
|
|
1979
|
+
get: (url: string, _params?: Record<string, unknown>, opts?: MockOpts) => { window.location.hash = url; settle(opts); },
|
|
1980
|
+
post: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] POST", url); settle(opts); },
|
|
1981
|
+
put: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PUT", url); settle(opts); },
|
|
1982
|
+
patch: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PATCH", url); settle(opts); },
|
|
1983
|
+
delete: (url: string, opts?: MockOpts) => { console.log("[mock] DELETE", url); settle(opts); },
|
|
1984
|
+
reload: (opts?: MockOpts) => { settle(opts); },
|
|
1943
1985
|
visit: (url: string) => { window.location.hash = url; },
|
|
1944
|
-
on: () => () => {},
|
|
1986
|
+
on: (_event?: string, _cb?: (event: unknown) => void) => () => {},
|
|
1987
|
+
poll: (_intervalMs?: number, _reload?: unknown, _opts?: unknown) => ({ stop: () => {}, start: () => {}, destroy: () => {} }),
|
|
1945
1988
|
};
|
|
1946
1989
|
`,
|
|
1947
1990
|
"mock/adapters/inertia-core.ts",
|
package/lib/scaffoldPRO.js
CHANGED
|
@@ -171,7 +171,7 @@ export function scaffoldMockEntities(targetDir) {
|
|
|
171
171
|
/**
|
|
172
172
|
* Move page contract examples to `mock/page-contracts/` for PRO path.
|
|
173
173
|
*
|
|
174
|
-
* In PRO, the
|
|
174
|
+
* In PRO, the demo contracts (login, tasks, task-form, task-detail) live
|
|
175
175
|
* in mock/page-contracts/ instead of src/pages/ because they are dev-only
|
|
176
176
|
* mock data, not production React components.
|
|
177
177
|
*
|
|
@@ -186,7 +186,7 @@ export function scaffoldMockPageContracts(targetDir) {
|
|
|
186
186
|
const destDir = join(targetDir, "mock", "page-contracts");
|
|
187
187
|
ensureDir(destDir);
|
|
188
188
|
|
|
189
|
-
const files = ["
|
|
189
|
+
const files = ["login.ts", "tasks.ts", "task-form.ts", "task-detail.ts"];
|
|
190
190
|
|
|
191
191
|
for (const file of files) {
|
|
192
192
|
const src = join(srcDir, file);
|
|
@@ -9,29 +9,21 @@ export function buildNavigation(activeKey: string) {
|
|
|
9
9
|
return {
|
|
10
10
|
tree: [
|
|
11
11
|
{
|
|
12
|
-
key: "
|
|
13
|
-
label: "
|
|
14
|
-
icon: "
|
|
12
|
+
key: "tasks",
|
|
13
|
+
label: "Tasks",
|
|
14
|
+
icon: "list-check",
|
|
15
15
|
href: "/",
|
|
16
16
|
children: [],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
key: "
|
|
20
|
-
label: "
|
|
21
|
-
icon: "
|
|
22
|
-
href: "/
|
|
23
|
-
children: [],
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
footer: [
|
|
27
|
-
{
|
|
28
|
-
key: "system.settings",
|
|
29
|
-
label: "Settings",
|
|
30
|
-
icon: "settings",
|
|
31
|
-
href: "/settings",
|
|
19
|
+
key: "tasks.new",
|
|
20
|
+
label: "New task",
|
|
21
|
+
icon: "plus",
|
|
22
|
+
href: "/tasks/new",
|
|
32
23
|
children: [],
|
|
33
24
|
},
|
|
34
25
|
],
|
|
26
|
+
footer: [],
|
|
35
27
|
activeKey,
|
|
36
28
|
};
|
|
37
29
|
}
|
|
@@ -11,9 +11,10 @@ import type { PageContract } from "@middag-io/react";
|
|
|
11
11
|
import { mockEntities } from "./entities";
|
|
12
12
|
import { buildNavigation } from "./navigation";
|
|
13
13
|
import { sharedProps, mockDemoPageProps } from "./data";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { loginContract } from "./page-contracts/login";
|
|
15
|
+
import { tasksContract } from "./page-contracts/tasks";
|
|
16
|
+
import { newTaskContract, editTaskContract } from "./page-contracts/task-form";
|
|
17
|
+
import { taskDetailContract } from "./page-contracts/task-detail";
|
|
17
18
|
import DemoPage from "../src/pages/DemoPage";
|
|
18
19
|
|
|
19
20
|
// Resolve shell at module level (not inside render — avoids react-hooks/static-components)
|
|
@@ -63,24 +64,26 @@ export function AppRoutes() {
|
|
|
63
64
|
return (
|
|
64
65
|
<>
|
|
65
66
|
{/* Contract pages (rendered by lib ContractPage) */}
|
|
67
|
+
<Route path="/login" element={<MockRoute contract={loginContract} activeKey="" />} />
|
|
68
|
+
<Route path="/" element={<MockRoute contract={tasksContract} activeKey="tasks" />} />
|
|
66
69
|
<Route
|
|
67
|
-
path="/"
|
|
68
|
-
element={<MockRoute contract={
|
|
70
|
+
path="/tasks/new"
|
|
71
|
+
element={<MockRoute contract={newTaskContract} activeKey="tasks.new" />}
|
|
69
72
|
/>
|
|
70
73
|
<Route
|
|
71
|
-
path="/
|
|
72
|
-
element={<MockRoute contract={
|
|
74
|
+
path="/tasks/:id/edit"
|
|
75
|
+
element={<MockRoute contract={editTaskContract} activeKey="tasks" />}
|
|
73
76
|
/>
|
|
74
77
|
<Route
|
|
75
|
-
path="/
|
|
76
|
-
element={<MockRoute contract={
|
|
78
|
+
path="/tasks/:id"
|
|
79
|
+
element={<MockRoute contract={taskDetailContract} activeKey="tasks" />}
|
|
77
80
|
/>
|
|
78
81
|
|
|
79
82
|
{/* Direct page (custom React component) */}
|
|
80
83
|
<Route
|
|
81
84
|
path="/demo"
|
|
82
85
|
element={
|
|
83
|
-
<MockDirectRoute activeKey="
|
|
86
|
+
<MockDirectRoute activeKey="tasks" pageProps={mockDemoPageProps}>
|
|
84
87
|
<DemoPage />
|
|
85
88
|
</MockDirectRoute>
|
|
86
89
|
}
|
|
@@ -15,6 +15,8 @@ import { usePage } from "@inertiajs/react";
|
|
|
15
15
|
interface DemoPageProps {
|
|
16
16
|
title: string;
|
|
17
17
|
items: Array<{ id: number; name: string; status: string }>;
|
|
18
|
+
// Inertia v3 usePage<T> constrains T to PageProps (string index signature).
|
|
19
|
+
[key: string]: unknown;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export default function DemoPage() {
|
|
@@ -15,14 +15,16 @@ import {
|
|
|
15
15
|
SidebarHeader,
|
|
16
16
|
SidebarProvider,
|
|
17
17
|
useSidebar,
|
|
18
|
-
SidebarNav,
|
|
19
|
-
PageHeader,
|
|
20
18
|
NavErrorBoundary,
|
|
21
19
|
type ShellProps,
|
|
22
20
|
type SharedProps,
|
|
23
21
|
type PageMeta,
|
|
24
22
|
type AdminTabsProps,
|
|
25
23
|
} from "@middag-io/react";
|
|
24
|
+
// SidebarNav/PageHeader are Pro chrome — exported from the react-pro runtime
|
|
25
|
+
// subpath (see react-demo MockProductShell). This shell is PRO-only (scaffolded
|
|
26
|
+
// only when isPro in cli.js); FREE Moodle uses BasicShell via registerDefaults().
|
|
27
|
+
import { SidebarNav, PageHeader } from "@middag-io/react-pro/runtime";
|
|
26
28
|
import { Tabs, TabsList, TabsTrigger } from "@middag-io/react/reui/tabs";
|
|
27
29
|
|
|
28
30
|
// ── Inner shell (must be inside SidebarProvider) ──────────────────────────────
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {PageContract} from "@middag-io/react";
|
|
2
2
|
import {ContractPage} from "@middag-io/react";
|
|
3
3
|
import {usePage} from "@inertiajs/react";
|
|
4
|
+
import type {ComponentType} from "react";
|
|
4
5
|
|
|
5
6
|
// Direct pages — eager loaded (custom React pages in pages/)
|
|
6
7
|
const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Record<
|
|
7
8
|
string,
|
|
8
|
-
|
|
9
|
+
{ default: ComponentType }
|
|
9
10
|
>;
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -14,15 +15,11 @@ const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Re
|
|
|
14
15
|
*/
|
|
15
16
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
16
17
|
function InertiaContractPage() {
|
|
17
|
-
const { props } = usePage<{
|
|
18
|
-
|
|
19
|
-
help?: ContractPageProps["help"];
|
|
20
|
-
inspector?: ContractPageProps["inspector"];
|
|
21
|
-
}>();
|
|
22
|
-
return <ContractPage contract={props.contract} help={props.help} inspector={props.inspector} />;
|
|
18
|
+
const { props } = usePage<{ contract: PageContract }>();
|
|
19
|
+
return <ContractPage contract={props.contract} />;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
const contractPageModule = { default: InertiaContractPage };
|
|
22
|
+
const contractPageModule: { default: ComponentType } = { default: InertiaContractPage };
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* Resolve an Inertia page name to a React component module.
|
|
@@ -38,7 +35,7 @@ const contractPageModule = { default: InertiaContractPage };
|
|
|
38
35
|
*
|
|
39
36
|
* Direct pages can also use ContractPage internally (hybrid pattern).
|
|
40
37
|
*/
|
|
41
|
-
export function resolvePageComponent(name: string):
|
|
38
|
+
export function resolvePageComponent(name: string): { default: ComponentType } {
|
|
42
39
|
// Direct page: matching .tsx file in pages/
|
|
43
40
|
const path = `../pages/${name}.tsx`;
|
|
44
41
|
const page = directPages[path];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* register — registration for this plugin's UI.
|
|
3
3
|
*
|
|
4
|
-
* Registers the
|
|
5
|
-
* fields and icons. The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Registers the 13 standard blocks plus shells, layouts, cell renderers, form
|
|
5
|
+
* fields and icons. The 6 Pro/interactive blocks (chart_panel, kanban_board,
|
|
6
|
+
* flow_editor, form_builder, condition_tree, sentence_builder) ship in
|
|
7
|
+
* @middag-io/react-pro and register via its registerProDefaults().
|
|
8
8
|
*
|
|
9
9
|
* For lean IIFE bundles (WordPress/Moodle), trim the blocks you don't use.
|
|
10
10
|
*
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
CardGridBlock,
|
|
40
40
|
ActionGridBlock,
|
|
41
41
|
LinkListBlock,
|
|
42
|
+
FormPanelBlock,
|
|
42
43
|
} from "@middag-io/react";
|
|
43
44
|
|
|
44
45
|
let registered = false;
|
|
@@ -71,6 +72,8 @@ export function registerDefaults(): void {
|
|
|
71
72
|
registerBlock("card_grid", CardGridBlock);
|
|
72
73
|
registerBlock("action_grid", ActionGridBlock);
|
|
73
74
|
registerBlock("link_list", LinkListBlock);
|
|
75
|
+
// form_panel pulls react-hook-form + zod; drop it if this bundle has no forms.
|
|
76
|
+
registerBlock("form_panel", FormPanelBlock);
|
|
74
77
|
|
|
75
78
|
// Cell renderers (status, timestamp, link, boolean, etc.)
|
|
76
79
|
registerDefaultCells();
|