mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  try:
9
9
  from gql import Client, gql
10
- from gql.transport.exceptions import TransportError
10
+ from gql.transport.exceptions import TransportError, TransportQueryError
11
11
  from gql.transport.httpx import HTTPXAsyncTransport
12
12
  except ImportError:
13
13
  # Handle missing gql dependency gracefully
@@ -15,6 +15,7 @@ except ImportError:
15
15
  gql = None
16
16
  HTTPXAsyncTransport = None
17
17
  TransportError = Exception
18
+ TransportQueryError = Exception
18
19
 
19
20
  from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
20
21
 
@@ -26,6 +27,7 @@ class LinearGraphQLClient:
26
27
  """Initialize the Linear GraphQL client.
27
28
 
28
29
  Args:
30
+ ----
29
31
  api_key: Linear API key
30
32
  timeout: Request timeout in seconds
31
33
 
@@ -38,9 +40,11 @@ class LinearGraphQLClient:
38
40
  """Create a new GraphQL client instance.
39
41
 
40
42
  Returns:
43
+ -------
41
44
  Configured GraphQL client
42
45
 
43
46
  Raises:
47
+ ------
44
48
  AuthenticationError: If API key is invalid
45
49
  AdapterError: If client creation fails
46
50
 
@@ -80,14 +84,17 @@ class LinearGraphQLClient:
80
84
  """Execute a GraphQL query with error handling and retries.
81
85
 
82
86
  Args:
87
+ ----
83
88
  query_string: GraphQL query string
84
89
  variables: Query variables
85
90
  retries: Number of retry attempts
86
91
 
87
92
  Returns:
93
+ -------
88
94
  Query result data
89
95
 
90
96
  Raises:
97
+ ------
91
98
  AuthenticationError: If authentication fails
92
99
  RateLimitError: If rate limit is exceeded
93
100
  AdapterError: If query execution fails
@@ -104,6 +111,33 @@ class LinearGraphQLClient:
104
111
  )
105
112
  return result
106
113
 
114
+ except TransportQueryError as e:
115
+ """
116
+ Handle GraphQL validation errors (e.g., duplicate label names).
117
+ TransportQueryError is a subclass of TransportError with .errors attribute.
118
+
119
+ Related: 1M-398 - Label duplicate error handling
120
+ """
121
+ if e.errors:
122
+ error_msg = e.errors[0].get("message", "Unknown GraphQL error")
123
+
124
+ # Check for duplicate label errors specifically
125
+ if (
126
+ "duplicate" in error_msg.lower()
127
+ and "label" in error_msg.lower()
128
+ ):
129
+ raise AdapterError(
130
+ f"Label already exists: {error_msg}", "linear"
131
+ ) from e
132
+
133
+ # Other validation errors
134
+ raise AdapterError(
135
+ f"Linear GraphQL validation error: {error_msg}", "linear"
136
+ ) from e
137
+
138
+ # Fallback if no errors attribute
139
+ raise AdapterError(f"Linear GraphQL error: {e}", "linear") from e
140
+
107
141
  except TransportError as e:
108
142
  # Handle HTTP errors
109
143
  if hasattr(e, "response") and e.response:
@@ -175,14 +209,17 @@ class LinearGraphQLClient:
175
209
  """Execute a GraphQL mutation with error handling.
176
210
 
177
211
  Args:
212
+ ----
178
213
  mutation_string: GraphQL mutation string
179
214
  variables: Mutation variables
180
215
  retries: Number of retry attempts
181
216
 
182
217
  Returns:
218
+ -------
183
219
  Mutation result data
184
220
 
185
221
  Raises:
222
+ ------
186
223
  AuthenticationError: If authentication fails
187
224
  RateLimitError: If rate limit is exceeded
188
225
  AdapterError: If mutation execution fails
@@ -194,9 +231,20 @@ class LinearGraphQLClient:
194
231
  """Test the connection to Linear API.
195
232
 
196
233
  Returns:
234
+ -------
197
235
  True if connection is successful, False otherwise
198
236
 
237
+ Design Decision: Enhanced Debug Logging (1M-431)
238
+ -------------------------------------------------
239
+ Added comprehensive logging to diagnose connection failures.
240
+ Logs API key preview, query results, and specific failure reasons
241
+ to help users troubleshoot authentication and configuration issues.
242
+
199
243
  """
