@josephyan/qingflow-app-user-mcp 0.2.0-beta.1 → 0.2.0-beta.11

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.
@@ -43,11 +43,30 @@ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
43
43
  "departments": PublicFieldType.department,
44
44
  }
45
45
 
46
+ FIELD_TYPE_ID_ALIASES: dict[int, PublicFieldType] = {
47
+ 2: PublicFieldType.text,
48
+ 3: PublicFieldType.long_text,
49
+ 4: PublicFieldType.date,
50
+ 5: PublicFieldType.member,
51
+ 6: PublicFieldType.email,
52
+ 7: PublicFieldType.phone,
53
+ 8: PublicFieldType.number,
54
+ 10: PublicFieldType.boolean,
55
+ 11: PublicFieldType.single_select,
56
+ 12: PublicFieldType.multi_select,
57
+ 13: PublicFieldType.attachment,
58
+ 18: PublicFieldType.subtable,
59
+ 21: PublicFieldType.address,
60
+ 22: PublicFieldType.department,
61
+ 25: PublicFieldType.relation,
62
+ }
63
+
46
64
 
47
65
  class PublicViewType(str, Enum):
48
66
  table = "table"
49
67
  card = "card"
50
68
  board = "board"
69
+ gantt = "gantt"
51
70
 
52
71
 
53
72
  class LayoutApplyMode(str, Enum):
@@ -69,6 +88,7 @@ class FlowPreset(str, Enum):
69
88
  class ViewsPreset(str, Enum):
70
89
  default_table = "default_table"
71
90
  status_board = "status_board"
91
+ default_gantt = "default_gantt"
72
92
 
73
93
 
74
94
  class PublicFlowNodeType(str, Enum):
@@ -82,6 +102,143 @@ class PublicFlowNodeType(str, Enum):
82
102
  end = "end"
83
103
 
84
104
 
105
+ class FlowConditionOperator(str, Enum):
106
+ eq = "eq"
107
+ neq = "neq"
108
+ in_ = "in"
109
+ contains = "contains"
110
+ gte = "gte"
111
+ lte = "lte"
112
+ is_empty = "is_empty"
113
+ not_empty = "not_empty"
114
+
115
+
116
+ class ViewFilterOperator(str, Enum):
117
+ eq = "eq"
118
+ neq = "neq"
119
+ in_ = "in"
120
+ contains = "contains"
121
+ gte = "gte"
122
+ lte = "lte"
123
+ is_empty = "is_empty"
124
+ not_empty = "not_empty"
125
+
126
+
127
+ class FlowAssigneePatch(StrictModel):
128
+ role_ids: list[int] = Field(default_factory=list)
129
+ role_names: list[str] = Field(default_factory=list)
130
+ member_uids: list[int] = Field(default_factory=list)
131
+ member_emails: list[str] = Field(default_factory=list)
132
+ member_names: list[str] = Field(default_factory=list)
133
+ include_sub_departs: bool | None = None
134
+
135
+
136
+ class FlowNodePermissionsPatch(StrictModel):
137
+ editable_fields: list[str] = Field(default_factory=list)
138
+
139
+
140
+ class FlowConditionRulePatch(StrictModel):
141
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
142
+ operator: FlowConditionOperator = Field(validation_alias=AliasChoices("operator", "op"))
143
+ values: list[Any] = Field(default_factory=list)
144
+
145
+ @model_validator(mode="before")
146
+ @classmethod
147
+ def normalize_aliases(cls, value: Any) -> Any:
148
+ if not isinstance(value, dict):
149
+ return value
150
+ payload = dict(value)
151
+ if "value" in payload and "values" not in payload:
152
+ payload["values"] = [payload.pop("value")]
153
+ raw_operator = payload.get("operator", payload.get("op"))
154
+ if isinstance(raw_operator, str):
155
+ normalized = raw_operator.strip().lower()
156
+ operator_aliases = {
157
+ "equals": FlowConditionOperator.eq.value,
158
+ "equal": FlowConditionOperator.eq.value,
159
+ "=": FlowConditionOperator.eq.value,
160
+ "not_equals": FlowConditionOperator.neq.value,
161
+ "not_equal": FlowConditionOperator.neq.value,
162
+ "!=": FlowConditionOperator.neq.value,
163
+ ">=": FlowConditionOperator.gte.value,
164
+ "<=": FlowConditionOperator.lte.value,
165
+ "any_of": FlowConditionOperator.in_.value,
166
+ "one_of": FlowConditionOperator.in_.value,
167
+ "between_any": FlowConditionOperator.in_.value,
168
+ "empty": FlowConditionOperator.is_empty.value,
169
+ "is blank": FlowConditionOperator.is_empty.value,
170
+ "blank": FlowConditionOperator.is_empty.value,
171
+ "not_empty": FlowConditionOperator.not_empty.value,
172
+ "not blank": FlowConditionOperator.not_empty.value,
173
+ }
174
+ if normalized in operator_aliases:
175
+ payload["operator"] = operator_aliases[normalized]
176
+ elif "operator" not in payload:
177
+ payload["operator"] = normalized
178
+ payload.pop("op", None)
179
+ return payload
180
+
181
+ @model_validator(mode="after")
182
+ def validate_shape(self) -> "FlowConditionRulePatch":
183
+ if self.operator in {FlowConditionOperator.is_empty, FlowConditionOperator.not_empty}:
184
+ self.values = []
185
+ return self
186
+ if not self.values:
187
+ raise ValueError("condition rule requires values")
188
+ return self
189
+
190
+
191
+ class ViewFilterRulePatch(StrictModel):
192
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
193
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
194
+ values: list[Any] = Field(default_factory=list)
195
+
196
+ @model_validator(mode="before")
197
+ @classmethod
198
+ def normalize_aliases(cls, value: Any) -> Any:
199
+ if not isinstance(value, dict):
200
+ return value
201
+ payload = dict(value)
202
+ if "value" in payload and "values" not in payload:
203
+ payload["values"] = [payload.pop("value")]
204
+ raw_operator = payload.get("operator", payload.get("op"))
205
+ if isinstance(raw_operator, str):
206
+ normalized = raw_operator.strip().lower()
207
+ operator_aliases = {
208
+ "equals": ViewFilterOperator.eq.value,
209
+ "equal": ViewFilterOperator.eq.value,
210
+ "=": ViewFilterOperator.eq.value,
211
+ "not_equals": ViewFilterOperator.neq.value,
212
+ "not_equal": ViewFilterOperator.neq.value,
213
+ "!=": ViewFilterOperator.neq.value,
214
+ ">=": ViewFilterOperator.gte.value,
215
+ "<=": ViewFilterOperator.lte.value,
216
+ "any_of": ViewFilterOperator.in_.value,
217
+ "one_of": ViewFilterOperator.in_.value,
218
+ "between_any": ViewFilterOperator.in_.value,
219
+ "empty": ViewFilterOperator.is_empty.value,
220
+ "is blank": ViewFilterOperator.is_empty.value,
221
+ "blank": ViewFilterOperator.is_empty.value,
222
+ "not_empty": ViewFilterOperator.not_empty.value,
223
+ "not blank": ViewFilterOperator.not_empty.value,
224
+ }
225
+ if normalized in operator_aliases:
226
+ payload["operator"] = operator_aliases[normalized]
227
+ elif "operator" not in payload:
228
+ payload["operator"] = normalized
229
+ payload.pop("op", None)
230
+ return payload
231
+
232
+ @model_validator(mode="after")
233
+ def validate_shape(self) -> "ViewFilterRulePatch":
234
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
235
+ self.values = []
236
+ return self
237
+ if not self.values:
238
+ raise ValueError("view filter rule requires values")
239
+ return self
240
+
241
+
85
242
  class FieldSelector(StrictModel):
