Improve linear app (#20453)
- Add front component form to create linear issue <img width="1512" height="831" alt="image" src="https://github.com/user-attachments/assets/ffbb223f-30a8-4c64-ac6d-002c29b604c1" /> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/a5ed2464-35a9-4a60-804c-5f15eb0043b4" /> - improve marketplace Linear app page <img width="1302" height="834" alt="image" src="https://github.com/user-attachments/assets/cdec7ec2-953d-4a49-a797-5369834b03c1" /> - update admin settings to display non secret values <img width="861" height="473" alt="image" src="https://github.com/user-attachments/assets/41dadf02-aa5d-4eb6-befe-0ad8ad4049b2" />
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { CREATE_LINEAR_ISSUE_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { createLinearIssueHandler } from 'src/logic-functions/handlers/create-linear-issue-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const body = event.body as Record<string, unknown> | null;
|
||||
|
||||
return createLinearIssueHandler({
|
||||
teamId: body?.teamId as string | undefined,
|
||||
title: body?.title as string | undefined,
|
||||
description: body?.description as string | undefined,
|
||||
priority: body?.priority as number | undefined,
|
||||
stateId: body?.stateId as string | undefined,
|
||||
assigneeId: body?.assigneeId as string | undefined,
|
||||
projectId: body?.projectId as string | undefined,
|
||||
estimate: body?.estimate as number | undefined,
|
||||
labelIds: body?.labelIds as string[] | undefined,
|
||||
cycleId: body?.cycleId as string | undefined,
|
||||
dueDate: body?.dueDate as string | undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: CREATE_LINEAR_ISSUE_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'create-linear-issue-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/linear/issues',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
+47
@@ -27,6 +27,45 @@ export default defineLogicFunction({
|
||||
type: 'string',
|
||||
description: 'Optional issue description (Markdown supported).',
|
||||
},
|
||||
priority: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Issue priority: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.',
|
||||
minimum: 0,
|
||||
maximum: 4,
|
||||
},
|
||||
stateId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The workflow state ID for the issue status. Use list-linear-issue-options to discover available states for a team.',
|
||||
},
|
||||
assigneeId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The user ID to assign the issue to. Use list-linear-issue-options to discover team members.',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'The project ID to associate the issue with.',
|
||||
},
|
||||
estimate: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The estimate value for the issue. Must match the team estimate scale.',
|
||||
},
|
||||
labelIds: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of label IDs to apply to the issue.',
|
||||
},
|
||||
cycleId: {
|
||||
type: 'string',
|
||||
description: 'The cycle ID to add the issue to.',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
description: 'Due date in YYYY-MM-DD format.',
|
||||
},
|
||||
},
|
||||
required: ['teamId', 'title'],
|
||||
},
|
||||
@@ -40,6 +79,14 @@ export default defineLogicFunction({
|
||||
teamId: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
priority: { type: 'number' },
|
||||
stateId: { type: 'string' },
|
||||
assigneeId: { type: 'string' },
|
||||
projectId: { type: 'string' },
|
||||
estimate: { type: 'number' },
|
||||
labelIds: { type: 'array', items: { type: 'string' } },
|
||||
cycleId: { type: 'string' },
|
||||
dueDate: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+11
@@ -49,6 +49,17 @@ export const createLinearIssueHandler = async (
|
||||
teamId: input.teamId,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
...(input.priority !== undefined && { priority: input.priority }),
|
||||
...(input.stateId !== undefined && { stateId: input.stateId }),
|
||||
...(input.assigneeId !== undefined && {
|
||||
assigneeId: input.assigneeId,
|
||||
}),
|
||||
...(input.projectId !== undefined && { projectId: input.projectId }),
|
||||
...(input.estimate !== undefined && { estimate: input.estimate }),
|
||||
...(input.labelIds !== undefined &&
|
||||
input.labelIds.length > 0 && { labelIds: input.labelIds }),
|
||||
...(input.cycleId !== undefined && { cycleId: input.cycleId }),
|
||||
...(input.dueDate !== undefined && { dueDate: input.dueDate }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
|
||||
|
||||
type LinearMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type LinearProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type LinearLabel = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type LinearCycle = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
number: number;
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
};
|
||||
|
||||
type LinearWorkflowState = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type IssueOptionsQueryResult = {
|
||||
team: {
|
||||
states: { nodes: LinearWorkflowState[] };
|
||||
members: { nodes: LinearMember[] };
|
||||
cycles: { nodes: LinearCycle[] };
|
||||
issueEstimationType: string;
|
||||
issueEstimationAllowZero: boolean;
|
||||
};
|
||||
projects: { nodes: LinearProject[] };
|
||||
issueLabels: { nodes: LinearLabel[] };
|
||||
};
|
||||
|
||||
type IssueOptions = {
|
||||
states: LinearWorkflowState[];
|
||||
members: LinearMember[];
|
||||
projects: LinearProject[];
|
||||
labels: LinearLabel[];
|
||||
cycles: LinearCycle[];
|
||||
estimationType: string;
|
||||
estimationAllowZero: boolean;
|
||||
};
|
||||
|
||||
type HandlerResult =
|
||||
| { success: true; options: IssueOptions }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const listLinearIssueOptionsHandler = async (input: {
|
||||
teamId?: string;
|
||||
}): Promise<HandlerResult> => {
|
||||
if (!input.teamId) {
|
||||
return { success: false, error: '`teamId` is required.' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection" first.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await callLinearGraphQL<IssueOptionsQueryResult>({
|
||||
accessToken: connection.accessToken,
|
||||
query: `
|
||||
query IssueOptions($teamId: String!) {
|
||||
team(id: $teamId) {
|
||||
states { nodes { id name type position } }
|
||||
members { nodes { id name displayName } }
|
||||
cycles { nodes { id name number startsAt endsAt } }
|
||||
issueEstimationType
|
||||
issueEstimationAllowZero
|
||||
}
|
||||
projects { nodes { id name } }
|
||||
issueLabels { nodes { id name color } }
|
||||
}
|
||||
`,
|
||||
variables: { teamId: input.teamId },
|
||||
});
|
||||
|
||||
if (result.errors || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
|
||||
};
|
||||
}
|
||||
|
||||
const { team, projects, issueLabels } = result.data;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const activeCycles = team.cycles.nodes.filter((c) => c.endsAt >= now);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
options: {
|
||||
states: team.states.nodes.sort((a, b) => a.position - b.position),
|
||||
members: team.members.nodes.sort((a, b) =>
|
||||
a.displayName.localeCompare(b.displayName),
|
||||
),
|
||||
projects: projects.nodes.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
labels: issueLabels.nodes.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
cycles: activeCycles.sort(
|
||||
(a, b) =>
|
||||
new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(),
|
||||
),
|
||||
estimationType: team.issueEstimationType,
|
||||
estimationAllowZero: team.issueEstimationAllowZero,
|
||||
},
|
||||
};
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
|
||||
|
||||
type LinearWorkflowState = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type WorkflowStatesQueryResult = {
|
||||
team: { states: { nodes: LinearWorkflowState[] } };
|
||||
};
|
||||
|
||||
type HandlerResult =
|
||||
| { success: true; states: LinearWorkflowState[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const listLinearWorkflowStatesHandler = async (input: {
|
||||
teamId?: string;
|
||||
}): Promise<HandlerResult> => {
|
||||
if (!input.teamId) {
|
||||
return { success: false, error: '`teamId` is required.' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection" first.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await callLinearGraphQL<WorkflowStatesQueryResult>({
|
||||
accessToken: connection.accessToken,
|
||||
query: `
|
||||
query WorkflowStates($teamId: String!) {
|
||||
team(id: $teamId) {
|
||||
states { nodes { id name type position } }
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { teamId: input.teamId },
|
||||
});
|
||||
|
||||
if (result.errors || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
|
||||
};
|
||||
}
|
||||
|
||||
const states = result.data.team.states.nodes.sort(
|
||||
(a, b) => a.position - b.position,
|
||||
);
|
||||
|
||||
return { success: true, states };
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { LIST_LINEAR_ISSUE_OPTIONS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { listLinearIssueOptionsHandler } from 'src/logic-functions/handlers/list-linear-issue-options-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
return listLinearIssueOptionsHandler({
|
||||
teamId: event.queryStringParameters?.teamId,
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_LINEAR_ISSUE_OPTIONS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'list-linear-issue-options-route',
|
||||
timeoutSeconds: 15,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/linear/issue-options',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { LIST_LINEAR_ISSUE_OPTIONS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { listLinearIssueOptionsHandler } from 'src/logic-functions/handlers/list-linear-issue-options-handler';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_LINEAR_ISSUE_OPTIONS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'list-linear-issue-options',
|
||||
description:
|
||||
'Returns available options for creating a Linear issue in a specific team: workflow states, members, projects, labels, cycles, and estimation settings. Requires a teamId (call list-linear-teams to discover one).',
|
||||
timeoutSeconds: 15,
|
||||
handler: listLinearIssueOptionsHandler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The Linear team ID to fetch issue options for. Use list-linear-teams to discover available teams.',
|
||||
},
|
||||
},
|
||||
required: ['teamId'],
|
||||
},
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Linear Issue Options',
|
||||
inputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
states: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
position: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
displayName: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
color: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
cycles: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
number: { type: 'number' },
|
||||
startsAt: { type: 'string' },
|
||||
endsAt: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
estimationType: { type: 'string' },
|
||||
estimationAllowZero: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { LIST_LINEAR_TEAMS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { listLinearTeamsHandler } from 'src/logic-functions/handlers/list-linear-teams-handler';
|
||||
|
||||
const handler = async (_event: RoutePayload) => {
|
||||
return listLinearTeamsHandler();
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_LINEAR_TEAMS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'list-linear-teams-route',
|
||||
timeoutSeconds: 15,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/linear/teams',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
@@ -16,4 +16,33 @@ export default defineLogicFunction({
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Linear Teams',
|
||||
inputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
teams: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
key: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { LIST_LINEAR_WORKFLOW_STATES_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { listLinearWorkflowStatesHandler } from 'src/logic-functions/handlers/list-linear-workflow-states-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
return listLinearWorkflowStatesHandler({
|
||||
teamId: event.queryStringParameters?.teamId,
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_LINEAR_WORKFLOW_STATES_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'list-linear-workflow-states-route',
|
||||
timeoutSeconds: 15,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/linear/workflow-states',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
+8
@@ -2,4 +2,12 @@ export type CreateIssueInput = {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
stateId?: string;
|
||||
assigneeId?: string;
|
||||
projectId?: string;
|
||||
estimate?: number;
|
||||
labelIds?: string[];
|
||||
cycleId?: string;
|
||||
dueDate?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user