blixify-ui-web-mcp 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +5 -0
  2. package/lib/components/action/button/README.md +75 -0
  3. package/lib/components/action/button/button.stories.tsx +138 -0
  4. package/lib/components/action/buttonGroup/README.md +79 -0
  5. package/lib/components/action/buttonGroup/buttonGroup.stories.tsx +104 -0
  6. package/lib/components/action/iconButton/README.md +39 -0
  7. package/lib/components/action/iconButton/iconButton.stories.tsx +22 -0
  8. package/lib/components/action/radioList/README.md +98 -0
  9. package/lib/components/action/radioList/radioList.stories.tsx +181 -0
  10. package/lib/components/action/slider/README.md +86 -0
  11. package/lib/components/action/slider/slider.stories.tsx +133 -0
  12. package/lib/components/action/textButton/README.md +34 -0
  13. package/lib/components/action/textButton/textButton.stories.tsx +16 -0
  14. package/lib/components/action/toggle/README.md +39 -0
  15. package/lib/components/action/toggle/toggle.stories.tsx +23 -0
  16. package/lib/components/advanced/collapse/README.md +106 -0
  17. package/lib/components/advanced/collapse/collapse.stories.tsx +165 -0
  18. package/lib/components/advanced/infoWindow/README.md +69 -0
  19. package/lib/components/advanced/infoWindow/infoWindow.stories.tsx +112 -0
  20. package/lib/components/advanced/popUp/README.md +95 -0
  21. package/lib/components/advanced/popUp/popUp.stories.tsx +189 -0
  22. package/lib/components/auth/bareSignIn/README.md +78 -0
  23. package/lib/components/auth/bareSignIn/bareSignIn.stories.tsx +141 -0
  24. package/lib/components/auth/bareSignUp/README.md +130 -0
  25. package/lib/components/auth/bareSignUp/bareSignUp.stories.tsx +159 -0
  26. package/lib/components/auth/signIn/README.md +53 -0
  27. package/lib/components/auth/signIn/signIn.stories.tsx +120 -0
  28. package/lib/components/auth/signUp/README.md +136 -0
  29. package/lib/components/auth/signUp/signUp.stories.tsx +193 -0
  30. package/lib/components/chart/README.md +13 -0
  31. package/lib/components/chart/areaChart/README.md +133 -0
  32. package/lib/components/chart/areaChart/areaChart.stories.tsx +199 -0
  33. package/lib/components/chart/barChart/README.md +193 -0
  34. package/lib/components/chart/barChart/barChart.stories.tsx +318 -0
  35. package/lib/components/chart/boxPlotChart/README.md +124 -0
  36. package/lib/components/chart/boxPlotChart/boxPlotChart.stories.tsx +104 -0
  37. package/lib/components/chart/bubbleChart/README.md +166 -0
  38. package/lib/components/chart/bubbleChart/bubbleChart.stories.tsx +144 -0
  39. package/lib/components/chart/card/README.md +107 -0
  40. package/lib/components/chart/card/card.stories.tsx +89 -0
  41. package/lib/components/chart/ganttChart/README.md +142 -0
  42. package/lib/components/chart/ganttChart/ganttChart.stories.tsx +138 -0
  43. package/lib/components/chart/heatmapChart/README.md +125 -0
  44. package/lib/components/chart/heatmapChart/heatmapChart.stories.tsx +106 -0
  45. package/lib/components/chart/lineChart/README.md +140 -0
  46. package/lib/components/chart/lineChart/lineChart.stories.tsx +152 -0
  47. package/lib/components/chart/pieChart/README.md +117 -0
  48. package/lib/components/chart/pieChart/pieChart.stories.tsx +180 -0
  49. package/lib/components/chart/polarAreaChart/README.md +126 -0
  50. package/lib/components/chart/polarAreaChart/polarAreaChart.stories.tsx +106 -0
  51. package/lib/components/chart/radarChart/README.md +57 -0
  52. package/lib/components/chart/radarChart/radarChart.stories.tsx +36 -0
  53. package/lib/components/chart/radialBar/README.md +152 -0
  54. package/lib/components/chart/radialBar/radialChart.stories.tsx +136 -0
  55. package/lib/components/chart/rangeBarChart/README.md +323 -0
  56. package/lib/components/chart/rangeBarChart/rangeBarChart.stories.tsx +208 -0
  57. package/lib/components/chart/statsCard/README.md +97 -0
  58. package/lib/components/chart/statsCard/statsCard.stories.tsx +183 -0
  59. package/lib/components/chart/treemapChart/README.md +142 -0
  60. package/lib/components/chart/treemapChart/treemapChart.stories.tsx +122 -0
  61. package/lib/components/chat/chatAnchor/README.md +113 -0
  62. package/lib/components/chat/chatAnchor/chatAnchor.stories.tsx +156 -0
  63. package/lib/components/chat/chatBase/README.md +223 -0
  64. package/lib/components/chat/chatBase/chatBase.stories.tsx +306 -0
  65. package/lib/components/chat/chatDashboard/README.md +161 -0
  66. package/lib/components/chat/chatDashboard/chatDashboard.stories.tsx +154 -0
  67. package/lib/components/data/dataGraph/README.md +13 -0
  68. package/lib/components/data/dataGraph/dataGraph.stories.tsx +84 -0
  69. package/lib/components/data/dataReference/README.md +62 -0
  70. package/lib/components/data/dataReference/dataReference.stories.tsx +53 -0
  71. package/lib/components/data/dataTemplate/README.md +903 -0
  72. package/lib/components/data/dataTemplate/dataModule.stories.tsx +196 -0
  73. package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +337 -0
  74. package/lib/components/data/dataWrapper/README.md +1077 -0
  75. package/lib/components/data/dataWrapper/dataWrapper.stories.tsx +831 -0
  76. package/lib/components/data/queryForm/README.md +56 -0
  77. package/lib/components/data/queryForm/queryForm.stories.tsx +85 -0
  78. package/lib/components/data/readQuery/README.md +282 -0
  79. package/lib/components/data/readQuery/readQuery.stories.tsx +256 -0
  80. package/lib/components/data/readQueryHoc/README.md +89 -0
  81. package/lib/components/data/readQueryHoc/readQueryHoc.stories.tsx +33 -0
  82. package/lib/components/data/writeQuery/README.md +178 -0
  83. package/lib/components/data/writeQuery/writeQuery.stories.tsx +209 -0
  84. package/lib/components/design/actionSection/README.md +47 -0
  85. package/lib/components/design/actionSection/actionSection.stories.tsx +74 -0
  86. package/lib/components/design/banner/README.md +43 -0
  87. package/lib/components/design/banner/banner.stories.tsx +34 -0
  88. package/lib/components/design/bannerAction/README.md +44 -0
  89. package/lib/components/design/bannerAction/bannerAction.stories.tsx +25 -0
  90. package/lib/components/design/blogSection/README.md +111 -0
  91. package/lib/components/design/blogSection/blogSection.stories.tsx +72 -0
  92. package/lib/components/design/carouselSection/README.md +82 -0
  93. package/lib/components/design/carouselSection/carouselSection.stories.tsx +52 -0
  94. package/lib/components/design/designList/README.md +60 -0
  95. package/lib/components/design/designList/designList.stories.tsx +44 -0
  96. package/lib/components/design/faq/README.md +121 -0
  97. package/lib/components/design/faq/faq.stories.tsx +103 -0
  98. package/lib/components/design/fileGrid/README.md +87 -0
  99. package/lib/components/design/fileGrid/fileGrid.stories.tsx +110 -0
  100. package/lib/components/design/fileViewer/README.md +40 -0
  101. package/lib/components/design/fileViewer/fileViewer.stories.tsx +28 -0
  102. package/lib/components/design/footerSection/README.md +62 -0
  103. package/lib/components/design/footerSection/footerSection.stories.tsx +84 -0
  104. package/lib/components/design/googleReview/README.md +45 -0
  105. package/lib/components/design/googleReview/googleReview.stories.tsx +33 -0
  106. package/lib/components/design/heroSection/README.md +48 -0
  107. package/lib/components/design/heroSection/heroSection.stories.tsx +85 -0
  108. package/lib/components/design/iconList/README.md +60 -0
  109. package/lib/components/design/iconList/iconList.stories.tsx +41 -0
  110. package/lib/components/design/imageGallery/README.md +56 -0
  111. package/lib/components/design/imageGallery/imageGallery.stories.tsx +45 -0
  112. package/lib/components/design/imageZoom/README.md +47 -0
  113. package/lib/components/design/imageZoom/imageZoom.stories.tsx +30 -0
  114. package/lib/components/design/leftContent/README.md +47 -0
  115. package/lib/components/design/leftContent/leftContent.stories.tsx +32 -0
  116. package/lib/components/design/logoCloud/README.md +42 -0
  117. package/lib/components/design/logoCloud/logoCloud.stories.tsx +44 -0
  118. package/lib/components/design/onboarding/README.md +100 -0
  119. package/lib/components/design/onboarding/onboarding.stories.tsx +113 -0
  120. package/lib/components/design/pricing/README.md +112 -0
  121. package/lib/components/design/pricing/pricing.stories.tsx +86 -0
  122. package/lib/components/design/rightContent/README.md +47 -0
  123. package/lib/components/design/rightContent/rightContent.stories.tsx +32 -0
  124. package/lib/components/design/sectionList/README.md +51 -0
  125. package/lib/components/design/sectionList/sectionList.stories.tsx +35 -0
  126. package/lib/components/design/teams/README.md +62 -0
  127. package/lib/components/design/teams/teams.stories.tsx +61 -0
  128. package/lib/components/display/alert/README.md +82 -0
  129. package/lib/components/display/alert/alert.stories.tsx +71 -0
  130. package/lib/components/display/calendar/README.md +113 -0
  131. package/lib/components/display/calendar/calendar.stories.tsx +159 -0
  132. package/lib/components/display/clipboard/README.md +36 -0
  133. package/lib/components/display/clipboard/clipboard.stories.tsx +32 -0
  134. package/lib/components/display/detailList/README.md +61 -0
  135. package/lib/components/display/detailList/detailList.stories.tsx +131 -0
  136. package/lib/components/display/drawer/README.md +74 -0
  137. package/lib/components/display/drawer/drawer.stories.tsx +66 -0
  138. package/lib/components/display/emptyState/README.md +38 -0
  139. package/lib/components/display/emptyState/emptyState.stories.tsx +22 -0
  140. package/lib/components/display/feeds/README.md +80 -0
  141. package/lib/components/display/feeds/feeds.stories.tsx +75 -0
  142. package/lib/components/display/flexTable/README.md +160 -0
  143. package/lib/components/display/flexTable/flexTable.stories.tsx +313 -0
  144. package/lib/components/display/grid/README.md +121 -0
  145. package/lib/components/display/grid/grid.stories.tsx +234 -0
  146. package/lib/components/display/kanban/README.md +78 -0
  147. package/lib/components/display/kanban/kanban.stories.tsx +133 -0
  148. package/lib/components/display/list/README.md +109 -0
  149. package/lib/components/display/list/list.stories.tsx +188 -0
  150. package/lib/components/display/loading/README.md +25 -0
  151. package/lib/components/display/loading/loading.stories.tsx +15 -0
  152. package/lib/components/display/mapView/README.md +99 -0
  153. package/lib/components/display/mapView/mapView.stories.tsx +225 -0
  154. package/lib/components/display/modal/README.md +43 -0
  155. package/lib/components/display/modal/modal.stories.tsx +81 -0
  156. package/lib/components/display/notification/README.md +43 -0
  157. package/lib/components/display/notification/notification.stories.tsx +30 -0
  158. package/lib/components/display/progress/README.md +25 -0
  159. package/lib/components/display/progress/progress.stories.tsx +15 -0
  160. package/lib/components/display/stepper/README.md +96 -0
  161. package/lib/components/display/stepper/stepper.stories.tsx +175 -0
  162. package/lib/components/display/table/README.md +176 -0
  163. package/lib/components/display/table/table.stories.tsx +330 -0
  164. package/lib/components/display/tag/README.md +74 -0
  165. package/lib/components/display/tag/tag.stories.tsx +79 -0
  166. package/lib/components/input/address/README.md +132 -0
  167. package/lib/components/input/address/address.stories.tsx +147 -0
  168. package/lib/components/input/canvas/README.md +131 -0
  169. package/lib/components/input/canvas/canvas.stories.tsx +165 -0
  170. package/lib/components/input/checkbox/README.md +61 -0
  171. package/lib/components/input/checkbox/checkbox.stories.tsx +30 -0
  172. package/lib/components/input/datePicker/README.md +124 -0
  173. package/lib/components/input/datePicker/datePicker.stories.tsx +197 -0
  174. package/lib/components/input/form/README.md +55 -0
  175. package/lib/components/input/form/form.stories.tsx +143 -0
  176. package/lib/components/input/markdown/README.md +146 -0
  177. package/lib/components/input/markdown/markdown.stories.tsx +40 -0
  178. package/lib/components/input/monthYearPicker/README.md +69 -0
  179. package/lib/components/input/monthYearPicker/monthYearPicker.stories.tsx +47 -0
  180. package/lib/components/input/netPromoterScore/README.md +52 -0
  181. package/lib/components/input/netPromoterScore/netPromoterScore.stories.tsx +30 -0
  182. package/lib/components/input/rating/README.md +51 -0
  183. package/lib/components/input/rating/rating.stories.tsx +30 -0
  184. package/lib/components/input/select/README.md +108 -0
  185. package/lib/components/input/select/select.stories.tsx +286 -0
  186. package/lib/components/input/signature/README.md +106 -0
  187. package/lib/components/input/signature/signature.stories.tsx +188 -0
  188. package/lib/components/input/textArea/README.md +55 -0
  189. package/lib/components/input/textArea/textArea.stories.tsx +30 -0
  190. package/lib/components/input/textInput/README.md +81 -0
  191. package/lib/components/input/textInput/textinput.stories.tsx +231 -0
  192. package/lib/components/input/uploadInput/README.md +134 -0
  193. package/lib/components/input/uploadInput/uploadInput.stories.tsx +193 -0
  194. package/lib/components/navigation/bars/README.md +40 -0
  195. package/lib/components/navigation/bars/bars.stories.tsx +85 -0
  196. package/lib/components/navigation/bottomTab/README.md +53 -0
  197. package/lib/components/navigation/bottomTab/bottomTab.stories.tsx +32 -0
  198. package/lib/components/navigation/breadcrumb/README.md +48 -0
  199. package/lib/components/navigation/breadcrumb/breadcrumb.stories.tsx +27 -0
  200. package/lib/components/navigation/header/README.md +149 -0
  201. package/lib/components/navigation/header/header.stories.tsx +107 -0
  202. package/lib/components/navigation/leftNav/README.md +91 -0
  203. package/lib/components/navigation/leftNav/leftNav.stories.tsx +106 -0
  204. package/lib/components/navigation/tabs/README.md +75 -0
  205. package/lib/components/navigation/tabs/tabs.stories.tsx +60 -0
  206. package/lib/components/structure/column/README.md +40 -0
  207. package/lib/components/structure/column/column.stories.tsx +26 -0
  208. package/lib/components/structure/container/README.md +34 -0
  209. package/lib/components/structure/container/container.stories.tsx +19 -0
  210. package/lib/components/structure/link/README.md +55 -0
  211. package/lib/components/structure/link/link.stories.tsx +30 -0
  212. package/lib/components/structure/text/README.md +42 -0
  213. package/lib/components/structure/text/text.stories.tsx +39 -0
  214. package/lib/components/template/authPage/authPage.stories.tsx +68 -0
  215. package/lib/components/template/landingPage/landingPage.stories.tsx +288 -0
  216. package/lib/components/template/productListing/productListing.stories.tsx +297 -0
  217. package/lib/components/template/productOverview/productOverview.stories.tsx +194 -0
  218. package/lib/components/template/shoppingCart/shoppingCart.stories.tsx +252 -0
  219. package/lib/components/template/userHomePage/userHomePage.stories.tsx +605 -0
  220. package/lib/components/template/userRankingDashboard/userRankingDashboard.stories.tsx +481 -0
  221. package/lib/mcp/server.d.ts +2 -0
  222. package/lib/mcp/server.d.ts.map +1 -0
  223. package/lib/mcp/server.js +33 -0
  224. package/lib/mcp/server.js.map +1 -0
  225. package/lib/mcp/tools/getBackendComponent.d.ts +13 -0
  226. package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -0
  227. package/lib/mcp/tools/getBackendComponent.js +119 -0
  228. package/lib/mcp/tools/getBackendComponent.js.map +1 -0
  229. package/lib/mcp/tools/getComponent.d.ts +17 -0
  230. package/lib/mcp/tools/getComponent.d.ts.map +1 -0
  231. package/lib/mcp/tools/getComponent.js +118 -0
  232. package/lib/mcp/tools/getComponent.js.map +1 -0
  233. package/lib/mcp/tools/listComponents.d.ts +13 -0
  234. package/lib/mcp/tools/listComponents.d.ts.map +1 -0
  235. package/lib/mcp/tools/listComponents.js +113 -0
  236. package/lib/mcp/tools/listComponents.js.map +1 -0
  237. package/lib/mcp/tools/listTemplates.d.ts +13 -0
  238. package/lib/mcp/tools/listTemplates.d.ts.map +1 -0
  239. package/lib/mcp/tools/listTemplates.js +68 -0
  240. package/lib/mcp/tools/listTemplates.js.map +1 -0
  241. package/lib/mcp/tools/suggestTemplate.d.ts +17 -0
  242. package/lib/mcp/tools/suggestTemplate.d.ts.map +1 -0
  243. package/lib/mcp/tools/suggestTemplate.js +212 -0
  244. package/lib/mcp/tools/suggestTemplate.js.map +1 -0
  245. package/package.json +23 -0
