Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow actions to run after other action #1027

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion core/actions/_lib/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod";

import type { ActionInstances } from "db/public";
import { Event } from "db/public";

import { defineRule } from "~/actions/types";
Expand Down Expand Up @@ -37,7 +38,30 @@ export const pubEnteredStage = defineRule({
});
export type PubEnteredStage = typeof pubEnteredStage;

export type Rules = PubInStageForDuration | PubLeftStage | PubEnteredStage;
export const actionSucceeded = defineRule({
event: Event.actionSucceeded,
display: {
base: "a specific action succeeds",
withConfig: (actionInstance: ActionInstances) => `${actionInstance.name} succeeds`,
},
});
export type ActionSucceeded = typeof actionSucceeded;

export const actionFailed = defineRule({
event: Event.actionFailed,
display: {
base: "a specific action fails",
withConfig: (actionInstance) => `${actionInstance.name} fails`,
},
});
export type ActionFailed = typeof actionFailed;

export type Rules =
| PubInStageForDuration
| PubLeftStage
| PubEnteredStage
| ActionSucceeded
| ActionFailed;

export type RuleForEvent<E extends Event> = Extract<Rules, { event: E }>;

Expand Down
21 changes: 21 additions & 0 deletions core/actions/_lib/zodTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,26 @@ class StringWithTokens extends z.ZodString {
}
}

const actionInstanceShape = {
name: z.string(),
description: z.string(),
icon: z.string(),
action: z.string(),
actionInstanceId: z.string().uuid(),
};

export type ActionInstanceConfig = z.infer<z.ZodObject<typeof actionInstanceShape>>;

class ActionInstance extends z.ZodObject<typeof actionInstanceShape, "strip", z.ZodTypeAny> {
static create = () =>
new ActionInstance({
typeName: "ActionInstance" as z.ZodFirstPartyTypeKind.ZodObject,
shape: () => actionInstanceShape,
catchall: z.never(),
unknownKeys: "strip",
});
}

export const markdown = Markdown.create;
export const stringWithTokens = StringWithTokens.create;
export const actionInstance = ActionInstance.create;
25 changes: 22 additions & 3 deletions core/actions/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

import type * as z from "zod";

import type { Event } from "db/public";
import type { ActionInstances, Event } from "db/public";

import { pubEnteredStage, pubInStageForDuration, pubLeftStage } from "../_lib/rules";
import type { ReferentialRuleEvent } from "../types";
import {
actionFailed,
actionSucceeded,
pubEnteredStage,
pubInStageForDuration,
pubLeftStage,
} from "../_lib/rules";
import * as datacite from "../datacite/action";
import * as email from "../email/action";
import * as googleDriveImport from "../googleDriveImport/action";
Expand All @@ -13,6 +20,7 @@ import * as log from "../log/action";
import * as move from "../move/action";
import * as pdf from "../pdf/action";
import * as pushToV6 from "../pushToV6/action";
import { isReferentialRuleEvent, referentialRuleEvents } from "../types";

export const actions = {
[log.action.name]: log.action,
Expand Down Expand Up @@ -41,22 +49,33 @@ export const rules = {
[pubInStageForDuration.event]: pubInStageForDuration,
[pubEnteredStage.event]: pubEnteredStage,
[pubLeftStage.event]: pubLeftStage,
[actionSucceeded.event]: actionSucceeded,
[actionFailed.event]: actionFailed,
} as const;

export const getRuleByName = <T extends Event>(name: T) => {
return rules[name];
};

export const isReferentialRule = (
rule: (typeof rules)[keyof typeof rules]
): rule is Extract<typeof rule, { event: ReferentialRuleEvent }> =>
referentialRuleEvents.includes(rule.event as any);

export const humanReadableEvent = <T extends Event>(
event: T,
config?: (typeof rules)[T]["additionalConfig"] extends undefined
? never
: z.infer<NonNullable<(typeof rules)[T]["additionalConfig"]>>
: z.infer<NonNullable<(typeof rules)[T]["additionalConfig"]>>,
watchedAction?: ActionInstances | null
) => {
const rule = getRuleByName(event);
if (config && rule.additionalConfig) {
return rule.display.withConfig(config);
}
if (watchedAction && isReferentialRule(rule)) {
return rule.display.withConfig(watchedAction);
}

return rule.display.base;
};
Expand Down
17 changes: 15 additions & 2 deletions core/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import type * as z from "zod";

import type { ProcessedPub } from "contracts";
import type {
ActionInstances,
Action as ActionName,
ActionRunsId,
CommunitiesId,
Event,
PubsId,
StagesId,
} from "db/public";
import type { LastModifiedBy } from "db/types";
import type { Dependency, FieldConfig, FieldConfigItem } from "ui/auto-form";
import type * as Icons from "ui/icon";
import { Event } from "db/public";

import type { CorePubField } from "./corePubFields";
import type { ClientExceptionOptions } from "~/lib/serverActions";
Expand Down Expand Up @@ -176,6 +177,12 @@ export const defineRun = <T extends Action = Action>(

export type Run = ReturnType<typeof defineRun>;

export const referentialRuleEvents = [Event.actionSucceeded, Event.actionFailed] as const;
export type ReferentialRuleEvent = (typeof referentialRuleEvents)[number];

export const isReferentialRuleEvent = (event: Event): event is ReferentialRuleEvent =>
referentialRuleEvents.includes(event as any);

export type EventRuleOptionsBase<
E extends Event,
AC extends Record<string, any> | undefined = undefined,
Expand All @@ -195,7 +202,13 @@ export type EventRuleOptionsBase<
/**
* The display name for this event when used in a rule
*/
[K in "withConfig" as AC extends Record<string, any> ? K : never]: (options: AC) => string;
[K in "withConfig" as AC extends Record<string, any>
? K
: E extends ReferentialRuleEvent
? K
: never]: (
options: AC extends Record<string, any> ? AC : ActionInstances
) => string;
};
};

