blixify-ui-web-mcp 0.1.61 → 0.1.63

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.
@@ -259,6 +259,38 @@ const isStatusClosed = liveFilters.some(
259
259
  );
260
260
  ```
261
261
 
262
+ **Filtering on a referenced record's fields**
263
+
264
+ `bareFlexFilterReferences` maps a `reference` or `listReference` column to the collection it points to and that collection's columns. The list and export filters then offer each of those columns as `<Column> › <Field>`, one level deep. A condition on one is sent with a `foreign` lookup (alias `ref_<column>`), which MongoWrapper joins before its `$match`. Hidden columns, `_id`, and reference, file and object columns of the referenced record are not offered.
265
+
266
+ ```tsx
267
+ bareSettings={{
268
+ bareFlexFilters: true,
269
+ bareFlexFilterReferences: {
270
+ lead: { collection: "master-leads", structure: LeadSchema },
271
+ },
272
+ }}
273
+
274
+ // "Lead › Status is Won" is sent as
275
+ // { type: "=", queryId: "ref_lead.status", value: "Won",
276
+ // foreign: [{ collection: "master-leads", localField: "lead", alias: "ref_lead", searchFields: [] }] }
277
+ ```
278
+
279
+ **Filtering on a listObject**
280
+
281
+ `bareFlexFilterListObjects: true` offers each `listObject` column's fields as `<List> › <Field>`, sent on their path (`crew.role`), so a record matches when any row does. It also offers `<List> › Count` with Is, Greater Than, Less Than and Between. A count is sent as whether the list's n-th row exists, so it holds on records where the list was never set:
282
+
283
+ | Count filter | Sent as |
284
+ | --- | --- |
285
+ | Is 3 | `crew.2` exists, `crew.3` does not |
286
+ | Greater Than 2 | `crew.2` exists |
287
+ | Less Than 2 | `crew.1` does not exist |
288
+ | Between 2 and 5 | `crew.1` exists, `crew.5` does not |
289
+
290
+ Two conditions on one list may each be met by a different row. Referenced records and listObjects are not offered on Firebase or `unionCollections` lists.
291
+
292
+ The standalone `FlexFilter` takes both as `extras={{ references, listObjects: true }}`; pass the same object to `renderFlexFilterConditions` when building the query.
293
+
262
294
  **8. newTabUrl (Optional)**
263
295
 
264
296
  Enables right-click → "Open in new tab" on list rows. The component builds the link as `default + recordId + addons`.
@@ -0,0 +1,183 @@
1
+ import { Transition } from "@headlessui/react";
2
+ import axios from "axios";
3
+ import React, { useState } from "react";
4
+ import { StorySection } from "../../storyUtils";
5
+ import { Loading } from "../../display/loading";
6
+ import { DataStructure, DevSettings } from "../dataTemplate/model";
7
+ import { DataTemplateWrapper } from "../dataWrapper";
8
+ import {
9
+ STORY_API_ENDPOINT,
10
+ STORY_NAMES,
11
+ storyWorkspace,
12
+ useStoryBackend,
13
+ } from "../dataWrapper/storyBackend.fixtures";
14
+ import {
15
+ FlexFilter,
16
+ FlexFilterReferences,
17
+ renderFlexFilterConditions,
18
+ } from ".";
19
+ import { FlexFilterData } from "./model";
20
+
21
+ const meta = { title: "Data/Flex Filter/Referenced Record" };
22
+ export default meta;
23
+
24
+ const LEAD_COLLECTION = "demo-ref-leads";
25
+ const REFERRAL_COLLECTION = "demo-ref-referrals";
26
+ const LEAD_STATUSES = ["New", "Quoted", "Won", "Lost"];
27
+
28
+ const leadSchema: DataStructure[] = [
29
+ { id: "name", name: "Name", type: "string" },
30
+ {
31
+ id: "status",
32
+ name: "Status",
33
+ type: "list",
34
+ listSelection: LEAD_STATUSES,
35
+ },
36
+ { id: "systemSize", name: "System Size (kWp)", type: "number" },
37
+ { id: "signedAt", name: "Signed Date", type: "date" },
38
+ ];
39
+
40
+ const referralSchema: DataStructure[] = [
41
+ { id: "referrer", name: "Referrer", type: "string" },
42
+ {
43
+ id: "leadId",
44
+ name: "Lead",
45
+ type: "reference",
46
+ referenceSpaceId: LEAD_COLLECTION,
47
+ referenceLabel: "name",
48
+ },
49
+ {
50
+ id: "payout",
51
+ name: "Payout",
52
+ type: "list",
53
+ listSelection: ["Pending", "Paid"],
54
+ },
55
+ { id: "createdAt", name: "Created At", type: "date" },
56
+ {
57
+ id: "followUps",
58
+ name: "Follow-ups",
59
+ type: "listObject",
60
+ objectData: [
61
+ {
62
+ id: "outcome",
63
+ name: "Outcome",
64
+ type: "list",
65
+ listSelection: ["No Answer", "Callback", "Site Visit"],
66
+ },
67
+ { id: "note", name: "Note", type: "string" },
68
+ ],
69
+ },
70
+ ];
71
+
72
+ const OUTCOMES = ["No Answer", "Callback", "Site Visit"];
73
+
74
+ const references: FlexFilterReferences = {
75
+ leadId: { collection: LEAD_COLLECTION, structure: leadSchema },
76
+ };
77
+
78
+ // INFO: dates spread around today, so "This month" and "Last 30 days" match some
79
+ const handleDaysAgo = (days: number) =>
80
+ new Date(Date.now() - days * 86400000).toISOString();
81
+
82
+ const leads = STORY_NAMES.slice(0, 12).map((name, index) => ({
83
+ _id: `lead-${index + 1}`,
84
+ name,
85
+ status: LEAD_STATUSES[index % LEAD_STATUSES.length],
86
+ systemSize: 4 + (index % 5) * 2,
87
+ signedAt: handleDaysAgo(index * 9),
88
+ }));
89
+
90
+ const referrals = leads.map((eachLead, index) => ({
91
+ _id: `referral-${index + 1}`,
92
+ referrer: STORY_NAMES[(index + 5) % STORY_NAMES.length],
93
+ leadId: eachLead._id,
94
+ payout: index % 3 === 0 ? "Paid" : "Pending",
95
+ createdAt: handleDaysAgo(index * 4),
96
+ // INFO: 0 to 3 follow-ups, so a count filter has something to split
97
+ followUps: Array.from({ length: index % 4 }, (_, row) => ({
98
+ outcome: OUTCOMES[(index + row) % OUTCOMES.length],
99
+ note: `Call ${row + 1}`,
100
+ })),
101
+ }));
102
+
103
+ const extras = { references, listObjects: true };
104
+
105
+ const devSettings: DevSettings = {
106
+ server: "mongoServer",
107
+ dbEndpoint: STORY_API_ENDPOINT,
108
+ imageEndpoint: "",
109
+ uploadEndpoint: "",
110
+ dbAPIToken: "",
111
+ dbUserToken: "",
112
+ schemaEndpoint: "",
113
+ bucketName: "",
114
+ organisationMembers: [],
115
+ };
116
+
117
+ export const ReferencedRecord = () => {
118
+ const mount = useStoryBackend({
119
+ collections: {
120
+ [LEAD_COLLECTION]: leads,
121
+ [REFERRAL_COLLECTION]: referrals,
122
+ },
123
+ });
124
+ const [filters, setFilters] = useState<FlexFilterData[]>([
125
+ { id: "init", columnId: "", operator: "=", value: undefined },
126
+ ]);
127
+ const query = renderFlexFilterConditions(filters, referralSchema, extras);
128
+
129
+ if (!mount) return <Loading />;
130
+
131
+ return (
132
+ <div className="flex flex-col space-y-12">
133
+ <StorySection
134
+ title="Referrals list filtered on the lead and the follow-ups"
135
+ description="Open the filter and pick a Lead › field, e.g. Lead › Status is Won, or Follow-ups › Count / Outcome."
136
+ >
137
+ <DataTemplateWrapper
138
+ title="Referrals"
139
+ type="list"
140
+ collectionId={REFERRAL_COLLECTION}
141
+ schema={referralSchema}
142
+ workspace={storyWorkspace()}
143
+ limit={20}
144
+ organise={{ breadcrumbType: "hidden" }}
145
+ bareSettings={{
146
+ bareNewDTW: true,
147
+ bareFlexFilters: true,
148
+ bareFlexFilterReferences: references,
149
+ bareFlexFilterListObjects: true,
150
+ }}
151
+ />
152
+ </StorySection>
153
+
154
+ <StorySection
155
+ title="Standalone filter (notification audience)"
156
+ description="The same extras on the bare FlexFilter, with the query it builds."
157
+ >
158
+ <div className="flex flex-col">
159
+ <div className="max-w-[700px]">
160
+ <FlexFilter
161
+ structure={referralSchema}
162
+ extras={extras}
163
+ filters={filters}
164
+ onChange={setFilters}
165
+ devSettings={devSettings}
166
+ dbModule={axios}
167
+ variant="bare"
168
+ lib={{ Transition }}
169
+ />
170
+ </div>
171
+ <div className="mt-4 p-4 bg-gray-50 rounded-lg border">
172
+ <p className="text-sm font-semibold mb-2">
173
+ Generated Query Conditions ({query.length})
174
+ </p>
175
+ <pre className="text-xs whitespace-pre-wrap">
176
+ {JSON.stringify(query, null, 2)}
177
+ </pre>
178
+ </div>
179
+ </div>
180
+ </StorySection>
181
+ </div>
182
+ );
183
+ };
@@ -79,7 +79,7 @@ Async / database search:
79
79
  | `disableKeyboard` | `boolean` | `false` | Disable keyboard navigation in the dropdown |
80
80
  | `showTag` | `boolean` | — | Show selected values as removable tags below the input (multi select only) |
81
81
  | `closeOnClickOutside` | `boolean` | — | Keep the dropdown open until the user clicks outside. Multi select only |
82
- | `mobileView` | `boolean` | — | Open a bottom-sheet modal on screens narrower than 768px instead of the inline dropdown |
82
+ | `mobileView` | `boolean` | — | Open a bottom-sheet modal on screens narrower than 768px instead of the inline dropdown. With an array `value` the sheet stays open after each pick and shows a checkbox per option |
83
83
  | `portalDropdown` | `boolean` | — | Render the dropdown via a React portal (fixed position). Useful inside overflow-hidden containers |
84
84
  | `portalDropdownScrollOffset` | `boolean` | `true` | Only applies when `portalDropdown` is true. When `true` (default), adds `window.scrollY/X` to the dropdown position — correct for normal page usage. Set to `false` inside tables or overflow containers where `getBoundingClientRect()` already returns viewport-relative coords (e.g. FlexTable hardcodes this to `false` automatically) |
85
85
  | `staticDropdown` | `boolean` | — | Render the dropdown in static position (takes up space below the input) instead of absolute |
@@ -53,6 +53,7 @@ const genderOptions: Option[] = [
53
53
  export const Select = () => {
54
54
  const [value, setValue] = useState("");
55
55
  const [multiValue, setMultiValue] = useState<string[]>(["M"]);
56
+ const [mobileMultiValue, setMobileMultiValue] = useState<string[]>([]);
56
57
  const [dbValue, setDBValue] = useState("");
57
58
  const [dbOptions, setDBOptions] = useState<Option[]>([
58
59
  { key: "M", label: "Male", secondaryLabel: "Secondary Label" },
@@ -208,7 +209,7 @@ export const Select = () => {
208
209
 
209
210
  <StorySection
210
211
  title="Mobile View"
211
- description="mobileView=true opens a bottom-sheet modal on screens narrower than 768px instead of the inline dropdown."
212
+ description="mobileView=true opens a bottom-sheet modal on screens narrower than 768px instead of the inline dropdown. A multi-select sheet stays open and shows a checkbox per option."
212
213
  >
213
214
  <div className="w-full md:w-1/2">
214
215
  <SelectImpl
@@ -219,6 +220,16 @@ export const Select = () => {
219
220
  onChange={(v: string) => setValue(v)}
220
221
  />
221
222
  </div>
223
+ <div className="mt-4 w-full md:w-1/2">
224
+ <SelectImpl
225
+ value={mobileMultiValue}
226
+ label="Mobile View Multi"
227
+ options={genderOptions}
228
+ mobileView
229
+ showTag
230
+ onChange={(v: string[]) => setMobileMultiValue(v)}
231
+ />
232
+ </div>
222
233
  </StorySection>
223
234
 
224
235
  <StorySection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-ui-web-mcp",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "license": "MIT",
5
5
  "main": "lib/mcp/server.js",
6
6
  "bin": {