@@ -0,0 +1,1077 @@
1
+ # Data Wrapper
2
+
3
+ <!-- STORY -->
4
+
5
+ <hr>
6
+
7
+ `DataTemplateWrapper` is a simplified wrapper around `DataTemplate` designed for embedding data views inside existing dashboards or app pages — without needing to manage routing, backend config, or library wiring yourself.
8
+
9
+ For most custom integrations, **`DataTemplateWrapper` is the recommended starting point**. You define the schema directly in code, pass a `workspace` config instead of building `devSettings` manually, and the wrapper handles axios, headlessUI, and papaparse internally. Use `DataTemplate` directly only when you have a Space object from the Blixify backend and want to drive the schema and config from the database.
10
+
11
+ ---
12
+
13
+ ### Props from Data Template that do NOT apply to DataTemplateWrapper
14
+
15
+ These `DataTemplate` props are handled internally and cannot be passed:
16
+
17
+ - `devSettings` — replaced by `workspace`
18
+ - `lib.axios`, `lib.Menu`, `lib.Transition`, `lib.papaParser` — always bundled
19
+ - `localURLEndpoint` — always `""`
20
+ - `model` — replaced by `schema`
21
+ - `onComplete` — replaced by `handleComplete`
22
+ - `organise.handleNavigate` — replaced by top-level `handleNavigate`
23
+ - `organise.additionalQuery` — replaced by top-level `additionalQuery`
24
+ - `organise.sort` — replaced by top-level `sort`
25
+ - `organise.extendedTabs` / `organise.viewId` — replaced by `viewSettings`
26
+
27
+ You can still pass `organise` for everything else (`breadcrumbType`, `tabAlign`, `splitView`, `exportSettings`, etc.).
28
+
29
+ ---
30
+
31
+ ## Import
32
+
33
+ ```js
34
+ import { DataTemplateWrapper } from "blixify-ui-web/data/dataWrapper";
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Step-by-Step Setup
40
+
41
+ **1. Create a ref and navigation state**
42
+
43
+ The wrapper is a class component, so use `useRef` to access its imperative methods. Store `type` and `id` in state — these drive which view is rendered.
44
+
45
+ ```tsx
46
+ const dataRef = useRef<any>(null);
47
+ const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
48
+ const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
49
+ ```
50
+
51
+ **2. Define your schema**
52
+
53
+ Same shape as `DataTemplate`'s `model`. Define the columns you want to display and edit.
54
+
55
+ ```tsx
56
+ const schema: DataStructure[] = [
57
+ { id: "name", name: "Name", type: "string" },
58
+ {
59
+ id: "status",
60
+ name: "Status",
61
+ type: "list",
62
+ listSelection: ["active", "inactive"],
63
+ },
64
+ { id: "date", name: "Date", type: "date" },
65
+ ];
66
+ ```
67
+
68
+ **3. Wire navigation**
69
+
70
+ The wrapper calls `handleNavigate` whenever the user clicks a row, a breadcrumb, or a create button. Update your state here.
71
+
72
+ ```tsx
73
+ const handleNavigate = (type: string, id?: string) => {
74
+ setViewType(type as any);
75
+ if (id !== undefined) setSelectedId(id);
76
+ };
77
+ ```
78
+
79
+ You can also use `handleNavigate` to redirect to a different page entirely instead of updating local state — useful when the read or update view lives on a separate route:
80
+
81
+ ```tsx
82
+ const handleNavigate = (type: string, id?: string) => {
83
+ if (type === "read" && id) {
84
+ router.push(`/records/${id}`); // React Router / Next.js
85
+ // or: window.location.href = `/records/${id}`;
86
+ } else if (type === "update" && id) {
87
+ router.push(`/records/${id}/edit`);
88
+ } else {
89
+ setViewType("list");
90
+ setSelectedId(undefined);
91
+ }
92
+ };
93
+ ```
94
+
95
+ **4. Configure the workspace**
96
+
97
+ Pass your backend connection details via `workspace`. No need to build `devSettings` manually.
98
+
99
+ ```tsx
100
+ const workspace = {
101
+ apiEndpointType: "mongoDB",
102
+ apiEndpoint: "https://api.example.com/api/mongoData",
103
+ apiEndpointToken: "your-token",
104
+ apiUserToken: "",
105
+ assetEndpoint: "https://storage.example.com/o/",
106
+ assetUploadEndpoint: "https://api.example.com/api/asset",
107
+ };
108
+ ```
109
+
110
+ **5. Render the component**
111
+
112
+ ```tsx
113
+ <DataTemplateWrapper
114
+ ref={dataRef}
115
+ title="Checklists"
116
+ description="O&M service checklists"
117
+ type={viewType}
118
+ id={selectedId}
119
+ collectionId="checklists"
120
+ schema={schema}
121
+ workspace={workspace}
122
+ handleNavigate={handleNavigate}
123
+ />
124
+ ```
125
+
126
+ **6. Override edit permissions (optional)**
127
+
128
+ The wrapper defaults to read-only (`disabledUpdate: true, deleteBtn: false`). Pass `edit` to enable create/update/delete.
129
+
130
+ ```tsx
131
+ edit={{ allEditable: true, deleteBtn: true }}
132
+ ```
133
+
134
+ When `edit` is not passed, the wrapper defaults to:
135
+
136
+ ```ts
137
+ {
138
+ allEditable: true,
139
+ listEditable: false,
140
+ disabledUpdate: true,
141
+ disableBatchUpdate: true,
142
+ deleteBtn: false,
143
+ }
144
+ ```
145
+
146
+ Pass your own `edit` object to override any of these.
147
+
148
+ ---
149
+
150
+ **7. Apply a pre-filter (optional)**
151
+
152
+ Use `bareSettings.bareListFilterQuery` to always filter the list by a condition — useful when embedding the component inside a page that has its own filter state.
153
+
154
+ ```tsx
155
+ const [statusFilter, setStatusFilter] = useState("active");
156
+
157
+ bareSettings={{
158
+ bareListFilterQuery: statusFilter !== "all"
159
+ ? [{ queryId: "status", type: "=", value: statusFilter }]
160
+ : [],
161
+ }}
162
+ ```
163
+
164
+ Call `dataRef.current?.handleRefresh()` after changing the filter to re-fetch.
165
+
166
+ **8. newTabUrl (Optional)**
167
+
168
+ Enables right-click → "Open in new tab" on list rows. The component builds the link as `default + recordId + addons`.
169
+
170
+ ```tsx
171
+ newTabUrl={{
172
+ default: "https://dashboard.example.com/records/", // base URL — record ID is appended here
173
+ addons: "?view=read", // optional suffix appended after the record ID
174
+ }}
175
+ // resulting link: https://dashboard.example.com/records/<id>?view=read
176
+ ```
177
+
178
+ Both fields are optional — omit `addons` if you don't need a suffix.
179
+
180
+ **9. Use a non-table view (optional)**
181
+
182
+ Pass `viewSettings` to activate an alternate view instead of the default table.
183
+
184
+ ```tsx
185
+ // Grid view — renders records as image cards
186
+ viewSettings={{
187
+ gridImage: "coverImage", // column ID of an image field
188
+ gridScroll: true, // gridScroll enables infinite scroll;
189
+ gridContain: false, //gridContain uses object-contain instead of object-cover
190
+ }}
191
+
192
+ // List view — renders records as a vertical image list
193
+ viewSettings={{
194
+ listImage: "coverImage", // column ID of an image field
195
+ }}
196
+
197
+ // Calendar view — renders records as calendar events
198
+ viewSettings={{
199
+ calendarSettings: {
200
+ title: "name", // column ID for event title
201
+ startDate: "startAt", // column ID for start date
202
+ endDate: "endAt", // column ID for end date (optional)
203
+ groupId: "category", // column ID to colour-group events (optional)
204
+ },
205
+ }}
206
+
207
+ // Custom calendar view — pass your own FullCalendar plugin via customPlugin
208
+ // When customPlugin is set, the default view switches to "timeGridWeek"
209
+ // unless you override it with defaultView
210
+ viewSettings={{
211
+ calendarSettings: {
212
+ title: "name",
213
+ startDate: "startAt",
214
+ endDate: "endAt",
215
+ customPlugin: myFullCalendarPlugin, // any FullCalendar-compatible plugin
216
+ defaultView: "timeGridWeek", // override the initial view (optional)
217
+ headerToolbar: { // override the toolbar buttons (optional)
218
+ left: "prev,next today",
219
+ center: "title",
220
+ right: "dayGridMonth,timeGridWeek,timeGridDay",
221
+ },
222
+ footerToolbar: { right: "prev,next" }, // optional footer toolbar
223
+ customButtons: { // define custom toolbar buttons (optional)
224
+ myBtn: { text: "Sync", click: () => handleSync() },
225
+ },
226
+ customView: { view: "week", duration: 2 }, // multi-week span (optional)
227
+ handleCalendarResize: (calendarRef, isMobileWidth) => { // respond to resize (optional)
228
+ if (isMobileWidth) calendarRef.current?.getApi().changeView("timeGridDay");
229
+ },
230
+ },
231
+ }}
232
+
233
+ // Feeds view — renders records as a timeline/activity feed
234
+ viewSettings={{
235
+ feedsSettings: {
236
+ title: "name", // column ID for feed item title
237
+ content: "notes", // column ID for feed item body
238
+ desc: "createdAt", // column ID for the right-corner element
239
+ icon: "HeartIcon", // heroicon name (optional)
240
+ iconClass: "text-red-500", // tailwind class for the icon (optional)
241
+ },
242
+ }}
243
+
244
+ // Kanban view — renders records as a drag-and-drop board
245
+ viewSettings={{
246
+ kanbanSettings: {
247
+ title: "name", // column ID for card title
248
+ useColumnName: true,
249
+ groupCluster: [
250
+ { "To Do": ["todo"] },
251
+ { "In Progress": ["in_progress"] },
252
+ { "Review": ["review"] },
253
+ { "Done": ["done"] },
254
+ ],
255
+ },
256
+ }}
257
+ ```
258
+
259
+ ---
260
+
261
+ ### Form section grouping
262
+
263
+ Fields in the update/create form can be grouped into named sections using `groupId` on each `DataStructure` entry. Fields without a `groupId` are placed in a default section at the bottom.
264
+
265
+ ```tsx
266
+ schema={[
267
+ { id: "name", name: "Name", type: "string", groupId: "Basic Info" },
268
+ { id: "email", name: "Email", type: "email", groupId: "Basic Info" },
269
+ { id: "region", name: "Region", type: "string", groupId: "Location" },
270
+ { id: "address", name: "Address", type: "address", groupId: "Location" },
271
+ { id: "notes", name: "Notes", type: "textArea" }, // no groupId → default section
272
+ ]}
273
+ ```
274
+
275
+ By default, groups render as labelled sections separated by a divider. To make groups collapsible, pass `groupIdCollapsible` via `organise`:
276
+
277
+ ```tsx
278
+ organise={{
279
+ groupIdCollapsible: {
280
+ "Basic Info": false, // false = expanded by default
281
+ "Location": true, // true = collapsed by default
282
+ },
283
+ }}
284
+ ```
285
+
286
+ When `groupIdCollapsible` is set, each group renders as a `Collapse` panel with a bold title. Groups are sorted alphabetically — fields without a `groupId` always render last.
287
+
288
+ You can also split the form into tabs using `tabId` on each field. Fields with the same `tabId` are grouped under that tab. Use `organise.tabAlign` to control the tab style (`"top"`, `"left"`, or `"stepper"`).
289
+
290
+ ```tsx
291
+ schema={[
292
+ { id: "name", name: "Name", type: "string", tabId: "Details" },
293
+ { id: "status", name: "Status", type: "list", tabId: "Details" },
294
+ { id: "notes", name: "Notes", type: "textArea", tabId: "Notes" },
295
+ { id: "signoff", name: "Sign Off", type: "signature", tabId: "Sign Off" },
296
+ ]}
297
+
298
+ organise={{ tabAlign: "stepper" }} // renders tabs as a step-by-step wizard
299
+ ```
300
+
301
+ `tabId` and `groupId` can be combined — fields are first split by tab, then grouped within each tab.
302
+
303
+ ---
304
+
305
+ ## Usage
306
+
307
+ ```tsx
308
+ <DataTemplateWrapper
309
+ ref={dataRef}
310
+ title="Maintenance Checklists"
311
+ description="O&M service checklists"
312
+ type={viewType} // "list" | "read" | "update"
313
+ id={selectedId}
314
+ collectionId="checklists"
315
+ schema={checklistSchema}
316
+ workspace={{
317
+ apiEndpointType: "mongoDB",
318
+ apiEndpoint: "https://api.example.com/api/mongoData",
319
+ apiEndpointToken: "your-token",
320
+ apiUserToken: "",
321
+ assetEndpoint: "https://storage.example.com/o/",
322
+ assetUploadEndpoint: "https://api.example.com/api/asset",
323
+ }}
324
+ edit={{ allEditable: true, deleteBtn: true }}
325
+ organise={{ breadcrumbType: "hidden", tabAlign: "left" }}
326
+ handleNavigate={(type, id) => {
327
+ setViewType(type);
328
+ setSelectedId(id);
329
+ }}
330
+ handleComplete={(id) => console.log("saved", id)}
331
+ limit={30}
332
+ />
333
+ ```
334
+
335
+ ---
336
+
337
+ ### Grid view customisation
338
+
339
+ Use these together with `viewSettings={{ gridImage: "..." }}` to control how grid cards look and behave.
340
+
341
+ ```tsx
342
+ bareSettings={{
343
+ // Fully replace the default grid card with your own component
344
+ bareListGridComponent: (data) => (
345
+ <div className="rounded-lg overflow-hidden border border-gray-200">
346
+ <img src={data.coverImage} className="w-full h-40 object-cover" />
347
+ <div className="p-3">
348
+ <p className="font-medium text-sm">{data.name}</p>
349
+ <p className="text-xs text-gray-500">{data.location}</p>
350
+ </div>
351
+ </div>
352
+ ),
353
+
354
+ // Tailwind col-span class controlling how many columns each card takes
355
+ bareListGridColClassName: "col-span-1",
356
+
357
+ // Tag badges shown on the default grid card
358
+ bareListGridTags: (data) => [data.category, data.status],
359
+
360
+ // Ribbon text overlaid on the card image (e.g. "New", "Sold")
361
+ bareListGridRibbonText: (data) => data.status === "new" ? "New" : "",
362
+
363
+ // Fallback shown when the image column is empty
364
+ bareListGridFallbackImg: () => "/placeholder.png",
365
+ }}
366
+ ```
367
+
368
+ ---
369
+
370
+ ### List view customisation
371
+
372
+ Use these to control the table/list rows — add row-level action buttons, bulk actions, intercept data, or apply permanent filters.
373
+
374
+ ```tsx
375
+ bareSettings={{
376
+ // Icon buttons appended to each row
377
+ bareListActionBtns: (data) => [
378
+ <button key="send" onClick={(e) => { e.stopPropagation(); sendItem(data._id); }}>
379
+ <PaperAirplaneIcon className="w-4 h-4 text-primary-500" />
380
+ </button>,
381
+ ],
382
+
383
+ // Bulk-action buttons shown when rows are selected
384
+ bareListMultipleSelectAddtionalActions: [
385
+ {
386
+ text: "Export Selected",
387
+ onClick: (ids, selectAll, query) => exportItems(ids),
388
+ },
389
+ ],
390
+
391
+ // Fully replace the list UI with your own renderer
392
+ bareListComponent: (data, prettierData, loading, pageIndex) => (
393
+ <ul className="divide-y">
394
+ {data.map((item) => (
395
+ <li key={item._id} className="py-3 flex justify-between">
396
+ <span>{item.name}</span>
397
+ <span className="text-sm text-gray-500">{item.status}</span>
398
+ </li>
399
+ ))}
400
+ </ul>
401
+ ),
402
+
403
+ // Always-on filter applied to every list fetch (e.g. scope to current user)
404
+ bareListFilterQuery: [{ queryId: "assigneeId", type: "=", value: currentUserId }],
405
+
406
+ // Custom renderer for individual field values in the list
407
+ bareListDescription: (key, value) => {
408
+ if (key === "status") return <StatusBadge value={value} />;
409
+ return null; // return null to use the default renderer
410
+ },
411
+
412
+ // Called whenever list data loads — use to sync external state
413
+ bareListHandleData: (data, prettierData) => setRowCount(data.length),
414
+
415
+ // Called when row selection changes
416
+ bareListHandleSelection: (selectAll, selectedIds) => setSelected(selectedIds),
417
+
418
+ // Called whenever any filter changes
419
+ bareListHandleFilter: (search, select, date, reference) => console.log(search),
420
+
421
+ // Called with the full filter state on every change
422
+ bareListFilterSelected: (searchParams, selection, reference, refDate) => {
423
+ syncFiltersToURL(searchParams);
424
+ },
425
+
426
+ // Extra filter components rendered in the filter bar
427
+ bareListFilterAddComponent: [
428
+ <MyCustomDateRangePicker key="range" onChange={setRange} />,
429
+ ],
430
+
431
+ // MongoDB aggregation pipeline stages appended to list queries
432
+ bareListPipeline: [
433
+ {
434
+ spaceReferenceId: "users",
435
+ spaceReferenceAs: "assigneeData",
436
+ condition: [{ type: "=", fromColumnId: "assigneeId", toColumnId: "_id" }],
437
+ referenceSpaceLabel: ["name", "email"],
438
+ referenceFirstItem: true,
439
+ },
440
+ ],
441
+ }}
442
+ ```
443
+
444
+ ---
445
+
446
+ ### Read view customisation
447
+
448
+ Use these to inject extra content into the read view, intercept data on load, or override how individual field values are displayed.
449
+
450
+ ```tsx
451
+ bareSettings={{
452
+ // Inject a custom component into the read view
453
+ // By default it renders below the field list
454
+ bareReadComponent: (data) => (
455
+ <div className="mt-4 p-4 bg-blue-50 rounded-lg">
456
+ <p className="text-sm font-medium">Custom summary for {data.name}</p>
457
+ <p className="text-xs text-gray-500">Last updated: {data.updatedAt}</p>
458
+ </div>
459
+ ),
460
+
461
+ // Render the custom component above the field list instead of below
462
+ bareReadComponentPos: "top",
463
+
464
+ // Hide the default field list entirely — show only bareReadComponent
465
+ bareReadComponentHide: true,
466
+
467
+ // Custom renderer for individual field values in the read view
468
+ bareReadDescription: (key, value) => {
469
+ if (key === "status") return <StatusBadge value={value} />;
470
+ return null; // return null to use the default renderer
471
+ },
472
+
473
+ // Called when read view data loads — use to sync external state
474
+ bareReadHandleData: (data) => setPageTitle(data.name),
475
+
476
+ // Control whether clicking a reference field opens its detail view
477
+ bareReadReferenceView: (dataId, collectionId, attributeId) => {
478
+ // return false to disable the view button for this reference
479
+ return collectionId !== "restricted-collection";
480
+ },
481
+
482
+ // Show list-style tabs in read view (useful without a schemaEndpoint)
483
+ bareReadListTabs: true,
484
+ }}
485
+ ```
486
+
487
+ ---
488
+
489
+ ### Update / form customisation
490
+
491
+ Use these to control the create/edit form layout, prefill values, inject custom components, intercept saves, or replace individual fields.
492
+
493
+ ```tsx
494
+ bareSettings={{
495
+ // Prefill form fields based on existing record data
496
+ bareUpdatePrefill: (data) => [
497
+ { key: "assigneeId", value: currentUserId },
498
+ { key: "region", value: data.siteRegion },
499
+ ],
500
+
501
+ // Inject a custom component into the form (renders below fields by default)
502
+ bareUpdateComponent: (data) => (
503
+ <div className="p-3 bg-yellow-50 rounded text-sm">
504
+ Note: changes will notify {data.assigneeName}
505
+ </div>
506
+ ),
507
+
508
+ // Render bareUpdateComponent above the fields instead of below
509
+ bareUpdateComponentAbove: true,
510
+
511
+ // Replace the default save/cancel buttons with your own
512
+ bareUpdateActionComponent: (data, formRef) => (
513
+ <div className="flex gap-2">
514
+ <button onClick={() => formRef?.handleFormSubmit()}>Save</button>
515
+ <button onClick={onCancel}>Discard</button>
516
+ </div>
517
+ ),
518
+
519
+ // Move action buttons to the bottom of the form
520
+ bareUpdateActionBottom: true,
521
+
522
+ // Form column layout: "one" = 2-col grid, "two"/"four" = 4-col grid
523
+ bareUpdateColumnType: "two",
524
+
525
+ // Mobile-optimised layout: horizontal label/input on mobile, standard on desktop
526
+ bareUpdateMobileView: true,
527
+
528
+ // Replace a specific field with a fully custom component
529
+ bareUpdateIndividualComponent: (columnId, ref, selectedData, setSelectedData) => {
530
+ if (columnId === "coordinates") {
531
+ return (
532
+ <MapPicker
533
+ value={selectedData.coordinates}
534
+ onChange={(val) => setSelectedData({ ...selectedData, coordinates: val })}
535
+ />
536
+ );
537
+ }
538
+ return null; // return null to use the default field component
539
+ },
540
+
541
+ // Called on every field change — use to react to form state
542
+ bareUpdateHandleData: (data, changedIds) => {
543
+ if (changedIds?.includes("siteId")) refetchSiteDetails(data.siteId);
544
+ },
545
+
546
+ // Called when the active form tab changes
547
+ bareUpdateTabId: (tabId) => setActiveTab(tabId),
548
+
549
+ // Called before save — return false to cancel submission
550
+ bareUpdateHandlePreComplete: async (data) => {
551
+ const ok = await validateWithServer(data);
552
+ if (!ok) { showError("Validation failed"); return false; }
553
+ return true;
554
+ },
555
+
556
+ // Called after save completes
557
+ bareUpdateHandleComplete: async (data) => {
558
+ await sendNotification(data._id);
559
+ },
560
+
561
+ // Called after all post-save operations complete
562
+ bareUpdateHandlePostComplete: async (data) => {
563
+ await refreshRelatedList(data.projectId);
564
+ },
565
+
566
+ // Block deletion when this record is referenced elsewhere
567
+ bareReferenceDel: [
568
+ {
569
+ collectionId: "work-orders",
570
+ collectionName: "Work Orders",
571
+ columnId: "siteId",
572
+ schema: workOrderSchema,
573
+ },
574
+ ],
575
+
576
+ // Enable offline data caching via IndexedDB
577
+ bareOffline: true,
578
+
579
+ // Custom CSV import/export endpoints
580
+ bareCSVImportEndpoint: "https://api.example.com/api/import",
581
+ bareCSVExportEndpoint: "https://api.example.com/api/export",
582
+ }}
583
+ ```
584
+
585
+ ---
586
+
587
+ ## Offline Mode
588
+
589
+ Set `bareOffline: true` in `bareSettings` to enable offline data caching via IndexedDB. When active, the component saves form submissions locally when there is no network connection. Once the user is back online, they click the "Submit Offline Data" button (or you trigger `dataRef.current?.handleUploadOfflineModal()` programmatically) to flush the cached records to the server.
590
+
591
+ ### How it works
592
+
593
+ 1. User fills in and submits a form while offline — the record is written to IndexedDB instead of the server.
594
+ 2. Only offline records are shown in the list (pulled from IndexedDB). The user can open them in read view and update view — all data is served from IndexedDB. These offline records are invisible when `bareOffline` is `false`.
595
+ 3. When connectivity resumes, the user clicks "Submit Offline Data" (or you call `handleUploadOfflineModal()`). The component reads the queue from IndexedDB and replays each submission against the server.
596
+
597
+ ### Pre-fetching reference options
598
+
599
+ Fields of type `reference` and `listReference` that pull their options from the server will be empty when offline. You must fetch those options while online and store them in IndexedDB yourself so the form stays usable offline.
600
+
601
+ Use the same database name the component uses and store each option set under a key with the format `[collectionId]-ref` (e.g. meraque-cycles-ref). Each value must be an array of `Option` objects — `{ key, label, secondaryLabel?, color?, image?, icon? }` — where `key` is the record's `_id`.
602
+
603
+ ```tsx
604
+ // Call this once on app load (or when the user logs in) while online
605
+ async function prefetchReferenceOptions() {
606
+ const db = await openDB("blixify-offline", 1, {
607
+ upgrade(db) {
608
+ if (!db.objectStoreNames.contains("references")) {
609
+ db.createObjectStore("references");
610
+ }
611
+ },
612
+ });
613
+
614
+ // Fetch raw records for every collection your form references
615
+ const [siteRecords, userRecords] = await Promise.all([
616
+ fetch("https://api.example.com/api/mongoData", {
617
+ method: "POST",
618
+ headers: { Authorization: "Bearer your-token" },
619
+ body: JSON.stringify({ collectionId: "sites", query: [] }),
620
+ }).then((r) => r.json()),
621
+
622
+ fetch("https://api.example.com/api/mongoData", {
623
+ method: "POST",
624
+ headers: { Authorization: "Bearer your-token" },
625
+ body: JSON.stringify({ collectionId: "users", query: [] }),
626
+ }).then((r) => r.json()),
627
+ ]);
628
+
629
+ // Shape each record into an Option and store under "[collectionId]-ref"
630
+ await db.put(
631
+ "references",
632
+ siteRecords.map((r) => ({ key: r._id, label: r.name })),
633
+ "sites-ref"
634
+ );
635
+ await db.put(
636
+ "references",
637
+ userRecords.map((r) => ({
638
+ key: r._id,
639
+ label: r.name,
640
+ secondaryLabel: r.email,
641
+ })),
642
+ "users-ref"
643
+ );
644
+ }
645
+
646
+ // Call on mount, before rendering the form
647
+ useEffect(() => {
648
+ prefetchReferenceOptions();
649
+ }, []);
650
+ ```
651
+
652
+ > Use library [`idb`](https://github.com/jakearchibald/idb) (`yarn add idb`) for a promise-based IndexedDB API.
653
+
654
+ ### Full example
655
+
656
+ ```tsx
657
+ import { openDB } from "idb";
658
+
659
+ function ChecklistPage() {
660
+ const dataRef = useRef<any>(null);
661
+ const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
662
+ const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
663
+
664
+ // Pre-fetch reference options while online
665
+ useEffect(() => {
666
+ prefetchReferenceOptions();
667
+ }, []);
668
+
669
+ const schema: DataStructure[] = [
670
+ { id: "name", name: "Name", type: "string" },
671
+ { id: "siteId", name: "Site", type: "reference", referenceId: "sites" },
672
+ { id: "assignee", name: "Assignee", type: "listPerson" },
673
+ { id: "notes", name: "Notes", type: "textArea" },
674
+ ];
675
+
676
+ return (
677
+ <DataTemplateWrapper
678
+ ref={dataRef}
679
+ title="Checklists"
680
+ type={viewType}
681
+ id={selectedId}
682
+ collectionId="checklists"
683
+ schema={schema}
684
+ workspace={{
685
+ apiEndpointType: "mongoDB",
686
+ apiEndpoint: "https://api.example.com/api/mongoData",
687
+ apiEndpointToken: "your-token",
688
+ apiUserToken: "",
689
+ assetEndpoint: "https://storage.example.com/o/",
690
+ assetUploadEndpoint: "https://api.example.com/api/asset",
691
+ }}
692
+ edit={{ allEditable: true, deleteBtn: false }}
693
+ handleNavigate={(type, id) => {
694
+ setViewType(type as any);
695
+ if (id !== undefined) setSelectedId(id);
696
+ }}
697
+ bareSettings={{
698
+ bareOffline: true, // enable IndexedDB caching
699
+ }}
700
+ />
701
+ );
702
+ }
703
+ ```
704
+
705
+ ### Triggering the upload modal programmatically
706
+
707
+ If you want to prompt the user to sync as soon as connectivity is detected, listen for the `online` event and call `handleUploadOfflineModal()`:
708
+
709
+ ```tsx
710
+ useEffect(() => {
711
+ const handleOnline = () => {
712
+ dataRef.current?.handleUploadOfflineModal();
713
+ };
714
+ window.addEventListener("online", handleOnline);
715
+ return () => window.removeEventListener("online", handleOnline);
716
+ }, []);
717
+ ```
718
+
719
+ The modal lists all queued records and lets the user confirm before uploading. Records that upload successfully are removed from IndexedDB automatically.
720
+
721
+ ---
722
+
723
+ ## Union Collections
724
+
725
+ `unionCollections` is passed via `organise` and lets you merge data from multiple MongoDB collections into a single list view. Each entry defines a secondary collection to query, how to filter it, which fields to project, and an optional `source` label to identify which collection each row came from.
726
+
727
+ This is only supported with `server: "mongoServer"`.
728
+
729
+ ### Basic example — two collections, static source label
730
+
731
+ ```tsx
732
+ organise={{
733
+ unionCollections: [
734
+ {
735
+ collection: "work-orders", // secondary collection to union in
736
+ source: "Work Order", // static label added as a "source" field on each row
737
+ query: [
738
+ { queryId: "status", type: "=", value: "active" },
739
+ ],
740
+ projection: {
741
+ _id: 1,
742
+ name: 1,
743
+ status: 1,
744
+ assigneeId: 1,
745
+ createdAt: 1,
746
+ },
747
+ },
748
+ ],
749
+ sort: { sortId: "createdAt", type: "desc" },
750
+ }}
751
+ ```
752
+
753
+ The `source` field is automatically added to every row from that collection. Include `{ id: "source", name: "Source", type: "string", hideForm: true }` in your `schema` to display it as a column.
754
+
755
+ ---
756
+
757
+ ### Dynamic source label — derive the label from the row's own data
758
+
759
+ When `source` is `"dynamic"`, use `sourceLogic` to compute the label using a MongoDB `$cond` expression.
760
+
761
+ ```tsx
762
+ organise={{
763
+ unionCollections: [
764
+ {
765
+ collection: "deliveries",
766
+ source: "dynamic", // compute label per row
767
+ sourceLogic: {
768
+ $cond: {
769
+ if: { $eq: ["$type", "om"] },
770
+ then: "O&M Maintenance",
771
+ else: "PV Delivery",
772
+ },
773
+ },
774
+ query: [
775
+ { queryId: "type", type: "in", value: ["om", "pv"] },
776
+ ],
777
+ projection: {
778
+ _id: 1, name: 1, type: 1, status: 1, createdAt: 1,
779
+ },
780
+ },
781
+ ],
782
+ sort: { sortId: "createdAt", type: "desc" },
783
+ }}
784
+ ```
785
+
786
+ ---
787
+
788
+ ### addFields — compute a virtual field for sorting across collections
789
+
790
+ Use `addFields` to add a computed field (e.g. a unified sort date) that doesn't exist natively on every collection. Sort by that field so rows from different collections interleave correctly.
791
+
792
+ ```tsx
793
+ organise={{
794
+ unionCollections: [
795
+ {
796
+ collection: "deliveries",
797
+ source: "Delivery",
798
+ query: [],
799
+ projection: {
800
+ _id: 1, name: 1, proposed_date: 1, installation_date: 1, createdAt: 1,
801
+ },
802
+ // Add a virtual "sortDate" field — pick the first non-null date
803
+ addFields: {
804
+ sortDate: {
805
+ $ifNull: ["$proposed_date", "$createdAt"],
806
+ },
807
+ },
808
+ },
809
+ {
810
+ collection: "service-tickets",
811
+ source: "Service Ticket",
812
+ query: [],
813
+ projection: {
814
+ _id: 1, name: 1, service_date: 1, createdAt: 1,
815
+ },
816
+ addFields: {
817
+ sortDate: {
818
+ $ifNull: ["$service_date", "$createdAt"],
819
+ },
820
+ },
821
+ },
822
+ ],
823
+ // Sort all rows by the computed field across both collections
824
+ sort: { sortId: "sortDate", type: "desc" },
825
+ }}
826
+ ```
827
+
828
+ ---
829
+
830
+ ### Dynamic queries — rebuild unionCollections when filters change
831
+
832
+ Because `unionCollections` is inside `organise`, you can recompute it reactively when your filter state changes. Call `dataRef.current?.handleRefresh()` after updating state.
833
+
834
+ ```tsx
835
+ const [installer, setInstaller] = useState("");
836
+ const [startDate, setStartDate] = useState("");
837
+ const [endDate, setEndDate] = useState("");
838
+
839
+ const buildOrganise = () => {
840
+ const dateQuery = startDate && endDate
841
+ ? [{ queryId: "date", type: "><", value: [new Date(startDate).toISOString(), new Date(endDate).toISOString()] }]
842
+ : [];
843
+
844
+ const personQuery = installer
845
+ ? [{ type: "search", searchIds: ["installer", "inspector"], value: installer }]
846
+ : [];
847
+
848
+ return {
849
+ unionCollections: [
850
+ {
851
+ collection: "deliveries",
852
+ source: "Delivery",
853
+ query: [...dateQuery, ...personQuery],
854
+ projection: { _id: 1, name: 1, status: 1, installer: 1, date: 1 },
855
+ addFields: { sortDate: { $ifNull: ["$date", "$createdAt"] } },
856
+ },
857
+ {
858
+ collection: "service-tickets",
859
+ source: "Service Ticket",
860
+ query: [...dateQuery, ...personQuery],
861
+ projection: { _id: 1, name: 1, status: 1, installer: 1, service_date: 1 },
862
+ addFields: { sortDate: { $ifNull: ["$service_date", "$createdAt"] } },
863
+ },
864
+ ],
865
+ sort: { sortId: "sortDate", type: "desc" as const },
866
+ };
867
+ };
868
+
869
+ // Re-fetch whenever filters change
870
+ useEffect(() => {
871
+ dataRef.current?.handleRefresh();
872
+ }, [installer, startDate, endDate]);
873
+
874
+ <DataTemplateWrapper
875
+ ...
876
+ organise={buildOrganise()}
877
+ />
878
+ ```
879
+
880
+ ---
881
+
882
+ ### Schema for union views
883
+
884
+ All fields that may appear across any of the unioned collections must be declared in `schema`. Mark them `optional: true` and `hideForm: true` since they are read-only display columns.
885
+
886
+ ```tsx
887
+ schema={[
888
+ { id: "source", name: "Source", type: "string", hideForm: true },
889
+ { id: "name", name: "Name", type: "string", hideForm: true, optional: true },
890
+ { id: "status", name: "Status", type: "string", hideForm: true, optional: true },
891
+ { id: "installer", name: "Installer", type: "string", hideForm: true, optional: true },
892
+ { id: "date", name: "Date", type: "date", hideForm: true, optional: true },
893
+ { id: "service_date", name: "Svc Date", type: "date", hideForm: true, optional: true },
894
+ ]}
895
+ ```
896
+
897
+ ---
898
+
899
+ ## Ref Methods
900
+
901
+ | Method | Description |
902
+ | ---------------------------------- | ----------------------------------------------------------------- |
903
+ | `handleRefresh()` | Re-fetches the current list data |
904
+ | `handleFormSubmit()` | Programmatically submits the form |
905
+ | `handleFormCheck()` | Validates the form without submitting |
906
+ | `handleUpdateSelectedData(fields)` | Prefills form fields. `fields` is `{ key: string; value: any }[]` |
907
+ | `handleDeleteModal()` | Opens the delete confirmation modal |
908
+ | `handleExportReadPDF()` | Triggers PDF export of the current read view |
909
+ | `handleUploadOfflineModal()` | Opens the offline data upload modal |
910
+
911
+ ---
912
+
913
+ ## Props
914
+
915
+ | Name | Type | Default | Description |
916
+ | ----------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------- |
917
+ | `title` | `string \| ReactElement` | — | Title shown in the header |
918
+ | `description` | `string \| ReactElement` | `""` | Description shown below the title |
919
+ | `type` | `"list" \| "read" \| "update"` | — | Current view mode |
920
+ | `id` | `string` | — | Selected record ID. Pass `"new"` for a blank create form |
921
+ | `collectionId` | `string` | — | Database collection ID |
922
+ | `schema` | `DataStructure[]` | — | Column/field definitions (same shape as `DataTemplate`'s `model`) |
923
+ | `workspace` | `WorkspaceDevSettings` | — | Backend connection config — see Workspace below |
924
+ | `space` | `Space` | — | Full Space object — enables tabs, sort, and additional queries |
925
+ | `edit` | `Edit` | see below | Create/update/delete permissions — see DataTemplate's Edit |
926
+ | `organise` | `Organise` | — | View, sort, filter, export, and tab config — see DataTemplate's Organise |
927
+ | `bareSettings` | `BareSettings` | — | Custom components and callbacks — see Bare Settings below |
928
+ | `renderSettings` | `RenderSettings` | — | Override the list or loading UI |
929
+ | `viewSettings` | `ViewSettings` | — | Shorthand for setting up grid/list/calendar/feeds/kanban views — see View Settings below |
930
+ | `additionalQuery` | `AdditionalQuery[]` | — | Filter controls rendered above the list |
931
+ | `sort` | `Sort` | — | Default sort applied to list queries |
932
+ | `limit` | `number` | `10` | Records per page |
933
+ | `groupId` | `string` | — | Groups list results by this field |
934
+ | `uniqueLabel` | `string` | — | Appended to internal query keys to avoid collisions |
935
+ | `schemaReference` | `{ [key: string]: DataStructure[] }` | — | Hardcoded schema reference (bypasses schema endpoint) |
936
+ | `newTabUrl` | `{ default?: string; addons?: string }` | — | URL config for row links |
937
+ | `lib` | `LibSettings` | — | Firebase storage helpers — see Lib below |
938
+ | `handleNavigate` | `(type, selectedId, viewId) => void` | — | Called when the component needs to navigate between views |
939
+ | `handleComplete` | `(id: string) => void` | — | Called after a create/update operation completes |
940
+
941
+ ## Workspace
942
+
943
+ Replaces `DataTemplate`'s `devSettings`. The wrapper calls `parseWorkspaceSettings` internally to build the full config.
944
+
945
+ | Name | Type | Description |
946
+ | --------------------- | ----------------------------------- | ---------------------------------------------- |
947
+ | `apiEndpointType` | `"mongoDB" \| "firebase" \| string` | Maps to `devSettings.server` |
948
+ | `apiEndpoint` | `string` | Database API base URL |
949
+ | `apiEndpointToken` | `string` | API token |
950
+ | `apiUserToken` | `string` | Per-user auth token |
951
+ | `assetEndpoint` | `string` | Asset retrieval base URL |
952
+ | `assetUploadEndpoint` | `string` | Upload endpoint |
953
+ | `assetBucket` | `string` | Storage bucket name (AWS/GCS) |
954
+ | `maxFileSize` | `number` | Global max file size in MB |
955
+ | `organisationMembers` | `Option[]` | Member list for `person` / `listPerson` fields |
956
+
957
+ ---
958
+
959
+ ## Lib
960
+
961
+ Only Firebase storage helpers are needed here — axios, headlessUI, and papaparse are bundled internally.
962
+
963
+ | Name | Type | Description |
964
+ | ---------------- | ----- | ---------------------------------- |
965
+ | `storage` | `any` | Firebase Storage instance |
966
+ | `ref` | `any` | Firebase `ref` function |
967
+ | `uploadBytes` | `any` | Firebase `uploadBytes` function |
968
+ | `getDownloadURL` | `any` | Firebase `getDownloadURL` function |
969
+
970
+ ---
971
+
972
+ ## Organise
973
+
974
+ The `organise` prop is the same type as `DataTemplate`'s `Organise` — refer to [DataTemplate's Organise prop](?path=/story/data--data-template) for the full list of options.
975
+
976
+ A few keys are handled differently in `DataTemplateWrapper`:
977
+
978
+ | Key | Behaviour |
979
+ | ------------------------- | -------------------------------------------------------------------------------- |
980
+ | `handleNavigate` | Use the top-level `handleNavigate` prop instead — it is merged in automatically |
981
+ | `additionalQuery` | Use the top-level `additionalQuery` prop instead — it is merged in automatically |
982
+ | `sort` | Use the top-level `sort` prop instead — it is merged in automatically |
983
+ | `viewId` / `extendedTabs` | Use `viewSettings` instead — the wrapper builds these for you |
984
+
985
+ Everything else (`breadcrumbType`, `tabAlign`, `splitView`, `exportSettings`, `advSortEnabled`, `advFilterEnabled`, `groupIdCollapsible`, etc.) works exactly the same as in `DataTemplate` and can be passed directly via `organise`.
986
+
987
+ ---
988
+
989
+ ## View Settings
990
+
991
+ A shorthand that builds the `extendedTabs` and `viewId` config for you. Only one view type can be active at a time — the first matching key wins in this order: `listImage` → `calendarSettings` → `feedsSettings` → `kanbanSettings` → `gridImage`.
992
+
993
+ | Name | Type | Description |
994
+ | ------------------ | ------------------ | --------------------------------------------------------------- |
995
+ | `gridImage` | `string` | Column ID of an image field — activates grid view |
996
+ | `gridScroll` | `boolean` | Enables infinite scroll in grid view |
997
+ | `gridContain` | `boolean` | Uses `object-contain` instead of `object-cover` for grid images |
998
+ | `listImage` | `string` | Column ID of an image field — activates list view |
999
+ | `calendarSettings` | `CalendarSettings` | Activates calendar view — see CalendarSettings below |
1000
+ | `feedsSettings` | `FeedsSettings` | Activates feeds view — see DataTemplate's Feeds Settings |
1001
+ | `kanbanSettings` | `KanbanSettings` | Activates kanban view — see DataTemplate's Kanban Settings |
1002
+
1003
+ ### CalendarSettings
1004
+
1005
+ | Name | Type | Description | Optional |
1006
+ | ---------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------- |
1007
+ | `title` | `string` | Column ID for the event title | |
1008
+ | `startDate` | `string` | Column ID for the event start date | |
1009
+ | `endDate` | `string` | Column ID for the event end date | ✓ |
1010
+ | `groupId` | `string` | Column ID — events with the same value share a background colour | ✓ |
1011
+ | `groupColor` | `Color[]` | Background colours for groups | ✓ |
1012
+ | `defaultStartDate` | `Date` | Initial date the calendar opens on | ✓ |
1013
+ | `defaultView` | `string` | Initial calendar view — e.g. `"dayGridMonth"`, `"timeGridWeek"`, `"timeGridDay"`, `"listMonth"` | ✓ |
1014
+ | `customPlugin` | `any` | A FullCalendar-compatible plugin. When set, the default view switches to `"timeGridWeek"` unless overridden | ✓ |
1015
+ | `customView` | `{ view: "week"; duration: number }` | Multi-week span for week view — e.g. `{ view: "week", duration: 2 }` shows a 2-week range | ✓ |
1016
+ | `headerToolbar` | `{ left?: string; center?: string; right?: string }` | Override the calendar header toolbar buttons | ✓ |
1017
+ | `footerToolbar` | `{ left?: string; center?: string; right?: string }` | Override the calendar footer toolbar buttons | ✓ |
1018
+ | `customButtons` | `{ [key: string]: any }` | Define custom toolbar buttons — keys match button names used in `headerToolbar`/`footerToolbar` | ✓ |
1019
+ | `customHeader` | `string` | Static string to override the toolbar title | ✓ |
1020
+ | `handleCalendarResize` | `(calendarRef: RefObject<any>, isMobileWidth: boolean) => void` | Called when the calendar container resizes — use to switch views on mobile | ✓ |
1021
+
1022
+ ---
1023
+
1024
+ ### Bare Settings
1025
+
1026
+ | Name | Type | Description |
1027
+ | ---------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
1028
+ | `bareReadComponent` | `(data: any) => ReactElement` | Custom component rendered in read view |
1029
+ | `bareReadComponentPos` | `"top"` | Renders `bareReadComponent` above the default read content |
1030
+ | `bareReadComponentHide` | `boolean` | Hides the default read content |
1031
+ | `bareReadDescription` | `(key, value) => any` | Custom renderer for read view field values |
1032
+ | `bareReadReferenceView` | `(dataId, collectionId, attributeId) => boolean \| void` | Controls whether a reference field opens a view |
1033
+ | `bareReadHandleData` | `(data: any) => void` | Called when read view data is loaded |
1034
+ | `bareReadListTabs` | `boolean` | Shows list-style tabs in read view |
1035
+ | `bareListComponent` | `(data, prettierData, loading, pageIndex) => ReactElement` | Fully replaces the list UI |
1036
+ | `bareListActionBtns` | `(data: any) => ReactElement[]` | Extra icon buttons on each list row |
1037
+ | `bareListMultipleSelectAddtionalActions` | `{ text: string; className?: string; onClick: (ids, selectAll, query) => void }[]` | Extra bulk-action buttons when rows are selected |
1038
+ | `bareListGridComponent` | `(data: any) => ReactElement` | Custom card renderer for grid view |
1039
+ | `bareListGridColClassName` | `string` | Tailwind class for grid column width |
1040
+ | `bareListGridTags` | `(data: any) => string[]` | Tag strings shown on grid cards |
1041
+ | `bareListGridRibbonText` | `(data: any) => string` | Ribbon text on grid cards |
1042
+ | `bareListGridFallbackImg` | `(data: any) => string \| ReactElement` | Fallback image for grid cards |
1043
+ | `bareListHandleData` | `(data, prettierData) => void` | Called when list data is loaded |
1044
+ | `bareListHandleFilter` | `(search, select, date, reference) => void` | Called when any filter changes |
1045
+ | `bareListHandleSelection` | `(selectAll, selectedIds, query?) => void` | Called when row selection changes |
1046
+ | `bareListDescription` | `(key, value) => any` | Custom renderer for list view field values |
1047
+ | `bareListFilterQuery` | `Query[]` | Additional queries always applied to list fetches |
1048
+ | `bareListFilterAddComponent` | `ReactElement[]` | Extra filter components in the filter bar |
1049
+ | `bareListPipeline` | `PipeLine[]` | Additional MongoDB aggregation pipeline stages |
1050
+ | `bareListFilterSelected` | `(filterSearchParams, filterSelection, filterReference, filterReferenceDate) => void` | Called with the current filter state on every change |
1051
+ | `bareUpdatePrefill` | `(data: any) => { key: string; value: any }[]` | Returns prefill values for the update form |
1052
+ | `bareUpdateComponent` | `(data: any) => ReactElement` | Custom component rendered inside the update form |
1053
+ | `bareUpdateComponentAbove` | `boolean` | Renders `bareUpdateComponent` above the form fields |
1054
+ | `bareUpdateActionComponent` | `(data: any) => ReactElement` | Custom action buttons in the form footer |
1055
+ | `bareUpdateColumnType` | `"one" \| "two" \| "four"` | Form column layout |
1056
+ | `bareUpdateMobileView` | `boolean` | Mobile-optimised form layout |
1057
+ | `bareUpdateActionBottom` | `boolean` | Moves action buttons to the bottom of the form |
1058
+ | `bareUpdateHandleData` | `(data, changedAttributeIds?) => void` | Called on every form field change |
1059
+ | `bareUpdateHandlePreComplete` | `(data: any) => Promise<boolean>` | Called before save — return `false` to cancel |
1060
+ | `bareUpdateHandleComplete` | `(data: any) => void` | Called after save completes |
1061
+ | `bareUpdateHandlePostComplete` | `(data: any) => void` | Called after post-save operations complete |
1062
+ | `bareUpdateTabId` | `(tabId: string) => void` | Called when the active form tab changes |
1063
+ | `bareUpdateTabComponent` | `(tabIds: string[]) => { tabId: string; icon?: ReactElement; skip?: boolean }[]` | Customise tab icons or skip specific tabs in the form |
1064
+ | `bareUpdateIndividualComponent` | `(columnId, ref, selectedData, setSelectedData) => ReactElement \| null` | Replaces a specific form field with a custom component |
1065
+ | `bareReferenceDel` | `{ collectionId, collectionName, columnId, schema }[]` | Blocks deletion when the record is referenced elsewhere |
1066
+ | `bareOffline` | `boolean` | Enables offline data caching |
1067
+ | `bareCSVImportEndpoint` | `string` | Custom CSV import endpoint |
1068
+ | `bareCSVExportEndpoint` | `string` | Custom CSV export endpoint |
1069
+
1070
+ ---
1071
+
1072
+ ## Render Settings
1073
+
1074
+ | Name | Type | Description |
1075
+ | ----------------- | ------------------------------------------------------------------- | ------------------------------------ |
1076
+ | `renderListUI` | `(actionComponent, filterComponent, dataComponent) => ReactElement` | Fully replaces the list layout |
1077
+ | `renderLoadingUI` | `() => ReactElement` | Replaces the default loading spinner |