Expand Down
17 changes: 9 additions & 8 deletions core/app/c/[communitySlug]/stages/manage/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { revalidateTagsForCommunity } from "~/lib/server/cache/revalidate";
import { findCommunityBySlug } from "~/lib/server/community";
import { defineServerAction } from "~/lib/server/defineServerAction";
import { insertStageMember } from "~/lib/server/member";
import { createRule, removeRule } from "~/lib/server/rules";
import { createRule, createRuleWithCycleCheck, removeRule, RuleError } from "~/lib/server/rules";
import {
createMoveConstraint as createMoveConstraintDb,
createStage as createStageDb,
Expand Down Expand Up @@ -390,22 +390,23 @@ export const addRule = defineServerAction(async function addRule({
}

try {
await createRule({
await createRuleWithCycleCheck({
actionInstanceId: data.actionInstanceId as ActionInstancesId,
event: data.event,
config: "additionalConfiguration" in data ? data.additionalConfiguration : null,
}).executeTakeFirstOrThrow();
watchedActionId:
"watchedActionInstanceId" in data ? data.watchedActionInstanceId : undefined,
});
} catch (error) {
logger.error(error);
if (error.message?.includes("unique constraint")) {
if (error instanceof RuleError) {
return {
title: "Rule already exists",
error: `A rule for '${humanReadableEvent(data.event)}' and this action already exists. Please add another action
of the same type to this stage in order to have the same action trigger
multiple times for '${humanReadableEvent(data.event)}'.`,
title: "Error creating rule",
error: error.message,
cause: error,
};
}
console.log(error);

return {
error: "Failed to add rule",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { useCallback } from "react";

import type { Action, CommunitiesId, Event, RulesId, StagesId } from "db/public";
import type { Action, ActionInstances, CommunitiesId, Event, RulesId, StagesId } from "db/public";
import { Button } from "ui/button";
import { Trash } from "ui/icon";
import { cn } from "utils";

import type { RuleForEvent } from "~/actions/_lib/rules";
import type { RuleConfig } from "~/actions/types";
Expand All @@ -18,15 +19,15 @@ type Props = {
rule: {
id: RulesId;
event: Event;
instanceName: string;
action: Action;
actionInstance: ActionInstances;
watchedActionInstance?: ActionInstances | null;
config?: RuleConfig<RuleForEvent<Event>> | null;
};
};

const actionIcon = (actionName: Action) => {
const action = getActionByName(actionName);
return <action.icon className="inline text-sm" />;
const ActionIcon = (props: { actionName: Action; className?: string }) => {
const action = getActionByName(props.actionName);
return <action.icon className={cn("inline text-sm", props.className)} />;
};

export const StagePanelRule = (props: Props) => {
Expand All @@ -40,15 +41,33 @@ export const StagePanelRule = (props: Props) => {
<div className="w-full space-y-2 border px-3 py-2">
<div className="flex w-full items-center justify-between space-x-4 text-sm">
<div className="flex items-center gap-2 overflow-auto">
{actionIcon(rule.action)}
<span className="flex-grow-0 overflow-auto text-ellipsis">
If{" "}
<span className="italic underline decoration-dotted">
{rule.instanceName}
</span>{" "}
will run when{" "}
<span className="italic underline decoration-dotted">
{humanReadableEvent(rule.event, rule.config ?? undefined)}
{rule.watchedActionInstance ? (
<>
<ActionIcon
actionName={rule.watchedActionInstance.action}
className="mr-1 h-4 w-4 text-xs"
/>
{humanReadableEvent(
rule.event,
rule.config ?? undefined,
rule.watchedActionInstance
)}
</>
) : (
humanReadableEvent(rule.event, rule.config ?? undefined)
)}
</span>
, run{" "}
<span className="italic underline decoration-dotted">
<ActionIcon
actionName={rule.actionInstance.action}
className="mx-1 h-4 w-4 text-xs"
/>
{rule.actionInstance.name}
</span>{" "}
</span>
</div>
<div className="flex-gap-1">
Expand Down
Loading
Loading