244
+ import logging
245
+
246
+ logger = logging.getLogger(__name__)
247
+
200
248
  try:
201
249
  # Simple query to test authentication
202
250
  test_query = """
@@ -204,23 +252,53 @@ class LinearGraphQLClient:
204
252
  viewer {
205
253
  id
206
254
  name
255
+ email
207
256
  }
208
257
  }
209
258
  """
210
259
 
260
+ logger.debug(
261
+ f"Testing Linear API connection with API key: {self.api_key[:20]}..."
262
+ )
211
263
  result = await self.execute_query(test_query)
212
- return bool(result.get("viewer"))
213
264
 
214
- except Exception:
265
+ # Log the actual response for debugging
266
+ logger.debug(f"Linear API test response: {result}")
267
+
268
+ viewer = result.get("viewer")
269
+
270
+ if not viewer:
271
+ logger.warning(
272
+ f"Linear test connection query succeeded but returned no viewer data. "
273
+ f"Response: {result}"
274
+ )
275
+ return False
276
+
277
+ if not viewer.get("id"):
278
+ logger.warning(f"Linear viewer missing id field. Viewer data: {viewer}")
279
+ return False
280
+
281
+ logger.info(
282
+ f"Linear API connected successfully as: {viewer.get('name')} ({viewer.get('email')})"
283
+ )
284
+ return True
285
+
286
+ except Exception as e:
287
+ logger.error(
288
+ f"Linear connection test failed: {type(e).__name__}: {e}",
289
+ exc_info=True,
290
+ )
215
291
  return False
216
292
 
217
293
  async def get_team_info(self, team_id: str) -> dict[str, Any] | None:
218
294
  """Get team information by ID.
219
295
 
220
296
  Args:
297
+ ----
221
298
  team_id: Linear team ID
222
299
 
223
300
  Returns:
301
+ -------
224
302
  Team information or None if not found
225
303
 
226
304
  """
@@ -246,9 +324,11 @@ class LinearGraphQLClient:
246
324
  """Get user information by email.
247
325
 
248
326
  Args:
327
+ ----
249
328
  email: User email address
250
329
 
251
330
  Returns:
331
+ -------
252
332
  User information or None if not found
253
333
 
254
334
  """
@@ -278,9 +358,11 @@ class LinearGraphQLClient:
278
358
  """Search users by display name or full name.
279
359
 
280
360
  Args:
361
+ ----
281
362
  name: Display name or full name to search for
282
363
 
283
364
  Returns:
365
+ -------
284
366
  List of matching users (may be empty)
285
367
 
286
368
  """
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from datetime import datetime
6
6
  from typing import Any
7
7
 
8
- from ...core.models import Comment, Epic, Priority, Task, TicketState
8
+ from ...core.models import Attachment, Comment, Epic, Priority, Task, TicketState
9
9
  from .types import extract_linear_metadata, get_universal_priority, get_universal_state
10
10
 
11
11
 
@@ -16,9 +16,11 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
16
16
  with a parent issue).
17
17
 
18
18
  Args:
19
+ ----
19
20
  issue_data: Raw Linear issue data from GraphQL
20
21
 
21
22
  Returns:
23
+ -------
22
24
  Universal Task model
23
25
 