86
243
  field_id: str | None = None
87
244
  que_id: int | None = None
@@ -179,8 +336,54 @@ class FlowNodePatch(StrictModel):
179
336
  id: str
180
337
  type: PublicFlowNodeType
181
338
  name: str
339
+ assignees: FlowAssigneePatch = Field(default_factory=FlowAssigneePatch)
340
+ permissions: FlowNodePermissionsPatch = Field(default_factory=FlowNodePermissionsPatch)
341
+ conditions: list[FlowConditionRulePatch] = Field(default_factory=list)
342
+ condition_groups: list[list[FlowConditionRulePatch]] = Field(default_factory=list)
182
343
  config: dict[str, Any] = Field(default_factory=dict)
183
344
 
345
+ @model_validator(mode="before")
346
+ @classmethod
347
+ def normalize_aliases(cls, value: Any) -> Any:
348
+ if not isinstance(value, dict):
349
+ return value
350
+ payload = dict(value)
351
+ assignees = dict(payload.get("assignees") or {})
352
+ permissions = dict(payload.get("permissions") or {})
353
+
354
+ for key in ("role_ids", "role_names", "member_uids", "member_emails", "member_names", "include_sub_departs"):
355
+ if key in payload and key not in assignees:
356
+ assignees[key] = payload.pop(key)
357
+ for key in ("editable_fields",):
358
+ if key in payload and key not in permissions:
359
+ permissions[key] = payload.pop(key)
360
+ if "filters" in payload and "conditions" not in payload:
361
+ payload["conditions"] = payload.pop("filters")
362
+ if "rules" in payload and "conditions" not in payload:
363
+ payload["conditions"] = payload.pop("rules")
364
+ if "conditionRules" in payload and "condition_groups" not in payload:
365
+ payload["condition_groups"] = payload.pop("conditionRules")
366
+ if "conditionGroups" in payload and "condition_groups" not in payload:
367
+ payload["condition_groups"] = payload.pop("conditionGroups")
368
+ if "owners" in payload and "member_names" not in assignees:
369
+ assignees["member_names"] = payload.pop("owners")
370
+ if "approvers" in payload and "role_names" not in assignees:
371
+ assignees["role_names"] = payload.pop("approvers")
372
+ if assignees:
373
+ payload["assignees"] = assignees
374
+ if permissions:
375
+ payload["permissions"] = permissions
376
+ return payload
377
+
378
+ @model_validator(mode="after")
379
+ def validate_branch_conditions(self) -> "FlowNodePatch":
380
+ if self.conditions:
381
+ self.condition_groups = [list(self.conditions), *self.condition_groups]
382
+ self.conditions = []
383
+ if self.type != PublicFlowNodeType.condition and self.condition_groups:
384
+ raise ValueError("condition_groups are only allowed on condition nodes")
385
+ return self
386
+
184
387
 
