Added views and view submission support to Slack integration
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/slack": patch
|
||||
---
|
||||
|
||||
Added views and view submission support to Slack integration
|
||||
@@ -46,22 +46,36 @@ function SlackInteraction({
|
||||
{trigger.name}
|
||||
</Header2>
|
||||
</div>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
|
||||
Block
|
||||
</Body>
|
||||
<Header2 size="small" className="text-slate-300 mb-2">
|
||||
{trigger.source.blockId}
|
||||
</Header2>
|
||||
</div>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
|
||||
Action
|
||||
</Body>
|
||||
<Header2 size="small" className="text-slate-300 mb-2">
|
||||
{trigger.source.actionIds.join(", ")}
|
||||
</Header2>
|
||||
</div>
|
||||
{trigger.source.type === "block_action" && (
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
|
||||
Block
|
||||
</Body>
|
||||
<Header2 size="small" className="text-slate-300 mb-2">
|
||||
{trigger.source.blockId}
|
||||
</Header2>
|
||||
</div>
|
||||
)}
|
||||
{trigger.source.type === "block_action" && (
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
|
||||
Action
|
||||
</Body>
|
||||
<Header2 size="small" className="text-slate-300 mb-2">
|
||||
{trigger.source.actionIds.join(", ")}
|
||||
</Header2>
|
||||
</div>
|
||||
)}
|
||||
{trigger.source.type === "view_submission" && (
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
|
||||
Callback IDs
|
||||
</Body>
|
||||
<Header2 size="small" className="text-slate-300 mb-2">
|
||||
{trigger.source.callbackIds.join(", ")}
|
||||
</Header2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,11 +247,17 @@ function triggerProperties(
|
||||
internalSource.source
|
||||
);
|
||||
|
||||
const title =
|
||||
slackSource.type === "block_action"
|
||||
? `block_id = ${slackSource.blockId}`
|
||||
: `callback_id = ${slackSource.callbackIds.join(", ")}`;
|
||||
|
||||
return {
|
||||
type: workflow.type,
|
||||
typeTitle: "Slack interaction",
|
||||
title: `block_id = ${slackSource.blockId}`,
|
||||
title: title,
|
||||
properties:
|
||||
slackSource.type === "block_action" &&
|
||||
slackSource.actionIds.length > 0
|
||||
? [{ key: "Action ID", value: slackSource.actionIds.join(", ") }]
|
||||
: undefined,
|
||||
|
||||
@@ -15,10 +15,13 @@ export async function action({ request }: ActionArgs) {
|
||||
const service = new HandleSlackInteractivity();
|
||||
|
||||
try {
|
||||
await service.call(parsedPayload);
|
||||
return await service.call(parsedPayload);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return { status: 200 };
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { schemas } from "@trigger.dev/slack/internal";
|
||||
import { ulid } from "ulid";
|
||||
import type { z } from "zod";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { IngestEvent } from "../events/ingest.server";
|
||||
import type { OutputUnit } from "@cfworker/json-schema";
|
||||
import { Validator } from "@cfworker/json-schema";
|
||||
|
||||
export class HandleSlackInteractivity {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -15,7 +19,7 @@ export class HandleSlackInteractivity {
|
||||
public async call(payload: unknown) {
|
||||
console.log("payload", JSON.stringify(payload, null, 2));
|
||||
|
||||
const parsedPayload = schemas.blockAction.safeParse(payload);
|
||||
const parsedPayload = schemas.InteractivityPayloadSchema.safeParse(payload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
console.error(
|
||||
@@ -23,38 +27,26 @@ export class HandleSlackInteractivity {
|
||||
generateErrorMessage(parsedPayload.error.issues)
|
||||
);
|
||||
|
||||
return;
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
if (parsedPayload.data.type !== "block_actions") {
|
||||
return;
|
||||
switch (parsedPayload.data.type) {
|
||||
case "block_actions":
|
||||
return this.#handleBlockActionInteraction(parsedPayload.data);
|
||||
case "view_submission":
|
||||
return this.#handleViewSubmissionInteraction(parsedPayload.data);
|
||||
case "view_closed":
|
||||
return this.#handleViewClosedInteraction(parsedPayload.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedPayload.data.message) {
|
||||
return;
|
||||
}
|
||||
async #handleBlockActionInteraction(
|
||||
payload: z.infer<typeof schemas.BlockActionInteractivityPayloadSchema>
|
||||
) {
|
||||
const apiKey = await this.#getApiKeyForBlockActionPayload(payload);
|
||||
|
||||
if (!parsedPayload.data.message.metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { requestId } = parsedPayload.data.message.metadata.event_payload;
|
||||
|
||||
const integrationRequest =
|
||||
await this.#prismaClient.integrationRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
workflow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!integrationRequest) {
|
||||
return;
|
||||
if (!apiKey) {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
const ingestService = new IngestEvent();
|
||||
@@ -64,10 +56,335 @@ export class HandleSlackInteractivity {
|
||||
type: "SLACK_INTERACTION",
|
||||
name: "block.action",
|
||||
service: "slack",
|
||||
payload: parsedPayload.data,
|
||||
apiKey: integrationRequest.run.environment.apiKey,
|
||||
payload: payload,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return true;
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
async #getApiKeyForBlockActionPayload(
|
||||
payload: z.infer<typeof schemas.BlockActionInteractivityPayloadSchema>
|
||||
) {
|
||||
if (payload.message) {
|
||||
if (!payload.message.metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { requestId } = payload.message.metadata.event_payload;
|
||||
|
||||
const integrationRequest =
|
||||
await this.#prismaClient.integrationRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
workflow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!integrationRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
return integrationRequest.run.environment.apiKey;
|
||||
} else if (payload.view) {
|
||||
if (typeof payload.view.private_metadata !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const privateMetadataRaw = safeJsonParse(payload.view.private_metadata);
|
||||
|
||||
if (!privateMetadataRaw) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPrivateMetadata =
|
||||
schemas.ViewPrivateMetadataSchema.safeParse(privateMetadataRaw);
|
||||
|
||||
if (!parsedPrivateMetadata.success) {
|
||||
console.error(
|
||||
"Invalid private metadata",
|
||||
generateErrorMessage(parsedPrivateMetadata.error.issues)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const runId = parsedPrivateMetadata.data.__trigger.runId;
|
||||
|
||||
const run = await this.#prismaClient.workflowRun.findUnique({
|
||||
where: { id: runId },
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return;
|
||||
}
|
||||
|
||||
return run.environment.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
async #handleViewSubmissionInteraction(
|
||||
payload: z.infer<typeof schemas.ViewSubmissionInteractivityPayloadSchema>
|
||||
) {
|
||||
if (typeof payload.view.private_metadata !== "string") {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
const privateMetadataRaw = safeJsonParse(payload.view.private_metadata);
|
||||
|
||||
if (!privateMetadataRaw) {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
const parsedPrivateMetadata =
|
||||
schemas.ViewPrivateMetadataSchema.safeParse(privateMetadataRaw);
|
||||
|
||||
if (!parsedPrivateMetadata.success) {
|
||||
console.error(
|
||||
"Invalid private metadata",
|
||||
generateErrorMessage(parsedPrivateMetadata.error.issues)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const runId = parsedPrivateMetadata.data.__trigger.runId;
|
||||
|
||||
const run = await this.#prismaClient.workflowRun.findUnique({
|
||||
where: { id: runId },
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
if (
|
||||
parsedPrivateMetadata.data.__trigger.validationSchema &&
|
||||
payload.view.state
|
||||
) {
|
||||
const validator = new Validator(
|
||||
parsedPrivateMetadata.data.__trigger.validationSchema,
|
||||
"7",
|
||||
false
|
||||
);
|
||||
|
||||
const viewData = prepareValidationData(payload.view.state.values);
|
||||
|
||||
console.log("Validating view submission", {
|
||||
viewData,
|
||||
schema: JSON.stringify(
|
||||
parsedPrivateMetadata.data.__trigger.validationSchema
|
||||
),
|
||||
});
|
||||
|
||||
const result = validator.validate(viewData);
|
||||
|
||||
if (!result.valid) {
|
||||
console.log("view submission validation errors", {
|
||||
errors: result.errors,
|
||||
});
|
||||
|
||||
const errors = prepareValidationErrors(result.errors);
|
||||
|
||||
console.log("prepared view submission validation errors", {
|
||||
errors,
|
||||
});
|
||||
|
||||
return json({
|
||||
response_action: "errors",
|
||||
errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ingestService = new IngestEvent();
|
||||
|
||||
await ingestService.call({
|
||||
id: payload.view.hash,
|
||||
type: "SLACK_INTERACTION",
|
||||
name: "view.submission",
|
||||
service: "slack",
|
||||
payload: payload,
|
||||
apiKey: run.environment.apiKey,
|
||||
});
|
||||
|
||||
return parsedPrivateMetadata.data.__trigger.onSubmit === "clear"
|
||||
? json({ response_action: "clear" })
|
||||
: parsedPrivateMetadata.data.__trigger.onSubmit === "close"
|
||||
? new Response(null, { status: 200 })
|
||||
: json({ response_action: "none" });
|
||||
}
|
||||
|
||||
async #handleViewClosedInteraction(
|
||||
payload: z.infer<typeof schemas.ViewClosedInteractivityPayloadSchema>
|
||||
) {
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
function prepareValidationData(
|
||||
values: Record<string, Record<string, any>>
|
||||
): Record<string, any> {
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
Object.keys(values).forEach((blockId) => {
|
||||
const actionId = Object.keys(values[blockId])[0];
|
||||
|
||||
if (actionId) {
|
||||
const actionData = values[blockId][actionId];
|
||||
|
||||
switch (actionData.type) {
|
||||
case "plain_text_input":
|
||||
data[blockId] = actionData.value;
|
||||
break;
|
||||
case "static_select":
|
||||
data[blockId] = actionData.selected_option.value;
|
||||
break;
|
||||
case "external_select":
|
||||
data[blockId] = actionData.selected_option.value;
|
||||
break;
|
||||
case "users_select":
|
||||
data[blockId] = actionData.selected_user;
|
||||
break;
|
||||
case "conversations_select":
|
||||
data[blockId] = actionData.selected_conversation;
|
||||
break;
|
||||
case "channels_select":
|
||||
data[blockId] = actionData.selected_channel;
|
||||
break;
|
||||
case "overflow":
|
||||
data[blockId] = actionData.selected_option.value;
|
||||
break;
|
||||
case "datepicker":
|
||||
data[blockId] = actionData.selected_date;
|
||||
break;
|
||||
case "datetimepicker":
|
||||
if (typeof actionData.selected_date_time !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
data[blockId] = new Date(
|
||||
actionData.selected_date_time * 1000
|
||||
).toISOString();
|
||||
|
||||
break;
|
||||
case "timepicker":
|
||||
data[blockId] = actionData.selected_time;
|
||||
break;
|
||||
case "radio_buttons":
|
||||
data[blockId] = actionData.selected_option.value;
|
||||
break;
|
||||
case "checkboxes":
|
||||
data[blockId] = actionData.selected_options.map(
|
||||
(option: any) => option.value
|
||||
);
|
||||
break;
|
||||
case "multi_static_select":
|
||||
data[blockId] = actionData.selected_options.map(
|
||||
(option: any) => option.value
|
||||
);
|
||||
break;
|
||||
case "multi_external_select":
|
||||
data[blockId] = actionData.selected_options.map(
|
||||
(option: any) => option.value
|
||||
);
|
||||
break;
|
||||
case "multi_users_select":
|
||||
data[blockId] = actionData.selected_users;
|
||||
break;
|
||||
case "multi_conversations_select":
|
||||
data[blockId] = actionData.selected_conversations;
|
||||
break;
|
||||
case "multi_channels_select":
|
||||
data[blockId] = actionData.selected_channels;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return removeUndefinedValues(data);
|
||||
}
|
||||
|
||||
function removeUndefinedValues(obj: Record<string, any>) {
|
||||
const newObj: Record<string, any> = {};
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key]) {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
function prepareValidationErrors(
|
||||
outputUnits: OutputUnit[]
|
||||
): Record<string, string> {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
outputUnits.forEach((outputUnit) => {
|
||||
if (outputUnit.keyword === "required") {
|
||||
const blockId = parseBlockIdFromRequiredError(outputUnit.error);
|
||||
|
||||
if (blockId) {
|
||||
errors[blockId] = `This field is required`;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const blockId = getBlockIdForInstanceLocation(outputUnit.instanceLocation);
|
||||
|
||||
if (!blockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
errors[blockId] = outputUnit.error;
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// If instanceLocation is in the form of #/nameField then we should return nameField, but if it's in the form of #/nameField/0 or #/properties/nameField then we should return undefined
|
||||
function getBlockIdForInstanceLocation(
|
||||
instanceLocation: string
|
||||
): string | undefined {
|
||||
const parts = instanceLocation.split("/");
|
||||
|
||||
if (parts.length === 2) {
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// error will be the string like: Instance does not have required property "issueAtField"
|
||||
// We want to return issueAtField
|
||||
function parseBlockIdFromRequiredError(error: string): string | undefined {
|
||||
const regex = /property "(.*)"/;
|
||||
|
||||
const match = error.match(regex);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(json: string): unknown | undefined {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.186.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.186.0",
|
||||
"@cfworker/json-schema": "^1.12.4",
|
||||
"@cfworker/json-schema": "^1.12.5",
|
||||
"@codemirror/autocomplete": "^6.3.1",
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
@@ -191,4 +191,4 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +354,7 @@ export class TriggerServer {
|
||||
topic: Topics.triggers,
|
||||
subscription: `websocketserver-${this.#workflowId}-${this.#apiKey}`,
|
||||
subscriptionType: "Shared",
|
||||
subscriptionInitialPosition: "Earliest",
|
||||
subscriptionInitialPosition: "Latest",
|
||||
},
|
||||
handlers: {
|
||||
TRIGGER_WORKFLOW: async (id, data, properties, messageAttributes) => {
|
||||
|
||||
@@ -218,7 +218,7 @@ new Trigger({
|
||||
return slack.addReaction("React to message", {
|
||||
name: "cry",
|
||||
timestamp: event.message.ts,
|
||||
channelId: event.channel.id,
|
||||
channelId: event.channel!.id,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -229,7 +229,7 @@ new Trigger({
|
||||
return slack.addReaction("React to message", {
|
||||
name: "sos",
|
||||
timestamp: event.message.ts,
|
||||
channelId: event.channel.id,
|
||||
channelId: event.channel!.id,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -243,7 +243,7 @@ new Trigger({
|
||||
//only the user pressing the button will see this message
|
||||
return slack.postMessageResponse(
|
||||
"Added a comment to the issue",
|
||||
event.response_url,
|
||||
event.response_url!,
|
||||
{
|
||||
text: `You rated your day ${action.selected_option?.value} stars`,
|
||||
replace_original: false,
|
||||
@@ -277,7 +277,7 @@ new Trigger({
|
||||
await slack.addReaction("React to message", {
|
||||
name: "thumbsup",
|
||||
timestamp: event.message.ts,
|
||||
channelId: event.channel.id,
|
||||
channelId: event.channel!.id,
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@examples/slack-modals",
|
||||
"version": "0.0.1",
|
||||
"description": "Open a modal window in Slack and do something with the response",
|
||||
"dependencies": {
|
||||
"@trigger.dev/slack": "workspace:*",
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"jsx-slack": "^5.3.0",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/node": "16",
|
||||
"tsx": "^3.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.tsx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
import { customEvent, Trigger } from "@trigger.dev/sdk";
|
||||
import * as slack from "@trigger.dev/slack";
|
||||
import JSXSlack, {
|
||||
Actions,
|
||||
Blocks,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
DateTimePicker,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
RadioButton,
|
||||
RadioButtonGroup,
|
||||
Section,
|
||||
Select,
|
||||
Option,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
Context,
|
||||
Image,
|
||||
Field,
|
||||
Header,
|
||||
Overflow,
|
||||
OverflowItem,
|
||||
} from "jsx-slack";
|
||||
import { z } from "zod";
|
||||
|
||||
const IssueBlockID = "issue.action";
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modals",
|
||||
name: "Initial Slack Modal Flow",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: customEvent({ name: "slack.modal.initiate", schema: z.any() }),
|
||||
run: async (event, ctx) => {
|
||||
await slack.postMessage("jsx-test", {
|
||||
channelName: "test-integrations",
|
||||
//text appears in Slack notifications on mobile/desktop
|
||||
text: "New github issue",
|
||||
//import and use JSXSlack to make creating rich messages much easier
|
||||
blocks: JSXSlack(
|
||||
<Blocks>
|
||||
<Section>New GitHub Issue, would you like to reply?</Section>
|
||||
<Actions blockId={IssueBlockID}>
|
||||
<Button value="issue_1234" actionId="reply-to-issue">
|
||||
Reply
|
||||
</Button>
|
||||
<Button value="issue_1234" actionId="close-issue">
|
||||
Close
|
||||
</Button>
|
||||
</Actions>
|
||||
</Blocks>
|
||||
),
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modals-initiate-reply",
|
||||
name: "Slack Modals Initiate Reply",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: slack.events.blockActionInteraction({
|
||||
blockId: IssueBlockID,
|
||||
actionId: ["reply-to-issue", "close-issue"],
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
//create promises from all the actions
|
||||
const promises = event.actions.map(async (action) => {
|
||||
switch (action.action_id) {
|
||||
case "reply-to-issue": {
|
||||
// Use the trigger_id to open a modal
|
||||
await ctx.logger.info(`Replying to issue ${action.action_ts}`, {
|
||||
action,
|
||||
});
|
||||
|
||||
if (event.trigger_id) {
|
||||
const response = await slack.openView(
|
||||
`Opening modal for ${action.action_ts}`,
|
||||
event.trigger_id,
|
||||
JSXSlack(
|
||||
<Modal
|
||||
title="My first modal"
|
||||
close="Cancel"
|
||||
callbackId="reply-to-issue-modal"
|
||||
>
|
||||
<Section>
|
||||
<p>
|
||||
<strong>It's my first modal!</strong> :sunglasses:
|
||||
</p>
|
||||
<p>jsx-slack also has supported Slack Modals.</p>
|
||||
</Section>
|
||||
<Divider />
|
||||
|
||||
<Actions id="view-interaction">
|
||||
<Button value="push" actionId="push">
|
||||
Push View
|
||||
</Button>
|
||||
<Button value="update" actionId="update">
|
||||
Update view
|
||||
</Button>
|
||||
</Actions>
|
||||
|
||||
<Input
|
||||
name="name"
|
||||
label="Name"
|
||||
maxLength={50}
|
||||
id="nameField"
|
||||
placeholder="Your name"
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
name="message"
|
||||
label="Message"
|
||||
placeholder="Your message"
|
||||
maxLength={500}
|
||||
id="messageField"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
name="closeAt"
|
||||
label="Close At"
|
||||
id="closeAtField"
|
||||
initialDate={new Date(Date.now() + 1000 * 60 * 60 * 24)}
|
||||
/>
|
||||
|
||||
<TimePicker
|
||||
name="remindMeAtTime"
|
||||
label="Remind me at"
|
||||
id="remindMeAtTimeField"
|
||||
/>
|
||||
|
||||
<DateTimePicker
|
||||
name="issueAt"
|
||||
label="Issue At"
|
||||
id="issueAtField"
|
||||
/>
|
||||
|
||||
<Input type="hidden" name="postId" value="xxxx" />
|
||||
<Input type="submit" value="Send" />
|
||||
</Modal>
|
||||
),
|
||||
{
|
||||
validationSchema: z.object({
|
||||
nameField: z.string().min(3),
|
||||
issueAtField: z.string().datetime(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
await ctx.logger.info("Modal response", { response });
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "close-issue": {
|
||||
// Use the trigger_id to open a modal
|
||||
await ctx.logger.info(`Closing issue ${action.action_ts}`, {
|
||||
action,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modals-block-actions-in-view",
|
||||
name: "Slack Modals Block Actions in View",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: slack.events.blockActionInteraction({
|
||||
blockId: "view-interaction",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const action = event.actions[0];
|
||||
|
||||
await ctx.logger.info("View interaction", { action });
|
||||
|
||||
if (!event.trigger_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should be able to update the view, or push a new modal
|
||||
|
||||
if (action.action_id === "push") {
|
||||
await slack.pushView(
|
||||
"Pushing view",
|
||||
event.trigger_id,
|
||||
JSXSlack(
|
||||
<Modal
|
||||
title="My pushed modal"
|
||||
close="Cancel"
|
||||
callbackId="reply-to-issue-modal3"
|
||||
>
|
||||
<Section>
|
||||
<p>
|
||||
<strong>This is an pushed model!</strong>
|
||||
</p>
|
||||
</Section>
|
||||
<Divider />
|
||||
|
||||
<CheckboxGroup
|
||||
id="anotherField"
|
||||
name="anotherInput"
|
||||
label="Should we actually close this view"
|
||||
required
|
||||
>
|
||||
<Checkbox value="yes">Yes Please :hamburger:</Checkbox>
|
||||
<Checkbox value="no">No keep it going :pizza:</Checkbox>
|
||||
</CheckboxGroup>
|
||||
|
||||
<Input type="submit" value="Send" />
|
||||
</Modal>
|
||||
),
|
||||
{
|
||||
onSubmit: "close",
|
||||
}
|
||||
);
|
||||
} else if (event.view) {
|
||||
await slack.updateView(
|
||||
"Updating view",
|
||||
event.view,
|
||||
JSXSlack(
|
||||
<Modal
|
||||
title="My first modal"
|
||||
close="Cancel"
|
||||
callbackId="reply-to-issue-modal2"
|
||||
>
|
||||
<Section>
|
||||
<p>
|
||||
<strong>This is an updated model!</strong>
|
||||
</p>
|
||||
</Section>
|
||||
<Divider />
|
||||
|
||||
<CheckboxGroup
|
||||
id="foodsField"
|
||||
name="foods"
|
||||
label="What do you want to eat for the party in this Friday?"
|
||||
required
|
||||
>
|
||||
<Checkbox value="burger">Burger :hamburger:</Checkbox>
|
||||
<Checkbox value="pizza">Pizza :pizza:</Checkbox>
|
||||
<Checkbox value="taco">Tex-Mex taco :taco:</Checkbox>
|
||||
<Checkbox value="sushi">Sushi :sushi:</Checkbox>
|
||||
</CheckboxGroup>
|
||||
|
||||
<Input type="submit" value="Send" />
|
||||
</Modal>
|
||||
),
|
||||
{
|
||||
onSubmit: "clear",
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modals-handle-reply-update",
|
||||
name: "Slack Modals Handle Reply Update",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: slack.events.viewSubmissionInteraction({
|
||||
callbackId: "reply-to-issue-modal2",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("Modal submission", { event });
|
||||
|
||||
return event;
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "send-slack-modal-catalog-message",
|
||||
name: "Send Slack Modal Catalog Message",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: customEvent({
|
||||
name: "slack.modal.catalog",
|
||||
schema: z.any(),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await slack.postMessage("Send Modal Catalog Message", {
|
||||
channelName: "test-integrations",
|
||||
text: "Select a modal to open",
|
||||
blocks: JSXSlack(
|
||||
<Blocks>
|
||||
<Section>Which modal would you like to test?</Section>
|
||||
<Actions blockId="modal-catalog">
|
||||
<Button value="poll" actionId="poll">
|
||||
Poll
|
||||
</Button>
|
||||
<Button value="searchResults" actionId="searchResults">
|
||||
Search Results
|
||||
</Button>
|
||||
|
||||
<Button value="appMenu" actionId="appMenu">
|
||||
App Menu (Settings)
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
value="notificationSettings"
|
||||
actionId="notificationSettings"
|
||||
>
|
||||
Notification Settings
|
||||
</Button>
|
||||
|
||||
<Button value="yourItinerary" actionId="yourItinerary">
|
||||
Your Itinerary
|
||||
</Button>
|
||||
|
||||
<Button value="ticketApp" actionId="ticketApp">
|
||||
Ticket App
|
||||
</Button>
|
||||
</Actions>
|
||||
</Blocks>
|
||||
),
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modal-catalog-block-interaction",
|
||||
name: "Slack Modal Catalog Handle Block Interaction",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: slack.events.blockActionInteraction({
|
||||
blockId: "modal-catalog",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
if (!event.trigger_id) {
|
||||
await ctx.logger.error("No trigger_id", { event });
|
||||
return;
|
||||
}
|
||||
|
||||
const action = event.actions[0];
|
||||
|
||||
switch (action.action_id) {
|
||||
case "poll": {
|
||||
await slack.openView("Opening view", event.trigger_id, PollModal, {
|
||||
onSubmit: "close",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "searchResults": {
|
||||
await slack.openView(
|
||||
"Opening view",
|
||||
event.trigger_id,
|
||||
SearchResultsModal,
|
||||
{
|
||||
onSubmit: "close",
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "appMenu": {
|
||||
await slack.openView("Opening view", event.trigger_id, AppMenuModal, {
|
||||
onSubmit: "close",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "notificationSettings": {
|
||||
await slack.openView(
|
||||
"Opening view",
|
||||
event.trigger_id,
|
||||
NotificationSettingsModal,
|
||||
{
|
||||
onSubmit: "close",
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "yourItinerary": {
|
||||
await slack.openView(
|
||||
"Opening view",
|
||||
event.trigger_id,
|
||||
YourItineraryModal,
|
||||
{
|
||||
onSubmit: "close",
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "ticketApp": {
|
||||
await slack.openView("Opening view", event.trigger_id, TicketAppModal, {
|
||||
onSubmit: "close",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}).listen();
|
||||
|
||||
new Trigger({
|
||||
id: "slack-modals-handle-catalog-submission",
|
||||
name: "Slack Modals Handle Catalog Submission",
|
||||
apiKey: "trigger_development_GJE9dEaqhqes",
|
||||
endpoint: "ws://localhost:8889/ws",
|
||||
logLevel: "debug",
|
||||
on: slack.events.viewSubmissionInteraction({
|
||||
callbackId: "modal-catalog-submission",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("Modal submission", { event });
|
||||
|
||||
return event;
|
||||
},
|
||||
}).listen();
|
||||
|
||||
const PollModal = JSXSlack(
|
||||
<Modal
|
||||
title="Workplace check-in"
|
||||
close="Cancel"
|
||||
callbackId="modal-catalog-submission"
|
||||
>
|
||||
<Section>
|
||||
<p>:wave: Hey David!</p>
|
||||
<p>
|
||||
We'd love to hear from you how we can make this place the best place
|
||||
you’ve ever worked.
|
||||
</p>
|
||||
</Section>
|
||||
<Divider />
|
||||
|
||||
<RadioButtonGroup label="You enjoy working here at Pistachio & Co" required>
|
||||
<RadioButton value="1">Strongly agree</RadioButton>
|
||||
<RadioButton value="2">Agree</RadioButton>
|
||||
<RadioButton value="3">Neither agree nor disagree</RadioButton>
|
||||
<RadioButton value="4">Disagree</RadioButton>
|
||||
<RadioButton value="5">Strongly disagree</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
|
||||
<Select
|
||||
label="What do you want for our team weekly lunch?"
|
||||
placeholder="Select your favorites"
|
||||
multiple
|
||||
required
|
||||
>
|
||||
<Option value="value-0">:pizza: Pizza</Option>
|
||||
<Option value="value-1">:fried_shrimp: Thai food</Option>
|
||||
<Option value="value-2">:desert_island: Hawaiian</Option>
|
||||
<Option value="value-3">:meat_on_bone: Texas BBQ</Option>
|
||||
<Option value="value-4">:hamburger: Burger</Option>
|
||||
<Option value="value-5">:taco: Tacos</Option>
|
||||
<Option value="value-6">:green_salad: Salad</Option>
|
||||
<Option value="value-7">:stew: Indian</Option>
|
||||
</Select>
|
||||
|
||||
<Textarea
|
||||
label="What can we do to improve your experience working here?"
|
||||
required
|
||||
/>
|
||||
<Textarea label="Anything else you want to tell us?" />
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const SearchResultsModal = JSXSlack(
|
||||
<Modal
|
||||
title="Your accommodation"
|
||||
close="Cancel"
|
||||
callbackId="modal-catalog-submission"
|
||||
>
|
||||
<Section>
|
||||
Please choose an option where you'd like to stay from Oct 21 - Oct 23 (2
|
||||
nights).
|
||||
</Section>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>Airstream Suite</b>
|
||||
<br />
|
||||
<b>Share with another person</b>. Private walk-in bathroom. TV. Heating.
|
||||
Kitchen with microwave, basic cooking utensils, wine glasses and
|
||||
silverware.
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/Streamline-Beach.png"
|
||||
alt="Airstream Suite"
|
||||
/>
|
||||
</Section>
|
||||
<Context>
|
||||
1x Queen Bed
|
||||
<span>|</span>
|
||||
$220 / night
|
||||
</Context>
|
||||
<Actions>
|
||||
<Button value="click_me_123">Choose</Button>
|
||||
<Button value="click_me_123">View Details</Button>
|
||||
</Actions>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>Redwood Suite</b>
|
||||
<br />
|
||||
<b>Share with 2 other person</b>. Studio home. Modern bathroom. TV.
|
||||
Heating. Full kitchen. Patio with lounge chairs and campfire style fire
|
||||
pit and grill.
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/redwoodcabin.png"
|
||||
alt="Redwood Suite"
|
||||
/>
|
||||
</Section>
|
||||
<Context>
|
||||
1x King Bed
|
||||
<span>|</span>
|
||||
$350 / night
|
||||
</Context>
|
||||
<Actions>
|
||||
<Button value="click_me_123" style="primary">
|
||||
✓ Your Choice
|
||||
</Button>
|
||||
<Button value="click_me_123">View Details</Button>
|
||||
</Actions>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>Luxury Tent</b>
|
||||
<br />
|
||||
<b>One person only</b>. Shared modern bathrooms and showers in lounge
|
||||
building. Temperature control with heated blankets. Lights and electrical
|
||||
outlets.
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/tent.png"
|
||||
alt="Redwood Suite"
|
||||
/>
|
||||
</Section>
|
||||
<Context>
|
||||
1x Queen Bed
|
||||
<span>|</span>
|
||||
$260 / night
|
||||
</Context>
|
||||
<Actions>
|
||||
<Button value="click_me_123">Choose</Button>
|
||||
<Button value="click_me_123">View Details</Button>
|
||||
</Actions>
|
||||
<Divider />
|
||||
<Input type="submit" value="Submit" />
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const AppMenuModal = JSXSlack(
|
||||
<Modal title="App menu" close="Cancel">
|
||||
<Section>
|
||||
<b>
|
||||
Hi <a href="fakelink.toUser.com">@David</a>!
|
||||
</b>{" "}
|
||||
Here's how I can help you:
|
||||
</Section>
|
||||
<Divider />
|
||||
<Section>
|
||||
:calendar: <b>Create event</b>
|
||||
<br />
|
||||
Create a new event
|
||||
<Button value="click_me_123" style="primary">
|
||||
Create event
|
||||
</Button>
|
||||
</Section>
|
||||
<Section>
|
||||
:clipboard: <b>List of events</b>
|
||||
<br />
|
||||
Choose from different event lists
|
||||
<Select placeholder="Choose list">
|
||||
<Option value="value-0">My events</Option>
|
||||
<Option value="value-1">All events</Option>
|
||||
<Option value="value-2">Event invites</Option>
|
||||
</Select>
|
||||
</Section>
|
||||
<Section>
|
||||
:gear: <b>Settings</b>
|
||||
<br />
|
||||
Manage your notifications and team settings
|
||||
<Select placeholder="Edit settings">
|
||||
<Option value="value-0">Notifications</Option>
|
||||
<Option value="value-1">Team settings</Option>
|
||||
</Select>
|
||||
</Section>
|
||||
<Actions>
|
||||
<Button value="click_me_123">Send feedback</Button>
|
||||
<Button value="click_me_123">FAQs</Button>
|
||||
</Actions>
|
||||
<Input type="submit" value="Submit" />
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const NotificationSettingsModal = JSXSlack(
|
||||
<Modal title="Notification settings" close="Cancel">
|
||||
<Section>
|
||||
<p>
|
||||
<b>
|
||||
<a href="fakelink.toUrl.com">PR Strategy 2019</a> posts into{" "}
|
||||
<a href="fakelink.toChannel.com">#public-relations</a>
|
||||
</b>
|
||||
</p>
|
||||
<p>Select which notifications to send:</p>
|
||||
</Section>
|
||||
<Actions>
|
||||
<CheckboxGroup>
|
||||
<Checkbox value="tasks">
|
||||
New tasks
|
||||
<small>When new tasks are added to project</small>
|
||||
</Checkbox>
|
||||
<Checkbox value="comments">
|
||||
New comments
|
||||
<small>When new comments are added</small>
|
||||
</Checkbox>
|
||||
<Checkbox value="updates">
|
||||
Project updates
|
||||
<small>When project is updated</small>
|
||||
</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</Actions>
|
||||
<Input type="submit" value="Submit" />
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const YourItineraryModal = JSXSlack(
|
||||
<Modal title="Your itinerary" close="Cancel">
|
||||
<Header>:tada: You're all set! This is your booking summary.</Header>
|
||||
<Divider />
|
||||
<Section>
|
||||
<Field>
|
||||
<b>Attendee</b>
|
||||
<br />
|
||||
Katie Chen
|
||||
</Field>
|
||||
<Field>
|
||||
<b>Date</b>
|
||||
<br />
|
||||
Oct 22-23
|
||||
</Field>
|
||||
</Section>
|
||||
|
||||
<Context>:house: Accommodation</Context>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>Redwood Suite</b>
|
||||
<br />
|
||||
<b>Share with 2 other person</b>. Studio home. Modern bathroom. TV.
|
||||
Heating. Full kitchen. Patio with lounge chairs and campfire style fire
|
||||
pit and grill.
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/redwood-suite.png"
|
||||
alt="Redwood Suite"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Context>:fork_and_knife: Food & Dietary restrictions</Context>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>All-rounder</b>
|
||||
<br />
|
||||
You eat most meats, seafood, dairy and vegetables.
|
||||
</Section>
|
||||
|
||||
<Context>:woman-running: Activities</Context>
|
||||
<Divider />
|
||||
<Section>
|
||||
<b>Winery tour and tasting</b>
|
||||
<Field>Wednesday, Oct 22 2019, 2pm-5pm</Field>
|
||||
<Field>Hosted by Sandra Mullens</Field>
|
||||
</Section>
|
||||
<Section>
|
||||
<b>Sunrise hike to Mount Amazing</b>
|
||||
<Field>Thursday, Oct 23 2019, 5:30am</Field>
|
||||
<Field>Hosted by Jordan Smith</Field>
|
||||
</Section>
|
||||
<Section>
|
||||
<b>Design systems brainstorm</b>
|
||||
<Field>Thursday, Oct 23 2019, 11a</Field>
|
||||
<Field>Hosted by Mary Lee</Field>
|
||||
</Section>
|
||||
|
||||
<Input type="submit" value="Submit" />
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const TicketAppModal = JSXSlack(
|
||||
<Modal title="Ticket app" close="Cancel">
|
||||
<Section>
|
||||
Pick a ticket list from the dropdown
|
||||
<Select placeholder="Select an item">
|
||||
<Option value="all_tickets">All Tickets</Option>
|
||||
<Option value="assigned_to_me" selected>
|
||||
Assigned To Me
|
||||
</Option>
|
||||
<Option value="issued_by_me">Issued By Me</Option>
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Divider />
|
||||
<Context>
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/highpriority.png"
|
||||
alt="High Priority"
|
||||
/>
|
||||
<b>High Priority</b>
|
||||
</Context>
|
||||
<Divider />
|
||||
|
||||
<Section>
|
||||
<b>
|
||||
<a href="fakelink.com">WEB-1098 Adjust borders on homepage graphic</a>
|
||||
</b>
|
||||
<Overflow>
|
||||
<OverflowItem value="done">
|
||||
:white_check_mark: Mark as done
|
||||
</OverflowItem>
|
||||
<OverflowItem value="edit">:pencil: Edit</OverflowItem>
|
||||
<OverflowItem value="delete">:x: Delete</OverflowItem>
|
||||
</Overflow>
|
||||
</Section>
|
||||
<Context>
|
||||
Awaiting Release
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/task-icon.png"
|
||||
alt="Task Icon"
|
||||
/>{" "}
|
||||
Task
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/profile_1.png"
|
||||
alt="Michael Scott"
|
||||
/>{" "}
|
||||
<a href="fakelink.toUser.com">Michael Scott</a>
|
||||
</Context>
|
||||
|
||||
<Section>
|
||||
<b>
|
||||
<a href="fakelink.com">
|
||||
MOB-2011 Deep-link from web search results to product page
|
||||
</a>
|
||||
</b>
|
||||
<Overflow>
|
||||
<OverflowItem value="done">
|
||||
:white_check_mark: Mark as done
|
||||
</OverflowItem>
|
||||
<OverflowItem value="edit">:pencil: Edit</OverflowItem>
|
||||
<OverflowItem value="delete">:x: Delete</OverflowItem>
|
||||
</Overflow>
|
||||
</Section>
|
||||
<Context>
|
||||
Open
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/newfeature.png"
|
||||
alt="New Feature Icon"
|
||||
/>{" "}
|
||||
New Feature
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/profile_2.png"
|
||||
alt="Pam Beasely"
|
||||
/>{" "}
|
||||
<a href="fakelink.toUser.com">Pam Beasely</a>
|
||||
</Context>
|
||||
|
||||
<Divider />
|
||||
<Context>
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/mediumpriority.png"
|
||||
alt="palm tree"
|
||||
/>
|
||||
<b>Medium Priority</b>
|
||||
</Context>
|
||||
<Divider />
|
||||
|
||||
<Section>
|
||||
<b>
|
||||
<a href="fakelink.com">WEB-1098 Adjust borders on homepage graphic</a>
|
||||
</b>
|
||||
<Overflow>
|
||||
<OverflowItem value="done">
|
||||
:white_check_mark: Mark as done
|
||||
</OverflowItem>
|
||||
<OverflowItem value="edit">:pencil: Edit</OverflowItem>
|
||||
<OverflowItem value="delete">:x: Delete</OverflowItem>
|
||||
</Overflow>
|
||||
</Section>
|
||||
<Context>
|
||||
Awaiting Release
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/task-icon.png"
|
||||
alt="Task Icon"
|
||||
/>{" "}
|
||||
Task
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/profile_1.png"
|
||||
alt="Michael Scott"
|
||||
/>{" "}
|
||||
<a href="fakelink.toUser.com">Michael Scott</a>
|
||||
</Context>
|
||||
|
||||
<Section>
|
||||
<b>
|
||||
<a href="fakelink.com">
|
||||
MOB-2011 Deep-link from web search results to product page
|
||||
</a>
|
||||
</b>
|
||||
<Overflow>
|
||||
<OverflowItem value="done">
|
||||
:white_check_mark: Mark as done
|
||||
</OverflowItem>
|
||||
<OverflowItem value="edit">:pencil: Edit</OverflowItem>
|
||||
<OverflowItem value="delete">:x: Delete</OverflowItem>
|
||||
</Overflow>
|
||||
</Section>
|
||||
<Context>
|
||||
Open
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/newfeature.png"
|
||||
alt="New Feature Icon"
|
||||
/>{" "}
|
||||
New Feature
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/profile_2.png"
|
||||
alt="Pam Beasely"
|
||||
/>{" "}
|
||||
<a href="fakelink.toUser.com">Pam Beasely</a>
|
||||
</Context>
|
||||
|
||||
<Section>
|
||||
<b>
|
||||
<a href="fakelink.com">WEB-1098 Adjust borders on homepage graphic</a>
|
||||
</b>
|
||||
<Overflow>
|
||||
<OverflowItem value="done">
|
||||
:white_check_mark: Mark as done
|
||||
</OverflowItem>
|
||||
<OverflowItem value="edit">:pencil: Edit</OverflowItem>
|
||||
<OverflowItem value="delete">:x: Delete</OverflowItem>
|
||||
</Overflow>
|
||||
</Section>
|
||||
<Context>
|
||||
Awaiting Release
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/task-icon.png"
|
||||
alt="Task Icon"
|
||||
/>{" "}
|
||||
Task
|
||||
<Image
|
||||
src="https://api.slack.com/img/blocks/bkb_template_images/profile_1.png"
|
||||
alt="Michael Scott"
|
||||
/>{" "}
|
||||
<a href="fakelink.toUser.com">Michael Scott</a>
|
||||
</Context>
|
||||
|
||||
<Input type="submit" value="Submit" />
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/examples.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "**/*.test.*"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx", // or "react-jsxdev" for development
|
||||
"jsxImportSource": "jsx-slack"
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.20.2",
|
||||
"zod-to-json-schema": "^3.20.2"
|
||||
}
|
||||
}
|
||||
@@ -22,14 +22,18 @@ export const mrkdwnOptionSchema = z.object({
|
||||
text: mrkdwnElementSchema,
|
||||
value: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
description: plainTextElementSchema.optional(),
|
||||
description: z
|
||||
.discriminatedUnion("type", [mrkdwnElementSchema, plainTextElementSchema])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const plainTextOptionSchema = z.object({
|
||||
text: plainTextElementSchema,
|
||||
value: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
description: plainTextElementSchema.optional(),
|
||||
description: z
|
||||
.discriminatedUnion("type", [mrkdwnElementSchema, plainTextElementSchema])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const optionSchema = z.union([
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { TriggerEvent } from "@trigger.dev/sdk";
|
||||
import { blockAction } from "./interactivity";
|
||||
import {
|
||||
BlockActionInteractivityPayloadSchema,
|
||||
ViewSubmissionInteractivityPayloadSchema,
|
||||
} from "./interactivity";
|
||||
|
||||
export function blockActionInteraction(params: {
|
||||
blockId: string;
|
||||
actionId?: string | string[];
|
||||
}): TriggerEvent<typeof blockAction> {
|
||||
}): TriggerEvent<typeof BlockActionInteractivityPayloadSchema> {
|
||||
const actionIds =
|
||||
typeof params.actionId === "undefined"
|
||||
? []
|
||||
@@ -28,10 +31,44 @@ export function blockActionInteraction(params: {
|
||||
event: ["block.action"],
|
||||
},
|
||||
source: {
|
||||
type: "block_action",
|
||||
blockId: params.blockId,
|
||||
actionIds,
|
||||
},
|
||||
},
|
||||
schema: blockAction,
|
||||
schema: BlockActionInteractivityPayloadSchema,
|
||||
};
|
||||
}
|
||||
|
||||
export function viewSubmissionInteraction(params: {
|
||||
callbackId?: string | string[];
|
||||
}): TriggerEvent<typeof ViewSubmissionInteractivityPayloadSchema> {
|
||||
const callbackIds =
|
||||
typeof params.callbackId === "undefined"
|
||||
? []
|
||||
: Array.isArray(params.callbackId)
|
||||
? params.callbackId
|
||||
: [params.callbackId];
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
type: "SLACK_INTERACTION",
|
||||
service: "slack",
|
||||
name: "view.submission",
|
||||
filter: {
|
||||
service: ["slack"],
|
||||
payload: {
|
||||
view: {
|
||||
callback_id: callbackIds,
|
||||
},
|
||||
},
|
||||
event: ["view.submission"],
|
||||
},
|
||||
source: {
|
||||
type: "view_submission",
|
||||
callbackIds,
|
||||
},
|
||||
},
|
||||
schema: ViewSubmissionInteractivityPayloadSchema,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getTriggerRun } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import * as events from "./events";
|
||||
import * as schemas from "./schemas";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
|
||||
export { events };
|
||||
|
||||
@@ -95,3 +96,136 @@ export async function addReaction(
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export type OpenViewResponse = z.infer<
|
||||
typeof schemas.OpenViewSuccessResponseSchema
|
||||
>;
|
||||
|
||||
export type OpenViewOptions = z.infer<typeof schemas.ModalSchema>;
|
||||
|
||||
export type OpenViewInteractionOptions = {
|
||||
onSubmit?: "clear" | "close" | "none";
|
||||
validationSchema?: z.ZodObject<any, any>;
|
||||
};
|
||||
|
||||
export async function openView(
|
||||
key: string,
|
||||
triggerId: string,
|
||||
view: OpenViewOptions,
|
||||
options?: OpenViewInteractionOptions
|
||||
): Promise<OpenViewResponse> {
|
||||
const run = getTriggerRun();
|
||||
|
||||
if (!run) {
|
||||
throw new Error("Cannot call openView outside of a trigger run");
|
||||
}
|
||||
|
||||
view.private_metadata = decoratePrivateMetadata(
|
||||
run.id,
|
||||
view.private_metadata,
|
||||
options
|
||||
);
|
||||
|
||||
const output = await run.performRequest(key, {
|
||||
service: "slack",
|
||||
endpoint: "views.open",
|
||||
params: { trigger_id: triggerId, view },
|
||||
response: {
|
||||
schema: schemas.OpenViewSuccessResponseSchema,
|
||||
},
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Cannot be called when the trigger is a viewSubmissionInteraction event, only a blockActionInteraction event
|
||||
export async function updateView(
|
||||
key: string,
|
||||
view: { id: string; hash: string; external_id?: string },
|
||||
updatedView: OpenViewOptions,
|
||||
options?: OpenViewInteractionOptions
|
||||
): Promise<OpenViewResponse> {
|
||||
const run = getTriggerRun();
|
||||
|
||||
if (!run) {
|
||||
throw new Error("Cannot call updateView outside of a trigger run");
|
||||
}
|
||||
|
||||
updatedView.private_metadata = decoratePrivateMetadata(
|
||||
run.id,
|
||||
updatedView.private_metadata,
|
||||
options
|
||||
);
|
||||
|
||||
const output = await run.performRequest(key, {
|
||||
service: "slack",
|
||||
endpoint: "views.update",
|
||||
params: {
|
||||
hash: view.hash,
|
||||
view_id: view.id,
|
||||
external_id: view.id ? undefined : view.external_id,
|
||||
view: updatedView,
|
||||
},
|
||||
response: {
|
||||
schema: schemas.OpenViewSuccessResponseSchema,
|
||||
},
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Cannot be called when the trigger is a viewSubmissionInteraction event, only a blockActionInteraction event
|
||||
export async function pushView(
|
||||
key: string,
|
||||
triggerId: string,
|
||||
view: OpenViewOptions,
|
||||
options?: OpenViewInteractionOptions
|
||||
): Promise<OpenViewResponse> {
|
||||
const run = getTriggerRun();
|
||||
|
||||
if (!run) {
|
||||
throw new Error("Cannot call pushView outside of a trigger run");
|
||||
}
|
||||
|
||||
view.private_metadata = decoratePrivateMetadata(
|
||||
run.id,
|
||||
view.private_metadata,
|
||||
options
|
||||
);
|
||||
|
||||
const output = await run.performRequest(key, {
|
||||
service: "slack",
|
||||
endpoint: "views.push",
|
||||
params: { trigger_id: triggerId, view },
|
||||
response: {
|
||||
schema: schemas.OpenViewSuccessResponseSchema,
|
||||
},
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function decoratePrivateMetadata(
|
||||
runId: string,
|
||||
existingMetadata?: string,
|
||||
options?: OpenViewInteractionOptions
|
||||
): string {
|
||||
if (!existingMetadata) {
|
||||
existingMetadata = "{}";
|
||||
}
|
||||
|
||||
const onSubmit = options?.onSubmit;
|
||||
const validationSchema = options?.validationSchema;
|
||||
|
||||
const privateMetadata = JSON.parse(existingMetadata);
|
||||
|
||||
privateMetadata.__trigger = {
|
||||
runId: runId,
|
||||
onSubmit: typeof onSubmit === "undefined" ? "none" : onSubmit,
|
||||
validationSchema: validationSchema
|
||||
? zodToJsonSchema(validationSchema.passthrough(), { errorMessages: true })
|
||||
: null,
|
||||
};
|
||||
|
||||
return JSON.stringify(privateMetadata);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ const textSchema = z.discriminatedUnion("type", [
|
||||
plainTextElementSchema,
|
||||
mrkdwnElementSchema,
|
||||
]);
|
||||
const blockActionType = z.union([
|
||||
z.literal("block_actions"),
|
||||
z.literal("interactive_message"),
|
||||
]);
|
||||
|
||||
const sourceType = z.literal("message");
|
||||
|
||||
@@ -135,13 +131,23 @@ const userSchema = z.object({
|
||||
team_id: z.string(),
|
||||
});
|
||||
|
||||
const containerSchema = z.object({
|
||||
type: sourceType,
|
||||
const viewContainerSchema = z.object({
|
||||
type: z.literal("view"),
|
||||
view_id: z.string(),
|
||||
});
|
||||
|
||||
const messageContainerSchema = z.object({
|
||||
type: z.literal("message"),
|
||||
message_ts: z.string(),
|
||||
channel_id: z.string(),
|
||||
is_ephemeral: z.boolean(),
|
||||
});
|
||||
|
||||
const containerSchema = z.discriminatedUnion("type", [
|
||||
viewContainerSchema,
|
||||
messageContainerSchema,
|
||||
]);
|
||||
|
||||
const teamSchema = z.object({ id: z.string(), domain: z.string() });
|
||||
const channelSchema = z.object({ id: z.string(), name: z.string() });
|
||||
|
||||
@@ -154,7 +160,7 @@ const viewActionDataSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
hash: z.string(),
|
||||
previous_view_id: z.string().optional(),
|
||||
previous_view_id: z.string().nullable().optional(),
|
||||
root_view_id: z.string().optional(),
|
||||
app_id: z.string().optional(),
|
||||
app_installed_team_id: z.string().optional(),
|
||||
@@ -182,34 +188,94 @@ const messageActionSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const blockAction: Zod.ZodObject<{
|
||||
type: typeof blockActionType;
|
||||
export const BlockActionInteractivityPayloadSchema: Zod.ZodObject<{
|
||||
type: z.ZodLiteral<"block_actions">;
|
||||
team: typeof teamSchema;
|
||||
user: typeof userSchema;
|
||||
api_app_id: Zod.ZodString;
|
||||
container: typeof containerSchema;
|
||||
container: z.ZodOptional<typeof containerSchema>;
|
||||
trigger_id: z.ZodOptional<Zod.ZodString>;
|
||||
team: typeof teamSchema;
|
||||
enterprise: Zod.ZodAny;
|
||||
is_enterprise_install: Zod.ZodBoolean;
|
||||
channel: typeof channelSchema;
|
||||
channel: z.ZodOptional<typeof channelSchema>;
|
||||
view: z.ZodOptional<typeof viewActionSchema>;
|
||||
message: z.ZodOptional<typeof messageActionSchema>;
|
||||
state: z.ZodOptional<typeof stateSchema>;
|
||||
response_url: Zod.ZodString;
|
||||
response_url: z.ZodOptional<Zod.ZodString>;
|
||||
actions: Zod.ZodArray<typeof actionSchema>;
|
||||
}> = z.object({
|
||||
type: blockActionType,
|
||||
type: z.literal("block_actions"),
|
||||
team: teamSchema,
|
||||
user: userSchema,
|
||||
api_app_id: z.string(),
|
||||
container: containerSchema,
|
||||
container: containerSchema.optional(),
|
||||
trigger_id: z.string().optional(),
|
||||
team: teamSchema,
|
||||
enterprise: z.any(),
|
||||
is_enterprise_install: z.boolean(),
|
||||
channel: channelSchema,
|
||||
channel: channelSchema.optional(),
|
||||
view: viewActionSchema.optional(),
|
||||
message: messageActionSchema.optional(),
|
||||
state: stateSchema.optional(),
|
||||
response_url: z.string(),
|
||||
response_url: z.string().optional(),
|
||||
actions: z.array(actionSchema),
|
||||
});
|
||||
|
||||
const ResponseUrlObjectSchema = z.object({
|
||||
response_url: z.string(),
|
||||
block_id: z.string(),
|
||||
action_id: z.string(),
|
||||
channel_id: z.string(),
|
||||
});
|
||||
|
||||
export const ViewSubmissionInteractivityPayloadSchema: Zod.ZodObject<{
|
||||
type: z.ZodLiteral<"view_submission">;
|
||||
team: typeof teamSchema;
|
||||
user: typeof userSchema;
|
||||
view: typeof viewActionSchema;
|
||||
response_urls: Zod.ZodArray<typeof ResponseUrlObjectSchema>;
|
||||
api_app_id: Zod.ZodString;
|
||||
trigger_id: z.ZodOptional<Zod.ZodString>;
|
||||
token: z.ZodOptional<Zod.ZodString>;
|
||||
}> = z.object({
|
||||
type: z.literal("view_submission"),
|
||||
team: teamSchema,
|
||||
user: userSchema,
|
||||
view: viewActionSchema,
|
||||
response_urls: z.array(ResponseUrlObjectSchema),
|
||||
api_app_id: z.string(),
|
||||
trigger_id: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ViewClosedInteractivityPayloadSchema: Zod.ZodObject<{
|
||||
type: z.ZodLiteral<"view_closed">;
|
||||
team: typeof teamSchema;
|
||||
user: typeof userSchema;
|
||||
view: typeof viewActionSchema;
|
||||
is_cleared: Zod.ZodBoolean;
|
||||
api_app_id: Zod.ZodString;
|
||||
}> = z.object({
|
||||
type: z.literal("view_closed"),
|
||||
team: teamSchema,
|
||||
user: userSchema,
|
||||
view: viewActionSchema,
|
||||
is_cleared: z.boolean(),
|
||||
api_app_id: z.string(),
|
||||
});
|
||||
|
||||
export const InteractivityPayloadSchema: Zod.ZodDiscriminatedUnion<
|
||||
"type",
|
||||
[
|
||||
typeof BlockActionInteractivityPayloadSchema,
|
||||
typeof ViewSubmissionInteractivityPayloadSchema,
|
||||
typeof ViewClosedInteractivityPayloadSchema
|
||||
]
|
||||
> = z.discriminatedUnion("type", [
|
||||
BlockActionInteractivityPayloadSchema,
|
||||
ViewSubmissionInteractivityPayloadSchema,
|
||||
ViewClosedInteractivityPayloadSchema,
|
||||
]);
|
||||
|
||||
export const ViewPrivateMetadataSchema = z.object({
|
||||
__trigger: z.object({
|
||||
runId: z.string(),
|
||||
onSubmit: z.enum(["clear", "close", "none"]),
|
||||
validationSchema: z.any().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -17,10 +17,13 @@ import {
|
||||
JoinConversationBodySchema,
|
||||
JoinConversationResponseSchema,
|
||||
ListConversationsResponseSchema,
|
||||
OpenViewBodySchema,
|
||||
OpenViewResponseSchema,
|
||||
PostMessageBodySchema,
|
||||
PostMessageOptionsSchema,
|
||||
PostMessageResponseOptionsSchema,
|
||||
PostMessageResponseSchema,
|
||||
UpdateViewBodySchema,
|
||||
} from "../schemas";
|
||||
|
||||
const log = debug("trigger:integrations:slack");
|
||||
@@ -66,6 +69,33 @@ export class SlackRequestIntegration implements RequestIntegration {
|
||||
path: "/reactions.add",
|
||||
});
|
||||
|
||||
#openViewEndpoint = new HttpEndpoint<
|
||||
typeof OpenViewResponseSchema,
|
||||
typeof OpenViewBodySchema
|
||||
>({
|
||||
response: OpenViewResponseSchema,
|
||||
method: "POST",
|
||||
path: "/views.open",
|
||||
});
|
||||
|
||||
#pushViewEndpoint = new HttpEndpoint<
|
||||
typeof OpenViewResponseSchema,
|
||||
typeof OpenViewBodySchema
|
||||
>({
|
||||
response: OpenViewResponseSchema,
|
||||
method: "POST",
|
||||
path: "/views.push",
|
||||
});
|
||||
|
||||
#updateViewEndpoint = new HttpEndpoint<
|
||||
typeof OpenViewResponseSchema,
|
||||
typeof UpdateViewBodySchema
|
||||
>({
|
||||
response: OpenViewResponseSchema,
|
||||
method: "POST",
|
||||
path: "/views.update",
|
||||
});
|
||||
|
||||
constructor(private readonly baseUrl: string = "https://slack.com/api") {}
|
||||
|
||||
perform(options: PerformRequestOptions): Promise<PerformedRequestResponse> {
|
||||
@@ -94,6 +124,30 @@ export class SlackRequestIntegration implements RequestIntegration {
|
||||
options.metadata
|
||||
);
|
||||
}
|
||||
case "views.open": {
|
||||
return this.#openView(
|
||||
options.accessInfo,
|
||||
options.params,
|
||||
options.cache,
|
||||
options.metadata
|
||||
);
|
||||
}
|
||||
case "views.update": {
|
||||
return this.#updateView(
|
||||
options.accessInfo,
|
||||
options.params,
|
||||
options.cache,
|
||||
options.metadata
|
||||
);
|
||||
}
|
||||
case "views.push": {
|
||||
return this.#pushView(
|
||||
options.accessInfo,
|
||||
options.params,
|
||||
options.cache,
|
||||
options.metadata
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown endpoint: ${options.endpoint}`);
|
||||
}
|
||||
@@ -132,6 +186,24 @@ export class SlackRequestIntegration implements RequestIntegration {
|
||||
],
|
||||
};
|
||||
}
|
||||
case "views.open": {
|
||||
return {
|
||||
title: `Open view`,
|
||||
properties: [],
|
||||
};
|
||||
}
|
||||
case "views.update": {
|
||||
return {
|
||||
title: `Update view`,
|
||||
properties: [],
|
||||
};
|
||||
}
|
||||
case "views.push": {
|
||||
return {
|
||||
title: `Push view`,
|
||||
properties: [],
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unknown endpoint: ${endpoint}`);
|
||||
@@ -335,7 +407,178 @@ export class SlackRequestIntegration implements RequestIntegration {
|
||||
},
|
||||
};
|
||||
|
||||
log("chat.postMessage performedRequest %O", performedRequest);
|
||||
log("reactions.add performedRequest %O", performedRequest);
|
||||
|
||||
return performedRequest;
|
||||
}
|
||||
|
||||
async #openView(
|
||||
accessInfo: AccessInfo,
|
||||
params: any,
|
||||
cache?: CacheService,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<PerformedRequestResponse> {
|
||||
const parsedParams = OpenViewBodySchema.parse(params);
|
||||
|
||||
log("views.open %O", parsedParams);
|
||||
|
||||
const accessToken = getAccessToken(accessInfo);
|
||||
|
||||
const service = new HttpService({
|
||||
accessToken,
|
||||
baseUrl: this.baseUrl,
|
||||
});
|
||||
|
||||
const response = await service.performRequest(
|
||||
this.#openViewEndpoint,
|
||||
parsedParams
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
log("views.open failed %O", response);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.error,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ok = response.data.ok;
|
||||
|
||||
const performedRequest = {
|
||||
ok,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.data,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
log("views.open performedRequest %O", performedRequest);
|
||||
|
||||
return performedRequest;
|
||||
}
|
||||
|
||||
async #updateView(
|
||||
accessInfo: AccessInfo,
|
||||
params: any,
|
||||
cache?: CacheService,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<PerformedRequestResponse> {
|
||||
const parsedParams = UpdateViewBodySchema.parse(params);
|
||||
|
||||
log("views.update %O", parsedParams);
|
||||
|
||||
const accessToken = getAccessToken(accessInfo);
|
||||
|
||||
const service = new HttpService({
|
||||
accessToken,
|
||||
baseUrl: this.baseUrl,
|
||||
});
|
||||
|
||||
const response = await service.performRequest(
|
||||
this.#updateViewEndpoint,
|
||||
parsedParams
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
log("views.update failed %O", response);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.error,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ok = response.data.ok;
|
||||
|
||||
const performedRequest = {
|
||||
ok,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.data,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
log("views.update performedRequest %O", performedRequest);
|
||||
|
||||
return performedRequest;
|
||||
}
|
||||
|
||||
async #pushView(
|
||||
accessInfo: AccessInfo,
|
||||
params: any,
|
||||
cache?: CacheService,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<PerformedRequestResponse> {
|
||||
const parsedParams = OpenViewBodySchema.parse(params);
|
||||
|
||||
log("views.push %O", parsedParams);
|
||||
|
||||
const accessToken = getAccessToken(accessInfo);
|
||||
|
||||
const service = new HttpService({
|
||||
accessToken,
|
||||
baseUrl: this.baseUrl,
|
||||
});
|
||||
|
||||
const response = await service.performRequest(
|
||||
this.#pushViewEndpoint,
|
||||
parsedParams
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
log("views.push failed %O", response);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.error,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ok = response.data.ok;
|
||||
|
||||
const performedRequest = {
|
||||
ok,
|
||||
isRetryable: this.#isRetryable(response.statusCode),
|
||||
response: {
|
||||
output: response.data,
|
||||
context: {
|
||||
statusCode: response.statusCode,
|
||||
headers: response.headers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
log("views.push performedRequest %O", performedRequest);
|
||||
|
||||
return performedRequest;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { knownBlockSchema } from "./blocks";
|
||||
import { blockAction } from "./interactivity";
|
||||
import { knownBlockSchema, plainTextElementSchema } from "./blocks";
|
||||
|
||||
export { blockAction };
|
||||
export * from "./interactivity";
|
||||
|
||||
export const PostMessageSuccessResponseSchema = z.object({
|
||||
ok: z.literal(true),
|
||||
@@ -117,3 +116,45 @@ export const AddReactionResponseSchema = z.discriminatedUnion("ok", [
|
||||
AddReactionSuccessResponseSchema,
|
||||
ErrorResponseSchema,
|
||||
]);
|
||||
|
||||
export const ModalSchema = z.object({
|
||||
type: z.literal("modal"),
|
||||
title: plainTextElementSchema,
|
||||
blocks: z.array(knownBlockSchema),
|
||||
private_metadata: z.string().optional(),
|
||||
callback_id: z.string().optional(),
|
||||
close: plainTextElementSchema.optional(),
|
||||
submit: plainTextElementSchema.optional(),
|
||||
clear_on_close: z.boolean().optional(),
|
||||
notify_on_close: z.boolean().optional(),
|
||||
external_id: z.string().optional(),
|
||||
submit_disabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const OpenViewBodySchema = z.object({
|
||||
trigger_id: z.string(),
|
||||
view: ModalSchema,
|
||||
});
|
||||
|
||||
export const OpenViewSuccessResponseSchema = z.object({
|
||||
ok: z.literal(true),
|
||||
view: z.object({
|
||||
id: z.string(),
|
||||
team_id: z.string(),
|
||||
type: z.string(),
|
||||
private_metadata: z.string(),
|
||||
callback_id: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const OpenViewResponseSchema = z.discriminatedUnion("ok", [
|
||||
OpenViewSuccessResponseSchema,
|
||||
ErrorResponseSchema,
|
||||
]);
|
||||
|
||||
export const UpdateViewBodySchema = z.object({
|
||||
view_id: z.string().optional(),
|
||||
hash: z.string().optional(),
|
||||
external_id: z.string().optional(),
|
||||
view: ModalSchema,
|
||||
});
|
||||
|
||||
@@ -90,11 +90,22 @@ export const ManualWebhookSourceSchema = z.object({
|
||||
event: z.string(),
|
||||
});
|
||||
|
||||
export const SlackInteractionSourceSchema = z.object({
|
||||
export const SlackBlockInteractionSourceSchema = z.object({
|
||||
type: z.literal("block_action"),
|
||||
blockId: z.string(),
|
||||
actionIds: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const SlackViewSubmissionInteractionSourceSchema = z.object({
|
||||
type: z.literal("view_submission"),
|
||||
callbackIds: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const SlackInteractionSourceSchema = z.discriminatedUnion("type", [
|
||||
SlackBlockInteractionSourceSchema,
|
||||
SlackViewSubmissionInteractionSourceSchema,
|
||||
]);
|
||||
|
||||
export type SlackInteractionSource = z.infer<
|
||||
typeof SlackInteractionSourceSchema
|
||||
>;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"types": "./src/index.ts",
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"pulsar-client": "1.7.0",
|
||||
"@types/node": "16",
|
||||
"@types/node-fetch": "2.6",
|
||||
"pulsar-client": "1.7.0",
|
||||
"typescript": "^4.9.4",
|
||||
"undici": "^5.14.0"
|
||||
},
|
||||
@@ -18,7 +18,8 @@
|
||||
"@trigger.dev/common-schemas": "workspace:*",
|
||||
"node-fetch": "2.6",
|
||||
"ulid": "^2.3.0",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.20.2",
|
||||
"zod-error": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
import { Logger } from "../logger";
|
||||
import { MessageCatalogSchema } from "./messageCatalogSchema";
|
||||
import { ulid } from "ulid";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
|
||||
import { z, ZodError } from "zod";
|
||||
import { ZodPubSubStatus } from "./types";
|
||||
@@ -171,7 +172,8 @@ export class ZodPublisher<PublisherSchema extends MessageCatalogSchema> {
|
||||
this.#logger.error(
|
||||
"[ZodPublisher] Could not publish invalid message data or properties",
|
||||
data,
|
||||
properties
|
||||
properties,
|
||||
generateErrorMessage(e.issues)
|
||||
);
|
||||
} else {
|
||||
this.#logger.error("[ZodPublisher] Error handling message", e);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MessageData,
|
||||
MessageDataSchema,
|
||||
} from "./messageCatalogSchema";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
|
||||
import { z, ZodError } from "zod";
|
||||
import { ZodPubSubStatus } from "./types";
|
||||
@@ -153,7 +154,7 @@ export class ZodSubscriber<SubscriberSchema extends MessageCatalogSchema> {
|
||||
"[ZodSubscriber] Received invalid message data or properties",
|
||||
messageData,
|
||||
properties,
|
||||
e.format()
|
||||
generateErrorMessage(e.issues)
|
||||
);
|
||||
} else {
|
||||
this.#logger.error("[ZodSubscriber] Error handling message", e);
|
||||
|
||||
@@ -471,6 +471,7 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
fetch: fetchFunction,
|
||||
workflowId: data.meta.workflowId,
|
||||
appOrigin: data.meta.appOrigin,
|
||||
id: data.id,
|
||||
},
|
||||
() => {
|
||||
this.#logger.debug("Running trigger...");
|
||||
|
||||
@@ -20,6 +20,7 @@ type TriggerRunLocalStorage = {
|
||||
fetch: TriggerFetch;
|
||||
workflowId: string;
|
||||
appOrigin: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const triggerRunLocalStorage =
|
||||
|
||||
Generated
+24
-2
@@ -39,7 +39,7 @@ importers:
|
||||
specifiers:
|
||||
'@aws-sdk/client-s3': ^3.186.0
|
||||
'@aws-sdk/s3-request-presigner': ^3.186.0
|
||||
'@cfworker/json-schema': ^1.12.4
|
||||
'@cfworker/json-schema': ^1.12.5
|
||||
'@codemirror/autocomplete': ^6.3.1
|
||||
'@codemirror/commands': ^6.1.2
|
||||
'@codemirror/lang-javascript': ^6.1.1
|
||||
@@ -548,6 +548,25 @@ importers:
|
||||
'@types/node': 16.18.11
|
||||
tsx: 3.12.2
|
||||
|
||||
examples/slack-modals:
|
||||
specifiers:
|
||||
'@trigger.dev/sdk': workspace:*
|
||||
'@trigger.dev/slack': workspace:*
|
||||
'@trigger.dev/tsconfig': workspace:*
|
||||
'@types/node': '16'
|
||||
jsx-slack: ^5.3.0
|
||||
tsx: ^3.12.0
|
||||
zod: ^3.20.2
|
||||
dependencies:
|
||||
'@trigger.dev/sdk': link:../../packages/trigger-sdk
|
||||
'@trigger.dev/slack': link:../../integrations/slack
|
||||
jsx-slack: 5.3.0
|
||||
zod: 3.20.2
|
||||
devDependencies:
|
||||
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
|
||||
'@types/node': 16.18.11
|
||||
tsx: 3.12.2
|
||||
|
||||
examples/smoke-test:
|
||||
specifiers:
|
||||
'@trigger.dev/sdk': workspace:*
|
||||
@@ -680,9 +699,11 @@ importers:
|
||||
rimraf: ^3.0.2
|
||||
tsup: ^6.5.0
|
||||
zod: ^3.20.2
|
||||
zod-to-json-schema: ^3.20.2
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
zod: 3.20.2
|
||||
zod-to-json-schema: 3.20.2_zod@3.20.2
|
||||
devDependencies:
|
||||
'@trigger.dev/integration-sdk': link:../../packages/integration-sdk
|
||||
'@trigger.dev/sdk': link:../../packages/trigger-sdk
|
||||
@@ -880,11 +901,13 @@ importers:
|
||||
ulid: ^2.3.0
|
||||
undici: ^5.14.0
|
||||
zod: ^3.20.2
|
||||
zod-error: ^1.1.0
|
||||
dependencies:
|
||||
'@trigger.dev/common-schemas': link:../common-schemas
|
||||
node-fetch: 2.6.7
|
||||
ulid: 2.3.0
|
||||
zod: 3.20.2
|
||||
zod-error: 1.1.0
|
||||
devDependencies:
|
||||
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
|
||||
'@types/node': 16.18.11
|
||||
@@ -16909,7 +16932,6 @@ packages:
|
||||
zod: ^3.20.0
|
||||
dependencies:
|
||||
zod: 3.20.2
|
||||
dev: true
|
||||
|
||||
/zod/3.20.2:
|
||||
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
|
||||
|
||||
Reference in New Issue
Block a user