@dxos/plugin-automation 0.8.4-staging.60fe92afc8 → 0.9.1-main.c7dcc2e112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/{AutomationArticle-GN36NUX2.mjs → AutomationArticle-CG4ZML3C.mjs} +3 -2
- package/dist/lib/neutral/{AutomationCompanion-M26WR6VP.mjs → AutomationCompanion-67LW2WZS.mjs} +12 -13
- package/dist/lib/neutral/AutomationCompanion-67LW2WZS.mjs.map +7 -0
- package/dist/lib/neutral/AutomationPlugin.mjs +1 -1
- package/dist/lib/neutral/AutomationPlugin.node.mjs +4 -4
- package/dist/lib/neutral/AutomationPlugin.node.mjs.map +3 -3
- package/dist/lib/neutral/{AutomationSettings-YXUJDRQA.mjs → AutomationSettings-2XCCFX6X.mjs} +5 -5
- package/dist/lib/neutral/{AutomationSettings-YXUJDRQA.mjs.map → AutomationSettings-2XCCFX6X.mjs.map} +3 -3
- package/dist/lib/neutral/{TriggerSettings-XCHIZPOR.mjs → TriggerSettings-ABOTKRUA.mjs} +2 -2
- package/dist/lib/neutral/{app-graph-builder-BTTHS4VK.mjs → app-graph-builder-VX54SXD6.mjs} +6 -6
- package/dist/lib/neutral/app-graph-builder-VX54SXD6.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +3 -3
- package/dist/lib/neutral/capabilities/node.mjs +1 -1
- package/dist/lib/neutral/{chunk-2JP77CMN.mjs → chunk-73DGSL37.mjs} +2 -2
- package/dist/lib/neutral/chunk-73DGSL37.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-HPUHQ3L6.mjs → chunk-77QU5RSC.mjs} +3 -3
- package/dist/lib/neutral/chunk-77QU5RSC.mjs.map +7 -0
- package/dist/lib/neutral/chunk-D4RCXOP4.mjs +13 -0
- package/dist/lib/neutral/chunk-D4RCXOP4.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-SONGOJRB.mjs → chunk-EJ5J22XS.mjs} +2 -2
- package/dist/lib/neutral/chunk-HHIFH3N3.mjs +45 -0
- package/dist/lib/neutral/chunk-HHIFH3N3.mjs.map +7 -0
- package/dist/lib/neutral/chunk-HQU5QWF4.mjs +349 -0
- package/dist/lib/neutral/chunk-HQU5QWF4.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-FE7YFBX7.mjs → chunk-LVUL7GIN.mjs} +151 -60
- package/dist/lib/neutral/chunk-LVUL7GIN.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-GARB7S5R.mjs → chunk-YHK7FWGV.mjs} +5 -3
- package/dist/lib/neutral/chunk-YHK7FWGV.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +36 -2
- package/dist/lib/neutral/components/index.mjs.map +3 -3
- package/dist/lib/neutral/containers/index.mjs +4 -4
- package/dist/lib/neutral/{create-automation-OE3TNXYE.mjs → create-automation-AGYTF62E.mjs} +5 -9
- package/dist/lib/neutral/create-automation-AGYTF62E.mjs.map +7 -0
- package/dist/lib/neutral/{create-trigger-from-template-TERHKWJM.mjs → create-trigger-from-template-TYGCSR3Z.mjs} +5 -5
- package/dist/lib/neutral/create-trigger-from-template-TYGCSR3Z.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +10 -2
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/meta.mjs +1 -1
- package/dist/lib/neutral/{navigation-resolver-I3L5FHJO.mjs → navigation-resolver-FSJNF3DQ.mjs} +3 -3
- package/dist/lib/neutral/navigation-resolver-FSJNF3DQ.mjs.map +7 -0
- package/dist/lib/neutral/operations/index.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +2 -2
- package/dist/lib/neutral/{react-surface-IBRWUKPC.mjs → react-surface-W3J3CDHA.mjs} +2 -2
- package/dist/lib/neutral/{react-surface-IBRWUKPC.mjs.map → react-surface-W3J3CDHA.mjs.map} +3 -3
- package/dist/lib/neutral/testing.mjs +1 -1
- package/dist/lib/neutral/translations.mjs +6 -2
- package/dist/lib/neutral/translations.mjs.map +3 -3
- package/dist/lib/neutral/types/index.mjs +1 -1
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +8 -13
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -1
- package/dist/types/src/capabilities/node.d.ts +5 -5
- package/dist/types/src/capabilities/node.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +2 -2
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/components/CronBuilder/CronBuilder.d.ts +16 -0
- package/dist/types/src/components/CronBuilder/CronBuilder.d.ts.map +1 -0
- package/dist/types/src/components/CronBuilder/CronBuilder.stories.d.ts +194 -0
- package/dist/types/src/components/CronBuilder/CronBuilder.stories.d.ts.map +1 -0
- package/dist/types/src/components/CronBuilder/cron.d.ts +17 -0
- package/dist/types/src/components/CronBuilder/cron.d.ts.map +1 -0
- package/dist/types/src/components/CronBuilder/cron.test.d.ts +2 -0
- package/dist/types/src/components/CronBuilder/cron.test.d.ts.map +1 -0
- package/dist/types/src/components/CronBuilder/index.d.ts +4 -0
- package/dist/types/src/components/CronBuilder/index.d.ts.map +1 -0
- package/dist/types/src/components/CronBuilder/schema.d.ts +96 -0
- package/dist/types/src/components/CronBuilder/schema.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/AutomationArticle/AutomationArticle.d.ts +6 -0
- package/dist/types/src/containers/AutomationArticle/AutomationArticle.d.ts.map +1 -1
- package/dist/types/src/containers/AutomationArticle/AutomationArticle.stories.d.ts +120 -0
- package/dist/types/src/containers/AutomationArticle/AutomationArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/AutomationCompanion/AutomationCompanion.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +28 -2
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/paths.d.ts +2 -0
- package/dist/types/src/paths.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +9 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/AutomationCapabilities.d.ts +6 -1
- package/dist/types/src/types/AutomationCapabilities.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dx.config.ts +34 -0
- package/package.json +40 -38
- package/src/AutomationPlugin.test.ts +1 -1
- package/src/AutomationPlugin.tsx +1 -1
- package/src/capabilities/app-graph-builder.ts +5 -5
- package/src/capabilities/navigation-resolver.ts +2 -2
- package/src/capabilities/react-surface.tsx +1 -1
- package/src/commands/trigger/create/queue.ts +2 -2
- package/src/commands/trigger/update/queue.ts +2 -2
- package/src/components/CreateAutomationPanel/CreateAutomationPanel.tsx +1 -1
- package/src/components/CronBuilder/CronBuilder.stories.tsx +72 -0
- package/src/components/CronBuilder/CronBuilder.tsx +94 -0
- package/src/components/CronBuilder/cron.test.ts +131 -0
- package/src/components/CronBuilder/cron.ts +138 -0
- package/src/components/CronBuilder/index.ts +7 -0
- package/src/components/CronBuilder/schema.ts +119 -0
- package/src/components/index.ts +1 -0
- package/src/containers/AutomationArticle/AutomationArticle.stories.tsx +73 -0
- package/src/containers/AutomationArticle/AutomationArticle.tsx +186 -56
- package/src/containers/AutomationCompanion/AutomationCompanion.tsx +79 -72
- package/src/containers/AutomationSettings/AutomationSettings.tsx +3 -3
- package/src/containers/TriggerSettings/TriggerSettings.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/meta.ts +2 -26
- package/src/operations/create-trigger-from-template.ts +3 -3
- package/src/paths.ts +7 -2
- package/src/translations.ts +7 -2
- package/src/types/AutomationCapabilities.ts +9 -1
- package/src/types/AutomationOperation.ts +1 -1
- package/src/types/schema.ts +1 -1
- package/dist/lib/neutral/AutomationCompanion-M26WR6VP.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-BTTHS4VK.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2JP77CMN.mjs.map +0 -7
- package/dist/lib/neutral/chunk-DUGOIM7G.mjs +0 -36
- package/dist/lib/neutral/chunk-DUGOIM7G.mjs.map +0 -7
- package/dist/lib/neutral/chunk-FE7YFBX7.mjs.map +0 -7
- package/dist/lib/neutral/chunk-GARB7S5R.mjs.map +0 -7
- package/dist/lib/neutral/chunk-HPUHQ3L6.mjs.map +0 -7
- package/dist/lib/neutral/create-automation-OE3TNXYE.mjs.map +0 -7
- package/dist/lib/neutral/create-trigger-from-template-TERHKWJM.mjs.map +0 -7
- package/dist/lib/neutral/navigation-resolver-I3L5FHJO.mjs.map +0 -7
- /package/dist/lib/neutral/{AutomationArticle-GN36NUX2.mjs.map → AutomationArticle-CG4ZML3C.mjs.map} +0 -0
- /package/dist/lib/neutral/{TriggerSettings-XCHIZPOR.mjs.map → TriggerSettings-ABOTKRUA.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-SONGOJRB.mjs.map → chunk-EJ5J22XS.mjs.map} +0 -0
|
@@ -2,36 +2,49 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
5
6
|
import * as Schema from 'effect/Schema';
|
|
6
|
-
import React, { type ReactNode, useCallback, useMemo } from 'react';
|
|
7
|
+
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
|
|
7
8
|
|
|
9
|
+
import { useProcessManagerRuntime } from '@dxos/app-framework/ui';
|
|
8
10
|
import { type AppSurface } from '@dxos/app-toolkit/ui';
|
|
9
|
-
import { Operation, Routine, Trigger } from '@dxos/compute';
|
|
11
|
+
import { Operation, Routine, ServiceResolver, Trigger, TriggerEvent } from '@dxos/compute';
|
|
12
|
+
import { Context } from '@dxos/context';
|
|
10
13
|
import { type Database, Entity, Feed, Filter, JsonSchema, Obj, Query, Ref, Scope, Type } from '@dxos/echo';
|
|
14
|
+
import { KEY_FEED_CURSOR, TriggerDispatcher } from '@dxos/functions-runtime';
|
|
15
|
+
import { FunctionsServiceClient } from '@dxos/functions-runtime/edge';
|
|
11
16
|
import { DXN } from '@dxos/keys';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
17
|
+
import { log } from '@dxos/log';
|
|
18
|
+
import { useClient } from '@dxos/react-client';
|
|
19
|
+
import { type Space, getSpace, useObject, useQuery } from '@dxos/react-client/echo';
|
|
20
|
+
import { DropdownMenu, IconButton, Input, useTranslation } from '@dxos/react-ui';
|
|
14
21
|
import {
|
|
15
22
|
Form,
|
|
16
|
-
type
|
|
23
|
+
type FormFieldRendererProps,
|
|
17
24
|
type FormFieldMap,
|
|
18
25
|
RefField,
|
|
19
26
|
SelectField,
|
|
20
27
|
Settings,
|
|
21
28
|
} from '@dxos/react-ui-form';
|
|
29
|
+
import { Accordion } from '@dxos/react-ui-list';
|
|
22
30
|
import { ParentLabelAnnotation } from '@dxos/schema';
|
|
23
31
|
|
|
24
32
|
import { meta } from '#meta';
|
|
25
33
|
import { Automation } from '#types';
|
|
26
34
|
|
|
35
|
+
import { describeCron, fromCron, toCron } from '../../components/CronBuilder/cron';
|
|
36
|
+
import { CronBuilder } from '../../components/CronBuilder/CronBuilder';
|
|
37
|
+
import { type CronSpecType, FrequencyDefaults } from '../../components/CronBuilder/schema';
|
|
38
|
+
|
|
27
39
|
const RUN_ROUTINE_DXN = 'org.dxos.function.prompt';
|
|
28
40
|
|
|
29
41
|
export type AutomationArticleProps = AppSurface.ObjectArticleProps<Automation.Automation>;
|
|
30
42
|
|
|
31
43
|
export const AutomationArticle = ({ role, subject }: AutomationArticleProps) => {
|
|
32
|
-
const { t } = useTranslation(meta.
|
|
44
|
+
const { t } = useTranslation(meta.profile.key);
|
|
33
45
|
const db = Obj.getDatabase(subject);
|
|
34
46
|
const trigger = usePrimaryTrigger(subject);
|
|
47
|
+
const space = getSpace(subject);
|
|
35
48
|
|
|
36
49
|
if (!db) {
|
|
37
50
|
return null;
|
|
@@ -48,6 +61,7 @@ export const AutomationArticle = ({ role, subject }: AutomationArticleProps) =>
|
|
|
48
61
|
<Settings.Section title={t('trigger-picker.title')} description={t('trigger-picker.description')}>
|
|
49
62
|
<Settings.Panel>
|
|
50
63
|
<TriggerSection db={db} automation={subject} trigger={trigger} />
|
|
64
|
+
{space && trigger && <TriggerTestingSection space={space} trigger={trigger} />}
|
|
51
65
|
</Settings.Panel>
|
|
52
66
|
</Settings.Section>
|
|
53
67
|
|
|
@@ -104,8 +118,8 @@ const EnabledField = ({
|
|
|
104
118
|
canEnable,
|
|
105
119
|
messageKey,
|
|
106
120
|
...props
|
|
107
|
-
}:
|
|
108
|
-
const { t } = useTranslation(meta.
|
|
121
|
+
}: FormFieldRendererProps & { canEnable: boolean; messageKey?: string }) => {
|
|
122
|
+
const { t } = useTranslation(meta.profile.key);
|
|
109
123
|
return (
|
|
110
124
|
<div className='flex items-center gap-2 pt-form-padding'>
|
|
111
125
|
<Input.Root>
|
|
@@ -121,6 +135,113 @@ const EnabledField = ({
|
|
|
121
135
|
);
|
|
122
136
|
};
|
|
123
137
|
|
|
138
|
+
//
|
|
139
|
+
// Trigger testing section
|
|
140
|
+
//
|
|
141
|
+
|
|
142
|
+
type TriggerTestingSectionProps = {
|
|
143
|
+
space: Space;
|
|
144
|
+
trigger: Trigger.Trigger;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const TESTING_ITEM = { id: 'testing' };
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Collapsed "Testing" section within the Trigger panel. Provides kind-specific manual
|
|
151
|
+
* affordances: force-run for timer triggers, cursor reset for feed triggers.
|
|
152
|
+
* Not rendered for trigger kinds with no applicable action (email, webhook, subscription).
|
|
153
|
+
*/
|
|
154
|
+
const TriggerTestingSection = ({ space, trigger }: TriggerTestingSectionProps) => {
|
|
155
|
+
const { t } = useTranslation(meta.profile.key);
|
|
156
|
+
const client = useClient();
|
|
157
|
+
const processManagerRuntime = useProcessManagerRuntime();
|
|
158
|
+
const [properties] = useObject(space.properties);
|
|
159
|
+
const computeEnvironment = properties.computeEnvironment ?? 'local';
|
|
160
|
+
const functionsServiceClient = useMemo(() => FunctionsServiceClient.fromClient(client), [client]);
|
|
161
|
+
|
|
162
|
+
const spec = Obj.getSnapshot(trigger).spec;
|
|
163
|
+
const kind = spec?.kind;
|
|
164
|
+
|
|
165
|
+
const cursor = kind === 'feed' ? Obj.getKeys(trigger, KEY_FEED_CURSOR).at(0)?.id : undefined;
|
|
166
|
+
|
|
167
|
+
const handleForceRun = useCallback(async () => {
|
|
168
|
+
if (computeEnvironment === 'disabled') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (computeEnvironment === 'local') {
|
|
172
|
+
await processManagerRuntime
|
|
173
|
+
.runPromise(
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const dispatcher = yield* TriggerDispatcher;
|
|
176
|
+
yield* dispatcher.invokeTrigger({
|
|
177
|
+
trigger,
|
|
178
|
+
event: { tick: Date.now() } satisfies TriggerEvent.TimerEvent,
|
|
179
|
+
});
|
|
180
|
+
}).pipe(Effect.provide(ServiceResolver.provide({ space: space.id }, TriggerDispatcher))),
|
|
181
|
+
)
|
|
182
|
+
.catch((error: unknown) => log.catch(error));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await functionsServiceClient
|
|
186
|
+
.forceRunCronTrigger(Context.default(), space.id, trigger.id)
|
|
187
|
+
.catch((error: unknown) => log.catch(error));
|
|
188
|
+
}, [computeEnvironment, functionsServiceClient, processManagerRuntime, space.id, trigger]);
|
|
189
|
+
|
|
190
|
+
const handleResetCursor = useCallback(async () => {
|
|
191
|
+
Obj.update(trigger, (trigger) => {
|
|
192
|
+
Obj.deleteKeys(trigger, KEY_FEED_CURSOR);
|
|
193
|
+
});
|
|
194
|
+
await space.db.flush({ indexes: true });
|
|
195
|
+
}, [space.db, trigger]);
|
|
196
|
+
|
|
197
|
+
if (kind !== 'timer' && kind !== 'feed') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Accordion.Root items={[TESTING_ITEM]}>
|
|
203
|
+
{({ items }) =>
|
|
204
|
+
items.map((item) => (
|
|
205
|
+
<Accordion.Item key={item.id} item={item}>
|
|
206
|
+
<Accordion.ItemHeader>{t('testing.title')}</Accordion.ItemHeader>
|
|
207
|
+
<Accordion.ItemBody>
|
|
208
|
+
{kind === 'timer' && (
|
|
209
|
+
<IconButton
|
|
210
|
+
icon='ph--play--regular'
|
|
211
|
+
label={t('force-run.label')}
|
|
212
|
+
disabled={computeEnvironment === 'disabled'}
|
|
213
|
+
onClick={handleForceRun}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
{kind === 'feed' && (
|
|
217
|
+
<DropdownMenu.Root>
|
|
218
|
+
<DropdownMenu.Trigger asChild>
|
|
219
|
+
<IconButton
|
|
220
|
+
icon='ph--arrow-clockwise--regular'
|
|
221
|
+
label={t('reset-cursor.label')}
|
|
222
|
+
disabled={!cursor}
|
|
223
|
+
/>
|
|
224
|
+
</DropdownMenu.Trigger>
|
|
225
|
+
<DropdownMenu.Portal>
|
|
226
|
+
<DropdownMenu.Content side='top'>
|
|
227
|
+
<DropdownMenu.Viewport>
|
|
228
|
+
<DropdownMenu.Item onClick={handleResetCursor}>
|
|
229
|
+
{t('reset-cursor-confirm.label')}
|
|
230
|
+
</DropdownMenu.Item>
|
|
231
|
+
</DropdownMenu.Viewport>
|
|
232
|
+
<DropdownMenu.Arrow />
|
|
233
|
+
</DropdownMenu.Content>
|
|
234
|
+
</DropdownMenu.Portal>
|
|
235
|
+
</DropdownMenu.Root>
|
|
236
|
+
)}
|
|
237
|
+
</Accordion.ItemBody>
|
|
238
|
+
</Accordion.Item>
|
|
239
|
+
))
|
|
240
|
+
}
|
|
241
|
+
</Accordion.Root>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
124
245
|
//
|
|
125
246
|
// Action
|
|
126
247
|
//
|
|
@@ -191,7 +312,7 @@ const ActionInputEditor = ({
|
|
|
191
312
|
operation: Operation.PersistentOperation;
|
|
192
313
|
trigger: Trigger.Trigger;
|
|
193
314
|
}) => {
|
|
194
|
-
const { t } = useTranslation(meta.
|
|
315
|
+
const { t } = useTranslation(meta.profile.key);
|
|
195
316
|
const effectSchema = useMemo(
|
|
196
317
|
() => (operation.inputSchema ? JsonSchema.toEffectSchema(operation.inputSchema) : undefined),
|
|
197
318
|
[operation.inputSchema],
|
|
@@ -217,7 +338,7 @@ const ActionInputEditor = ({
|
|
|
217
338
|
|
|
218
339
|
return (
|
|
219
340
|
<>
|
|
220
|
-
<Form.Label label={t('action-input.label')}
|
|
341
|
+
<Form.Label label={t('action-input.label')} standalone />
|
|
221
342
|
<Form.Root
|
|
222
343
|
key={operation.id}
|
|
223
344
|
schema={effectSchema}
|
|
@@ -269,11 +390,14 @@ const triggerFormValues = (spec?: Trigger.Spec): TriggerFormInput =>
|
|
|
269
390
|
? { kind: 'feed', feed: spec.feed }
|
|
270
391
|
: { kind: 'timer', cron: spec?.kind === 'timer' ? spec.cron : '' };
|
|
271
392
|
|
|
393
|
+
// Fallback cron used when no schedule has been set yet.
|
|
394
|
+
const DEFAULT_TIMER_CRON = toCron(FrequencyDefaults.daily);
|
|
395
|
+
|
|
272
396
|
// Build a trigger spec from the form's values. Returned as just the two specs we construct (not the full
|
|
273
397
|
// `Trigger.Spec` union) so the subscription spec's deep readonly query AST never enters the type and the
|
|
274
398
|
// result stays assignable to the mutable `trigger.spec`.
|
|
275
399
|
const triggerFormSpec = (values: TriggerFormInput): Trigger.TimerSpec | Trigger.FeedSpec =>
|
|
276
|
-
values.kind === 'feed' ? { kind: 'feed', feed: values.feed } : Trigger.specTimer(values.cron
|
|
400
|
+
values.kind === 'feed' ? { kind: 'feed', feed: values.feed } : Trigger.specTimer(values.cron || DEFAULT_TIMER_CRON);
|
|
277
401
|
|
|
278
402
|
export const TriggerSection = ({
|
|
279
403
|
db,
|
|
@@ -284,31 +408,22 @@ export const TriggerSection = ({
|
|
|
284
408
|
automation: Automation.Automation;
|
|
285
409
|
trigger?: Trigger.Trigger;
|
|
286
410
|
}) => {
|
|
287
|
-
const {
|
|
288
|
-
const { defaultValues, fieldMap, handleValuesChanged, handleRemove } = useTriggerForm(db, automation, trigger);
|
|
411
|
+
const { defaultValues, fieldMap, handleValuesChanged } = useTriggerForm(db, automation, trigger);
|
|
289
412
|
|
|
290
413
|
return (
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
>
|
|
301
|
-
<Form.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
</Form.Root>
|
|
305
|
-
{trigger && (
|
|
306
|
-
<Button variant='ghost' classNames='gap-1 self-start' onClick={handleRemove}>
|
|
307
|
-
<Icon icon='ph--trash--regular' size={4} />
|
|
308
|
-
<span>{t('remove-trigger.label')}</span>
|
|
309
|
-
</Button>
|
|
310
|
-
)}
|
|
311
|
-
</div>
|
|
414
|
+
<Form.Root
|
|
415
|
+
// Remount when the bound trigger changes so the uncontrolled form picks up its spec.
|
|
416
|
+
key={trigger?.id ?? 'new'}
|
|
417
|
+
schema={TriggerForm}
|
|
418
|
+
defaultValues={defaultValues}
|
|
419
|
+
db={db}
|
|
420
|
+
fieldMap={fieldMap}
|
|
421
|
+
onValuesChanged={handleValuesChanged}
|
|
422
|
+
>
|
|
423
|
+
<Form.Content>
|
|
424
|
+
<Form.FieldSet />
|
|
425
|
+
</Form.Content>
|
|
426
|
+
</Form.Root>
|
|
312
427
|
);
|
|
313
428
|
};
|
|
314
429
|
|
|
@@ -335,7 +450,7 @@ export const AutomationInlineForm = ({
|
|
|
335
450
|
automation: Automation.Automation;
|
|
336
451
|
db: Database.Database;
|
|
337
452
|
}) => {
|
|
338
|
-
const { t } = useTranslation(meta.
|
|
453
|
+
const { t } = useTranslation(meta.profile.key);
|
|
339
454
|
const trigger = usePrimaryTrigger(automation);
|
|
340
455
|
|
|
341
456
|
return (
|
|
@@ -353,6 +468,34 @@ export const AutomationInlineForm = ({
|
|
|
353
468
|
);
|
|
354
469
|
};
|
|
355
470
|
|
|
471
|
+
//
|
|
472
|
+
// Cron field
|
|
473
|
+
//
|
|
474
|
+
|
|
475
|
+
/** Renders the CronBuilder with a live cronstrue description below it. */
|
|
476
|
+
const CronField = (props: FormFieldRendererProps) => {
|
|
477
|
+
const existingCron = props.getValue() as string | undefined;
|
|
478
|
+
const initialSpec = useMemo(() => (existingCron ? fromCron(existingCron) : FrequencyDefaults.daily), []);
|
|
479
|
+
const [description, setDescription] = useState(() => describeCron(existingCron ?? toCron(initialSpec)));
|
|
480
|
+
|
|
481
|
+
const handleChange = useCallback(
|
|
482
|
+
(spec: CronSpecType, cron: string) => {
|
|
483
|
+
setDescription(describeCron(cron));
|
|
484
|
+
props.onValueChange(props.type, cron);
|
|
485
|
+
},
|
|
486
|
+
[props.type, props.onValueChange],
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<div className='flex flex-col gap-1 mbs-2'>
|
|
491
|
+
<CronBuilder value={initialSpec} onChange={handleChange} />
|
|
492
|
+
<p className='text-sm text-description pli-1 text-right'>{description}</p>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
CronField.displayName = 'AutomationArticle.CronField';
|
|
498
|
+
|
|
356
499
|
//
|
|
357
500
|
// Hooks
|
|
358
501
|
//
|
|
@@ -389,7 +532,6 @@ const useGeneralForm = (automation: Automation.Automation, trigger?: Trigger.Tri
|
|
|
389
532
|
// Read once per trigger identity; the uncontrolled form owns edits after mount.
|
|
390
533
|
const defaultValues = useMemo<Partial<GeneralFormValues>>(
|
|
391
534
|
() => ({ name: auto.name, enabled: (trigger?.enabled ?? false) && canEnable }),
|
|
392
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
393
535
|
[automation, trigger],
|
|
394
536
|
);
|
|
395
537
|
|
|
@@ -412,7 +554,7 @@ const useGeneralForm = (automation: Automation.Automation, trigger?: Trigger.Tri
|
|
|
412
554
|
|
|
413
555
|
/** Form state for the Action section: pick an operation|routine and bind it to the trigger's function/input. */
|
|
414
556
|
const useActionForm = (db: Database.Database, automation: Automation.Automation, trigger?: Trigger.Trigger) => {
|
|
415
|
-
const { t } = useTranslation(meta.
|
|
557
|
+
const { t } = useTranslation(meta.profile.key);
|
|
416
558
|
const [auto, updateAuto] = useObject(automation);
|
|
417
559
|
// Query by typename DXN so results stay untyped (`Entity.Any[]`), as RefField.useResults expects.
|
|
418
560
|
const operations = useQuery(
|
|
@@ -458,7 +600,6 @@ const useActionForm = (db: Database.Database, automation: Automation.Automation,
|
|
|
458
600
|
return { kind: 'operation', operation: auto.runnable };
|
|
459
601
|
}
|
|
460
602
|
return { kind: 'operation' };
|
|
461
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
462
603
|
}, [automation, trigger]);
|
|
463
604
|
|
|
464
605
|
const handleValuesChanged = useCallback(
|
|
@@ -503,7 +644,7 @@ const useActionForm = (db: Database.Database, automation: Automation.Automation,
|
|
|
503
644
|
|
|
504
645
|
/** Form state for the Trigger section: the timer|feed spec, plus create-on-first-edit and remove handlers. */
|
|
505
646
|
const useTriggerForm = (db: Database.Database, automation: Automation.Automation, trigger?: Trigger.Trigger) => {
|
|
506
|
-
const { t } = useTranslation(meta.
|
|
647
|
+
const { t } = useTranslation(meta.profile.key);
|
|
507
648
|
const kindOptions = useMemo(
|
|
508
649
|
() => [
|
|
509
650
|
{ value: 'timer', label: t('trigger-kind.timer.label') },
|
|
@@ -512,15 +653,14 @@ const useTriggerForm = (db: Database.Database, automation: Automation.Automation
|
|
|
512
653
|
[t],
|
|
513
654
|
);
|
|
514
655
|
const fieldMap = useMemo<FormFieldMap>(
|
|
515
|
-
() => ({
|
|
656
|
+
() => ({
|
|
657
|
+
kind: (props) => <SelectField {...props} options={kindOptions} />,
|
|
658
|
+
cron: (props) => <CronField {...props} />,
|
|
659
|
+
}),
|
|
516
660
|
[kindOptions],
|
|
517
661
|
);
|
|
518
662
|
// Read once per trigger identity (uncontrolled Form); default to an empty timer spec.
|
|
519
|
-
const defaultValues = useMemo<Partial<TriggerFormValues>>(
|
|
520
|
-
() => triggerFormValues(trigger?.spec),
|
|
521
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
522
|
-
[trigger],
|
|
523
|
-
);
|
|
663
|
+
const defaultValues = useMemo<Partial<TriggerFormValues>>(() => triggerFormValues(trigger?.spec), [trigger]);
|
|
524
664
|
|
|
525
665
|
const handleValuesChanged = useCallback(
|
|
526
666
|
(values: Partial<TriggerFormValues>) => {
|
|
@@ -543,17 +683,7 @@ const useTriggerForm = (db: Database.Database, automation: Automation.Automation
|
|
|
543
683
|
[db, automation, trigger],
|
|
544
684
|
);
|
|
545
685
|
|
|
546
|
-
|
|
547
|
-
if (!trigger) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
Obj.update(automation, (automation) => {
|
|
551
|
-
automation.triggers = automation.triggers.filter((ref) => ref.target?.id !== trigger.id);
|
|
552
|
-
});
|
|
553
|
-
db.remove(trigger);
|
|
554
|
-
}, [db, automation, trigger]);
|
|
555
|
-
|
|
556
|
-
return { defaultValues, fieldMap, handleValuesChanged, handleRemove };
|
|
686
|
+
return { defaultValues, fieldMap, handleValuesChanged };
|
|
557
687
|
};
|
|
558
688
|
|
|
559
689
|
//
|
|
@@ -7,7 +7,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
|
7
7
|
import { useCapabilities, useOperationInvoker } from '@dxos/app-framework/ui';
|
|
8
8
|
import { type Database, Filter, Obj, Type } from '@dxos/echo';
|
|
9
9
|
import { useQuery } from '@dxos/react-client/echo';
|
|
10
|
-
import { DropdownMenu, Icon, Tooltip, useTranslation } from '@dxos/react-ui';
|
|
10
|
+
import { DropdownMenu, Icon, Panel, Toolbar, Tooltip, useTranslation } from '@dxos/react-ui';
|
|
11
11
|
import { Accordion } from '@dxos/react-ui-list';
|
|
12
12
|
|
|
13
13
|
import { meta } from '#meta';
|
|
@@ -16,21 +16,22 @@ import { Automation, AutomationCapabilities, AutomationOperation } from '#types'
|
|
|
16
16
|
import { AutomationInlineForm } from '../../containers/AutomationArticle';
|
|
17
17
|
import { connectedAutomationsQuery } from '../../util/automations-for-object';
|
|
18
18
|
|
|
19
|
+
/** Association state of a row relative to the companion's object. */
|
|
20
|
+
type Status = 'associated' | 'pending' | 'detached';
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Use type.
|
|
19
23
|
export type AutomationCompanionProps = {
|
|
20
24
|
db: Database.Database;
|
|
21
25
|
object: Obj.Unknown;
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
/** Association state of a row relative to the companion's object. */
|
|
25
|
-
type Status = 'associated' | 'pending' | 'detached';
|
|
26
|
-
|
|
27
28
|
/**
|
|
28
29
|
* Renders the automations connected to an object as an accordion (see `useConnectedAutomations` for the
|
|
29
30
|
* session-stable list it draws from), flagging non-associated rows with a warning badge. New automations are
|
|
30
31
|
* created from a template dropdown (no dialog).
|
|
31
32
|
*/
|
|
32
33
|
export const AutomationCompanion = ({ db, object }: AutomationCompanionProps) => {
|
|
33
|
-
const { t } = useTranslation(meta.
|
|
34
|
+
const { t } = useTranslation(meta.profile.key);
|
|
34
35
|
const templates = useCapabilities(AutomationCapabilities.Template);
|
|
35
36
|
// Only offer templates applicable to this companion's subject (e.g. a CRM template needs a Mailbox).
|
|
36
37
|
const applicableTemplates = useMemo(
|
|
@@ -40,73 +41,79 @@ export const AutomationCompanion = ({ db, object }: AutomationCompanionProps) =>
|
|
|
40
41
|
const { items, statusFor, open, setOpen, handleCreate } = useConnectedAutomations(db, object);
|
|
41
42
|
|
|
42
43
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
44
|
+
<Panel.Root>
|
|
45
|
+
<Panel.Toolbar asChild>
|
|
46
|
+
<Toolbar.Root />
|
|
47
|
+
</Panel.Toolbar>
|
|
48
|
+
<Panel.Content>
|
|
49
|
+
{items.length === 0 ? (
|
|
50
|
+
<p className='text-sm text-description p-2'>{t('no-automations.message')}</p>
|
|
51
|
+
) : (
|
|
52
|
+
<Accordion.Root<Automation.Automation> items={items} value={open} onValueChange={setOpen}>
|
|
53
|
+
{({ items }) => (
|
|
54
|
+
<div className='flex flex-col divide-y divide-separator'>
|
|
55
|
+
{items.map((automation) => {
|
|
56
|
+
const status = statusFor(automation.id);
|
|
57
|
+
return (
|
|
58
|
+
<Accordion.Item key={automation.id} item={automation}>
|
|
59
|
+
<Accordion.ItemHeader icon='ph--lightning--regular'>
|
|
60
|
+
<span className='flex-1 truncate'>
|
|
61
|
+
{Obj.getLabel(automation) ??
|
|
62
|
+
t('object-name.placeholder', { ns: Type.getTypename(Automation.Automation) })}
|
|
63
|
+
</span>
|
|
64
|
+
{status !== 'associated' && (
|
|
65
|
+
<Tooltip.Trigger
|
|
66
|
+
asChild
|
|
67
|
+
side='bottom'
|
|
68
|
+
content={t(
|
|
69
|
+
status === 'pending'
|
|
70
|
+
? 'automation-not-associated.message'
|
|
71
|
+
: 'automation-detached.message',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-warning-text shrink-0 mr-2' />
|
|
75
|
+
</Tooltip.Trigger>
|
|
76
|
+
)}
|
|
77
|
+
</Accordion.ItemHeader>
|
|
78
|
+
<Accordion.ItemBody>
|
|
79
|
+
<AutomationInlineForm automation={automation} db={db} />
|
|
80
|
+
</Accordion.ItemBody>
|
|
81
|
+
</Accordion.Item>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</Accordion.Root>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className='border-t border-separator'>
|
|
90
|
+
<DropdownMenu.Root>
|
|
91
|
+
<DropdownMenu.Trigger asChild>
|
|
92
|
+
{/* Mirror the accordion item header layout (p-2 + icon mr-2) so the icon aligns with the rows above. */}
|
|
93
|
+
<button type='button' className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
|
|
94
|
+
<Icon icon='ph--plus--regular' size={4} classNames='mr-2 shrink-0' />
|
|
95
|
+
<span className='flex-1 truncate'>
|
|
96
|
+
{t('add-object.label', { ns: Type.getTypename(Automation.Automation) })}
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
</DropdownMenu.Trigger>
|
|
100
|
+
<DropdownMenu.Portal>
|
|
101
|
+
<DropdownMenu.Content>
|
|
102
|
+
<DropdownMenu.Viewport>
|
|
103
|
+
{applicableTemplates.map((template) => (
|
|
104
|
+
<DropdownMenu.Item key={template.id} onClick={() => void handleCreate(template.id)}>
|
|
105
|
+
<Icon icon={template.icon ?? 'ph--lightning--regular'} size={4} />
|
|
106
|
+
<span>{template.label}</span>
|
|
107
|
+
</DropdownMenu.Item>
|
|
108
|
+
))}
|
|
109
|
+
</DropdownMenu.Viewport>
|
|
110
|
+
<DropdownMenu.Arrow />
|
|
111
|
+
</DropdownMenu.Content>
|
|
112
|
+
</DropdownMenu.Portal>
|
|
113
|
+
</DropdownMenu.Root>
|
|
114
|
+
</div>
|
|
115
|
+
</Panel.Content>
|
|
116
|
+
</Panel.Root>
|
|
110
117
|
);
|
|
111
118
|
};
|
|
112
119
|
|
|
@@ -20,12 +20,12 @@ export type AutomationSettingsProps = AppSurface.SpaceArticleProps;
|
|
|
20
20
|
* automations execute.
|
|
21
21
|
*/
|
|
22
22
|
export const AutomationSettings = ({ space }: AutomationSettingsProps) => {
|
|
23
|
-
const { t } = useTranslation(meta.
|
|
23
|
+
const { t } = useTranslation(meta.profile.key);
|
|
24
24
|
return (
|
|
25
25
|
<Settings.Viewport>
|
|
26
26
|
<Settings.Section
|
|
27
|
-
title={t('automation-verbose.label', { ns: meta.
|
|
28
|
-
description={t('automation.description', { ns: meta.
|
|
27
|
+
title={t('automation-verbose.label', { ns: meta.profile.key })}
|
|
28
|
+
description={t('automation.description', { ns: meta.profile.key })}
|
|
29
29
|
>
|
|
30
30
|
<TriggersSettings space={space} />
|
|
31
31
|
</Settings.Section>
|
|
@@ -13,7 +13,7 @@ import { Settings } from '@dxos/react-ui-form';
|
|
|
13
13
|
import { meta } from '#meta';
|
|
14
14
|
|
|
15
15
|
export const TriggersSettings = ({ space }: { space: Space }) => {
|
|
16
|
-
const { t } = useTranslation(meta.
|
|
16
|
+
const { t } = useTranslation(meta.profile.key);
|
|
17
17
|
const [properties, changeProperties] = useObject(space.properties);
|
|
18
18
|
const selected = properties.computeEnvironment ?? 'local';
|
|
19
19
|
|
package/src/index.ts
CHANGED
package/src/meta.ts
CHANGED
|
@@ -3,31 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Plugin } from '@dxos/app-framework';
|
|
6
|
-
import { DXN } from '@dxos/keys';
|
|
7
|
-
import { trim } from '@dxos/util';
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
key: DXN.make('org.dxos.plugin.automation'),
|
|
11
|
-
name: 'Automation',
|
|
12
|
-
author: 'DXOS',
|
|
13
|
-
description: trim`
|
|
14
|
-
Event-driven workflow automation engine for DXOS Composer.
|
|
15
|
-
An Automation is a user-facing object pairing a trigger ("when" — a timer schedule or an
|
|
16
|
-
incoming feed) with an action ("then" — a persistent compute operation or routine), enabling
|
|
17
|
-
automated pipelines that react to changes in real time without manual intervention.
|
|
7
|
+
import config from '../dx.config';
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
companion that lists the automations connected to each object. A per-space TriggerDispatcher
|
|
21
|
-
manages execution: running locally in the browser when computeEnvironment is "local", or
|
|
22
|
-
delegating to the DXOS edge for server-side reliability; the space settings page chooses the
|
|
23
|
-
runtime location.
|
|
24
|
-
|
|
25
|
-
Operation handlers and blueprints contributed by any plugin in the application are
|
|
26
|
-
automatically merged and made available to every space's OperationRegistry, so new
|
|
27
|
-
capabilities registered by other plugins become instantly invocable from automations.
|
|
28
|
-
`,
|
|
29
|
-
icon: 'ph--atom--regular',
|
|
30
|
-
source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-automation',
|
|
31
|
-
spec: 'PLUGIN.mdl',
|
|
32
|
-
tags: ['system'],
|
|
33
|
-
});
|
|
9
|
+
export const meta = Plugin.getMetaFromConfig(config);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Effect from 'effect/Effect';
|
|
6
6
|
|
|
7
|
-
import { LayoutOperation,
|
|
7
|
+
import { LayoutOperation, Paths } from '@dxos/app-toolkit';
|
|
8
8
|
import { Operation, Script, Trigger } from '@dxos/compute';
|
|
9
9
|
import { type Feed, Filter, Obj, Ref } from '@dxos/echo';
|
|
10
10
|
import { SpaceOperation } from '@dxos/plugin-space';
|
|
@@ -60,8 +60,8 @@ const handler: Operation.WithHandler<typeof AutomationOperation.CreateTriggerFro
|
|
|
60
60
|
hidden: true,
|
|
61
61
|
});
|
|
62
62
|
yield* Operation.invoke(LayoutOperation.Open, {
|
|
63
|
-
subject: [`${getSpacePath(db.spaceId)}/settings/${meta.
|
|
64
|
-
workspace: getSpacePath(db.spaceId),
|
|
63
|
+
subject: [`${Paths.getSpacePath(db.spaceId)}/settings/${meta.profile.key}.automations`],
|
|
64
|
+
workspace: Paths.getSpacePath(db.spaceId),
|
|
65
65
|
});
|
|
66
66
|
}),
|
|
67
67
|
),
|