Added views and view submission support to Slack integration

This commit is contained in:
Eric Allam
2023-02-02 16:17:30 +00:00
parent 96306eb5e0
commit 0e4ec8d8e6
25 changed files with 1897 additions and 97 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/slack": patch
---
Added views and view submission support to Slack integration
+30 -16
View File
@@ -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;
}
}
+2 -2
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -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) => {
+4 -4
View File
@@ -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();
+20
View File
@@ -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"
}
}
+860
View File
@@ -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
youve 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 &amp; 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>
);
+9
View File
@@ -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"
}
}
+2 -1
View File
@@ -31,6 +31,7 @@
},
"dependencies": {
"debug": "^4.3.4",
"zod": "^3.20.2"
"zod": "^3.20.2",
"zod-to-json-schema": "^3.20.2"
}
}
+6 -2
View File
@@ -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([
+40 -3
View File
@@ -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,
};
}
+134
View File
@@ -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);
}
+88 -22
View File
@@ -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(),
}),
});
+244 -1
View File
@@ -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;
}
+44 -3
View File
@@ -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,
});
+12 -1
View File
@@ -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
>;
+3 -2
View File
@@ -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);
+1
View File
@@ -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...");
+1
View File
@@ -20,6 +20,7 @@ type TriggerRunLocalStorage = {
fetch: TriggerFetch;
workflowId: string;
appOrigin: string;
id: string;
};
export const triggerRunLocalStorage =
+24 -2
View File
@@ -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==}