185
388
  class FlowTransitionPatch(StrictModel):
186
389
  source: str = Field(alias="from")
@@ -192,6 +395,10 @@ class ViewUpsertPatch(StrictModel):
192
395
  type: PublicViewType
193
396
  columns: list[str] = Field(default_factory=list)
194
397
  group_by: str | None = None
398
+ filters: list[ViewFilterRulePatch] = Field(default_factory=list)
399
+ start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
400
+ end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
401
+ title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
195
402
 
196
403
  @model_validator(mode="before")
197
404
  @classmethod
@@ -201,6 +408,14 @@ class ViewUpsertPatch(StrictModel):
201
408
  payload = dict(value)
202
409
  if "fields" in payload and "columns" not in payload:
203
410
  payload["columns"] = payload.pop("fields")
411
+ if "column_names" in payload and "columns" not in payload:
412
+ payload["columns"] = payload.pop("column_names")
413
+ if "columnNames" in payload and "columns" not in payload:
414
+ payload["columns"] = payload.pop("columnNames")
415
+ if "filter_rules" in payload and "filters" not in payload:
416
+ payload["filters"] = payload.pop("filter_rules")
417
+ if "filterRules" in payload and "filters" not in payload:
418
+ payload["filters"] = payload.pop("filterRules")
204
419
  raw_type = payload.get("type")
205
420
  if isinstance(raw_type, str):
206
421
  normalized = raw_type.strip().lower()
@@ -210,6 +425,8 @@ class ViewUpsertPatch(StrictModel):
210
425
  payload["type"] = "card"
211
426
  elif normalized == "kanban":
212
427
  payload["type"] = "board"
428
+ elif normalized == "ganttview":
429
+ payload["type"] = "gantt"
213
430
  return payload
214
431
 
215
432
  @model_validator(mode="after")
@@ -218,6 +435,8 @@ class ViewUpsertPatch(StrictModel):
218
435
  raise ValueError("table/card views require columns")
219
436
  if self.type == PublicViewType.board and not self.group_by:
220
437
  raise ValueError("board view requires group_by")
438
+ if self.type == PublicViewType.gantt and not (self.start_field and self.end_field):
439
+ raise ValueError("gantt view requires start_field and end_field")
221
440
  return self
222
441
 
223
442
 
@@ -303,6 +522,22 @@ class FlowPlanRequest(StrictModel):
303
522
  payload = dict(value)
304
523
  if str(payload.get("mode") or "").strip().lower() == "overwrite":
305
524
  payload["mode"] = "replace"
525
+ raw_preset = payload.get("preset")
526
+ if raw_preset is None and isinstance(payload.get("base_preset"), str):
527
+ raw_preset = payload["base_preset"]
528
+ payload["preset"] = raw_preset
529
+ if isinstance(raw_preset, str):
530
+ normalized_preset = raw_preset.strip().lower()
531
+ preset_aliases = {
532
+ "default_approval": FlowPreset.basic_approval.value,
533
+ "approval": FlowPreset.basic_approval.value,
534
+ "basic approval": FlowPreset.basic_approval.value,
535
+ "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
536
+ "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
537
+ "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
538
+ }
539
+ if normalized_preset in preset_aliases:
540
+ payload["preset"] = preset_aliases[normalized_preset]
306
541
  return payload
307
542
 
308
543
 
@@ -332,7 +567,14 @@ def _normalize_field_payload(value: Any) -> Any:
332
567
  if not isinstance(value, dict):
333
568
  return value
334
569
  payload = dict(value)
570
+ if "fields" in payload and "subfields" not in payload:
571
+ payload["subfields"] = payload.pop("fields")
335
572
  raw_type = payload.get("type")
573
+ if isinstance(raw_type, int):
574
+ normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
575
+ if normalized_from_id is not None:
576
+ payload["type"] = normalized_from_id.value
577
+ return payload
336
578
  if isinstance(raw_type, str):
337
579
  normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
338
580
  if normalized is not None: