-
Notifications
You must be signed in to change notification settings - Fork 962
Build out task list view #717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09b4e3e
43beb67
7626447
5e65dba
ca4655b
b4392d2
c65f417
d01e460
a145da3
40a73f6
6c5e442
dcaa7b7
3574dd6
1d48e20
4dde9c6
6d8e747
ccfc522
2fb1f80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,67 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { LinearClient } from "@linear/sdk"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { buildConflictUpdateColumns } from "@superset/db"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { db } from "@superset/db/client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { taskStatuses } from "@superset/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { calculateProgressForStates } from "./utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function syncWorkflowStates({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| client, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| client: LinearClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| organizationId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const teams = await client.teams(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const team of teams.nodes) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const states = await team.states(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for Linear API calls. The 🔧 Proposed fix with error handling console.log("[syncWorkflowStates] Fetching teams");
- const teams = await client.teams();
+ let teams;
+ try {
+ teams = await client.teams();
+ } catch (error) {
+ console.error("[syncWorkflowStates] Failed to fetch teams:", error);
+ throw error;
+ }
for (const team of teams.nodes) {
console.log(`[syncWorkflowStates] Processing team: ${team.name}`);
- const states = await team.states();
+ let states;
+ try {
+ states = await team.states();
+ } catch (error) {
+ console.error(`[syncWorkflowStates] Failed to fetch states for team ${team.name}:`, error);
+ continue; // Skip this team and continue with others
+ }📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const statesByType = new Map<string, typeof states.nodes>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const state of states.nodes) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!statesByType.has(state.type)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statesByType.set(state.type, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statesByType.get(state.type)?.push(state); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const startedStates = statesByType.get("started") || []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const progressMap = calculateProgressForStates( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| startedStates.map((s) => ({ name: s.name, position: s.position })), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const values = states.nodes.map((state) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| organizationId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: state.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| color: state.color, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: state.type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| position: state.position, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| progressPercent: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| state.type === "started" ? (progressMap.get(state.name) ?? null) : null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| externalProvider: "linear" as const, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| externalId: state.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (values.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await db | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .insert(taskStatuses) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .values(values) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .onConflictDoUpdate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| target: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| taskStatuses.organizationId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| taskStatuses.externalProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| taskStatuses.externalId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| set: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ...buildConflictUpdateColumns(taskStatuses, [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "name", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "color", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "type", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "position", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "progressPercent", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updatedAt: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import type { LinearClient } from "@linear/sdk"; | ||
| import { mapPriorityFromLinear } from "@superset/trpc/integrations/linear"; | ||
| import { subMonths } from "date-fns"; | ||
|
|
||
| export interface LinearIssue { | ||
| id: string; | ||
|
|
@@ -9,11 +10,18 @@ export interface LinearIssue { | |
| priority: number; | ||
| estimate: number | null; | ||
| dueDate: string | null; | ||
| createdAt: string; | ||
| url: string; | ||
| startedAt: string | null; | ||
| completedAt: string | null; | ||
| assignee: { id: string; email: string } | null; | ||
| state: { id: string; name: string; color: string; type: string }; | ||
| state: { | ||
| id: string; | ||
| name: string; | ||
| color: string; | ||
| type: string; | ||
| position: number; | ||
| }; | ||
| labels: { nodes: Array<{ id: string; name: string }> }; | ||
| } | ||
|
|
||
|
|
@@ -24,6 +32,51 @@ interface IssuesQueryResponse { | |
| }; | ||
| } | ||
|
|
||
| interface WorkflowStateWithPosition { | ||
| name: string; | ||
| position: number; | ||
| } | ||
|
|
||
| /** | ||
| * Calculates progress percentage for "started" type workflow states | ||
| * using Linear's rendering formula: | ||
| * - 1 state: 50% | ||
| * - 2 states: [50%, 75%] | ||
| * - 3+ states: evenly spaced using (index + 1) / (total + 1) | ||
| */ | ||
| export function calculateProgressForStates( | ||
| states: WorkflowStateWithPosition[], | ||
| ): Map<string, number> { | ||
| const progressMap = new Map<string, number>(); | ||
|
|
||
| if (states.length === 0) { | ||
| return progressMap; | ||
| } | ||
|
|
||
| const sorted = [...states].sort((a, b) => a.position - b.position); | ||
|
|
||
| const total = sorted.length; | ||
|
|
||
| for (let i = 0; i < total; i++) { | ||
| const state = sorted[i]; | ||
| if (!state) continue; | ||
|
|
||
| let progress: number; | ||
|
|
||
| if (total === 1) { | ||
| progress = 50; | ||
| } else if (total === 2) { | ||
| progress = i === 0 ? 50 : 75; | ||
| } else { | ||
| progress = ((i + 1) / (total + 1)) * 100; | ||
| } | ||
|
|
||
| progressMap.set(state.name, Math.round(progress)); | ||
| } | ||
|
|
||
| return progressMap; | ||
| } | ||
|
|
||
| const ISSUES_QUERY = ` | ||
| query Issues($first: Int!, $after: String, $filter: IssueFilter) { | ||
| issues(first: $first, after: $after, filter: $filter) { | ||
|
|
@@ -39,6 +92,7 @@ const ISSUES_QUERY = ` | |
| priority | ||
| estimate | ||
| dueDate | ||
| createdAt | ||
| url | ||
| startedAt | ||
| completedAt | ||
|
|
@@ -51,6 +105,7 @@ const ISSUES_QUERY = ` | |
| name | ||
| color | ||
| type | ||
| position | ||
| } | ||
| labels { | ||
| nodes { | ||
|
|
@@ -68,6 +123,7 @@ export async function fetchAllIssues( | |
| ): Promise<LinearIssue[]> { | ||
| const allIssues: LinearIssue[] = []; | ||
| let cursor: string | undefined; | ||
| const threeMonthsAgo = subMonths(new Date(), 3); | ||
|
|
||
| do { | ||
| const response = await client.client.request< | ||
|
|
@@ -76,7 +132,7 @@ export async function fetchAllIssues( | |
| >(ISSUES_QUERY, { | ||
| first: 100, | ||
| after: cursor, | ||
| filter: { state: { type: { nin: ["canceled", "completed"] } } }, | ||
| filter: { updatedAt: { gte: threeMonthsAgo.toISOString() } }, | ||
| }); | ||
| allIssues.push(...response.issues.nodes); | ||
| cursor = | ||
|
|
@@ -93,27 +149,32 @@ export function mapIssueToTask( | |
| organizationId: string, | ||
| creatorId: string, | ||
| userByEmail: Map<string, string>, | ||
| statusByExternalId: Map<string, string>, | ||
| ) { | ||
| const assigneeId = issue.assignee?.email | ||
| ? (userByEmail.get(issue.assignee.email) ?? null) | ||
| : null; | ||
|
|
||
| const statusId = statusByExternalId.get(issue.state.id); | ||
| if (!statusId) { | ||
| throw new Error(`Status not found for state ${issue.state.id}`); | ||
| } | ||
|
Comment on lines
+158
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check how mapIssueToTask is called and error handling context
cd apps/api
rg -n -B5 -A10 'mapIssueToTask' --type tsRepository: superset-sh/superset Length of output: 4174 🏁 Script executed: #!/bin/bash
# Read the full route.ts file to understand the complete sync flow
cd apps/api
cat -n src/app/api/integrations/linear/jobs/initial-sync/route.tsRepository: superset-sh/superset Length of output: 4848 🏁 Script executed: #!/bin/bash
# Read syncWorkflowStates implementation
cd apps/api
cat -n src/app/api/integrations/linear/jobs/initial-sync/syncWorkflowStates.tsRepository: superset-sh/superset Length of output: 2251 Replace Error with null-return pattern for graceful handling of missing workflow states. When an issue references a workflow state that wasn't synced (due to deletion, archival, or race conditions), throwing an Error aborts the entire sync batch. Return 🤖 Prompt for AI Agents |
||
|
|
||
| return { | ||
| organizationId, | ||
| creatorId, | ||
| slug: issue.identifier, | ||
| title: issue.title, | ||
| description: issue.description, | ||
| status: issue.state.name, | ||
| statusColor: issue.state.color, | ||
| statusType: issue.state.type, | ||
| statusId, | ||
| priority: mapPriorityFromLinear(issue.priority), | ||
| assigneeId, | ||
| estimate: issue.estimate, | ||
| dueDate: issue.dueDate ? new Date(issue.dueDate) : null, | ||
| labels: issue.labels.nodes.map((l) => l.name), | ||
| startedAt: issue.startedAt ? new Date(issue.startedAt) : null, | ||
| completedAt: issue.completedAt ? new Date(issue.completedAt) : null, | ||
| createdAt: new Date(issue.createdAt), | ||
| externalProvider: "linear" as const, | ||
| externalId: issue.id, | ||
| externalKey: issue.identifier, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.