diff --git a/core/actions/_lib/rules.tsx b/core/actions/_lib/automations.tsx similarity index 62% rename from core/actions/_lib/rules.tsx rename to core/actions/_lib/automations.tsx index a05265e14e..4ca85ce63a 100644 --- a/core/actions/_lib/rules.tsx +++ b/core/actions/_lib/automations.tsx @@ -8,16 +8,16 @@ import { } from "lucide-react"; import { z } from "zod"; -import type { RulesId } from "db/public"; +import type { AutomationsId } from "db/public"; import { Event } from "db/public"; import { CopyButton } from "ui/copy-button"; -import { defineRule } from "~/actions/types"; +import { defineAutomation } from "~/actions/types"; export const intervals = ["minute", "hour", "day", "week", "month", "year"] as const; export type Interval = (typeof intervals)[number]; -export const pubInStageForDuration = defineRule({ +export const pubInStageForDuration = defineAutomation({ event: Event.pubInStageForDuration, additionalConfig: z.object({ duration: z.number().int().min(1), @@ -32,7 +32,7 @@ export const pubInStageForDuration = defineRule({ }); export type PubInStageForDuration = typeof pubInStageForDuration; -export const pubLeftStage = defineRule({ +export const pubLeftStage = defineAutomation({ event: Event.pubLeftStage, display: { icon: ArrowRightFromLine, @@ -41,7 +41,7 @@ export const pubLeftStage = defineRule({ }); export type PubLeftStage = typeof pubLeftStage; -export const pubEnteredStage = defineRule({ +export const pubEnteredStage = defineAutomation({ event: Event.pubEnteredStage, display: { icon: ArrowRightToLine, @@ -50,7 +50,7 @@ export const pubEnteredStage = defineRule({ }); export type PubEnteredStage = typeof pubEnteredStage; -export const actionSucceeded = defineRule({ +export const actionSucceeded = defineAutomation({ event: Event.actionSucceeded, display: { icon: CheckCircle, @@ -60,7 +60,7 @@ export const actionSucceeded = defineRule({ }); export type ActionSucceeded = typeof actionSucceeded; -export const actionFailed = defineRule({ +export const actionFailed = defineAutomation({ event: Event.actionFailed, display: { icon: XCircle, @@ -70,25 +70,28 @@ export const actionFailed = defineRule({ }); export type ActionFailed = typeof actionFailed; -export const constructWebhookUrl = (ruleId: RulesId, communitySlug: string) => - `/api/v0/c/${communitySlug}/site/webhook/${ruleId}`; +export const constructWebhookUrl = (automationId: AutomationsId, communitySlug: string) => + `/api/v0/c/${communitySlug}/site/webhook/${automationId}`; -export const webhook = defineRule({ +export const webhook = defineAutomation({ event: Event.webhook, display: { icon: Globe, base: ({ community }) => ( a request is made to{" "} - {constructWebhookUrl("" as RulesId, community.slug)} + + {constructWebhookUrl("" as AutomationsId, community.slug)} + ), - hydrated: ({ rule, community }) => ( + hydrated: ({ automation: automation, community }) => ( - a request is made to {constructWebhookUrl(rule.id, community.slug)} + a request is made to{" "} + {constructWebhookUrl(automation.id, community.slug)} @@ -97,7 +100,7 @@ export const webhook = defineRule({ }, }); -export type Rules = +export type Automation = | PubInStageForDuration | PubLeftStage | PubEnteredStage @@ -109,13 +112,15 @@ export type SchedulableEvent = | Event.actionFailed | Event.actionSucceeded; -export type RuleForEvent = E extends E ? Extract : never; +export type AutomationForEvent = E extends E + ? Extract + : never; -export type SchedulableRule = RuleForEvent; +export type SchedulableAutomation = AutomationForEvent; -export type RuleConfig = Rule extends Rule +export type AutomationConfig = A extends A ? { - ruleConfig: NonNullable["_input"] extends infer RC + automationConfig: NonNullable["_input"] extends infer RC ? undefined extends RC ? null : RC @@ -124,4 +129,4 @@ export type RuleConfig = Rule extends Rule } : never; -export type RuleConfigs = RuleConfig | undefined; +export type AutomationConfigs = AutomationConfig | undefined; diff --git a/core/actions/_lib/runActionInstance.ts b/core/actions/_lib/runActionInstance.ts index abf86a8d3f..aee6d985b3 100644 --- a/core/actions/_lib/runActionInstance.ts +++ b/core/actions/_lib/runActionInstance.ts @@ -24,9 +24,9 @@ import { env } from "~/lib/env/env"; import { createLastModifiedBy } from "~/lib/lastModifiedBy"; import { ApiError, getPubsWithRelatedValues } from "~/lib/server"; import { getActionConfigDefaults } from "~/lib/server/actions"; +import { MAX_STACK_DEPTH } from "~/lib/server/automations"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { getCommunity } from "~/lib/server/community"; -import { MAX_STACK_DEPTH } from "~/lib/server/rules"; import { isClientExceptionOptions } from "~/lib/serverActions"; import { getActionByName } from "../api"; import { ActionConfigBuilder } from "./ActionConfigBuilder"; @@ -375,13 +375,13 @@ export const runInstancesForEvent = async ( ) => { const instances = await trx .selectFrom("action_instances") - .innerJoin("rules", "rules.actionInstanceId", "action_instances.id") + .innerJoin("automations", "automations.actionInstanceId", "action_instances.id") .select([ "action_instances.id as actionInstanceId", - "rules.config as ruleConfig", + "automations.config as automationConfig", "action_instances.name as actionInstanceName", ]) - .where("rules.event", "=", event) + .where("automations.event", "=", event) .where("action_instances.stageId", "=", stageId) .execute(); @@ -396,7 +396,7 @@ export const runInstancesForEvent = async ( communityId, actionInstanceId: instance.actionInstanceId, event, - actionInstanceArgs: instance.ruleConfig ?? null, + actionInstanceArgs: instance.automationConfig ?? null, stack, }, trx diff --git a/core/actions/_lib/scheduleActionInstance.ts b/core/actions/_lib/scheduleActionInstance.ts index 90a77e8662..1d6d2b4beb 100644 --- a/core/actions/_lib/scheduleActionInstance.ts +++ b/core/actions/_lib/scheduleActionInstance.ts @@ -3,11 +3,11 @@ import type { ActionInstancesId, ActionRunsId, PubsId, StagesId } from "db/publi import { ActionRunStatus, Event } from "db/public"; import { logger } from "logger"; -import type { SchedulableRule } from "./rules"; -import type { GetEventRuleOptions } from "~/lib/db/queries"; +import type { SchedulableAutomation } from "./automations"; +import type { GetEventAutomationOptions } from "~/lib/db/queries"; import { db } from "~/kysely/database"; import { addDuration } from "~/lib/dates"; -import { getStageRules } from "~/lib/db/queries"; +import { getStageAutomations } from "~/lib/db/queries"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug"; import { getJobsClient, getScheduledActionJobKey } from "~/lib/server/jobs"; @@ -17,7 +17,7 @@ type Shared = { stack: ActionRunsId[]; /* Config for the action instance */ config?: Record | null; -} & GetEventRuleOptions; +} & GetEventAutomationOptions; type ScheduleActionInstanceForPubOptions = Shared & { pubId: PubsId; @@ -42,85 +42,85 @@ export const scheduleActionInstances = async (options: ScheduleActionInstanceOpt throw new Error("PubId or body is required"); } - const [rules, jobsClient] = await Promise.all([ - getStageRules(options.stageId, options).execute(), + const [automations, jobsClient] = await Promise.all([ + getStageAutomations(options.stageId, options).execute(), getJobsClient(), ]); - if (!rules.length) { + if (!automations.length) { logger.debug({ msg: `No action instances found for stage ${options.stageId}. Most likely this is because a Pub is moved into a stage without action instances.`, pubId: options.pubId, stageId: options.stageId, - rules, + automations, }); return; } - const validRules = rules + const validAutomations = automations .filter( - (rule): rule is typeof rule & SchedulableRule => - rule.event === Event.actionFailed || - rule.event === Event.actionSucceeded || - rule.event === Event.webhook || - (rule.event === Event.pubInStageForDuration && + (automation): automation is typeof automation & SchedulableAutomation => + automation.event === Event.actionFailed || + automation.event === Event.actionSucceeded || + automation.event === Event.webhook || + (automation.event === Event.pubInStageForDuration && Boolean( - typeof rule.config === "object" && - rule.config && - "duration" in rule.config && - rule.config.duration && - "interval" in rule.config && - rule.config.interval + typeof automation.config === "object" && + automation.config && + "duration" in automation.config && + automation.config.duration && + "interval" in automation.config && + automation.config.interval )) ) - .map((rule) => ({ - ...rule, - duration: rule.config?.ruleConfig?.duration || 0, - interval: rule.config?.ruleConfig?.interval || "minute", + .map((automation) => ({ + ...automation, + duration: automation.config?.automationConfig?.duration || 0, + interval: automation.config?.automationConfig?.interval || "minute", })); const results = await Promise.all( - validRules.flatMap(async (rule) => { + validAutomations.flatMap(async (automation) => { const runAt = addDuration({ - duration: rule.duration, - interval: rule.interval, + duration: automation.duration, + interval: automation.interval, }).toISOString(); const scheduledActionRun = await autoRevalidate( db .insertInto("action_runs") .values({ - actionInstanceId: rule.actionInstance.id, + actionInstanceId: automation.actionInstance.id, pubId: options.pubId, json: options.json, status: ActionRunStatus.scheduled, - config: options.config ?? rule.actionInstance.config, + config: options.config ?? automation.actionInstance.config, result: { scheduled: `Action scheduled for ${runAt}` }, - event: rule.event, + event: automation.event, sourceActionRunId: options.stack.at(-1), }) .returning("id") ).executeTakeFirstOrThrow(); const job = await jobsClient.scheduleAction({ - actionInstanceId: rule.actionInstance.id, - duration: rule.duration, - interval: rule.interval, + actionInstanceId: automation.actionInstance.id, + duration: automation.duration, + interval: automation.interval, stageId: options.stageId, community: { slug: await getCommunitySlug(), }, stack: options.stack, scheduledActionRunId: scheduledActionRun.id, - event: rule.event, + event: automation.event, ...(options.pubId ? { pubId: options.pubId } : { json: options.json! }), - config: options.config ?? rule.actionInstance.config ?? null, + config: options.config ?? automation.actionInstance.config ?? null, }); return { result: job, - actionInstanceId: rule.actionInstance.id, - actionInstanceName: rule.actionInstance.name, + actionInstanceId: automation.actionInstance.id, + actionInstanceName: automation.actionInstance.name, runAt, }; }) diff --git a/core/actions/api/index.ts b/core/actions/api/index.ts index 3ff6ab8673..66245c68e8 100644 --- a/core/actions/api/index.ts +++ b/core/actions/api/index.ts @@ -2,9 +2,9 @@ import type * as z from "zod"; -import type { ActionInstances, Communities, Event, Rules } from "db/public"; +import type { ActionInstances, Automations, Communities, Event } from "db/public"; -import type { SequentialRuleEvent } from "../types"; +import type { SequentialAutomationEvent } from "../types"; import { actionFailed, actionSucceeded, @@ -12,7 +12,7 @@ import { pubInStageForDuration, pubLeftStage, webhook, -} from "../_lib/rules"; +} from "../_lib/automations"; import * as buildJournalSite from "../buildJournalSite/action"; import * as datacite from "../datacite/action"; import * as email from "../email/action"; @@ -20,7 +20,7 @@ import * as googleDriveImport from "../googleDriveImport/action"; import * as http from "../http/action"; import * as log from "../log/action"; import * as move from "../move/action"; -import { sequentialRuleEvents } from "../types"; +import { sequentialAutomationEvents } from "../types"; export const actions = { [log.action.name]: log.action, @@ -44,76 +44,87 @@ export const getActionNames = () => { return Object.keys(actions) as (keyof typeof actions)[]; }; -export const rules = { +export const automations = { [pubInStageForDuration.event]: pubInStageForDuration, [pubEnteredStage.event]: pubEnteredStage, [pubLeftStage.event]: pubLeftStage, [actionSucceeded.event]: actionSucceeded, [actionFailed.event]: actionFailed, [webhook.event]: webhook, -} as const; -// export type Rules = typeof rules; +} as const satisfies Record; -export const getRuleByName = (name: T) => { - return rules[name]; +export const getAutomationByName = (name: T) => { + return automations[name]; }; -export const isReferentialRule = ( - rule: (typeof rules)[keyof typeof rules] -): rule is Extract => - sequentialRuleEvents.includes(rule.event as any); +export const isReferentialAutomation = ( + automation: (typeof automations)[keyof typeof automations] +): automation is Extract => + sequentialAutomationEvents.includes(automation.event as any); export const humanReadableEventBase = (event: T, community: Communities) => { - const rule = getRuleByName(event); + const automation = getAutomationByName(event); - if (typeof rule.display.base === "function") { - return rule.display.base({ community }); + if (typeof automation.display.base === "function") { + return automation.display.base({ community }); } - return rule.display.base; + return automation.display.base; }; export const humanReadableEventHydrated = ( event: T, community: Communities, options: { - rule: Rules; - config?: (typeof rules)[T]["additionalConfig"] extends undefined + automation: Automations; + config?: (typeof automations)[T]["additionalConfig"] extends undefined ? never - : z.infer>; + : z.infer>; sourceAction?: ActionInstances | null; } ) => { - const ruleConf = getRuleByName(event); - if (options.config && ruleConf.additionalConfig && ruleConf.display.hydrated) { - return ruleConf.display.hydrated({ rule: options.rule, community, config: options.config }); + const automationConf = getAutomationByName(event); + if (options.config && automationConf.additionalConfig && automationConf.display.hydrated) { + return automationConf.display.hydrated({ + automation: options.automation, + community, + config: options.config, + }); } - if (options.sourceAction && isReferentialRule(ruleConf) && ruleConf.display.hydrated) { - return ruleConf.display.hydrated({ - rule: options.rule, + if ( + options.sourceAction && + isReferentialAutomation(automationConf) && + automationConf.display.hydrated + ) { + return automationConf.display.hydrated({ + automation: options.automation, community, config: options.sourceAction, }); } - if (ruleConf.display.hydrated && !ruleConf.additionalConfig) { - return ruleConf.display.hydrated({ rule: options.rule, community, config: {} as any }); + if (automationConf.display.hydrated && !automationConf.additionalConfig) { + return automationConf.display.hydrated({ + automation: options.automation, + community, + config: {} as any, + }); } - if (typeof ruleConf.display.base === "function") { - return ruleConf.display.base({ community }); + if (typeof automationConf.display.base === "function") { + return automationConf.display.base({ community }); } - return ruleConf.display.base; + return automationConf.display.base; }; -export const humanReadableRule = ( - rule: R, +export const humanReadableAutomation = ( + automation: A, community: Communities, instanceName: string, - config?: (typeof rules)[R["event"]]["additionalConfig"] extends undefined + config?: (typeof automations)[A["event"]]["additionalConfig"] extends undefined ? never - : z.infer>, + : z.infer>, sourceAction?: ActionInstances | null ) => - `${instanceName} will run when ${humanReadableEventHydrated(rule.event, community, { rule, config, sourceAction })}`; + `${instanceName} will run when ${humanReadableEventHydrated(automation.event, community, { automation: automation, config, sourceAction })}`; diff --git a/core/actions/types.ts b/core/actions/types.ts index afa912127f..761f47169b 100644 --- a/core/actions/types.ts +++ b/core/actions/types.ts @@ -5,9 +5,9 @@ import type { ActionInstances, Action as ActionName, ActionRunsId, + Automations, Communities, CommunitiesId, - Rules, StagesId, UsersId, } from "db/public"; @@ -137,28 +137,28 @@ export const defineRun = ( export type Run = ReturnType; -export const sequentialRuleEvents = [Event.actionSucceeded, Event.actionFailed] as const; -export type SequentialRuleEvent = (typeof sequentialRuleEvents)[number]; +export const sequentialAutomationEvents = [Event.actionSucceeded, Event.actionFailed] as const; +export type SequentialAutomationEvent = (typeof sequentialAutomationEvents)[number]; -export const isSequentialRuleEvent = (event: Event): event is SequentialRuleEvent => - sequentialRuleEvents.includes(event as any); +export const isSequentialAutomationEvent = (event: Event): event is SequentialAutomationEvent => + sequentialAutomationEvents.includes(event as any); -export const scheduableRuleEvents = [ +export const schedulableAutomationEvents = [ Event.pubInStageForDuration, Event.actionFailed, Event.actionSucceeded, ] as const; -export type ScheduableRuleEvent = (typeof scheduableRuleEvents)[number]; +export type SchedulableAutomationEvent = (typeof schedulableAutomationEvents)[number]; -export const isScheduableRuleEvent = (event: Event): event is ScheduableRuleEvent => - scheduableRuleEvents.includes(event as any); +export const isSchedulableAutomationEvent = (event: Event): event is SchedulableAutomationEvent => + schedulableAutomationEvents.includes(event as any); -export type EventRuleOptionsBase< +export type EventAutomationOptionsBase< E extends Event, AC extends Record | undefined = undefined, > = { event: E; - canBeRunAfterAddingRule?: boolean; + canBeRunAfterAddingAutomation?: boolean; additionalConfig?: AC extends Record ? z.ZodType : undefined; /** * The display name options for this event @@ -166,31 +166,34 @@ export type EventRuleOptionsBase< display: { icon: (typeof Icons)[keyof typeof Icons]; /** - * The base display name for this rule, shown e.g. when selecting the event for a rule + * The base display name for this automation, shown e.g. when selecting the event for a automation */ base: React.ReactNode | ((options: { community: Communities }) => React.ReactNode); /** - * String to use when viewing the rule on the stage. - * Useful if you want to show some configuration or rule-specific information + * String to use when viewing the automation on the stage. + * Useful if you want to show some configuration or automation-specific information */ hydrated?: ( options: { - rule: Rules; + automation: Automations; community: Communities; } & (AC extends Record ? { config: AC } - : E extends SequentialRuleEvent + : E extends SequentialAutomationEvent ? { config: ActionInstances } : {}) ) => React.ReactNode; }; }; -export const defineRule = | undefined = undefined>( - options: EventRuleOptionsBase +export const defineAutomation = < + E extends Event, + AC extends Record | undefined = undefined, +>( + options: EventAutomationOptionsBase ) => options; -export type { RuleConfig, RuleConfigs } from "./_lib/rules"; +export type { AutomationConfig, AutomationConfigs } from "./_lib/automations"; export type ConfigOf = T extends Action ? z.infer : never; diff --git a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts index e77bf1a65c..7381c2cd9f 100644 --- a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts +++ b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts @@ -1,16 +1,13 @@ -import type z from "zod"; - import { createNextHandler } from "@ts-rest/serverless/next"; import type { + AutomationsId, CommunitiesId, CommunityMembershipsId, PubsId, PubTypesId, - RulesId, StagesId, } from "db/public"; -import { interpolate } from "@pubpub/json-interpolate"; import { siteApi, TOTAL_PUBS_COUNT_HEADER } from "contracts"; import { ApiAccessScope, @@ -23,8 +20,6 @@ import { } from "db/public"; import { logger } from "logger"; -import { createPubProxy } from "~/actions/_lib/pubProxy"; -import { getActionByName } from "~/actions/api"; import { scheduleActionInstances } from "~/actions/api/server"; import { checkAuthorization, @@ -51,11 +46,11 @@ import { updatePub, upsertPubRelations, } from "~/lib/server"; +import { getAutomation } from "~/lib/server/automations"; import { findCommunityBySlug } from "~/lib/server/community"; import { getForm } from "~/lib/server/form"; import { validateFilter } from "~/lib/server/pub-filters-validate"; import { getPubType, getPubTypesForCommunity } from "~/lib/server/pubtype"; -import { getRule } from "~/lib/server/rules"; import { getStages } from "~/lib/server/stages"; import { getMember, getSuggestedUsers } from "~/lib/server/user"; @@ -737,12 +732,12 @@ const handler = createNextHandler( throw new NotFoundError(`Community not found`); } - const ruleId = params.ruleId as RulesId; + const automationId = params.automationId as AutomationsId; - const rule = await getRule(ruleId, community.id).executeTakeFirst(); + const automation = await getAutomation(automationId, community.id).executeTakeFirst(); - if (!rule) { - throw new NotFoundError(`Rule ${ruleId} not found`); + if (!automation) { + throw new NotFoundError(`Automation ${automationId} not found`); } if (!body) { @@ -755,9 +750,9 @@ const handler = createNextHandler( await scheduleActionInstances({ event: Event.webhook, stack: [], - stageId: rule.actionInstance.stageId, + stageId: automation.actionInstance.stageId, json: body, - config: rule.config?.actionConfig, + config: automation.config?.actionConfig, }); return { diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 2df15a5d99..19ae92d0ef 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -5,16 +5,16 @@ import { captureException } from "@sentry/nextjs"; import type { Action, ActionInstancesId, + AutomationsId, CommunitiesId, FormsId, - RulesId, StagesId, UsersId, } from "db/public"; import { Capabilities, Event, MemberRole, MembershipType, stagesIdSchema } from "db/public"; import { logger } from "logger"; -import type { CreateRuleSchema } from "./components/panel/actionsTab/StagePanelRuleForm"; +import type { CreateAutomationsSchema } from "./components/panel/actionsTab/StagePanelAutomationForm"; import { unscheduleAction } from "~/actions/_lib/scheduleActionInstance"; import { db } from "~/kysely/database"; import { isUniqueConstraintError } from "~/kysely/errors"; @@ -27,12 +27,16 @@ import { removeActionInstance, updateActionInstance, } from "~/lib/server/actions"; +import { + AutomationError, + createOrUpdateAutomationWithCycleCheck, + removeAutomation, +} from "~/lib/server/automations"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { revalidateTagsForCommunity } from "~/lib/server/cache/revalidate"; import { findCommunityBySlug } from "~/lib/server/community"; import { defineServerAction } from "~/lib/server/defineServerAction"; import { insertStageMemberships } from "~/lib/server/member"; -import { createOrUpdateRuleWithCycleCheck, removeRule, RuleError } from "~/lib/server/rules"; import { createMoveConstraint as createMoveConstraintDb, createStage as createStageDb, @@ -369,14 +373,14 @@ export const deleteAction = defineServerAction(async function deleteAction( } }); -export const addOrUpdateRule = defineServerAction(async function addOrUpdateRule({ +export const addOrUpdateAutomation = defineServerAction(async function addOrUpdateAutomation({ stageId, - ruleId, + automationId, data, }: { stageId: StagesId; - ruleId?: RulesId; - data: CreateRuleSchema; + automationId?: AutomationsId; + data: CreateAutomationsSchema; }) { const loginData = await getLoginData(); if (!loginData || !loginData.user) { @@ -394,37 +398,40 @@ export const addOrUpdateRule = defineServerAction(async function addOrUpdateRule } try { - await createOrUpdateRuleWithCycleCheck({ - ruleId, + await createOrUpdateAutomationWithCycleCheck({ + automationId, actionInstanceId: data.actionInstanceId as ActionInstancesId, event: data.event, config: { actionConfig: data.actionConfig ?? null, - ruleConfig: "ruleConfig" in data && data.ruleConfig ? data.ruleConfig : null, + automationConfig: + "automationConfig" in data && data.automationConfig + ? data.automationConfig + : null, }, sourceActionInstanceId: "sourceActionInstanceId" in data ? data.sourceActionInstanceId : undefined, }); } catch (error) { logger.error(error); - if (error instanceof RuleError) { + if (error instanceof AutomationError) { return { - title: ruleId ? "Error updating rule" : "Error creating rule", + title: automationId ? "Error updating automation" : "Error creating automation", error: error.message, cause: error, }; } return { - error: ruleId ? "Failed to update rule" : "Failed to create rule", + error: automationId ? "Failed to update automation" : "Failed to create automation", cause: error, }; } finally { } }); -export const deleteRule = defineServerAction(async function deleteRule( - ruleId: RulesId, +export const deleteAutomation = defineServerAction(async function deleteAutomation( + automationId: AutomationsId, stageId: StagesId ) { const loginData = await getLoginData(); @@ -443,30 +450,30 @@ export const deleteRule = defineServerAction(async function deleteRule( } try { - const deletedRule = await autoRevalidate( - removeRule(ruleId).qb.returningAll() + const deletedAutomation = await autoRevalidate( + removeAutomation(automationId).qb.returningAll() ).executeTakeFirstOrThrow(); - if (!deletedRule) { + if (!deletedAutomation) { return { - error: "Failed to delete rule", - cause: `Rule with id ${ruleId} not found`, + error: "Failed to delete automation", + cause: `Automation with id ${automationId} not found`, }; } - if (deletedRule.event !== Event.pubInStageForDuration) { + if (deletedAutomation.event !== Event.pubInStageForDuration) { return; } const actionInstance = await getActionInstance( - deletedRule.actionInstanceId + deletedAutomation.actionInstanceId ).executeTakeFirst(); if (!actionInstance) { // something is wrong here captureException( new Error( - `Action instance not found for rule ${ruleId} while trying to unschedule jobs` + `Action instance not found for automation ${automationId} while trying to unschedule jobs` ) ); return; @@ -474,11 +481,11 @@ export const deleteRule = defineServerAction(async function deleteRule( const pubsInStage = await getPubIdsInStage(actionInstance.stageId).executeTakeFirst(); if (!pubsInStage) { - // we don't need to unschedule any jobs, as there are no pubs this rule could have been applied to + // we don't need to unschedule any jobs, as there are no pubs this automation could have been applied to return; } - logger.debug(`Unscheduling jobs for rule ${ruleId}`); + logger.debug(`Unscheduling jobs for automation ${automationId}`); await Promise.all( pubsInStage.pubIds.map(async (pubInStageId) => unscheduleAction({ @@ -492,7 +499,7 @@ export const deleteRule = defineServerAction(async function deleteRule( } catch (error) { logger.error(error); return { - error: "Failed to delete rule", + error: "Failed to delete automation", cause: error, }; } finally { diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanel.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanel.tsx index b39d3c04ac..01f2ab3b92 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanel.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanel.tsx @@ -6,7 +6,7 @@ import { Tabs, TabsContent, TabsList } from "ui/tabs"; import { getStage } from "~/lib/db/queries"; import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug"; import { StagePanelActions } from "./actionsTab/StagePanelActions"; -import { StagePanelRules } from "./actionsTab/StagePanelRules"; +import { StagePanelAutomations } from "./actionsTab/StagePanelAutomations"; import { StagePanelMembers } from "./StagePanelMembers"; import { StagePanelOverview } from "./StagePanelOverview"; import { StagePanelPubs } from "./StagePanelPubs"; @@ -56,7 +56,7 @@ export const StagePanel = async (props: Props) => { - + diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx similarity index 56% rename from core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx rename to core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx index 4b45607f0a..9f5fc4575e 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx @@ -7,29 +7,29 @@ import type { Action, ActionInstances, ActionInstancesId, + AutomationsId, CommunitiesId, Event, - RulesId, StagesId, } from "db/public"; import { Button } from "ui/button"; import { Pencil } from "ui/icon"; import { cn } from "utils"; -import type { RuleForEvent } from "~/actions/_lib/rules"; -import type { RuleConfig } from "~/actions/types"; -import { getActionByName, getRuleByName, humanReadableEventHydrated } from "~/actions/api"; +import type { AutomationForEvent } from "~/actions/_lib/automations"; +import type { AutomationConfig } from "~/actions/types"; +import { getActionByName, getAutomationByName, humanReadableEventHydrated } from "~/actions/api"; import { useCommunity } from "~/app/components/providers/CommunityProvider"; type Props = { stageId: StagesId; communityId: CommunitiesId; - rule: { - id: RulesId; + automation: { + id: AutomationsId; event: Event; actionInstance: ActionInstances; sourceActionInstance?: ActionInstances | null; - config: RuleConfig> | null; + config: AutomationConfig> | null; createdAt: Date; updatedAt: Date; actionInstanceId: ActionInstancesId; @@ -42,16 +42,19 @@ const ActionIcon = (props: { actionName: Action; className?: string }) => { return ; }; -export const StagePanelRule = (props: Props) => { - const { rule } = props; +export const StagePanelAutomation = (props: Props) => { + const { automation } = props; - const [, setEditingRuleId] = useQueryState("rule-id", parseAsString.withDefault("new-rule")); + const [, setEditingAutomationId] = useQueryState( + "automation-id", + parseAsString.withDefault("new-automation") + ); const onEditClick = useCallback(() => { - setEditingRuleId(rule.id); - }, [rule.id, setEditingRuleId]); + setEditingAutomationId(automation.id); + }, [automation.id, setEditingAutomationId]); const community = useCommunity(); - const ruleSettings = getRuleByName(rule.event); + const automationSettings = getAutomationByName(automation.event); return (
@@ -60,34 +63,36 @@ export const StagePanelRule = (props: Props) => { When{" "} - {} - {rule.sourceActionInstance ? ( + { + + } + {automation.sourceActionInstance ? ( <> - {humanReadableEventHydrated(rule.event, community, { - rule, - config: rule.config?.ruleConfig ?? undefined, - sourceAction: rule.sourceActionInstance, + {humanReadableEventHydrated(automation.event, community, { + automation: automation, + config: automation.config?.automationConfig ?? undefined, + sourceAction: automation.sourceActionInstance, })} ) : ( - humanReadableEventHydrated(rule.event, community, { - rule, - config: rule.config?.ruleConfig ?? undefined, - sourceAction: rule.sourceActionInstance, + humanReadableEventHydrated(automation.event, community, { + automation: automation, + config: automation.config?.automationConfig ?? undefined, + sourceAction: automation.sourceActionInstance, }) )}
run{" "} - {rule.actionInstance.name} + {automation.actionInstance.name} {" "}
diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleForm.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx similarity index 78% rename from core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleForm.tsx rename to core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx index 75f405314a..e62ac8f313 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleForm.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx @@ -9,7 +9,7 @@ import { useQueryState } from "nuqs"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import type { ActionInstances, CommunitiesId, RulesId, StagesId } from "db/public"; +import type { ActionInstances, AutomationsId, CommunitiesId, StagesId } from "db/public"; import { actionInstancesIdSchema, Event } from "db/public"; import { logger } from "logger"; import { Button } from "ui/button"; @@ -27,27 +27,27 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "u import { FormSubmitButton } from "ui/submit-button"; import { cn } from "utils"; -import type { RuleConfig, RuleForEvent, Rules } from "~/actions/_lib/rules"; +import type { Automation, AutomationConfig, AutomationForEvent } from "~/actions/_lib/automations"; import type { getStageActions } from "~/lib/db/queries"; import type { AutoReturnType } from "~/lib/types"; import { ActionFormContext } from "~/actions/_lib/ActionForm"; -import { actions, getRuleByName, humanReadableEventBase, rules } from "~/actions/api"; +import { actions, automations, getAutomationByName, humanReadableEventBase } from "~/actions/api"; import { getActionFormComponent } from "~/actions/forms"; import { useCommunity } from "~/app/components/providers/CommunityProvider"; import { isClientException, useServerAction } from "~/lib/serverActions"; -import { addOrUpdateRule, deleteRule } from "../../../actions"; +import { addOrUpdateAutomation, deleteAutomation } from "../../../actions"; type Props = { stageId: StagesId; actionInstances: AutoReturnType["execute"]; communityId: CommunitiesId; - rules: { - id: RulesId; + automations: { + id: AutomationsId; event: Event; actionInstance: ActionInstances; sourceAction?: ActionInstances; - config?: RuleConfig> | null; + config?: AutomationConfig> | null; }[]; }; @@ -59,7 +59,10 @@ const ActionSelector = ({ disabledActionId, dataTestIdPrefix, }: { - fieldProps: Omit, "name">; + fieldProps: Omit< + ControllerRenderProps, + "name" + >; actionInstances: AutoReturnType["execute"]; label: string; placeholder: string; @@ -133,12 +136,12 @@ const baseSchema = z.discriminatedUnion("event", [ actionInstanceId: actionInstancesIdSchema, actionConfig: z.object({}), }), - ...Object.values(rules) + ...Object.values(automations) .filter( ( - rule - ): rule is Exclude< - Rules, + automation + ): automation is Exclude< + Automation, { event: | Event.pubEnteredStage @@ -154,13 +157,15 @@ const baseSchema = z.discriminatedUnion("event", [ Event.actionSucceeded, Event.actionFailed, Event.webhook, - ].includes(rule.event) + ].includes(automation.event) ) - .map((rule) => + .map((automation) => z.object({ - event: z.literal(rule.event), + event: z.literal(automation.event), actionInstanceId: actionInstancesIdSchema, - ruleConfig: rule.additionalConfig ? rule.additionalConfig : z.null().optional(), + automationConfig: automation.additionalConfig + ? automation.additionalConfig + : z.null().optional(), }) ), ]); @@ -175,31 +180,32 @@ const refineSchema = (schema: T) => { ctx.addIssue({ path: ["sourceActionInstanceId"], code: z.ZodIssueCode.custom, - message: "Rules may not trigger actions in a loop", + message: "Automations may not trigger actions in a loop", }); } }); }; -export type CreateRuleSchema = z.infer & { +export type CreateAutomationsSchema = z.infer & { actionConfig: Record | null; }; export const StagePanelAutomationForm = (props: Props) => { - const [currentlyEditingRuleId, setCurrentlyEditingRuleId] = useQueryState("rule-id"); - const runUpsertRule = useServerAction(addOrUpdateRule); + const [currentlyEditingAutomationId, setCurrentlyEditingAutomationId] = + useQueryState("automation-id"); + const runUpsertAutomation = useServerAction(addOrUpdateAutomation); const [isOpen, setIsOpen] = useState(false); const onSubmit = useCallback( - async (data: CreateRuleSchema) => { - const result = await runUpsertRule({ + async (data: CreateAutomationsSchema) => { + const result = await runUpsertAutomation({ stageId: props.stageId, data, - ruleId: currentlyEditingRuleId as RulesId | undefined, + automationId: currentlyEditingAutomationId as AutomationsId | undefined, }); if (!isClientException(result)) { setIsOpen(false); - setCurrentlyEditingRuleId(null); + setCurrentlyEditingAutomationId(null); setSelectedActionInstance(null); form.reset(); return; @@ -207,7 +213,7 @@ export const StagePanelAutomationForm = (props: Props) => { form.setError("root", { message: result.error }); }, - [props.stageId, runUpsertRule] + [props.stageId, runUpsertAutomation] ); const [selectedActionInstance, setSelectedActionInstance] = useState< @@ -277,7 +283,7 @@ export const StagePanelAutomationForm = (props: Props) => { return refineSchema(schemaWithAction); }, [selectedActionInstance, props.actionInstances, actionSchema]); - const form = useForm({ + const form = useForm({ resolver: zodResolver(schema), defaultValues: { actionInstanceId: undefined, @@ -300,28 +306,31 @@ export const StagePanelAutomationForm = (props: Props) => { if (!selectedActionInstanceId && !event) return { disallowedEvents: [], allowedEvents: Object.values(Event) }; - const disallowedEvents = props.rules - .filter((rule) => { + const disallowedEvents = props.automations + .filter((automation) => { // for regular events, disallow if same action+event already exists - if (rule.event !== Event.actionSucceeded && rule.event !== Event.actionFailed) { - return rule.actionInstance.id === selectedActionInstanceId; + if ( + automation.event !== Event.actionSucceeded && + automation.event !== Event.actionFailed + ) { + return automation.actionInstance.id === selectedActionInstanceId; } - // for action chaining events, allow multiple rules with different watched actions + // for action chaining events, allow multiple automations with different watched actions return ( - rule.actionInstance.id === selectedActionInstanceId && - rule.event === event && - rule.sourceAction?.id === sourceActionInstanceId + automation.actionInstance.id === selectedActionInstanceId && + automation.event === event && + automation.sourceAction?.id === sourceActionInstanceId ); }) - .map((rule) => rule.event); + .map((automation) => automation.event); const allowedEvents = Object.values(Event).filter( (event) => !disallowedEvents.includes(event) ); return { disallowedEvents, allowedEvents }; - }, [selectedActionInstanceId, event, props.rules, sourceActionInstanceId]); + }, [selectedActionInstanceId, event, props.automations, sourceActionInstanceId]); useEffect(() => { const actionInstance = @@ -337,49 +346,52 @@ export const StagePanelAutomationForm = (props: Props) => { }, [form, props.actionInstances, selectedActionInstanceId]); useEffect(() => { - const currentRule = props.rules.find((rule) => rule.id === currentlyEditingRuleId); + const currentAutomation = props.automations.find( + (automation) => automation.id === currentlyEditingAutomationId + ); - if (!currentRule) { + if (!currentAutomation) { return; } setIsOpen(true); const actionInstance = - props.actionInstances.find((action) => action.id === currentRule.actionInstance.id) ?? - null; + props.actionInstances.find( + (action) => action.id === currentAutomation.actionInstance.id + ) ?? null; setSelectedActionInstance(actionInstance); form.reset({ - actionInstanceId: currentRule.actionInstance.id, - event: currentRule.event, - actionConfig: currentRule.config?.actionConfig, - sourceActionInstanceId: currentRule.sourceAction?.id, - ruleConfig: currentRule.config?.ruleConfig, - } as CreateRuleSchema); - }, [currentlyEditingRuleId, props.actionInstances, props.rules]); + actionInstanceId: currentAutomation.actionInstance.id, + event: currentAutomation.event, + actionConfig: currentAutomation.config?.actionConfig, + sourceActionInstanceId: currentAutomation.sourceAction?.id, + automationConfig: currentAutomation.config?.automationConfig, + } as CreateAutomationsSchema); + }, [currentlyEditingAutomationId, props.actionInstances, props.automations]); const onOpenChange = useCallback( (open: boolean) => { if (!open) { form.reset(); setSelectedActionInstance(null); - setCurrentlyEditingRuleId(null); + setCurrentlyEditingAutomationId(null); } setIsOpen(open); }, [setSelectedActionInstance, setIsOpen] ); - const rule = getRuleByName(event); + const automation = getAutomationByName(event); - const runDeleteRule = useServerAction(deleteRule); + const runDeleteAutomation = useServerAction(deleteAutomation); const onDeleteClick = useCallback(async () => { - if (!currentlyEditingRuleId) { + if (!currentlyEditingAutomationId) { return; } - runDeleteRule(currentlyEditingRuleId as RulesId, props.stageId); - }, [currentlyEditingRuleId, props.stageId, runDeleteRule]); + runDeleteAutomation(currentlyEditingAutomationId as AutomationsId, props.stageId); + }, [currentlyEditingAutomationId, props.stageId, runDeleteAutomation]); const formId = useId(); @@ -391,20 +403,20 @@ export const StagePanelAutomationForm = (props: Props) => { return getActionFormComponent(selectedActionInstance.action); }, [selectedActionInstance]); - const isExistingRule = !!currentlyEditingRuleId; + const isExistingAutomation = !!currentlyEditingAutomationId; return (
- - {isExistingRule ? "Edit automation" : "Add automation"} + {isExistingAutomation ? "Edit automation" : "Add automation"} Set up an automation to run whenever a certain event is triggered. @@ -438,7 +450,7 @@ export const StagePanelAutomationForm = (props: Props) => { {field.value ? ( <> - + {humanReadableEventBase( field.value, community @@ -451,7 +463,8 @@ export const StagePanelAutomationForm = (props: Props) => { {allowedEvents.map((event) => { - const rule = getRuleByName(event); + const automation = + getAutomationByName(event); return ( { className="hover:bg-gray-100" data-testid={`event-select-item-${event}`} > - + {humanReadableEventBase( event, community @@ -553,10 +566,10 @@ export const StagePanelAutomationForm = (props: Props) => { - {currentlyEditingRuleId && ( + {currentlyEditingAutomationId && (