[SILO-482] fix: slack integration post release checklist (#4032)
* fix: silenced error for couldn't add bot to channel * fix: added expanded field for created_by and updated_by * fix: added expansion of created_by and updated_by for issues * fix: updated views to accomodate changes for created and updated fields * fix: removed reaction helpers and updated blocks * fix: removed options for enabling mentions * fix: build fixes * fix: added support for user lookup even if the user has not connected account * fix: modified issue block design for bolds * fix: resolved review comments
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { Request, RequestHandler, Response } from "express";
|
||||
import { validate as uuidValidate } from "uuid";
|
||||
import { E_ENTITY_CONNECTION_KEYS, E_INTEGRATION_KEYS, E_SILO_ERROR_CODES } from "@plane/etl/core";
|
||||
import {
|
||||
E_SLACK_ENTITY_TYPE,
|
||||
isUserMessage,
|
||||
SlackAuthState,
|
||||
SlackPlaneOAuthState,
|
||||
SlackUserAuthState, TBlockSuggestionPayload,
|
||||
SlackUserAuthState,
|
||||
TBlockSuggestionPayload,
|
||||
TSlackCommandPayload,
|
||||
TSlackPayload
|
||||
TSlackPayload,
|
||||
} from "@plane/etl/slack";
|
||||
import {
|
||||
E_PLANE_WEBHOOK_ACTION,
|
||||
@@ -30,6 +30,7 @@ import { EOAuthGrantType, ESourceAuthorizationType } from "@/types/oauth";
|
||||
import { integrationTaskManager } from "@/worker";
|
||||
import { Store } from "@/worker/base";
|
||||
import { authenticateSlackRequestMiddleware, slackAuth } from "../auth/auth";
|
||||
import { isValidIssueUpdateActivity } from "../helpers/activity";
|
||||
import { getConnectionDetails, updateUserMap } from "../helpers/connection-details";
|
||||
import { ACTIONS } from "../helpers/constants";
|
||||
import { convertToSlackOptions } from "../helpers/slack-options";
|
||||
@@ -673,7 +674,6 @@ export default class SlackController {
|
||||
@Middleware(authenticateSlackRequestMiddleware as RequestHandler)
|
||||
async slackOptions(req: Request, res: Response) {
|
||||
try {
|
||||
|
||||
const payload = JSON.parse(req.body.payload) as TBlockSuggestionPayload;
|
||||
|
||||
if (payload.type === "block_suggestion" && payload.action_id && payload.action_id === ACTIONS.LINK_WORK_ITEM) {
|
||||
@@ -712,7 +712,7 @@ export default class SlackController {
|
||||
const [projectId, fieldId] = values;
|
||||
|
||||
const details = await getConnectionDetails(payload.team.id, {
|
||||
id: payload.user.id
|
||||
id: payload.user.id,
|
||||
});
|
||||
if (!details) {
|
||||
logger.info(`[SLACK] No connection details found for team ${payload.team.id}`);
|
||||
@@ -727,14 +727,12 @@ export default class SlackController {
|
||||
}
|
||||
|
||||
const formUtilsService = getFormUtilsService(credentials.target_access_token);
|
||||
const optionsResponse = await formUtilsService.getOptionsForEntity(
|
||||
{
|
||||
slug: workspaceConnection.workspace_slug,
|
||||
projectId: projectId,
|
||||
typeIdentifier: fieldId,
|
||||
searchText: payload.value,
|
||||
}
|
||||
);
|
||||
const optionsResponse = await formUtilsService.getOptionsForEntity({
|
||||
slug: workspaceConnection.workspace_slug,
|
||||
projectId: projectId,
|
||||
typeIdentifier: fieldId,
|
||||
searchText: payload.value,
|
||||
});
|
||||
|
||||
const options = optionsResponse.map((option) => ({
|
||||
text: {
|
||||
@@ -748,7 +746,6 @@ export default class SlackController {
|
||||
return res.status(200).json({
|
||||
options: options,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return responseHandler(res, 500, error);
|
||||
}
|
||||
@@ -757,8 +754,6 @@ export default class SlackController {
|
||||
@Post("/plane/events")
|
||||
async planeEvents(req: Request, res: Response) {
|
||||
try {
|
||||
const isUUID = (id: string | null) => id && uuidValidate(id);
|
||||
|
||||
const payload = req.body as PlaneWebhookPayloadBase<ExIssue | ExIssueComment>;
|
||||
const id = payload.data.id;
|
||||
const workspace = payload.data.workspace;
|
||||
@@ -832,12 +827,7 @@ export default class SlackController {
|
||||
},
|
||||
Number(env.DEDUP_INTERVAL)
|
||||
);
|
||||
} else if (
|
||||
payload.activity.field &&
|
||||
!payload.activity.field.includes("_id") &&
|
||||
!isUUID(payload.activity.old_value) &&
|
||||
!isUUID(payload.activity.new_value)
|
||||
) {
|
||||
} else if (isValidIssueUpdateActivity(payload)) {
|
||||
const [entityConnection] = await apiClient.workspaceEntityConnection.listWorkspaceEntityConnections({
|
||||
workspace_id: workspace,
|
||||
project_id: project,
|
||||
|
||||
@@ -1,18 +1,149 @@
|
||||
import { PlaneWebhookPayloadBase, ExIssue, ExIssueComment } from "@plane/sdk";
|
||||
import { getIssueUrlFromSequenceId } from "@/helpers/urls";
|
||||
import { isUUID, titleCase } from "@/helpers/utils";
|
||||
import { ActivityForSlack } from "../types/types";
|
||||
import { getUserMarkdown } from "./user";
|
||||
|
||||
type ActivityFormatterContext = {
|
||||
workspaceSlug: string;
|
||||
userMap: Map<string, string>;
|
||||
activity: ActivityForSlack;
|
||||
fieldText: string; // Pre-formatted field name
|
||||
};
|
||||
|
||||
type ActivityFormatter = (context: ActivityFormatterContext) => string;
|
||||
|
||||
// Define custom formatters for specific fields
|
||||
const ACTIVITY_FORMATTERS: Record<string, ActivityFormatter> = {
|
||||
/**
|
||||
* Formats assignee changes showing added/removed users with mentions
|
||||
* @example
|
||||
* // Returns:
|
||||
* // > *Assignees*:
|
||||
* // > - Added <@U1234|john.doe>, <@U5678|jane.smith>
|
||||
* // > - Removed <@U9999|old.user>
|
||||
*/
|
||||
assignees: ({ activity, fieldText, userMap, workspaceSlug }) => {
|
||||
if (!activity.isArrayField) return "";
|
||||
|
||||
let changeText = `> *${fieldText}*:\n`;
|
||||
|
||||
if (activity.addedIdentifiers?.length > 0) {
|
||||
const addedUsers = activity.addedIdentifiers.map((id, index) =>
|
||||
getUserMarkdown(userMap, workspaceSlug, id, activity.added[index])
|
||||
).join(", ");
|
||||
changeText += `> - Added ${addedUsers}\n`;
|
||||
}
|
||||
|
||||
if (activity.removedIdentifiers?.length > 0) {
|
||||
const removedUsers = activity.removedIdentifiers.map((id, index) =>
|
||||
getUserMarkdown(userMap, workspaceSlug, id, activity.removed[index])
|
||||
).join(", ");
|
||||
changeText += `> - Removed ${removedUsers}\n`;
|
||||
}
|
||||
|
||||
return changeText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats parent issue changes with URL linking to the parent issue
|
||||
* @example
|
||||
* // Returns:
|
||||
* // "Added parent <https://app.plane.so/workspace/project/issues/ABC-123|ABC-123>"
|
||||
* // or for invalid format:
|
||||
* // "Added parent INVALID-ID"
|
||||
*/
|
||||
parent: ({ activity, workspaceSlug }) => {
|
||||
if (activity.isArrayField) return "";
|
||||
|
||||
if (!activity.newValue) return "";
|
||||
|
||||
const parts = activity.newValue.split("-");
|
||||
if (parts.length === 2) {
|
||||
const projectId = parts[0];
|
||||
const issueId = parts[1];
|
||||
return `Added parent <${getIssueUrlFromSequenceId(workspaceSlug, projectId, issueId)}|${activity.newValue}>`;
|
||||
}
|
||||
|
||||
return `Added parent ${activity.newValue}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats priority changes with title case formatting and strikethrough for old values
|
||||
* @example
|
||||
* // Returns:
|
||||
* // "> *Priority*: ~High~ → Medium\n" (when changing priority)
|
||||
* // "> *Priority*: removed ~Low~\n" (when removing priority)
|
||||
* // "> *Priority*: High\n" (when setting new priority)
|
||||
*/
|
||||
priority: ({ activity, fieldText }) => {
|
||||
|
||||
if (activity.isArrayField) return "";
|
||||
|
||||
const oldValue = activity.oldValue ? titleCase(activity.oldValue) : null;
|
||||
const newValue = activity.newValue ? titleCase(activity.newValue) : null;
|
||||
|
||||
if (oldValue && newValue) {
|
||||
return `> *${fieldText}*: ~${oldValue}~ → ${newValue}\n`;
|
||||
} else if (oldValue && !newValue) {
|
||||
return `> *${fieldText}*: removed ~${oldValue}~\n`;
|
||||
}
|
||||
|
||||
return `> *${fieldText}*: ${newValue}\n`;
|
||||
},
|
||||
};
|
||||
|
||||
// Default formatters for array and single fields
|
||||
/**
|
||||
* Default formatter for array fields showing added/removed items
|
||||
* @example
|
||||
* // Returns:
|
||||
* // > *Labels*:
|
||||
* // > - Added _bug, critical_
|
||||
* // > - Removed _enhancement_
|
||||
*/
|
||||
const formatArrayFieldDefault = ({ activity, fieldText }: ActivityFormatterContext): string => {
|
||||
if (!activity.isArrayField) return "";
|
||||
if (activity.added.length === 0 && activity.removed.length === 0) return "";
|
||||
|
||||
let changeText = `> *${fieldText}*:\n`;
|
||||
|
||||
if (activity.added.length > 0) {
|
||||
changeText += `> - Added _${activity.added.join(", ")}_\n`;
|
||||
}
|
||||
|
||||
if (activity.removed.length > 0) {
|
||||
changeText += `> - Removed _${activity.removed.join(", ")}_\n`;
|
||||
}
|
||||
|
||||
return changeText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default formatter for single fields with old/new value changes
|
||||
* @example
|
||||
* // Returns:
|
||||
* // "> *Status*: ~In Progress~ → Done\n" (when changing value)
|
||||
* // "> *Status*: removed ~Done~\n" (when removing value)
|
||||
* // "> *Status*: In Progress\n" (when setting new value)
|
||||
*/
|
||||
const formatSingleFieldDefault = ({ activity, fieldText }: ActivityFormatterContext): string => {
|
||||
if (activity.isArrayField) return "";
|
||||
|
||||
if (activity.oldValue && activity.newValue) {
|
||||
return `> *${fieldText}*: ~${activity.oldValue}~ → ${activity.newValue}\n`;
|
||||
} else if (activity.oldValue && !activity.newValue) {
|
||||
return `> *${fieldText}*: removed ~${activity.oldValue}~\n`;
|
||||
}
|
||||
|
||||
return `> *${fieldText}*: ${activity.newValue}\n`;
|
||||
};
|
||||
|
||||
export const formatActivityValue = (
|
||||
workspaceSlug: string,
|
||||
userMap: Map<string, string>,
|
||||
activity: ActivityForSlack
|
||||
) => {
|
||||
const titleCaseWord = (word: string) =>
|
||||
word
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
|
||||
const formatFieldName = (fieldName: string) =>
|
||||
fieldName
|
||||
.replace(/[-_]/g, " ")
|
||||
@@ -20,53 +151,30 @@ export const formatActivityValue = (
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
|
||||
let changeText = "";
|
||||
const fieldText = formatFieldName(activity.field);
|
||||
const context: ActivityFormatterContext = {
|
||||
workspaceSlug,
|
||||
userMap,
|
||||
activity,
|
||||
fieldText,
|
||||
};
|
||||
|
||||
if (activity.isArrayField === true) {
|
||||
if (activity.added.length > 0 || activity.removed.length > 0) {
|
||||
changeText += `> *${fieldText}*:\n`;
|
||||
if (activity.added.length > 0) {
|
||||
changeText += `> - Added _${activity.added.join(", ")}_\n`;
|
||||
}
|
||||
if (activity.removed.length > 0) {
|
||||
changeText += `> - Removed _${activity.removed.join(", ")}_\n`;
|
||||
}
|
||||
}
|
||||
// Check if we have a custom formatter for this field
|
||||
const customFormatter = ACTIVITY_FORMATTERS[activity.field];
|
||||
if (customFormatter) {
|
||||
return customFormatter(context);
|
||||
}
|
||||
|
||||
return changeText;
|
||||
// Fall back to default formatters
|
||||
if (activity.isArrayField) {
|
||||
return formatArrayFieldDefault(context);
|
||||
} else {
|
||||
if (activity.field === "reaction") {
|
||||
const emoji = String.fromCodePoint(parseInt(activity.newValue));
|
||||
return `${getUserMarkdown(userMap, workspaceSlug, activity.actorId, activity.actorDisplayName)} reacted with ${emoji}`;
|
||||
}
|
||||
|
||||
if (activity.field === "parent" && activity.newValue) {
|
||||
const parts = activity.newValue.split("-");
|
||||
if (parts.length === 2) {
|
||||
const projectId = parts[0];
|
||||
const issueId = parts[1];
|
||||
return `Added parent <${getIssueUrlFromSequenceId(workspaceSlug, projectId, issueId)}|${activity.newValue}>`;
|
||||
}
|
||||
|
||||
return `Added parent ${activity.newValue}`;
|
||||
}
|
||||
|
||||
if (activity.field === "priority") {
|
||||
if (activity.oldValue) {
|
||||
activity.oldValue = titleCaseWord(activity.oldValue);
|
||||
}
|
||||
if (activity.newValue) {
|
||||
activity.newValue = titleCaseWord(activity.newValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (activity.oldValue && activity.newValue) {
|
||||
return `> *${fieldText}*: ~${activity.oldValue}~ → ${activity.newValue}\n`;
|
||||
} else if (activity.oldValue && !activity.newValue) {
|
||||
return `> *${fieldText}*: removed ~${activity.oldValue}~\n`;
|
||||
}
|
||||
|
||||
return `> *${fieldText}*: ${activity.newValue}\n`;
|
||||
return formatSingleFieldDefault(context);
|
||||
}
|
||||
};
|
||||
|
||||
export const isValidIssueUpdateActivity = (payload: PlaneWebhookPayloadBase<ExIssue | ExIssueComment>) =>
|
||||
payload.activity.field &&
|
||||
!payload.activity.field.includes("_id") &&
|
||||
!isUUID(payload.activity.old_value) &&
|
||||
!isUUID(payload.activity.new_value);
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { ExIntakeIssue, IssueWithExpanded } from "@plane/sdk";
|
||||
|
||||
import { getUserMarkdown } from "./user";
|
||||
|
||||
export enum E_MUTATION_CONTEXT_ITEM_TYPE {
|
||||
WORK_ITEM = "work item",
|
||||
INTAKE = "intake"
|
||||
INTAKE = "intake",
|
||||
}
|
||||
|
||||
export enum E_MUTATION_CONTEXT_FORMAT_TYPE {
|
||||
CREATION_ONLY = "creation-only",
|
||||
CREATION_AND_UPDATE = "creation-and-update",
|
||||
UPDATE_ONLY = "update-only"
|
||||
UPDATE_ONLY = "update-only",
|
||||
}
|
||||
|
||||
export const createSlackLinkbackMutationContext = (params: {
|
||||
issue: Pick<ExIntakeIssue | IssueWithExpanded<any>, "created_by" | "updated_by">;
|
||||
issueCtx: {
|
||||
createdBy?: {
|
||||
id: string;
|
||||
display_name: string;
|
||||
};
|
||||
updatedBy?: {
|
||||
id: string;
|
||||
display_name: string;
|
||||
};
|
||||
};
|
||||
planeToSlackUserMap: Map<string, string>;
|
||||
workspaceSlug: string;
|
||||
options?: {
|
||||
@@ -23,7 +29,7 @@ export const createSlackLinkbackMutationContext = (params: {
|
||||
format?: E_MUTATION_CONTEXT_FORMAT_TYPE;
|
||||
};
|
||||
}) => {
|
||||
const { issue, planeToSlackUserMap, workspaceSlug, options = {} } = params;
|
||||
const { issueCtx, options = {} } = params;
|
||||
const {
|
||||
itemType = E_MUTATION_CONTEXT_ITEM_TYPE.WORK_ITEM,
|
||||
showUpdateInfo = false,
|
||||
@@ -32,28 +38,26 @@ export const createSlackLinkbackMutationContext = (params: {
|
||||
|
||||
// Handle update-only format
|
||||
if (format === E_MUTATION_CONTEXT_FORMAT_TYPE.UPDATE_ONLY) {
|
||||
if (!issue.updated_by) {
|
||||
if (!issueCtx.updatedBy) {
|
||||
return ""; // Return empty string if no update info available
|
||||
}
|
||||
|
||||
const updateUser = getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.updated_by);
|
||||
return `*${updateUser} updated this ${itemType}*`;
|
||||
const updateUser = issueCtx.updatedBy.display_name;
|
||||
return `_${updateUser}_ updated this ${itemType}`;
|
||||
}
|
||||
|
||||
// Determine the user to display (prefer created_by, fallback to updated_by)
|
||||
const user = issue.created_by
|
||||
? getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.created_by)
|
||||
: issue.updated_by
|
||||
? getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.updated_by)
|
||||
const user = issueCtx.createdBy
|
||||
? issueCtx.createdBy.display_name
|
||||
: issueCtx.updatedBy
|
||||
? issueCtx.updatedBy.display_name
|
||||
: "Unknown User";
|
||||
|
||||
|
||||
let content = `*${user} ${itemType === E_MUTATION_CONTEXT_ITEM_TYPE.INTAKE ? "created" : "added"} this ${itemType}*`;
|
||||
let content = `_${user}_ ${itemType === E_MUTATION_CONTEXT_ITEM_TYPE.INTAKE ? "created" : "added"} this ${itemType}`;
|
||||
|
||||
// Add update information if requested and available
|
||||
if (format === E_MUTATION_CONTEXT_FORMAT_TYPE.CREATION_AND_UPDATE && showUpdateInfo && issue.updated_by) {
|
||||
const updateUser = getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.updated_by);
|
||||
content += `\n*${updateUser} updated this ${itemType}*`;
|
||||
if (format === E_MUTATION_CONTEXT_FORMAT_TYPE.CREATION_AND_UPDATE && showUpdateInfo && issueCtx.updatedBy) {
|
||||
content += `\n_${issueCtx.updatedBy.display_name}_ updated this ${itemType}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
@@ -79,5 +79,11 @@ export const INTAKE_STATUSES = [
|
||||
{ id: 2, name: "Duplicate", emoji: "🔄" },
|
||||
];
|
||||
|
||||
|
||||
export const IGNORED_FIELD_UPDATES = ["description", "attachment", "sort_order", "link"]
|
||||
export const IGNORED_FIELD_UPDATES = [
|
||||
"description",
|
||||
"description_html",
|
||||
"attachment",
|
||||
"sort_order",
|
||||
"link",
|
||||
"reaction",
|
||||
];
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { TSlackConnectionDetails } from "../types/types"
|
||||
import { createSlackLinkback } from "../views/issue-linkback"
|
||||
import { getUserMapFromSlackWorkspaceConnection } from "./user"
|
||||
|
||||
import { TSlackConnectionDetails } from "../types/types";
|
||||
import { createSlackLinkback } from "../views/issue-linkback";
|
||||
import { enhanceUserMapWithSlackLookup, getUserMapFromSlackWorkspaceConnection } from "./user";
|
||||
|
||||
type TRefreshLinkbackProps = {
|
||||
details: TSlackConnectionDetails
|
||||
issueId: string
|
||||
projectId: string
|
||||
}
|
||||
details: TSlackConnectionDetails;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const refreshLinkback = async (props: TRefreshLinkbackProps) => {
|
||||
const { details, issueId, projectId } = props;
|
||||
const { workspaceConnection, planeClient } = details;
|
||||
const { workspaceConnection, planeClient, slackService } = details;
|
||||
|
||||
const issue = await planeClient.issue.getIssueWithFields(workspaceConnection.workspace_slug, projectId, issueId, [
|
||||
"state",
|
||||
"project",
|
||||
"assignees",
|
||||
"labels",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]);
|
||||
|
||||
const userMap = getUserMapFromSlackWorkspaceConnection(workspaceConnection);
|
||||
const enhancedUserMap = await enhanceUserMapWithSlackLookup({
|
||||
planeUsers: issue.assignees,
|
||||
currentUserMap: userMap,
|
||||
slackService,
|
||||
});
|
||||
|
||||
const updatedLinkback = createSlackLinkback(workspaceConnection.workspace_slug, issue, userMap, false);
|
||||
const updatedLinkback = createSlackLinkback(workspaceConnection.workspace_slug, issue, enhancedUserMap, false);
|
||||
|
||||
return updatedLinkback;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,7 +223,7 @@ async function mapFieldToSlackBlock(
|
||||
field: FormField,
|
||||
projectId: string,
|
||||
metadata?: SlackPrivateMetadata<typeof ENTITIES.SHORTCUT_PROJECT_SELECTION>,
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels"]>>
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>>
|
||||
) {
|
||||
if (field.id === E_KNOWN_FIELD_KEY.DESCRIPTION_HTML && metadata) {
|
||||
if (workItem) {
|
||||
@@ -268,7 +268,7 @@ async function getSlackBlocks(
|
||||
},
|
||||
type: "work-item" | "intake",
|
||||
metadata?: SlackPrivateMetadata<typeof ENTITIES.SHORTCUT_PROJECT_SELECTION>,
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels"]>>
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>>
|
||||
) {
|
||||
const { workspaceSlug, projectId, issueTypeId, accessToken } = params;
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { SlackService } from "@plane/etl/slack";
|
||||
import { TWorkspaceConnection } from "@plane/types";
|
||||
import { getUserProfileUrl } from "@/helpers/urls";
|
||||
import { invertStringMap } from "@/helpers/utils";
|
||||
import { logger } from "@/logger";
|
||||
import { TSlackWorkspaceConnectionConfig } from "../types/types";
|
||||
|
||||
export const getUserMarkdown = (planeToSlackUserMap: Map<string, string>, workspaceSlug: string, userId: string, displayName?: string) => {
|
||||
if (planeToSlackUserMap.has(userId)) {
|
||||
export const getUserMarkdown = (planeToSlackUserMap: Map<string, string>, workspaceSlug: string, userId: string, displayName?: string, disableMention: boolean = false) => {
|
||||
if (planeToSlackUserMap.has(userId) && !disableMention) {
|
||||
return `<@${planeToSlackUserMap.get(userId)}>`;
|
||||
}
|
||||
return `<${getUserProfileUrl(workspaceSlug, userId)}|${displayName ?? "Plane User"}>`;
|
||||
@@ -28,3 +31,49 @@ export const getUserMapFromSlackWorkspaceConnection = (workspaceConnection: TWor
|
||||
});
|
||||
return userMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enhance userMap by finding Slack users for Plane users who aren't mapped yet
|
||||
*/
|
||||
export const enhanceUserMapWithSlackLookup = async (props: {
|
||||
planeUsers: Array<{ id: string; email?: string; display_name: string }>,
|
||||
currentUserMap: Map<string, string>, // slack -> plane mapping
|
||||
slackService: SlackService,
|
||||
}): Promise<Map<string, string>> => {
|
||||
const { planeUsers, currentUserMap, slackService } = props;
|
||||
|
||||
const planeToSlackMap = invertStringMap(currentUserMap);
|
||||
const enhancedMap = new Map(currentUserMap);
|
||||
|
||||
// Find Plane users who aren't in the current userMap
|
||||
const unmappedUsers = planeUsers.filter(user =>
|
||||
user.email && !planeToSlackMap.has(user.id)
|
||||
);
|
||||
|
||||
if (unmappedUsers.length === 0) {
|
||||
return enhancedMap;
|
||||
}
|
||||
|
||||
try {
|
||||
// Match unmapped Plane users with Slack users by email
|
||||
for (const planeUser of unmappedUsers) {
|
||||
|
||||
if (!planeUser.email) {
|
||||
logger.warn("Skipping user lookup for user without email", { userId: planeUser.id });
|
||||
continue;
|
||||
}
|
||||
|
||||
const slackUser = await slackService.userLookupByEmail(planeUser.email);
|
||||
|
||||
if (slackUser.ok) {
|
||||
enhancedMap.set(slackUser.user.id, planeUser.id);
|
||||
} else {
|
||||
logger.warn("Failed to lookup Slack user by email", { email: planeUser.email, error: slackUser.error });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("Failed to enhance user map with Slack lookups", { error });
|
||||
}
|
||||
|
||||
return enhancedMap;
|
||||
};
|
||||
|
||||
@@ -107,7 +107,7 @@ export type TSlackWorkItemOrIntakeModalParams = {
|
||||
showThreadSync?: boolean;
|
||||
|
||||
// Work item to update
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels"]>>
|
||||
workItem?: Partial<IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>>
|
||||
disableIssueType?: boolean;
|
||||
|
||||
// Connection details
|
||||
@@ -141,10 +141,14 @@ export type ActivityForSlack = {
|
||||
isArrayField: true;
|
||||
removed: string[];
|
||||
added: string[];
|
||||
addedIdentifiers: string[];
|
||||
removedIdentifiers: string[];
|
||||
}
|
||||
| {
|
||||
isArrayField: false;
|
||||
newValue: string;
|
||||
oldValue?: string;
|
||||
newIdentifier?: string;
|
||||
oldIdentifier?: string;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -60,7 +60,6 @@ export const createActivityLinkback = (activity: ActivityProps) => {
|
||||
for (const [_, actorActivities] of Object.entries(activitiesByActor)) {
|
||||
let changeText = "";
|
||||
const actorId = actorActivities[0].actorId;
|
||||
const actorDisplayName = actorActivities[0].actorDisplayName;
|
||||
|
||||
for (const activity of actorActivities) {
|
||||
const { field } = activity;
|
||||
@@ -86,9 +85,11 @@ export const createActivityLinkback = (activity: ActivityProps) => {
|
||||
});
|
||||
|
||||
const mutationContext = createSlackLinkbackMutationContext({
|
||||
issue: {
|
||||
updated_by: actorId,
|
||||
created_by: actorId,
|
||||
issueCtx: {
|
||||
updatedBy: {
|
||||
id: actorId,
|
||||
display_name: actorActivities[0].actorDisplayName,
|
||||
},
|
||||
},
|
||||
planeToSlackUserMap,
|
||||
workspaceSlug,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExIntakeIssue } from "@plane/sdk";
|
||||
import { ExIntakeIssue, PlaneUser } from "@plane/sdk";
|
||||
import { getIntakeUrl } from "@/helpers/urls";
|
||||
import { invertStringMap } from "@/helpers/utils";
|
||||
import { createSlackLinkbackMutationContext, E_MUTATION_CONTEXT_FORMAT_TYPE, E_MUTATION_CONTEXT_ITEM_TYPE } from "../helpers/blocks";
|
||||
@@ -9,7 +9,8 @@ export const createSlackIntakeLinkback = (
|
||||
workspaceSlug: string,
|
||||
issue: ExIntakeIssue,
|
||||
userMap: Map<string, string>,
|
||||
showLogo = false
|
||||
showLogo = false,
|
||||
createdBy: PlaneUser | undefined,
|
||||
) => {
|
||||
const { issue_detail } = issue;
|
||||
const planeToSlackUserMap = invertStringMap(userMap);
|
||||
@@ -79,7 +80,13 @@ export const createSlackIntakeLinkback = (
|
||||
|
||||
// Build markdown content for creation/update info
|
||||
const mutationContext = createSlackLinkbackMutationContext({
|
||||
issue,
|
||||
issueCtx: {
|
||||
createdBy: createdBy ? {
|
||||
id: createdBy.id,
|
||||
display_name: createdBy.display_name,
|
||||
} : undefined,
|
||||
updatedBy: undefined,
|
||||
},
|
||||
planeToSlackUserMap,
|
||||
workspaceSlug,
|
||||
options: {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { IssueWithExpanded } from "@plane/sdk";
|
||||
import { getIssueUrlFromSequenceId } from "@/helpers/urls";
|
||||
import { invertStringMap } from "@/helpers/utils";
|
||||
import { createSlackLinkbackMutationContext, E_MUTATION_CONTEXT_FORMAT_TYPE, E_MUTATION_CONTEXT_ITEM_TYPE } from "../helpers/blocks";
|
||||
import {
|
||||
createSlackLinkbackMutationContext,
|
||||
E_MUTATION_CONTEXT_FORMAT_TYPE,
|
||||
E_MUTATION_CONTEXT_ITEM_TYPE,
|
||||
} from "../helpers/blocks";
|
||||
import { ACTIONS } from "../helpers/constants";
|
||||
import { getUserMarkdown } from "../helpers/user";
|
||||
|
||||
export const createSlackLinkback = (
|
||||
workspaceSlug: string,
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels"]>,
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>,
|
||||
userMap: Map<string, string>,
|
||||
isSynced: boolean,
|
||||
hideActions: boolean = false
|
||||
@@ -25,28 +29,30 @@ export const createSlackLinkback = (
|
||||
});
|
||||
|
||||
// Build markdown content for main section (fallback to mrkdwn for compatibility)
|
||||
let sectionContent = `> Project: *${issue.project.name}*`;
|
||||
let sectionContent = `> *Project*: ${issue.project.name}`;
|
||||
|
||||
if (issue.state) {
|
||||
sectionContent += `\n> State: *${issue.state.name}*`;
|
||||
sectionContent += `\n> *State*: ${issue.state.name}`;
|
||||
}
|
||||
|
||||
if (issue.priority && issue.priority !== "none") {
|
||||
sectionContent += `\n> Priority: *${issue.priority}*`;
|
||||
sectionContent += `\n> *Priority*: ${issue.priority}`;
|
||||
}
|
||||
|
||||
if (issue.assignees.length > 0) {
|
||||
const assigneeLabel = issue.assignees.length > 1 ? "Assignees" : "Assignee";
|
||||
const assignee =
|
||||
issue.assignees.length > 1
|
||||
? issue.assignees.map((a) => getUserMarkdown(planeToSlackUserMap, workspaceSlug, a.id)).join(", ")
|
||||
: getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.assignees[0].id);
|
||||
? issue.assignees
|
||||
.map((a) => getUserMarkdown(planeToSlackUserMap, workspaceSlug, a.id, a.display_name))
|
||||
.join(", ")
|
||||
: getUserMarkdown(planeToSlackUserMap, workspaceSlug, issue.assignees[0].id, issue.assignees[0].display_name);
|
||||
|
||||
sectionContent += `\n> ${assigneeLabel}: *${assignee}*`;
|
||||
sectionContent += `\n> *${assigneeLabel}*: ${assignee}`;
|
||||
}
|
||||
|
||||
if (issue.target_date) {
|
||||
sectionContent += `\n> Target Date: *${issue.target_date}*`;
|
||||
sectionContent += `\n> *Target Date*: ${issue.target_date}`;
|
||||
}
|
||||
|
||||
// Main section with issue details using mrkdwn for compatibility
|
||||
@@ -65,7 +71,10 @@ export const createSlackLinkback = (
|
||||
|
||||
// Build markdown content for creation/update info
|
||||
const mutationContext = createSlackLinkbackMutationContext({
|
||||
issue,
|
||||
issueCtx: {
|
||||
createdBy: issue.created_by,
|
||||
updatedBy: issue.updated_by,
|
||||
},
|
||||
planeToSlackUserMap,
|
||||
workspaceSlug,
|
||||
options: {
|
||||
|
||||
@@ -90,7 +90,7 @@ export const createLinkIssueModalView = (
|
||||
*/
|
||||
export const alreadyLinkedModalView = (
|
||||
workspaceSlug: string,
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels"]>,
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>,
|
||||
states: ExState[],
|
||||
userMap: Map<string, string>,
|
||||
privateMetadata: any = {}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getConnectionDetails } from "../../helpers/connection-details";
|
||||
import { getSlackContentParser } from "../../helpers/content-parser";
|
||||
import { extractRichTextElements, richTextBlockToMrkdwn } from "../../helpers/parse-issue-form";
|
||||
import { extractPlaneResource } from "../../helpers/parse-plane-resources";
|
||||
import { getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { enhanceUserMapWithSlackLookup, getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { TSlackConnectionDetails } from "../../types/types";
|
||||
import { createCycleLinkback } from "../../views/cycle-linkback";
|
||||
import { createSlackLinkback } from "../../views/issue-linkback";
|
||||
@@ -140,13 +140,19 @@ export const handleLinkSharedEvent = async (data: SlackEventPayload) => {
|
||||
workspaceConnection.workspace_slug,
|
||||
resource.projectIdentifier,
|
||||
Number(resource.issueKey),
|
||||
["state", "project", "assignees", "labels", "type"],
|
||||
["state", "project", "assignees", "labels", "type", "created_by", "updated_by"],
|
||||
true
|
||||
);
|
||||
|
||||
const enhancedUserMap = await enhanceUserMapWithSlackLookup({
|
||||
planeUsers: issue.assignees,
|
||||
currentUserMap: userMap,
|
||||
slackService,
|
||||
});
|
||||
|
||||
const hideActions = issue.type?.is_epic ?? false;
|
||||
|
||||
const linkBack = createSlackLinkback(workspaceConnection.workspace_slug, issue, userMap, false, hideActions);
|
||||
const linkBack = createSlackLinkback(workspaceConnection.workspace_slug, issue, enhancedUserMap, false, hideActions);
|
||||
unfurlMap[link.url] = {
|
||||
blocks: linkBack.blocks,
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ const handleLinkWorkItem = async (data: TMessageActionPayload) => {
|
||||
workspaceEntityConnections[0].workspace_slug,
|
||||
workspaceEntityConnections[0].project_id!,
|
||||
workspaceEntityConnections[0].issue_id!,
|
||||
["state", "project", "assignees", "labels"]
|
||||
["state", "project", "assignees", "labels", "created_by", "updated_by"]
|
||||
);
|
||||
const statesPromise = planeClient.state.list(
|
||||
workspaceEntityConnections[0].workspace_slug,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getSlackContentParser } from "../../helpers/content-parser";
|
||||
import { createSlackFormParser } from "../../helpers/field-parser/field-parser";
|
||||
import { parseLinkWorkItemFormData } from "../../helpers/parse-issue-form";
|
||||
import { getSlackThreadUrl } from "../../helpers/urls";
|
||||
import { getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { enhanceUserMapWithSlackLookup, getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { TIntakeFormResult, TWorkItemFormResult } from "../../types/fields";
|
||||
import {
|
||||
E_MESSAGE_ACTION_TYPES,
|
||||
@@ -70,7 +70,7 @@ export const handleLinkWorkItemViewSubmission = async (
|
||||
linkWorkItemValues.workspaceSlug,
|
||||
linkWorkItemValues.projectId,
|
||||
linkWorkItemValues.issueId,
|
||||
["state", "project", "assignees", "labels"]
|
||||
["state", "project", "assignees", "labels", "created_by", "updated_by"]
|
||||
);
|
||||
|
||||
// Check if the issue is already linked to a thread
|
||||
@@ -399,7 +399,7 @@ async function createIntakeIssueFromViewSubmission(
|
||||
},
|
||||
});
|
||||
|
||||
const linkBack = createSlackIntakeLinkback(workspaceConnection.workspace_slug, issue, userMap, false);
|
||||
const linkBack = createSlackIntakeLinkback(workspaceConnection.workspace_slug, issue, userMap, false, member);
|
||||
|
||||
if (metadata.entityPayload.type === ENTITIES.SHORTCUT_PROJECT_SELECTION) {
|
||||
const payload = metadata.entityPayload as ShortcutActionPayload;
|
||||
@@ -494,15 +494,20 @@ async function createWorkItemFromViewSubmission(
|
||||
workspaceConnection.workspace_slug,
|
||||
projectId,
|
||||
issueId,
|
||||
["state", "project", "assignees", "labels"]
|
||||
["state", "project", "assignees", "labels", "created_by", "updated_by"]
|
||||
);
|
||||
|
||||
const userMap = getUserMapFromSlackWorkspaceConnection(workspaceConnection);
|
||||
const enhancedUserMap = await enhanceUserMapWithSlackLookup({
|
||||
planeUsers: issueWithFields.assignees,
|
||||
currentUserMap: userMap,
|
||||
slackService,
|
||||
});
|
||||
|
||||
const linkBack = createSlackLinkback(
|
||||
workspaceConnection.workspace_slug,
|
||||
issueWithFields,
|
||||
userMap,
|
||||
enhancedUserMap,
|
||||
parsedData.data.enable_thread_sync ?? false
|
||||
);
|
||||
|
||||
@@ -724,7 +729,7 @@ async function processCustomFields(params: {
|
||||
}
|
||||
|
||||
export const createIssueErrorBlocks = (
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels"]>,
|
||||
issue: IssueWithExpanded<["state", "project", "assignees", "labels", "created_by", "updated_by"]>,
|
||||
channelName: string,
|
||||
threadLink: string,
|
||||
issueLink: string
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { ActivityForSlack, PlaneActivityWithTimestamp } from "../../types/types";
|
||||
import { createActivityLinkback } from "../../views/activity";
|
||||
|
||||
|
||||
export const handleIssueWebhook = async (payload: PlaneWebhookPayload) => {
|
||||
const activities = await getActivities(payload);
|
||||
// Ideally we won't hit this case, but just in case
|
||||
@@ -26,6 +25,7 @@ export const handleIssueWebhook = async (payload: PlaneWebhookPayload) => {
|
||||
|
||||
const { slackService, entityConnection, workspaceConnection } = details;
|
||||
|
||||
|
||||
const userMap = getUserMapFromSlackWorkspaceConnection(workspaceConnection);
|
||||
|
||||
const activityBlocks = createActivityLinkback({
|
||||
@@ -33,7 +33,7 @@ export const handleIssueWebhook = async (payload: PlaneWebhookPayload) => {
|
||||
workspaceSlug: entityConnection.workspace_slug,
|
||||
projectId: entityConnection.project_id!,
|
||||
issueId: payload.id,
|
||||
userMap,
|
||||
userMap: userMap,
|
||||
});
|
||||
|
||||
const entityData = entityConnection.entity_data as TSlackIssueEntityData;
|
||||
@@ -90,7 +90,7 @@ export const getActivities = async (payload: PlaneWebhookPayload): Promise<Activ
|
||||
const actorDisplayName = fieldActivities[0].actor.display_name;
|
||||
|
||||
if (isArrayField) {
|
||||
const { added, removed } = getArrayActivity(fieldActivities);
|
||||
const { added, removed, addedIdentifiers, removedIdentifiers } = getArrayActivity(fieldActivities);
|
||||
latestActivities.set(field, {
|
||||
isArrayField: true,
|
||||
field,
|
||||
@@ -98,6 +98,8 @@ export const getActivities = async (payload: PlaneWebhookPayload): Promise<Activ
|
||||
actorDisplayName,
|
||||
added,
|
||||
removed,
|
||||
addedIdentifiers,
|
||||
removedIdentifiers,
|
||||
timestamp: fieldActivities[fieldActivities.length - 1].timestamp,
|
||||
});
|
||||
} else {
|
||||
@@ -110,6 +112,8 @@ export const getActivities = async (payload: PlaneWebhookPayload): Promise<Activ
|
||||
actorDisplayName,
|
||||
newValue: latestActivity.new_value || "",
|
||||
oldValue: latestActivity.old_value || "",
|
||||
newIdentifier: latestActivity.new_identifier || "",
|
||||
oldIdentifier: latestActivity.old_identifier || "",
|
||||
timestamp: latestActivity.timestamp,
|
||||
});
|
||||
}
|
||||
@@ -128,14 +132,22 @@ export const getArrayActivity = (activities: PlaneActivityWithTimestamp[]) => {
|
||||
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
const addedIdentifiers: string[] = [];
|
||||
const removedIdentifiers: string[] = [];
|
||||
|
||||
for (const activity of activities) {
|
||||
if (activity.old_value === null && activity.new_value !== null) {
|
||||
added.push(activity.new_value);
|
||||
if (activity.new_identifier) {
|
||||
addedIdentifiers.push(activity.new_identifier);
|
||||
}
|
||||
} else if (activity.old_value !== null && activity.new_value === null) {
|
||||
removed.push(activity.old_value);
|
||||
if (activity.old_identifier) {
|
||||
removedIdentifiers.push(activity.old_identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed };
|
||||
return { added, removed, addedIdentifiers, removedIdentifiers };
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PlaneWebhookPayload } from "@plane/sdk";
|
||||
import { logger } from "@/logger";
|
||||
import { getAPIClient } from "@/services/client";
|
||||
import { getConnectionDetails } from "../../helpers/connection-details";
|
||||
import { getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { enhanceUserMapWithSlackLookup, getUserMapFromSlackWorkspaceConnection } from "../../helpers/user";
|
||||
import { createSlackLinkback } from "../../views/issue-linkback";
|
||||
|
||||
const apiClient = getAPIClient();
|
||||
@@ -42,12 +42,17 @@ export const handleProjectUpdateWebhook = async (payload: PlaneWebhookPayload) =
|
||||
workspaceConnection.workspace_slug,
|
||||
payload.project,
|
||||
payload.id,
|
||||
["state", "project", "assignees", "labels"]
|
||||
["state", "project", "assignees", "labels", "created_by", "updated_by"]
|
||||
);
|
||||
|
||||
const userMap = getUserMapFromSlackWorkspaceConnection(workspaceConnection);
|
||||
const enhancedUserMap = await enhanceUserMapWithSlackLookup({
|
||||
planeUsers: issue.assignees,
|
||||
currentUserMap: userMap,
|
||||
slackService,
|
||||
});
|
||||
|
||||
const linkback = createSlackLinkback(workspaceConnection.workspace_slug, issue, userMap, false);
|
||||
const linkback = createSlackLinkback(workspaceConnection.workspace_slug, issue, enhancedUserMap, false);
|
||||
|
||||
await Promise.all(
|
||||
entityConnections.map(async (entityConnection) => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import axios from "axios";
|
||||
import { parse, HTMLElement } from "node-html-parser";
|
||||
import { validate as uuidValidate } from "uuid";
|
||||
import { Client as PlaneClient } from "@plane/sdk";
|
||||
import { env } from "@/env";
|
||||
import { logger } from "@/logger";
|
||||
import { getValidCredentials } from "./credential";
|
||||
|
||||
export const removeSpanAroundImg = (htmlContent: string): string => {
|
||||
// Parse the HTML content
|
||||
const root = parse(htmlContent);
|
||||
@@ -101,6 +103,10 @@ export const createPlaneClient = async (workspaceId: string, userId: string, sou
|
||||
}
|
||||
};
|
||||
|
||||
export const titleCase = (word: string) => word.split(" ").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
||||
|
||||
export const isUUID = (id: string | null) => id && uuidValidate(id);
|
||||
|
||||
export const invertStringMap = (map: Map<string, string>) => {
|
||||
const invertedMap = new Map<string, string>();
|
||||
map.forEach((value, key) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SlackConversationHistoryResponse,
|
||||
SlackMessageResponse,
|
||||
SlackTokenRefreshResponse,
|
||||
SlackUserLookupByEmailResponse,
|
||||
SlackUserResponse,
|
||||
UnfurlMap,
|
||||
} from "../types";
|
||||
@@ -79,7 +80,7 @@ export class SlackService {
|
||||
try {
|
||||
const isBotInChannel = await this.ensureBotInChannel(channelId);
|
||||
if (!isBotInChannel) {
|
||||
throw new Error("Could not add bot to channel");
|
||||
console.error("Could not add bot to channel", { channelId });
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
@@ -161,7 +162,7 @@ export class SlackService {
|
||||
|
||||
const isBotInChannel = await this.ensureBotInChannel(channelId);
|
||||
if (!isBotInChannel) {
|
||||
throw new Error("Could not add bot to channel");
|
||||
console.error("Could not add bot to channel", { channelId });
|
||||
}
|
||||
|
||||
// Use the user token to authenticate the request
|
||||
@@ -211,7 +212,7 @@ export class SlackService {
|
||||
const isBotInChannel = await this.ensureBotInChannel(channelId);
|
||||
if (!isBotInChannel) {
|
||||
// If the bot is not in the channel, log it, but make the request anyway
|
||||
console.log("Could not add bot to channel");
|
||||
console.error("Could not add bot to channel", { channelId });
|
||||
}
|
||||
|
||||
const response = await this.client.post("chat.postMessage", payload);
|
||||
@@ -243,7 +244,7 @@ export class SlackService {
|
||||
// Check if bot is in channel
|
||||
const isBotInChannel = await this.ensureBotInChannel(channelId);
|
||||
if (!isBotInChannel) {
|
||||
throw new Error("Could not add bot to channel");
|
||||
console.error("Could not add bot to channel", { channelId });
|
||||
}
|
||||
|
||||
const response = await this.client.post("chat.postMessage", {
|
||||
@@ -318,6 +319,19 @@ export class SlackService {
|
||||
}
|
||||
}
|
||||
|
||||
async userLookupByEmail(email: string): Promise<SlackUserLookupByEmailResponse> {
|
||||
try {
|
||||
const response = await this.client.get("users.lookupByEmail", {
|
||||
params: { email },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getChannels() {
|
||||
try {
|
||||
const allChannels = [];
|
||||
|
||||
@@ -159,6 +159,14 @@ export type TSlackConnectionData = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SlackUserLookupByEmailResponse = {
|
||||
ok: true;
|
||||
user: SlackUser;
|
||||
} | {
|
||||
ok: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type TAppConnection = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
|
||||
@@ -88,9 +88,11 @@ type IIsssue = {
|
||||
export type ExpandableFields = {
|
||||
state: ExState;
|
||||
project: ExProject;
|
||||
assignees: ExUser[];
|
||||
assignees: PlaneUser[];
|
||||
labels: ExIssueLabel[];
|
||||
type: ExIssueType;
|
||||
created_by: PlaneUser | undefined;
|
||||
updated_by: PlaneUser | undefined;
|
||||
}
|
||||
// Create a type that can handle both expanded and unexpanded fields
|
||||
export type IssueWithExpanded<T extends Array<keyof ExpandableFields>> = Omit<ExIssue, T[number]> &
|
||||
|
||||
Reference in New Issue
Block a user