24
26
  """
@@ -31,10 +33,11 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
31
33
  linear_priority = issue_data.get("priority", 3)
32
34
  priority = get_universal_priority(linear_priority)
33
35
 
34
- # Map state
36
+ # Map state with synonym matching (1M-164)
35
37
  state_data = issue_data.get("state", {})
36
38
  state_type = state_data.get("type", "unstarted")
37
- state = get_universal_state(state_type)
39
+ state_name = state_data.get("name") # Extract state name for synonym matching
40
+ state = get_universal_state(state_type, state_name)
38
41
 
39
42
  # Extract assignee
40
43
  assignee = None
@@ -76,6 +79,9 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
76
79
  issue_data["updatedAt"].replace("Z", "+00:00")
77
80
  )
78
81
 
82
+ # Extract child issue IDs
83
+ children = extract_child_issue_ids(issue_data)
84
+
79
85
  # Extract Linear-specific metadata
80
86
  linear_metadata = extract_linear_metadata(issue_data)
81
87
  metadata = {"linear": linear_metadata} if linear_metadata else {}
@@ -91,6 +97,7 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
91
97
  tags=tags,
92
98
  parent_epic=parent_epic,
93
99
  parent_issue=parent_issue,
100
+ children=children,
94
101
  created_at=created_at,
95
102
  updated_at=updated_at,
96
103
  metadata=metadata,
@@ -101,9 +108,11 @@ def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
101
108
  """Convert Linear project data to universal Epic model.
102
109
 
103
110
  Args:
111
+ ----
104
112
  project_data: Raw Linear project data from GraphQL
105
113
 
106
114
  Returns:
115
+ -------
107
116
  Universal Epic model
108
117
 
109
118
  """
@@ -137,7 +146,7 @@ def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
137
146
  )
138
147
 
139
148
  # Extract Linear-specific metadata
140
- metadata = {"linear": {}}
149
+ metadata: dict[str, Any] = {"linear": {}}
141
150
  if project_data.get("url"):
142
151
  metadata["linear"]["linear_url"] = project_data["url"]
143
152
  if project_data.get("icon"):
@@ -165,10 +174,12 @@ def map_linear_comment_to_comment(
165
174
  """Convert Linear comment data to universal Comment model.
166
175
 
167
176
  Args:
177
+ ----
168
178
  comment_data: Raw Linear comment data from GraphQL
169
179
  ticket_id: ID of the ticket this comment belongs to
170
180
 
171
181
  Returns:
182
+ -------
172
183
  Universal Comment model
173
184
 
174
185
  """
@@ -212,10 +223,12 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
212
223
  or for a sub-issue when task.parent_issue is provided.
213
224
 
214
225
  Args:
226
+ ----
215
227
  task: Universal Task model
216
228
  team_id: Linear team ID
217
229
 
218
230
  Returns:
231
+ -------
219
232
  Linear issue input dictionary
220
233
 
221
234
  """
@@ -246,10 +259,19 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
246
259
  if task.parent_epic:
247
260
  issue_input["projectId"] = task.parent_epic
248
261
 
249
- # Add labels (tags) if provided
250
- if task.tags:
251
- # Note: This returns label names, will be resolved to IDs by adapter
252
- issue_input["labelIds"] = task.tags # Temporary - adapter will resolve
262
+ # DO NOT set labelIds here - adapter will handle label resolution
263
+ # Labels are resolved from names to UUIDs in LinearAdapter._create_task()
264
+ # If we set it here, we create a type mismatch (names vs UUIDs)
265
+ # The adapter's label resolution logic (lines 982-988) handles this properly
266
+ #
267
+ # Bug Fix (v1.1.1): Previously, setting labelIds to tag names here caused
268
+ # "Argument Validation Error" from Linear's GraphQL API. The API requires
269
+ # UUIDs (e.g., "uuid-1"), not names (e.g., "bug"). This was fixed by:
270
+ # 1. Removing labelIds assignment in mapper (this file)
271
+ # 2. Adding UUID validation in adapter._create_task() (adapter.py:1047-1060)
272
+ # 3. Changing GraphQL labelIds type to [String!]! (queries.py)
273
+ #
274
+ # See: docs/TROUBLESHOOTING.md#issue-argument-validation-error-when-creating-issues-with-labels
253
275
 
254
276
  # Add Linear-specific metadata
255
277
  if task.metadata and "linear" in task.metadata:
@@ -268,9 +290,11 @@ def build_linear_issue_update_input(updates: dict[str, Any]) -> dict[str, Any]:
268
290
  """Build Linear issue update input from update dictionary.
269
291
 
270
292
  Args:
293
+ ----
271
294
  updates: Dictionary of fields to update
272
295
 
273
296
  Returns:
297
+ -------
274
298
  Linear issue update input dictionary
275
299
 
276
300
  """
