blixify-ui-web-mcp 0.1.19 → 0.1.21
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/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
- package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
- package/lib/components/chart/statsCard/README.md +24 -0
- package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
- package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
- package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
- package/lib/components/data/dataTemplate/README.md +204 -85
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
- package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
- package/lib/components/data/dataWrapper/README.md +109 -22
- package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
- package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
- package/lib/components/display/calendar/calendar.stories.tsx +1 -1
- package/lib/components/display/detailList/README.md +11 -10
- package/lib/components/display/detailList/detailList.stories.tsx +16 -3
- package/lib/components/display/feeds/README.md +3 -3
- package/lib/components/display/feeds/feeds.stories.tsx +3 -3
- package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
- package/lib/components/display/modal/README.md +3 -0
- package/lib/components/display/modal/modal.stories.tsx +50 -1
- package/lib/components/display/newCalendar/README.md +0 -6
- package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
- package/lib/components/display/newFeeds/README.md +1 -5
- package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
- package/lib/components/display/newKanban/README.md +0 -7
- package/lib/components/display/newList/README.md +1 -4
- package/lib/components/display/newTable/README.md +29 -9
- package/lib/components/display/newTable/newTable.stories.tsx +120 -9
- package/lib/components/display/tag/README.md +1 -1
- package/lib/components/display/tag/tag.stories.tsx +7 -0
- package/lib/components/input/markdown/markdown.stories.tsx +3 -1
- package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
- package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
- package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
- package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
- package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getBackendComponent.js +7 -3
- package/lib/mcp/tools/getBackendComponent.js.map +1 -1
- package/lib/mcp/tools/suggestTemplate.js +3 -3
- package/lib/mcp/tools/suggestTemplate.js.map +1 -1
- package/package.json +1 -1
- package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
- package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
- package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
- package/lib/components/data/flexFilter/flexFilter.stories.tsx +0 -171
|
@@ -74,7 +74,7 @@ export const DataWrapperTest = () => {
|
|
|
74
74
|
apiEndpointToken: "",
|
|
75
75
|
apiUserToken: "",
|
|
76
76
|
assetEndpoint:
|
|
77
|
-
"https://firebasestorage.googleapis.com/v0/b/
|
|
77
|
+
"https://firebasestorage.googleapis.com/v0/b/demo-bucket.firebasestorage.app/o/",
|
|
78
78
|
assetUploadEndpoint: `${dtwBaseUrl}/api/asset`,
|
|
79
79
|
}}
|
|
80
80
|
edit={{
|
|
@@ -160,6 +160,11 @@ const InlineEditMatrixSchema = [
|
|
|
160
160
|
optional: true,
|
|
161
161
|
},
|
|
162
162
|
{ id: "due", name: "Due", type: "date" as const, optional: true },
|
|
163
|
+
// INFO: a composite editor — country picker beside a number field
|
|
164
|
+
{ id: "phone", name: "Phone", type: "contact" as const, optional: true },
|
|
165
|
+
// INFO: a string column some rows fill with an object, which is what a
|
|
166
|
+
// schema change or an upstream sync leaves behind
|
|
167
|
+
{ id: "place", name: "Place", type: "string" as const, optional: true },
|
|
163
168
|
{
|
|
164
169
|
id: "owner",
|
|
165
170
|
name: "Owner",
|
|
@@ -245,6 +250,11 @@ const handleBuildMatrixRecords = () => {
|
|
|
245
250
|
status: ["Draft", "Active", "Done"][index % 3],
|
|
246
251
|
tags: index % 2 === 0 ? ["North"] : [],
|
|
247
252
|
due: dayjs().add(index, "day").toISOString(),
|
|
253
|
+
phone: `+6012${String(index).padStart(7, "0")}`,
|
|
254
|
+
place:
|
|
255
|
+
index % 2 === 0
|
|
256
|
+
? { name: `${index} Jalan Object`, lat: 3.1, lng: 101.6 }
|
|
257
|
+
: `${index} Jalan Plain`,
|
|
248
258
|
owner: index % 2 === 0 ? "owner-01" : "owner-02",
|
|
249
259
|
helpers: [],
|
|
250
260
|
photo: "",
|
|
@@ -333,10 +343,10 @@ const handleInstallMatrixMock = (
|
|
|
333
343
|
.reply((config) => {
|
|
334
344
|
const body = handleParseBody(config);
|
|
335
345
|
const id = `matrix-${String(store.length + 1).padStart(2, "0")}`;
|
|
336
|
-
// INFO: baseDraft on purpose
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
346
|
+
// INFO: baseDraft on purpose — it is what the create path stamps on a new
|
|
347
|
+
// inline row. The React-element-in-a-cell shape that once turned into the
|
|
348
|
+
// literal text "div" through the list update path's JSON clone now comes
|
|
349
|
+
// from the coloured status column, which renders its value as a Tag.
|
|
340
350
|
store.unshift({ _id: id, baseDraft: true, ...(body.data ?? {}) });
|
|
341
351
|
return [200, { success: id }];
|
|
342
352
|
});
|
|
@@ -605,3 +615,344 @@ export const DTWEmbeddedInMarkdown = () => {
|
|
|
605
615
|
</div>
|
|
606
616
|
);
|
|
607
617
|
};
|
|
618
|
+
|
|
619
|
+
// ===================================================================================
|
|
620
|
+
|
|
621
|
+
// INFO: the only story that exercises a listObject through NewTable. Every other
|
|
622
|
+
// listObject fixture in this repo sets listFlexTable, which takes the FlexTable
|
|
623
|
+
// branch instead — so without this one the NewTable path has no visual coverage.
|
|
624
|
+
// listObjectTableView is what selects the table; leave it off and the field
|
|
625
|
+
// renders as a List.
|
|
626
|
+
|
|
627
|
+
const LIST_OBJECT_ENDPOINT = "http://localhost:3000/api/data";
|
|
628
|
+
const LIST_OBJECT_COLLECTION = "dtw-list-object-table";
|
|
629
|
+
|
|
630
|
+
// INFO: tabIds on purpose — the action pill only renders its Previous/Next
|
|
631
|
+
// navigation when the form is tabbed, so a flat schema cannot exercise them
|
|
632
|
+
const ListObjectTableSchema: any = [
|
|
633
|
+
{ id: "siteName", name: "Site Name", type: "string", tabId: "A. Site" },
|
|
634
|
+
{
|
|
635
|
+
id: "tasks",
|
|
636
|
+
name: "Tasks",
|
|
637
|
+
type: "listObject",
|
|
638
|
+
// INFO: this flag, not listFlexTable, is what routes the field to NewTable
|
|
639
|
+
listObjectTableView: true,
|
|
640
|
+
tabId: "B. Tasks",
|
|
641
|
+
optional: true,
|
|
642
|
+
objectData: [
|
|
643
|
+
{ id: "title", name: "Task", type: "string" },
|
|
644
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
645
|
+
{ id: "hours", name: "Hours", type: "number", optional: true },
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "emptyTasks",
|
|
650
|
+
name: "Tasks With No Rows",
|
|
651
|
+
type: "listObject",
|
|
652
|
+
listObjectTableView: true,
|
|
653
|
+
tabId: "B. Tasks",
|
|
654
|
+
optional: true,
|
|
655
|
+
objectData: [
|
|
656
|
+
{ id: "title", name: "Task", type: "string" },
|
|
657
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
const handleBuildListObjectRecord = () => ({
|
|
663
|
+
_id: "site-01",
|
|
664
|
+
siteName: "Bangi Rooftop",
|
|
665
|
+
tasks: [
|
|
666
|
+
{ id: "t1", title: "Wire the pump", owner: "Ali", hours: 4 },
|
|
667
|
+
{ id: "t2", title: "Seal the tank", owner: "Siti", hours: 2 },
|
|
668
|
+
{ id: "t3", title: "Commission inverter", owner: "Raj", hours: 6 },
|
|
669
|
+
],
|
|
670
|
+
emptyTasks: [],
|
|
671
|
+
createdAt: dayjs().toISOString(),
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
export const DTWListObjectTable = () => {
|
|
675
|
+
const [mount, setMount] = useState<boolean>(false);
|
|
676
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">(
|
|
677
|
+
"update",
|
|
678
|
+
);
|
|
679
|
+
const [selectedId, setSelectedId] = useState<string | undefined>("site-01");
|
|
680
|
+
const storeRef = useRef<any[]>([handleBuildListObjectRecord()]);
|
|
681
|
+
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
const store = storeRef.current;
|
|
684
|
+
const mock = new MockAdapter(axios, {
|
|
685
|
+
onNoMatch: "passthrough",
|
|
686
|
+
delayResponse: 80,
|
|
687
|
+
});
|
|
688
|
+
const handleParseBody = (config: any) => {
|
|
689
|
+
try {
|
|
690
|
+
return JSON.parse(config?.data ?? "{}");
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
mock
|
|
697
|
+
.onPost(`${LIST_OBJECT_ENDPOINT}/${LIST_OBJECT_COLLECTION}/list`)
|
|
698
|
+
.reply(() => [200, { data: store, count: store.length }]);
|
|
699
|
+
|
|
700
|
+
mock
|
|
701
|
+
.onPost(`${LIST_OBJECT_ENDPOINT}/${LIST_OBJECT_COLLECTION}/get`)
|
|
702
|
+
.reply((config) => {
|
|
703
|
+
const body = handleParseBody(config);
|
|
704
|
+
return [200, { data: store.find((row) => row._id === body.id) ?? null }];
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
mock
|
|
708
|
+
.onPost(`${LIST_OBJECT_ENDPOINT}/${LIST_OBJECT_COLLECTION}/update`)
|
|
709
|
+
.reply((config) => {
|
|
710
|
+
const body = handleParseBody(config);
|
|
711
|
+
const index = store.findIndex((row) => row._id === body.id);
|
|
712
|
+
if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
|
|
713
|
+
return [200, { success: body.id ?? true }];
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
setMount(true);
|
|
717
|
+
return () => mock.restore();
|
|
718
|
+
}, []);
|
|
719
|
+
|
|
720
|
+
if (!mount) return <Loading />;
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<div className="w-full">
|
|
724
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
725
|
+
DTW List Object Table
|
|
726
|
+
</div>
|
|
727
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
728
|
+
A listObject rendered through NewTable — the branch every other fixture
|
|
729
|
+
misses because they set listFlexTable. Click a row in Tasks and the edit
|
|
730
|
+
modal should open. Tasks With No Rows should read "No Data
|
|
731
|
+
Available". The form is tabbed so the bottom bar shows the full
|
|
732
|
+
pill — Previous / counter / Next as icon buttons at every width, plus
|
|
733
|
+
Delete and Submit on a solid grey surface.
|
|
734
|
+
</div>
|
|
735
|
+
<DataTemplateWrapper
|
|
736
|
+
title="Site"
|
|
737
|
+
type={viewType}
|
|
738
|
+
id={selectedId}
|
|
739
|
+
collectionId={LIST_OBJECT_COLLECTION}
|
|
740
|
+
schema={ListObjectTableSchema}
|
|
741
|
+
workspace={{
|
|
742
|
+
apiEndpointType: "mongoDB",
|
|
743
|
+
apiEndpoint: LIST_OBJECT_ENDPOINT,
|
|
744
|
+
apiEndpointToken: "",
|
|
745
|
+
apiUserToken: "",
|
|
746
|
+
}}
|
|
747
|
+
edit={{ allEditable: true, disabledUpdate: false, deleteBtn: true }}
|
|
748
|
+
organise={{ breadcrumbType: "button", tabAlign: true }}
|
|
749
|
+
handleNavigate={(type: any, id: any) => {
|
|
750
|
+
setViewType(type);
|
|
751
|
+
setSelectedId(id);
|
|
752
|
+
}}
|
|
753
|
+
limit={10}
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
);
|
|
757
|
+
};
|
|
758
|
+
DTWListObjectTable.storyName = "DTW List Object Table";
|
|
759
|
+
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
// DTW Callback Counts — used by e2e/dtw-callbacks.spec.ts
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
const CALLBACK_ENDPOINT = "https://callback-counts.blixify.invalid/api/data";
|
|
765
|
+
const CALLBACK_COLLECTION = "callback-records";
|
|
766
|
+
|
|
767
|
+
const CallbackCountsSchema = [
|
|
768
|
+
{ id: "name", name: "Name", type: "string" as const },
|
|
769
|
+
{ id: "notes", name: "Notes", type: "string" as const, optional: true },
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
type CallbackCounterKey =
|
|
773
|
+
| "list"
|
|
774
|
+
| "get"
|
|
775
|
+
| "create"
|
|
776
|
+
| "update"
|
|
777
|
+
| "delete"
|
|
778
|
+
| "preComplete"
|
|
779
|
+
| "postComplete"
|
|
780
|
+
| "navigate";
|
|
781
|
+
|
|
782
|
+
const CALLBACK_COUNTER_KEYS: CallbackCounterKey[] = [
|
|
783
|
+
"list",
|
|
784
|
+
"get",
|
|
785
|
+
"create",
|
|
786
|
+
"update",
|
|
787
|
+
"delete",
|
|
788
|
+
"preComplete",
|
|
789
|
+
"postComplete",
|
|
790
|
+
"navigate",
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
const handleInstallCallbackMock = (
|
|
794
|
+
store: any[],
|
|
795
|
+
onCount: (key: CallbackCounterKey) => void,
|
|
796
|
+
) => {
|
|
797
|
+
const mock = new MockAdapter(axios, {
|
|
798
|
+
onNoMatch: "passthrough",
|
|
799
|
+
delayResponse: 100,
|
|
800
|
+
});
|
|
801
|
+
const handleParseBody = (config: any) => {
|
|
802
|
+
try {
|
|
803
|
+
return JSON.parse(config?.data ?? "{}");
|
|
804
|
+
} catch (err) {
|
|
805
|
+
return {};
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const renderUrl = (type: string) =>
|
|
809
|
+
`${CALLBACK_ENDPOINT}/${CALLBACK_COLLECTION}/${type}`;
|
|
810
|
+
mock.onPost(renderUrl("list")).reply(() => {
|
|
811
|
+
onCount("list");
|
|
812
|
+
return [
|
|
813
|
+
200,
|
|
814
|
+
{ data: store.map((row) => ({ ...row })), count: store.length },
|
|
815
|
+
];
|
|
816
|
+
});
|
|
817
|
+
mock
|
|
818
|
+
.onPost(renderUrl("count"))
|
|
819
|
+
.reply(() => [200, { data: store.length, count: store.length }]);
|
|
820
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
821
|
+
onCount("get");
|
|
822
|
+
const body = handleParseBody(config);
|
|
823
|
+
const row = store.find((eachRow) => eachRow._id === body.id);
|
|
824
|
+
return [200, { data: row ? { ...row } : null }];
|
|
825
|
+
});
|
|
826
|
+
mock.onPost(renderUrl("create")).reply((config) => {
|
|
827
|
+
onCount("create");
|
|
828
|
+
store.push({ ...(handleParseBody(config).data ?? {}) });
|
|
829
|
+
return [200, { success: true }];
|
|
830
|
+
});
|
|
831
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
832
|
+
onCount("update");
|
|
833
|
+
const body = handleParseBody(config);
|
|
834
|
+
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
835
|
+
if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
|
|
836
|
+
return [200, { success: true }];
|
|
837
|
+
});
|
|
838
|
+
mock.onPost(renderUrl("delete")).reply((config) => {
|
|
839
|
+
onCount("delete");
|
|
840
|
+
const body = handleParseBody(config);
|
|
841
|
+
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
842
|
+
if (index >= 0) store.splice(index, 1);
|
|
843
|
+
return [200, { success: true }];
|
|
844
|
+
});
|
|
845
|
+
return mock;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* INFO: a full CRUD page that counts every request and every save callback on
|
|
850
|
+
* screen, so e2e can assert that one user action fires each of them exactly
|
|
851
|
+
* once. A doubled callback reaches consumers as a doubled email or upload,
|
|
852
|
+
* while the page itself looks fine — the counters are the only visible signal.
|
|
853
|
+
*/
|
|
854
|
+
export const DTWCallbackCounts = () => {
|
|
855
|
+
// 1. State
|
|
856
|
+
const [mount, setMount] = useState<boolean>(false);
|
|
857
|
+
const [, setTick] = useState<number>(0);
|
|
858
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
859
|
+
const [selectedId, setSelectedId] = useState<string>("");
|
|
860
|
+
const [lastNavigate, setLastNavigate] = useState<string>("");
|
|
861
|
+
|
|
862
|
+
// 2. Refs
|
|
863
|
+
const storeRef = useRef<any[]>([
|
|
864
|
+
{ _id: "r1", name: "Record One", notes: "first" },
|
|
865
|
+
{ _id: "r2", name: "Record Two", notes: "second" },
|
|
866
|
+
]);
|
|
867
|
+
const countersRef = useRef<Record<CallbackCounterKey, number>>({
|
|
868
|
+
list: 0,
|
|
869
|
+
get: 0,
|
|
870
|
+
create: 0,
|
|
871
|
+
update: 0,
|
|
872
|
+
delete: 0,
|
|
873
|
+
preComplete: 0,
|
|
874
|
+
postComplete: 0,
|
|
875
|
+
navigate: 0,
|
|
876
|
+
});
|
|
877
|
+
const mockRef = useRef<any>(null);
|
|
878
|
+
|
|
879
|
+
// 4. Effects
|
|
880
|
+
useEffect(() => {
|
|
881
|
+
mockRef.current = handleInstallCallbackMock(storeRef.current, handleCount);
|
|
882
|
+
setMount(true);
|
|
883
|
+
return () => {
|
|
884
|
+
mockRef.current?.restore();
|
|
885
|
+
mockRef.current = null;
|
|
886
|
+
};
|
|
887
|
+
}, []);
|
|
888
|
+
|
|
889
|
+
// 5. Handlers
|
|
890
|
+
function handleCount(key: CallbackCounterKey) {
|
|
891
|
+
countersRef.current[key] += 1;
|
|
892
|
+
setTick((previous) => previous + 1);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const handleNavigate = (type: any, id: any) => {
|
|
896
|
+
handleCount("navigate");
|
|
897
|
+
setLastNavigate(`${type}:${id ?? ""}`);
|
|
898
|
+
setViewType(type);
|
|
899
|
+
if (id !== undefined) setSelectedId(id);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// 7. Return
|
|
903
|
+
if (!mount) return <Loading />;
|
|
904
|
+
return (
|
|
905
|
+
<div className="w-full">
|
|
906
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
907
|
+
DTW Callback Counts
|
|
908
|
+
</div>
|
|
909
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
910
|
+
Every request and save callback is counted below. One create, update or
|
|
911
|
+
delete should move each of its counters by exactly one.
|
|
912
|
+
</div>
|
|
913
|
+
<div className="flex flex-wrap gap-4 mb-4 text-xs text-gray-600 font-mono">
|
|
914
|
+
{CALLBACK_COUNTER_KEYS.map((eachKey) => (
|
|
915
|
+
<span key={eachKey}>
|
|
916
|
+
{eachKey}:{" "}
|
|
917
|
+
<span data-testid={`callback-count-${eachKey}`}>
|
|
918
|
+
{countersRef.current[eachKey]}
|
|
919
|
+
</span>
|
|
920
|
+
</span>
|
|
921
|
+
))}
|
|
922
|
+
<span>
|
|
923
|
+
last navigate:{" "}
|
|
924
|
+
<span data-testid="callback-last-navigate">{lastNavigate}</span>
|
|
925
|
+
</span>
|
|
926
|
+
</div>
|
|
927
|
+
<DataTemplateWrapper
|
|
928
|
+
title="Records"
|
|
929
|
+
type={viewType}
|
|
930
|
+
id={selectedId}
|
|
931
|
+
collectionId={CALLBACK_COLLECTION}
|
|
932
|
+
schema={CallbackCountsSchema}
|
|
933
|
+
workspace={{
|
|
934
|
+
apiEndpointType: "mongoDB",
|
|
935
|
+
apiEndpoint: CALLBACK_ENDPOINT,
|
|
936
|
+
apiEndpointToken: "",
|
|
937
|
+
apiUserToken: "",
|
|
938
|
+
assetEndpoint: "",
|
|
939
|
+
assetUploadEndpoint: "",
|
|
940
|
+
}}
|
|
941
|
+
edit={{ allEditable: true, disabledUpdate: false, deleteBtn: true }}
|
|
942
|
+
organise={{ breadcrumbType: "button" }}
|
|
943
|
+
bareSettings={{
|
|
944
|
+
bareNewDTW: true,
|
|
945
|
+
bareUpdateHandlePreComplete: async () => {
|
|
946
|
+
handleCount("preComplete");
|
|
947
|
+
return true;
|
|
948
|
+
},
|
|
949
|
+
bareUpdateHandlePostComplete: async () => {
|
|
950
|
+
handleCount("postComplete");
|
|
951
|
+
},
|
|
952
|
+
}}
|
|
953
|
+
handleNavigate={handleNavigate}
|
|
954
|
+
/>
|
|
955
|
+
</div>
|
|
956
|
+
);
|
|
957
|
+
};
|
|
958
|
+
DTWCallbackCounts.storyName = "DTW Callback Counts";
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import React, { useEffect, useRef, useState } from "react";
|
|
5
5
|
import MockAdapter from "axios-mock-adapter";
|
|
6
|
+
import Papa from "papaparse";
|
|
7
|
+
import { Menu, Transition } from "@headlessui/react";
|
|
8
|
+
import { DataTemplate } from "../dataTemplate";
|
|
6
9
|
import axios from "axios";
|
|
7
10
|
import dayjs from "../../dayjs";
|
|
8
11
|
import { Button } from "../../action/button";
|
|
@@ -17,11 +20,11 @@ import {
|
|
|
17
20
|
FeedsSettings,
|
|
18
21
|
KanbanSettings,
|
|
19
22
|
} from "../dataTemplate/spaceModel";
|
|
20
|
-
import { DTWChecklistSchema } from "../testSchema";
|
|
21
23
|
import {
|
|
22
24
|
DataWrapperTestSchema,
|
|
23
25
|
SCROLL_TEST_API,
|
|
24
26
|
SCROLL_TEST_COLLECTION,
|
|
27
|
+
ServiceChecklistSchema,
|
|
25
28
|
} from "./dataWrapper.fixtures";
|
|
26
29
|
import { DataTemplateWrapper } from "./index";
|
|
27
30
|
import { StarIcon } from "@heroicons/react/24/outline";
|
|
@@ -154,14 +157,14 @@ export const DataWrapper = () => {
|
|
|
154
157
|
type={viewType}
|
|
155
158
|
id={selectedId}
|
|
156
159
|
collectionId={"dtw-test-collection"}
|
|
157
|
-
schema={
|
|
160
|
+
schema={ServiceChecklistSchema}
|
|
158
161
|
workspace={{
|
|
159
162
|
apiEndpointType: "mongoDB",
|
|
160
163
|
apiEndpoint: `${dtwBaseUrl}/api/mongoData`,
|
|
161
164
|
apiEndpointToken: "",
|
|
162
165
|
apiUserToken: "",
|
|
163
166
|
assetEndpoint:
|
|
164
|
-
"https://firebasestorage.googleapis.com/v0/b/
|
|
167
|
+
"https://firebasestorage.googleapis.com/v0/b/demo-bucket.appspot.com/o/",
|
|
165
168
|
assetUploadEndpoint: `${dtwBaseUrl}/api/asset`,
|
|
166
169
|
}}
|
|
167
170
|
edit={{
|
|
@@ -390,7 +393,7 @@ export const DataWrapper = () => {
|
|
|
390
393
|
);
|
|
391
394
|
};
|
|
392
395
|
|
|
393
|
-
// INFO: Live-data demo for a multi-tab, multi-group schema (
|
|
396
|
+
// INFO: Live-data demo for a multi-tab, multi-group schema (long grant-application style).
|
|
394
397
|
// Schema uses tabId to split fields into top-level tabs and groupId to split
|
|
395
398
|
// fields within a tab into named sections. organise.groupRead = "groupId"
|
|
396
399
|
// also renders the read view as grouped sections instead of a flat list.
|
|
@@ -976,7 +979,7 @@ export const DataWrapperDrawerTest = () => {
|
|
|
976
979
|
apiEndpointToken: "",
|
|
977
980
|
apiUserToken: "",
|
|
978
981
|
assetEndpoint:
|
|
979
|
-
"https://firebasestorage.googleapis.com/v0/b/
|
|
982
|
+
"https://firebasestorage.googleapis.com/v0/b/demo-bucket.firebasestorage.app/o/",
|
|
980
983
|
assetUploadEndpoint: `${dtwBaseUrl}/api/asset`,
|
|
981
984
|
}}
|
|
982
985
|
edit={{
|
|
@@ -1271,3 +1274,74 @@ export const DTWScrollTest = () => {
|
|
|
1271
1274
|
);
|
|
1272
1275
|
};
|
|
1273
1276
|
DTWScrollTest.storyName = "DTW Scroll Test";
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* INFO: the same drawer, hosted by DataTemplate itself rather than by the
|
|
1280
|
+
* wrapper. An app that renders DataTemplate directly — because it owns its own
|
|
1281
|
+
* tabs, roles and devSettings, which the wrapper derives and would replace —
|
|
1282
|
+
* can now opt in with one prop instead of changing component.
|
|
1283
|
+
*
|
|
1284
|
+
* The mechanism is self-recursion: with readDrawer set and type "read", the
|
|
1285
|
+
* outer instance keeps rendering its LIST and nests a second DataTemplate with
|
|
1286
|
+
* the flag stripped inside the panel. One level, then it terminates.
|
|
1287
|
+
*/
|
|
1288
|
+
export const DTReadDrawer = () => {
|
|
1289
|
+
const [mount, setMount] = useState<boolean>(false);
|
|
1290
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
1291
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
1292
|
+
const storeRef = useRef<any[]>(handleBuildDrawerDemoRecords());
|
|
1293
|
+
const mockRef = useRef<any>(null);
|
|
1294
|
+
|
|
1295
|
+
useEffect(() => {
|
|
1296
|
+
mockRef.current = handleInstallDrawerDemoMock(storeRef.current);
|
|
1297
|
+
setMount(true);
|
|
1298
|
+
return () => {
|
|
1299
|
+
mockRef.current?.restore();
|
|
1300
|
+
mockRef.current = null;
|
|
1301
|
+
};
|
|
1302
|
+
}, []);
|
|
1303
|
+
|
|
1304
|
+
const handleNavigate = (type: any, id: any) => {
|
|
1305
|
+
setViewType(type);
|
|
1306
|
+
setSelectedId(id);
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
if (!mount) return <Loading />;
|
|
1310
|
+
|
|
1311
|
+
return (
|
|
1312
|
+
<div className="w-full">
|
|
1313
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
1314
|
+
DataTemplate Read Drawer
|
|
1315
|
+
</div>
|
|
1316
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
1317
|
+
`viewSettings.readDrawer` on DataTemplate directly. Click a row: the
|
|
1318
|
+
record opens in a panel and the table stays mounted behind it, keeping
|
|
1319
|
+
its page index and filters. Nothing else about the props changes — no
|
|
1320
|
+
schema/model rename, no workspace mapping, and the caller keeps its own
|
|
1321
|
+
tabs.
|
|
1322
|
+
</div>
|
|
1323
|
+
<DataTemplate
|
|
1324
|
+
title="Drawer Demo"
|
|
1325
|
+
description=""
|
|
1326
|
+
type={viewType}
|
|
1327
|
+
id={selectedId}
|
|
1328
|
+
collectionId={DRAWER_DEMO_COLLECTION}
|
|
1329
|
+
model={DrawerDemoSchema as any}
|
|
1330
|
+
localURLEndpoint=""
|
|
1331
|
+
viewSettings={{ readDrawer: {} }}
|
|
1332
|
+
devSettings={{
|
|
1333
|
+
server: "mongoDB",
|
|
1334
|
+
dbEndpoint: DRAWER_DEMO_ENDPOINT,
|
|
1335
|
+
dbAPIToken: "",
|
|
1336
|
+
dbUserToken: "",
|
|
1337
|
+
schemaEndpoint: "",
|
|
1338
|
+
imageEndpoint: "",
|
|
1339
|
+
}}
|
|
1340
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
1341
|
+
edit={{ allEditable: true, deleteBtn: false }}
|
|
1342
|
+
bareSettings={{ bareNewDTW: true, bareCompactMode: true }}
|
|
1343
|
+
organise={{ handleNavigate }}
|
|
1344
|
+
/>
|
|
1345
|
+
</div>
|
|
1346
|
+
);
|
|
1347
|
+
};
|
|
@@ -42,16 +42,17 @@ import { DetailList } from "blixify-ui-web/components/display/detailList";
|
|
|
42
42
|
|
|
43
43
|
##### Optional props
|
|
44
44
|
|
|
45
|
-
| Name | Type | Description
|
|
46
|
-
| --------------------- | ------------------------------------------------ |
|
|
47
|
-
| `addTitleElement` | `ReactElement` | Additional react elemnt to be displayed next to title
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
45
|
+
| Name | Type | Description |
|
|
46
|
+
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
47
|
+
| `addTitleElement` | `ReactElement` | Additional react elemnt to be displayed next to title |
|
|
48
|
+
| `headerRightElement` | `ReactElement` | Element anchored to the top-right of the header, opposite the title/description |
|
|
49
|
+
| `description` | `string` | |
|
|
50
|
+
| `className` | `tailwind class` | |
|
|
51
|
+
| `model` | `Model Data Structure` | Only used in Data Template |
|
|
52
|
+
| `hideModal` | `boolean` | Only used in Data Template |
|
|
53
|
+
| `referenceModel` | `Model Data Structure` | Only used in Data Template |
|
|
54
|
+
| `splitView` | `boolean` | Able to split into two |
|
|
55
|
+
| `handleReferenceLink` | `(dataId: string, collectionId: string) => void` | Only used in Data Template, when reference being clicked |
|
|
55
56
|
|
|
56
57
|
##### Detail List class
|
|
57
58
|
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { DetailList as DetailListImpl } from ".";
|
|
3
3
|
import { Tag } from "../tag";
|
|
4
4
|
import { StorySection } from "../../storyUtils";
|
|
5
|
+
import { Button } from "../../action/button";
|
|
5
6
|
|
|
6
7
|
const meta = { title: "Display/Detail List" };
|
|
7
8
|
export default meta;
|
|
@@ -49,7 +50,7 @@ For more details visit [the official docs](https://example.com).
|
|
|
49
50
|
|
|
50
51
|
## Media
|
|
51
52
|
|
|
52
|
-
<img src="https://
|
|
53
|
+
<img src="https://placehold.co/160x120/e2e8f0/475569.png?text=Oil+drum" alt="Oil drum" width="160" style="border-radius:8px">
|
|
53
54
|
|
|
54
55
|
<video controls width="320">
|
|
55
56
|
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
|
|
@@ -93,6 +94,18 @@ export const DetailList = () => (
|
|
|
93
94
|
maxHeight="180px"
|
|
94
95
|
/>
|
|
95
96
|
</StorySection>
|
|
97
|
+
<StorySection
|
|
98
|
+
title="Header Right Element"
|
|
99
|
+
description="With custom element on the top right of header"
|
|
100
|
+
>
|
|
101
|
+
<DetailListImpl
|
|
102
|
+
title="Order #4812"
|
|
103
|
+
description="With custom element on the top right of header"
|
|
104
|
+
list={sampleList}
|
|
105
|
+
size="compact"
|
|
106
|
+
headerRightElement={<Button size="small" type="normal" text="Action" />}
|
|
107
|
+
/>
|
|
108
|
+
</StorySection>
|
|
96
109
|
|
|
97
110
|
<StorySection
|
|
98
111
|
title="splitView (legacy) — Two-Column Grid"
|
|
@@ -145,7 +158,7 @@ export const DetailList = () => (
|
|
|
145
158
|
title: "Image",
|
|
146
159
|
text: (
|
|
147
160
|
<img
|
|
148
|
-
src="https://
|
|
161
|
+
src="https://placehold.co/160x120/e2e8f0/475569.png?text=Oil+drum"
|
|
149
162
|
alt=""
|
|
150
163
|
/>
|
|
151
164
|
),
|
|
@@ -187,7 +200,7 @@ export const DetailList = () => (
|
|
|
187
200
|
title: "Image",
|
|
188
201
|
text: (
|
|
189
202
|
<img
|
|
190
|
-
src="https://
|
|
203
|
+
src="https://placehold.co/160x120/e2e8f0/475569.png?text=Oil+drum"
|
|
191
204
|
alt=""
|
|
192
205
|
/>
|
|
193
206
|
),
|
|
@@ -31,12 +31,12 @@ const feeds: Feed[] = [
|
|
|
31
31
|
{
|
|
32
32
|
iconClass: "bg-blue-500",
|
|
33
33
|
iconType: <HandThumbUpIcon className="h-5 w-5 text-white" />,
|
|
34
|
-
content: "Notify
|
|
34
|
+
content: "Notify the logistics team once you are ready to sell",
|
|
35
35
|
step: "Step 2",
|
|
36
36
|
addContent: (
|
|
37
37
|
<img
|
|
38
38
|
className="mt-4 w-36 sm:w-56 h-auto"
|
|
39
|
-
src="https://
|
|
39
|
+
src="https://placehold.co/160x120/e2e8f0/475569.png?text=Oil+drum"
|
|
40
40
|
alt=""
|
|
41
41
|
/>
|
|
42
42
|
),
|
|
@@ -44,7 +44,7 @@ const feeds: Feed[] = [
|
|
|
44
44
|
{
|
|
45
45
|
iconClass: "bg-green-500",
|
|
46
46
|
iconType: <CheckIcon className="h-5 w-5 text-white" />,
|
|
47
|
-
content: "View
|
|
47
|
+
content: "View payments made to you in the app",
|
|
48
48
|
step: "Step 3",
|
|
49
49
|
},
|
|
50
50
|
];
|
|
@@ -38,12 +38,12 @@ export const Feeds = () => {
|
|
|
38
38
|
id: "2",
|
|
39
39
|
iconClass: "bg-blue-500",
|
|
40
40
|
iconType: <HandThumbUpIcon className="h-5 w-5 text-white" />,
|
|
41
|
-
content: "Notify
|
|
41
|
+
content: "Notify the logistics team once you are ready to sell",
|
|
42
42
|
step: "2025-12-11",
|
|
43
43
|
addContent: (
|
|
44
44
|
<img
|
|
45
45
|
className="mt-4 w-36 sm:w-56 h-auto"
|
|
46
|
-
src="https://
|
|
46
|
+
src="https://placehold.co/160x120/e2e8f0/475569.png?text=Oil+drum"
|
|
47
47
|
alt=""
|
|
48
48
|
/>
|
|
49
49
|
),
|
|
@@ -52,7 +52,7 @@ export const Feeds = () => {
|
|
|
52
52
|
id: "3",
|
|
53
53
|
iconClass: "bg-green-500",
|
|
54
54
|
iconType: <CheckIcon className="h-5 w-5 text-white" />,
|
|
55
|
-
content: "View
|
|
55
|
+
content: "View payments made to you in the app",
|
|
56
56
|
step: "2025-12-15",
|
|
57
57
|
},
|
|
58
58
|
];
|