flutterflow-mcp 0.2.1 → 0.2.3
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/build/tools/find-component-usages.d.ts +14 -0
- package/build/tools/find-component-usages.js +2 -2
- package/build/tools/find-page-navigations.d.ts +19 -0
- package/build/tools/find-page-navigations.js +2 -2
- package/build/tools/get-component-summary.js +3 -1
- package/build/tools/get-page-summary.js +3 -1
- package/build/utils/page-summary/action-summarizer.d.ts +16 -1
- package/build/utils/page-summary/action-summarizer.js +3 -3
- package/build/utils/page-summary/formatter.js +9 -1
- package/build/utils/page-summary/node-extractor.d.ts +7 -0
- package/build/utils/page-summary/node-extractor.js +22 -2
- package/build/utils/page-summary/types.d.ts +2 -0
- package/docs/ff-yaml/03-components.md +62 -2
- package/docs/ff-yaml/05-actions.md +112 -2
- package/package.json +6 -2
|
@@ -4,4 +4,18 @@
|
|
|
4
4
|
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the value of a parameter pass to a readable string.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveParamValue(paramObj: Record<string, unknown>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Extract parent context (page or component name + ID) from a file key path.
|
|
13
|
+
* Examples:
|
|
14
|
+
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
15
|
+
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseParentFromKey(fileKey: string): {
|
|
18
|
+
type: "page" | "component";
|
|
19
|
+
id: string;
|
|
20
|
+
} | null;
|
|
7
21
|
export declare function registerFindComponentUsagesTool(server: McpServer): void;
|
|
@@ -18,7 +18,7 @@ async function batchProcess(items, batchSize, fn) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolve the value of a parameter pass to a readable string.
|
|
20
20
|
*/
|
|
21
|
-
function resolveParamValue(paramObj) {
|
|
21
|
+
export function resolveParamValue(paramObj) {
|
|
22
22
|
// Check for variable source (e.g. INTERNATIONALIZATION)
|
|
23
23
|
const variable = paramObj.variable;
|
|
24
24
|
if (variable) {
|
|
@@ -60,7 +60,7 @@ function resolveParamValue(paramObj) {
|
|
|
60
60
|
* "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Widget_yyy"
|
|
61
61
|
* "component/id-Container_xxx/component-widget-tree-outline/node/id-Widget_yyy"
|
|
62
62
|
*/
|
|
63
|
-
function parseParentFromKey(fileKey) {
|
|
63
|
+
export function parseParentFromKey(fileKey) {
|
|
64
64
|
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\//);
|
|
65
65
|
if (pageMatch)
|
|
66
66
|
return { type: "page", id: pageMatch[1] };
|
|
@@ -4,4 +4,23 @@
|
|
|
4
4
|
* Zero API calls: everything comes from the local .ff-cache.
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
/**
|
|
8
|
+
* Parse parent context from an action file key.
|
|
9
|
+
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseActionContext(fileKey: string): {
|
|
12
|
+
parentType: "page" | "component";
|
|
13
|
+
parentId: string;
|
|
14
|
+
widgetKey: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
19
|
+
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findNavigateAction(obj: unknown, targetScaffoldId: string, isDisabled: boolean, depth: number): {
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
allowBack: boolean;
|
|
24
|
+
passedParams: string[];
|
|
25
|
+
} | null;
|
|
7
26
|
export declare function registerFindPageNavigationsTool(server: McpServer): void;
|
|
@@ -18,7 +18,7 @@ async function batchProcess(items, batchSize, fn) {
|
|
|
18
18
|
* Parse parent context from an action file key.
|
|
19
19
|
* Example: "page/id-Scaffold_XXX/page-widget-tree-outline/node/id-Widget_YYY/trigger_actions/id-ON_TAP/action/id-zzz"
|
|
20
20
|
*/
|
|
21
|
-
function parseActionContext(fileKey) {
|
|
21
|
+
export function parseActionContext(fileKey) {
|
|
22
22
|
// Page action
|
|
23
23
|
const pageMatch = fileKey.match(/^page\/id-(Scaffold_\w+)\/.*\/node\/id-(\w+)\/trigger_actions\/id-([^/]+)\/action\//);
|
|
24
24
|
if (pageMatch) {
|
|
@@ -45,7 +45,7 @@ function parseActionContext(fileKey) {
|
|
|
45
45
|
* Recursively search an object for a navigate action targeting the given scaffold ID.
|
|
46
46
|
* Returns navigate details if found, including whether it's inside a disableAction.
|
|
47
47
|
*/
|
|
48
|
-
function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
|
|
48
|
+
export function findNavigateAction(obj, targetScaffoldId, isDisabled, depth) {
|
|
49
49
|
if (!obj || typeof obj !== "object" || depth > 12)
|
|
50
50
|
return null;
|
|
51
51
|
const o = obj;
|
|
@@ -110,6 +110,8 @@ outline) {
|
|
|
110
110
|
name: nodeInfo.name,
|
|
111
111
|
slot: outline.slot,
|
|
112
112
|
detail: nodeInfo.detail,
|
|
113
|
+
componentRef: nodeInfo.componentRef,
|
|
114
|
+
componentId: nodeInfo.componentId,
|
|
113
115
|
triggers,
|
|
114
116
|
children,
|
|
115
117
|
};
|
|
@@ -118,7 +120,7 @@ outline) {
|
|
|
118
120
|
// Tool registration
|
|
119
121
|
// ---------------------------------------------------------------------------
|
|
120
122
|
export function registerGetComponentSummaryTool(server) {
|
|
121
|
-
server.tool("get_component_summary", "Get a readable summary of a FlutterFlow component from local cache — widget tree, actions, params. No API calls. Run sync_project first if not cached.", {
|
|
123
|
+
server.tool("get_component_summary", "Get a readable summary of a FlutterFlow component from local cache — widget tree, actions, params. Nested component references are resolved to show [ComponentName] (ComponentId). No API calls. Run sync_project first if not cached.", {
|
|
122
124
|
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
123
125
|
componentName: z
|
|
124
126
|
.string()
|
|
@@ -131,6 +131,8 @@ outline) {
|
|
|
131
131
|
name: nodeInfo.name,
|
|
132
132
|
slot: outline.slot,
|
|
133
133
|
detail: nodeInfo.detail,
|
|
134
|
+
componentRef: nodeInfo.componentRef,
|
|
135
|
+
componentId: nodeInfo.componentId,
|
|
134
136
|
triggers,
|
|
135
137
|
children,
|
|
136
138
|
};
|
|
@@ -139,7 +141,7 @@ outline) {
|
|
|
139
141
|
// Tool registration
|
|
140
142
|
// ---------------------------------------------------------------------------
|
|
141
143
|
export function registerGetPageSummaryTool(server) {
|
|
142
|
-
server.tool("get_page_summary", "Get a readable summary of a FlutterFlow page from local cache — widget tree, actions, params, state. No API calls. Run sync_project first if not cached.", {
|
|
144
|
+
server.tool("get_page_summary", "Get a readable summary of a FlutterFlow page from local cache — widget tree, actions, params, state. Component references are resolved to show [ComponentName] (ComponentId) instead of plain Container. Use the ComponentId with get_component_summary to drill into a component. No API calls. Run sync_project first if not cached.", {
|
|
143
145
|
projectId: z.string().describe("The FlutterFlow project ID"),
|
|
144
146
|
pageName: z
|
|
145
147
|
.string()
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { TriggerSummary } from "./types.js";
|
|
1
|
+
import { ActionSummary, TriggerSummary } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collect all action keys referenced in a trigger chain.
|
|
4
|
+
* Flattens followUpAction chains, conditionActions, and parallelActions.
|
|
5
|
+
*/
|
|
6
|
+
export declare function collectActionKeys(node: Record<string, unknown>): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Recursively search an object tree for the first recognizable action.
|
|
9
|
+
* Used to unwrap disableAction nodes where the real action is buried
|
|
10
|
+
* inside conditionalActions or other nesting.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findDeepAction(obj: unknown, depth?: number): ActionSummary | null;
|
|
13
|
+
/**
|
|
14
|
+
* Classify an action YAML into a human-readable summary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function classifyAction(doc: Record<string, unknown>): ActionSummary;
|
|
2
17
|
/**
|
|
3
18
|
* Read all trigger_actions for a node and return summaries.
|
|
4
19
|
*
|
|
@@ -10,7 +10,7 @@ import { cacheRead, listCachedKeys } from "../cache.js";
|
|
|
10
10
|
* Collect all action keys referenced in a trigger chain.
|
|
11
11
|
* Flattens followUpAction chains, conditionActions, and parallelActions.
|
|
12
12
|
*/
|
|
13
|
-
function collectActionKeys(node) {
|
|
13
|
+
export function collectActionKeys(node) {
|
|
14
14
|
const keys = [];
|
|
15
15
|
// Direct action reference
|
|
16
16
|
const action = node.action;
|
|
@@ -69,7 +69,7 @@ const ACTION_TYPE_KEYS = [
|
|
|
69
69
|
* Used to unwrap disableAction nodes where the real action is buried
|
|
70
70
|
* inside conditionalActions or other nesting.
|
|
71
71
|
*/
|
|
72
|
-
function findDeepAction(obj, depth = 0) {
|
|
72
|
+
export function findDeepAction(obj, depth = 0) {
|
|
73
73
|
if (!obj || typeof obj !== "object" || depth > 8)
|
|
74
74
|
return null;
|
|
75
75
|
const o = obj;
|
|
@@ -91,7 +91,7 @@ function findDeepAction(obj, depth = 0) {
|
|
|
91
91
|
/**
|
|
92
92
|
* Classify an action YAML into a human-readable summary.
|
|
93
93
|
*/
|
|
94
|
-
function classifyAction(doc) {
|
|
94
|
+
export function classifyAction(doc) {
|
|
95
95
|
// Disabled action — unwrap and recursively find the inner action
|
|
96
96
|
if ("disableAction" in doc) {
|
|
97
97
|
const da = doc.disableAction;
|
|
@@ -19,7 +19,15 @@ function fmtSlot(slot) {
|
|
|
19
19
|
function nodeLabel(node) {
|
|
20
20
|
const parts = [];
|
|
21
21
|
parts.push(fmtSlot(node.slot));
|
|
22
|
-
|
|
22
|
+
if (node.componentRef) {
|
|
23
|
+
parts.push(`[${node.componentRef}]`);
|
|
24
|
+
if (node.componentId) {
|
|
25
|
+
parts.push(` (${node.componentId})`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
parts.push(node.type);
|
|
30
|
+
}
|
|
23
31
|
if (node.name) {
|
|
24
32
|
parts.push(` (${node.name})`);
|
|
25
33
|
}
|
|
@@ -2,7 +2,14 @@ export interface NodeInfo {
|
|
|
2
2
|
type: string;
|
|
3
3
|
name: string;
|
|
4
4
|
detail: string;
|
|
5
|
+
componentRef?: string;
|
|
6
|
+
componentId?: string;
|
|
5
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the inputValue from a FF value object.
|
|
10
|
+
* Returns the literal string, or "[dynamic]" for variable references.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveValue(obj: unknown): string;
|
|
6
13
|
/**
|
|
7
14
|
* Infer widget type from the key prefix (e.g. "Text_abc123" → "Text").
|
|
8
15
|
*/
|
|
@@ -8,7 +8,7 @@ import { cacheRead } from "../cache.js";
|
|
|
8
8
|
* Resolve the inputValue from a FF value object.
|
|
9
9
|
* Returns the literal string, or "[dynamic]" for variable references.
|
|
10
10
|
*/
|
|
11
|
-
function resolveValue(obj) {
|
|
11
|
+
export function resolveValue(obj) {
|
|
12
12
|
if (obj == null)
|
|
13
13
|
return "";
|
|
14
14
|
if (typeof obj === "string" || typeof obj === "number")
|
|
@@ -172,6 +172,18 @@ export function inferTypeFromKey(key) {
|
|
|
172
172
|
const match = key.match(/^([A-Z][a-zA-Z]*)_/);
|
|
173
173
|
return match ? match[1] : "Unknown";
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolve a component reference to its human-readable name.
|
|
177
|
+
* Reads the component definition file from cache (e.g. "component/id-Container_xxx").
|
|
178
|
+
*/
|
|
179
|
+
async function resolveComponentName(projectId, componentKey) {
|
|
180
|
+
const compFileKey = `component/id-${componentKey}`;
|
|
181
|
+
const content = await cacheRead(projectId, compFileKey);
|
|
182
|
+
if (!content)
|
|
183
|
+
return undefined;
|
|
184
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
185
|
+
return nameMatch ? nameMatch[1].trim() : undefined;
|
|
186
|
+
}
|
|
175
187
|
/**
|
|
176
188
|
* Read a node's cached file and extract type, name, and detail.
|
|
177
189
|
*
|
|
@@ -195,7 +207,15 @@ export async function extractNodeInfo(projectId, pagePrefix, nodeKey) {
|
|
|
195
207
|
const name = doc.name || "";
|
|
196
208
|
const props = doc.props || {};
|
|
197
209
|
const detail = extractDetail(type, props);
|
|
198
|
-
|
|
210
|
+
// Check for component reference
|
|
211
|
+
const compRef = doc.componentClassKeyRef;
|
|
212
|
+
let componentRef;
|
|
213
|
+
let componentId;
|
|
214
|
+
if (compRef?.key && typeof compRef.key === "string") {
|
|
215
|
+
componentId = compRef.key;
|
|
216
|
+
componentRef = await resolveComponentName(projectId, componentId);
|
|
217
|
+
}
|
|
218
|
+
return { type, name, detail, componentRef, componentId };
|
|
199
219
|
}
|
|
200
220
|
catch {
|
|
201
221
|
return {
|
|
@@ -414,7 +414,67 @@ node:
|
|
|
414
414
|
|
|
415
415
|
---
|
|
416
416
|
|
|
417
|
-
## 10.
|
|
417
|
+
## 10. Components in Summary Output
|
|
418
|
+
|
|
419
|
+
The `get_page_summary` and `get_component_summary` tools resolve component references and display them with a distinct format in the widget tree. This makes it easy to distinguish regular widgets from embedded components at a glance.
|
|
420
|
+
|
|
421
|
+
### Format
|
|
422
|
+
|
|
423
|
+
Component instances appear as `[ComponentName] (Container_ID)` instead of just `Container`:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
FeedHomePage (Scaffold_e5ows2lg) — folder: home
|
|
427
|
+
|
|
428
|
+
ON_INIT_STATE → [customAction: checkNotificationPermissionResult, ...]
|
|
429
|
+
|
|
430
|
+
Widget Tree:
|
|
431
|
+
└── [body] Container
|
|
432
|
+
└── Column
|
|
433
|
+
└── Column
|
|
434
|
+
├── [Header] (Container_ur4ml9qw)
|
|
435
|
+
├── [SearchBar] (Container_qw4kqc4l)
|
|
436
|
+
└── Container
|
|
437
|
+
└── [PostsList] (Container_pgvko7fz)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Reading the output
|
|
441
|
+
|
|
442
|
+
| Element | Meaning |
|
|
443
|
+
|---|---|
|
|
444
|
+
| `Container`, `Column`, `Row`, etc. | Regular widget — defined inline on this page/component |
|
|
445
|
+
| `[Header] (Container_ur4ml9qw)` | Component instance — `Header` is the component name, `Container_ur4ml9qw` is the component ID |
|
|
446
|
+
| `[body]`, `[appBar]`, etc. | Slot prefix — which slot of the parent widget this node fills |
|
|
447
|
+
| `→ ON_TAP → [navigate: to page]` | Trigger — action(s) attached to this widget |
|
|
448
|
+
|
|
449
|
+
### Drilling into components
|
|
450
|
+
|
|
451
|
+
The component ID in parentheses (e.g. `Container_ur4ml9qw`) can be used to retrieve the full component structure:
|
|
452
|
+
|
|
453
|
+
- **`get_component_summary`** — pass `componentId: "Container_ur4ml9qw"` (or `componentName: "Header"`) to see the component's internal widget tree, params, and actions
|
|
454
|
+
- **`get_project_yaml`** — pass `fileName: "component/id-Container_ur4ml9qw"` to get the raw component metadata YAML
|
|
455
|
+
- **`find_component_usages`** — pass `componentId: "Container_ur4ml9qw"` to find all pages and components where this component is used
|
|
456
|
+
|
|
457
|
+
### Nested components
|
|
458
|
+
|
|
459
|
+
Components can contain other components. The same `[Name] (ID)` format appears at any nesting level:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
PostsList (Container_pgvko7fz)
|
|
463
|
+
Params: customAudienceFilter (Enum), profileUserId (String), fetchType (Enum), itemId (String)
|
|
464
|
+
|
|
465
|
+
Widget Tree:
|
|
466
|
+
└── ConditionalBuilder
|
|
467
|
+
├── PlaceholderWidget
|
|
468
|
+
│ └── ListView
|
|
469
|
+
│ └── [PostCard] (Container_abc12345) → CALLBACK → [updateState]
|
|
470
|
+
└── PlaceholderWidget
|
|
471
|
+
└── Column
|
|
472
|
+
└── [EmptyState] (Container_xyz98765)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 11. Creating a New Component
|
|
418
478
|
|
|
419
479
|
Creating a component requires pushing multiple files in a **single** `update_project_yaml` call, similar to adding widgets to a page.
|
|
420
480
|
|
|
@@ -652,7 +712,7 @@ executeCallbackAction:
|
|
|
652
712
|
|
|
653
713
|
---
|
|
654
714
|
|
|
655
|
-
##
|
|
715
|
+
## 12. Refactoring Page Widgets into a Component
|
|
656
716
|
|
|
657
717
|
To extract existing page widgets into a reusable component:
|
|
658
718
|
|
|
@@ -726,7 +726,112 @@ trueAction:
|
|
|
726
726
|
|
|
727
727
|
## Disabled Actions
|
|
728
728
|
|
|
729
|
-
|
|
729
|
+
FlutterFlow uses `disableAction` to mark actions or conditionals as disabled. There are three distinct patterns depending on what is being disabled.
|
|
730
|
+
|
|
731
|
+
### Pattern 1: Disabling a Leaf Action (Unconditional)
|
|
732
|
+
|
|
733
|
+
To fully disable a single action (navigate, database, revenueCat, etc.), wrap the action content in `disableAction` at the **action file level**. The trigger chain continues to reference the same key — FlutterFlow reads the file, sees `disableAction`, and skips execution.
|
|
734
|
+
|
|
735
|
+
```yaml
|
|
736
|
+
# Action file: id-epxy2a0w.yaml
|
|
737
|
+
key: epxy2a0w
|
|
738
|
+
disableAction:
|
|
739
|
+
actionNode:
|
|
740
|
+
key: gm8dis03 # Internal node key (can be any unique key)
|
|
741
|
+
action:
|
|
742
|
+
navigate:
|
|
743
|
+
allowBack: false
|
|
744
|
+
passedParameters:
|
|
745
|
+
widgetClassNodeKeyRef:
|
|
746
|
+
key: Scaffold_tydsj8ql
|
|
747
|
+
isNavigateBack: false
|
|
748
|
+
transition:
|
|
749
|
+
transitionType: FADE_IN
|
|
750
|
+
durationMillis: 500
|
|
751
|
+
pageNodeKeyRef:
|
|
752
|
+
key: Scaffold_tydsj8ql
|
|
753
|
+
key: epxy2a0w # Must match the outer key
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
| Field | Description |
|
|
757
|
+
|-------|-------------|
|
|
758
|
+
| `key` (outer) | The action's original key — chain references this |
|
|
759
|
+
| `disableAction.actionNode.key` | Internal node key (any unique value) |
|
|
760
|
+
| `disableAction.actionNode.action` | The original action content, preserved for re-enabling |
|
|
761
|
+
| `disableAction.actionNode.action.key` | Must match the outer `key` |
|
|
762
|
+
|
|
763
|
+
**Important:** Disabling a leaf action at the file level does NOT disable `followUpAction` chains defined in the trigger YAML. If the trigger chain has a `followUpAction` after this action reference, that follow-up will still execute.
|
|
764
|
+
|
|
765
|
+
### Pattern 2: Disabling a Conditional Node
|
|
766
|
+
|
|
767
|
+
To disable an entire `conditionActions` block in the trigger chain, two changes are required:
|
|
768
|
+
|
|
769
|
+
**Step 1 — Create a new action file** that wraps the full conditional in `disableAction.actionNode.conditionActions`. Leaf actions within the conditional are inlined (not file references) with their own `disableAction` wrappers:
|
|
770
|
+
|
|
771
|
+
```yaml
|
|
772
|
+
# Action file: id-ds8cnd01.yaml (new file)
|
|
773
|
+
key: ds8cnd01
|
|
774
|
+
disableAction:
|
|
775
|
+
actionNode:
|
|
776
|
+
key: f441awwi # Original conditional node key from the chain
|
|
777
|
+
conditionActions:
|
|
778
|
+
falseAction:
|
|
779
|
+
key: wkklfcpy
|
|
780
|
+
action:
|
|
781
|
+
key: epxy2a0w
|
|
782
|
+
disableAction: # Leaf action inlined with its own disable
|
|
783
|
+
actionNode:
|
|
784
|
+
key: gm8dis03
|
|
785
|
+
action:
|
|
786
|
+
navigate:
|
|
787
|
+
allowBack: false
|
|
788
|
+
pageNodeKeyRef:
|
|
789
|
+
key: Scaffold_tydsj8ql
|
|
790
|
+
key: epxy2a0w
|
|
791
|
+
trueActions:
|
|
792
|
+
- condition:
|
|
793
|
+
revenueCatEntitlementResponse: {}
|
|
794
|
+
trueAction:
|
|
795
|
+
key: 5uq5p5vt
|
|
796
|
+
action:
|
|
797
|
+
key: bgxsmj4b
|
|
798
|
+
disableAction: # Another leaf action inlined
|
|
799
|
+
actionNode:
|
|
800
|
+
key: gm8dis04
|
|
801
|
+
action:
|
|
802
|
+
navigate:
|
|
803
|
+
allowBack: false
|
|
804
|
+
pageNodeKeyRef:
|
|
805
|
+
key: Scaffold_91ca3wwv
|
|
806
|
+
key: bgxsmj4b
|
|
807
|
+
hasMultiConditions: false
|
|
808
|
+
key: ev7ush66
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Step 2 — Update the trigger chain YAML.** Replace the `conditionActions` block with an `action` reference to a noop key (a key with no corresponding action file):
|
|
812
|
+
|
|
813
|
+
```yaml
|
|
814
|
+
# Before (in trigger chain):
|
|
815
|
+
followUpAction:
|
|
816
|
+
key: f441awwi
|
|
817
|
+
conditionActions:
|
|
818
|
+
falseAction: ...
|
|
819
|
+
trueActions: ...
|
|
820
|
+
hasMultiConditions: false
|
|
821
|
+
key: ev7ush66
|
|
822
|
+
|
|
823
|
+
# After (in trigger chain):
|
|
824
|
+
followUpAction:
|
|
825
|
+
key: f441awwi
|
|
826
|
+
action:
|
|
827
|
+
key: np8noop1 # Noop key — no action file exists for this key
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**Note:** The noop key has no corresponding action file. When pushed via the UI, FlutterFlow handles this internally. When pushed via the API, you may need to create the action file with the disabled conditional content under the noop key itself (see API Limitations).
|
|
831
|
+
|
|
832
|
+
### Pattern 3: Conditionally Disabled Action
|
|
833
|
+
|
|
834
|
+
Actions can be conditionally disabled using `disableAction` with a condition. When the condition evaluates to true, the wrapped action executes; otherwise it is skipped.
|
|
730
835
|
|
|
731
836
|
```yaml
|
|
732
837
|
key: hfzr0kmk
|
|
@@ -763,7 +868,12 @@ disableAction:
|
|
|
763
868
|
key: xsa3loej
|
|
764
869
|
```
|
|
765
870
|
|
|
766
|
-
|
|
871
|
+
### Key Rules
|
|
872
|
+
|
|
873
|
+
- `disableAction` is only valid in **action files**, never in the trigger chain YAML directly
|
|
874
|
+
- Leaf actions inlined inside `disableAction` use the full action body, not key references
|
|
875
|
+
- The original action content is preserved inside the wrapper for easy re-enabling
|
|
876
|
+
- Disabling a leaf action does NOT prevent `followUpAction` chains in the trigger from continuing
|
|
767
877
|
|
|
768
878
|
---
|
|
769
879
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flutterflow-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP server for the FlutterFlow Project API — AI-assisted FlutterFlow development through Claude and other MCP-compatible clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"build": "tsc && chmod 755 build/index.js",
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"start": "node build/index.js",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
19
22
|
"prepublishOnly": "npm run build"
|
|
20
23
|
},
|
|
21
24
|
"keywords": [
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@types/adm-zip": "^0.5.7",
|
|
52
55
|
"@types/node": "^25.2.3",
|
|
53
|
-
"typescript": "^5.9.3"
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"vitest": "^4.0.18"
|
|
54
58
|
}
|
|
55
59
|
}
|