@@ -320,9 +344,11 @@ def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
320
344
  """Extract child issue IDs from Linear issue data.
321
345
 
322
346
  Args:
347
+ ----
323
348
  issue_data: Raw Linear issue data from GraphQL
324
349
 
325
350
  Returns:
351
+ -------
326
352
  List of child issue identifiers
327
353
 
328
354
  """
@@ -330,3 +356,65 @@ def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
330
356
  if issue_data.get("children", {}).get("nodes"):
331
357
  child_ids = [child["identifier"] for child in issue_data["children"]["nodes"]]
332
358
  return child_ids
359
+
360
+
361
+ def map_linear_attachment_to_attachment(
362
+ attachment_data: dict[str, Any], ticket_id: str
363
+ ) -> Attachment:
364
+ """Convert Linear attachment data to universal Attachment model.
365
+
366
+ Args:
367
+ ----
368
+ attachment_data: Raw Linear attachment data from GraphQL
369
+ ticket_id: ID of the ticket this attachment belongs to
370
+
371
+ Returns:
372
+ -------
373
+ Universal Attachment model
374
+
375
+ Note:
376
+ ----
377
+ Linear attachment URLs require authentication with API key.
378
+ URLs are in format: https://files.linear.app/workspace/attachment-id/filename
379
+ Authentication header: Authorization: Bearer {api_key}
380
+
381
+ """
382
+ # Extract basic fields
383
+ attachment_id = attachment_data.get("id")
384
+ title = attachment_data.get("title", "Untitled")
385
+ url = attachment_data.get("url")
386
+ subtitle = attachment_data.get("subtitle")
387
+
388
+ # Extract dates
389
+ created_at = None
390
+ if attachment_data.get("createdAt"):
391
+ created_at = datetime.fromisoformat(
392
+ attachment_data["createdAt"].replace("Z", "+00:00")
393
+ )
394
+
395
+ if attachment_data.get("updatedAt"):
396
+ datetime.fromisoformat(attachment_data["updatedAt"].replace("Z", "+00:00"))
397
+
398
+ # Build metadata with Linear-specific fields
399
+ metadata = {
400
+ "linear": {
401
+ "id": attachment_id,
402
+ "title": title,
403
+ "subtitle": subtitle,
404
+ "metadata": attachment_data.get("metadata"),
405
+ "updated_at": attachment_data.get("updatedAt"),
406
+ }
407
+ }
408
+
409
+ return Attachment(
410
+ id=attachment_id,
411
+ ticket_id=ticket_id,
412
+ filename=title, # Linear uses 'title' as filename
413
+ url=url,
414
+ content_type=None, # Linear doesn't provide MIME type in GraphQL
415
+ size_bytes=None, # Linear doesn't provide size in GraphQL
416
+ created_at=created_at,
417
+ created_by=None, # Not included in standard fragment
418
+ description=subtitle,
419
+ metadata=metadata,
420
+ )
@@ -351,7 +351,8 @@ SEARCH_ISSUE_BY_IDENTIFIER_QUERY = """
351
351
  """
352
352
 
353
353
  LIST_PROJECTS_QUERY = (
354
- PROJECT_FRAGMENT
354
+ TEAM_FRAGMENT # Required by PROJECT_FRAGMENT which uses ...TeamFields
355
+ + PROJECT_FRAGMENT
355
356
  + """
356
357
  query ListProjects($filter: ProjectFilter, $first: Int!) {
357
358
  projects(filter: $filter, first: $first, orderBy: updatedAt) {
@@ -387,3 +388,169 @@ GET_CURRENT_USER_QUERY = (
387
388
  }
388
389
  """
389
390
  )
391
+
392
+ CREATE_LABEL_MUTATION = """
393
+ mutation CreateLabel($input: IssueLabelCreateInput!) {
394
+ issueLabelCreate(input: $input) {
395
+ success
396
+ issueLabel {
397
+ id
398
+ name
399
+ color
400
+ description
401
+ }
402
+ }
403
+ }
404
+ """
405
+
406
+ LIST_CYCLES_QUERY = """
407
+ query GetCycles($teamId: String!, $first: Int!, $after: String) {
408
+ team(id: $teamId) {
409
+ cycles(first: $first, after: $after) {
410
+ nodes {
411
+ id
412
+ name
413
+ number
414
+ startsAt
415
+ endsAt
416
+ completedAt
417
+ progress
418
+ }
419
+ pageInfo {
420
+ hasNextPage
421
+ endCursor
422
+ }
423
+ }
424
+ }
425
+ }
426
+ """
427
+
428
+ GET_ISSUE_STATUS_QUERY = """
429
+ query GetIssueStatus($issueId: String!) {
430
+ issue(id: $issueId) {
431
+ id
432
+ state {
433
+ id
434
+ name
435
+ type
436
+ color
437
+ description
438
+ position
439
+ }
440
+ }
441
+ }
442
+ """
443
+
444
+ LIST_ISSUE_STATUSES_QUERY = """
445
+ query GetWorkflowStates($teamId: String!) {
446
+ team(id: $teamId) {
447
+ states {
448
+ nodes {
449
+ id
450
+ name
451
+ type
452
+ color
453
+ description
454
+ position
455
+ }
456
+ }
457
+ }
458
+ }
459
+ """
460
+
461
+ GET_CUSTOM_VIEW_QUERY = (
462
+ ISSUE_LIST_FRAGMENTS
463
+ + """
464
+ query GetCustomView($viewId: String!, $first: Int!) {
465
+ customView(id: $viewId) {
466
+ id
467
+ name
468
+ description
469
+ issues(first: $first) {
470
+ nodes {
471
+ ...IssueCompactFields
472
+ }
473
+ pageInfo {
474
+ hasNextPage
475
+ }
476
+ }
477
+ }
478
+ }
479
+ """
480
+ )
481
+
482
+ # Project Update queries and mutations (1M-238)
483
+
484
+ PROJECT_UPDATE_FRAGMENT = """
485
+ fragment ProjectUpdateFields on ProjectUpdate {
486
+ id
487
+ body
488
+ health
489
+ createdAt
490
+ updatedAt
491
+ diffMarkdown
492
+ url
493
+ user {
494
+ id
495
+ name
496
+ email
497
+ }
498
+ project {
499
+ id
500
+ name
501
+ slugId
502
+ }
503
+ }
504
+ """
505
+
506
+ CREATE_PROJECT_UPDATE_MUTATION = (
507
+ PROJECT_UPDATE_FRAGMENT
508
+ + """
509
+ mutation ProjectUpdateCreate(
510
+ $projectId: String!
511
+ $body: String!
512
+ $health: ProjectUpdateHealthType
513
+ ) {
514
+ projectUpdateCreate(
515
+ input: {
516
+ projectId: $projectId
517
+ body: $body
518
+ health: $health
519
+ }
520
+ ) {
521
+ success
522
+ projectUpdate {
523
+ ...ProjectUpdateFields
524
+ }
525
+ }
526
+ }
527
+ """
528
+ )
529
+
530
+ LIST_PROJECT_UPDATES_QUERY = (
531
+ PROJECT_UPDATE_FRAGMENT
532
+ + """
533
+ query ProjectUpdates($projectId: String!, $first: Int) {
534
+ project(id: $projectId) {
535
+ id
536
+ name
537
+ projectUpdates(first: $first) {
538
+ nodes {
539
+ ...ProjectUpdateFields
540
+ }
541
+ }
542
+ }
543
+ }
544
+ """
545
+ )
546
+
547
+ GET_PROJECT_UPDATE_QUERY = (
548
+ PROJECT_UPDATE_FRAGMENT
549
+ + """
550
+ query ProjectUpdate($id: String!) {
551
+ projectUpdate(id: $id) {
552
+ ...ProjectUpdateFields
553
+ }
554
+ }
555
+ """
556
+ )