@hanzo/build 0.2.43 → 0.2.45
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/lib/api/auto.d.ts +15 -1
- package/lib/api/auto.js +14 -1
- package/lib/api/provider.d.ts +15 -1
- package/lib/api/provider.js +32 -1
- package/lib/automations.js +69 -21
- package/lib/settings/index.js +1 -1
- package/lib/settings/integrations.d.ts +7 -0
- package/lib/settings/integrations.js +33 -4
- package/package.json +1 -1
package/lib/api/auto.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* DELETE /v1/auto/automations/{id} delete it, its schedule and its runs
|
|
9
9
|
* POST /v1/auto/automations/{id}/run {run: {id, status}} one run, now
|
|
10
10
|
* GET /v1/auto/automations/{id}/runs {data: [run]} newest first
|
|
11
|
+
* POST /v1/auto/automations/{id}/runs/{run}/review {post} post a held post, or discard it
|
|
11
12
|
* GET /v1/auto/templates {data: [starter]} to start a new one from
|
|
12
13
|
*
|
|
13
14
|
* Each run is one Dev run in this organization, started as the person who last
|
|
@@ -38,6 +39,8 @@ export interface Draft {
|
|
|
38
39
|
schedule: Schedule;
|
|
39
40
|
permissions: Permissions;
|
|
40
41
|
notify: boolean;
|
|
42
|
+
/** The connected accounts each run's answer is posted to: x, linkedin, facebook. */
|
|
43
|
+
postTo: string[];
|
|
41
44
|
enabled: boolean;
|
|
42
45
|
}
|
|
43
46
|
export interface Last {
|
|
@@ -57,7 +60,13 @@ export interface Automation extends Draft {
|
|
|
57
60
|
created: string;
|
|
58
61
|
updated: string;
|
|
59
62
|
}
|
|
60
|
-
/**
|
|
63
|
+
/** Where one post went: its link, or the platform's reason it did not go. */
|
|
64
|
+
export interface Posted {
|
|
65
|
+
to: string;
|
|
66
|
+
url: string;
|
|
67
|
+
error: string;
|
|
68
|
+
}
|
|
69
|
+
/** One run: succeeded, failed, running, queued, review (a post waiting for you), skipped or refused. */
|
|
61
70
|
export interface Run {
|
|
62
71
|
id: string;
|
|
63
72
|
status: string;
|
|
@@ -66,6 +75,9 @@ export interface Run {
|
|
|
66
75
|
summary: string;
|
|
67
76
|
/** The Dev run it started, or null when it started none. */
|
|
68
77
|
session: string | null;
|
|
78
|
+
/** The post its agent wrote, for an automation that posts. */
|
|
79
|
+
draft: string;
|
|
80
|
+
posts: Posted[];
|
|
69
81
|
}
|
|
70
82
|
/** One automation as the platform answers it, or null for a row with no id. */
|
|
71
83
|
export declare function read(raw: unknown): Automation | null;
|
|
@@ -83,6 +95,8 @@ export declare function start(t: Target, id: string): Promise<{
|
|
|
83
95
|
status: string;
|
|
84
96
|
}>;
|
|
85
97
|
export declare function runs(t: Target, id: string): Promise<Run[]>;
|
|
98
|
+
/** Post a held post to the automation's accounts, or discard it. */
|
|
99
|
+
export declare function review(t: Target, id: string, runId: string, post: boolean): Promise<Run>;
|
|
86
100
|
/** A schedule the way a person says it. */
|
|
87
101
|
export declare function words(s: Schedule): string;
|
|
88
102
|
/** A ready-made automation to start a new one from. Its schedule names no zone: the page fills in the person's. */
|
package/lib/api/auto.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* DELETE /v1/auto/automations/{id} delete it, its schedule and its runs
|
|
9
9
|
* POST /v1/auto/automations/{id}/run {run: {id, status}} one run, now
|
|
10
10
|
* GET /v1/auto/automations/{id}/runs {data: [run]} newest first
|
|
11
|
+
* POST /v1/auto/automations/{id}/runs/{run}/review {post} post a held post, or discard it
|
|
11
12
|
* GET /v1/auto/templates {data: [starter]} to start a new one from
|
|
12
13
|
*
|
|
13
14
|
* Each run is one Dev run in this organization, started as the person who last
|
|
@@ -44,6 +45,7 @@ export function read(raw) {
|
|
|
44
45
|
schedule: schedule(o.schedule),
|
|
45
46
|
permissions: o.permissions === 'auto' ? 'auto' : 'ask',
|
|
46
47
|
notify: o.notify === true,
|
|
48
|
+
postTo: (Array.isArray(o.postTo) ? o.postTo : []).filter((x) => typeof x === 'string'),
|
|
47
49
|
enabled: o.enabled === true,
|
|
48
50
|
draft: o.draft === true,
|
|
49
51
|
next: nullable(o.next),
|
|
@@ -103,10 +105,21 @@ export async function runs(t, id) {
|
|
|
103
105
|
return list(await call(t, 'GET', `/v1/auto/automations/${seg(id)}/runs`))
|
|
104
106
|
.map((raw) => {
|
|
105
107
|
const o = obj(raw);
|
|
106
|
-
return
|
|
108
|
+
return run(o);
|
|
107
109
|
})
|
|
108
110
|
.filter((r) => r.id);
|
|
109
111
|
}
|
|
112
|
+
function run(o) {
|
|
113
|
+
const posts = (Array.isArray(o.posts) ? o.posts : [])
|
|
114
|
+
.map(obj)
|
|
115
|
+
.filter((p) => str(p.to))
|
|
116
|
+
.map((p) => ({ to: str(p.to), url: /^https:\/\//.test(str(p.url)) ? str(p.url) : '', error: str(p.error) }));
|
|
117
|
+
return { id: str(o.id), status: str(o.status), at: str(o.at), finished: nullable(o.finished), summary: str(o.summary), session: session(o.transcript), draft: str(o.draft), posts };
|
|
118
|
+
}
|
|
119
|
+
/** Post a held post to the automation's accounts, or discard it. */
|
|
120
|
+
export async function review(t, id, runId, post) {
|
|
121
|
+
return run(obj(await call(t, 'POST', `/v1/auto/automations/${seg(id)}/runs/${seg(runId)}/review`, { post })));
|
|
122
|
+
}
|
|
110
123
|
const DAYS = { mon: 'Mondays', tue: 'Tuesdays', wed: 'Wednesdays', thu: 'Thursdays', fri: 'Fridays', sat: 'Saturdays', sun: 'Sundays' };
|
|
111
124
|
/** A schedule the way a person says it. */
|
|
112
125
|
export function words(s) {
|
package/lib/api/provider.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The organization's connectors: a Slack workspace, the GitHub accounts the
|
|
3
|
-
* platform's GitHub App is installed on
|
|
3
|
+
* platform's GitHub App is installed on, and the social accounts automations
|
|
4
|
+
* post to.
|
|
4
5
|
*
|
|
5
6
|
* GET /v1/provider/{id} one connector and this org's connection to it
|
|
6
7
|
* POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
@@ -25,6 +26,19 @@ export interface Connector {
|
|
|
25
26
|
}
|
|
26
27
|
export declare function connector(raw: unknown, id?: string): Connector;
|
|
27
28
|
export declare function read(t: Target, id: string): Promise<Connector>;
|
|
29
|
+
/**
|
|
30
|
+
* The social accounts an organization connects. An automation posts text to
|
|
31
|
+
* the ones that take it; Instagram and TikTok take only images and video, and
|
|
32
|
+
* say so. `needs` is what connecting waits on while Hanzo's app with the
|
|
33
|
+
* platform is not set up.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SOCIAL: readonly {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
posts: boolean;
|
|
39
|
+
needs: string;
|
|
40
|
+
takes?: string;
|
|
41
|
+
}[];
|
|
28
42
|
/** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
|
|
29
43
|
export declare function authorize(t: Target, id: string, back?: string): Promise<string>;
|
|
30
44
|
export declare function disconnect(t: Target, id: string): Promise<void>;
|
package/lib/api/provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The organization's connectors: a Slack workspace, the GitHub accounts the
|
|
3
|
-
* platform's GitHub App is installed on
|
|
3
|
+
* platform's GitHub App is installed on, and the social accounts automations
|
|
4
|
+
* post to.
|
|
4
5
|
*
|
|
5
6
|
* GET /v1/provider/{id} one connector and this org's connection to it
|
|
6
7
|
* POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
@@ -34,7 +35,37 @@ export async function read(t, id) {
|
|
|
34
35
|
const CONSENT = {
|
|
35
36
|
slack: /^https:\/\/slack\.com\//,
|
|
36
37
|
github: /^https:\/\/github\.com\//,
|
|
38
|
+
x: /^https:\/\/(twitter|x)\.com\//,
|
|
39
|
+
linkedin: /^https:\/\/www\.linkedin\.com\//,
|
|
40
|
+
facebook: /^https:\/\/www\.facebook\.com\//,
|
|
41
|
+
instagram: /^https:\/\/www\.facebook\.com\//,
|
|
42
|
+
tiktok: /^https:\/\/www\.tiktok\.com\//,
|
|
37
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The social accounts an organization connects. An automation posts text to
|
|
46
|
+
* the ones that take it; Instagram and TikTok take only images and video, and
|
|
47
|
+
* say so. `needs` is what connecting waits on while Hanzo's app with the
|
|
48
|
+
* platform is not set up.
|
|
49
|
+
*/
|
|
50
|
+
export const SOCIAL = [
|
|
51
|
+
{ id: 'x', name: 'X', posts: true, needs: 'X posting needs Hanzo’s X developer app with paid API access; it is not set up yet.' },
|
|
52
|
+
{ id: 'linkedin', name: 'LinkedIn', posts: true, needs: 'LinkedIn posting needs Hanzo’s LinkedIn app with Share on LinkedIn; it is not set up yet.' },
|
|
53
|
+
{ id: 'facebook', name: 'Facebook', posts: true, needs: 'Facebook Page posting needs Hanzo’s Meta app, approved by Meta App Review to publish to Pages; it is not set up yet.' },
|
|
54
|
+
{
|
|
55
|
+
id: 'instagram',
|
|
56
|
+
name: 'Instagram',
|
|
57
|
+
posts: false,
|
|
58
|
+
needs: 'Instagram posting needs Hanzo’s Meta app, approved by Meta App Review; it is not set up yet.',
|
|
59
|
+
takes: 'Instagram takes photos and reels, not text, so automations do not post to it.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'tiktok',
|
|
63
|
+
name: 'TikTok',
|
|
64
|
+
posts: false,
|
|
65
|
+
needs: 'TikTok posting needs TikTok’s audit of Hanzo’s app; it is not set up yet.',
|
|
66
|
+
takes: 'TikTok takes videos and photos, not text, so automations do not post to it.',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
38
69
|
/** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
|
|
39
70
|
export async function authorize(t, id, back) {
|
|
40
71
|
const url = str(obj(await call(t, 'POST', `/v1/provider/${seg(id)}/connect`, back ? { return: back } : {})).authorizeUrl);
|
package/lib/automations.js
CHANGED
|
@@ -20,7 +20,11 @@ import { ChipSelect } from '@hanzo/ui/product';
|
|
|
20
20
|
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
21
21
|
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
22
22
|
import { ago } from './ago.js';
|
|
23
|
-
import { automation, automations, changes, create, remove, runs, save, start, starters, words } from './api/auto.js';
|
|
23
|
+
import { automation, automations, changes, create, remove, review, runs, save, start, starters, words } from './api/auto.js';
|
|
24
|
+
import { current, subscriptions } from './api/billing.js';
|
|
25
|
+
import { SOCIAL } from './api/provider.js';
|
|
26
|
+
import { Out } from './out.js';
|
|
27
|
+
import { useSocial } from './settings/integrations.js';
|
|
24
28
|
import { Refusal } from './api/call.js';
|
|
25
29
|
import { ENSO, limits as readLimits, models } from './api/models.js';
|
|
26
30
|
import { Choice, Confirm, Field, Line, Sheet } from './customize/ui.js';
|
|
@@ -57,8 +61,17 @@ const PERMISSIONS = [
|
|
|
57
61
|
{ id: 'auto', label: 'Act on its own' },
|
|
58
62
|
];
|
|
59
63
|
/** A run's status as the rail's dot draws it. */
|
|
60
|
-
const DOT = { succeeded: 'done', failed: 'error', running: 'running', queued: 'running', skipped: 'stopped', refused: 'stopped' };
|
|
61
|
-
const WORD = {
|
|
64
|
+
const DOT = { succeeded: 'done', failed: 'error', running: 'running', queued: 'running', review: 'paused', skipped: 'stopped', refused: 'stopped' };
|
|
65
|
+
const WORD = {
|
|
66
|
+
succeeded: 'Succeeded',
|
|
67
|
+
failed: 'Failed',
|
|
68
|
+
running: 'Running',
|
|
69
|
+
queued: 'Queued',
|
|
70
|
+
review: 'Waiting for your review',
|
|
71
|
+
skipped: 'Skipped',
|
|
72
|
+
refused: 'Refused',
|
|
73
|
+
};
|
|
74
|
+
const NAME = Object.fromEntries(SOCIAL.map((p) => [p.id, p.name]));
|
|
62
75
|
const live = (s) => s === 'running' || s === 'queued';
|
|
63
76
|
/** The schedule a kind starts from, keeping whatever of `was` still applies. */
|
|
64
77
|
function shaped(kind, was) {
|
|
@@ -107,6 +120,7 @@ const drafted = (a) => ({
|
|
|
107
120
|
schedule: a.schedule,
|
|
108
121
|
permissions: a.permissions,
|
|
109
122
|
notify: a.notify,
|
|
123
|
+
postTo: a.postTo,
|
|
110
124
|
enabled: a.draft ? true : a.enabled,
|
|
111
125
|
});
|
|
112
126
|
/** `next`, keeping each field of `cur` the person changed from `was` since. */
|
|
@@ -118,9 +132,9 @@ function rebase(cur, was, next) {
|
|
|
118
132
|
}
|
|
119
133
|
return out;
|
|
120
134
|
}
|
|
121
|
-
/** A run refused for want of credit. */
|
|
122
|
-
const
|
|
123
|
-
const
|
|
135
|
+
/** A run the platform refused for want of a plan: a free org with no credit for the run fee. */
|
|
136
|
+
const unplanned = (e) => e instanceof Refusal && e.status === 402;
|
|
137
|
+
const PLAN = 'Automations run on a paid plan; on Free each run needs $1 of credit, and this organization has none. Upgrade to run it.';
|
|
124
138
|
export function Automations() {
|
|
125
139
|
const host = useHost();
|
|
126
140
|
const t = useTarget();
|
|
@@ -146,12 +160,12 @@ export function Automations() {
|
|
|
146
160
|
/** One automation in the list: its name opens it, and its switch arms it. */
|
|
147
161
|
function Item({ a, onOpen, onSwitch }) {
|
|
148
162
|
const about = useId();
|
|
149
|
-
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${a.name}`, "aria-describedby": about, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, minW:
|
|
163
|
+
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${a.name}`, "aria-describedby": about, onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Workflow, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, minW: 120, style: { textAlign: 'left' }, children: a.name }), _jsxs(XStack, { id: about, items: "center", gap: "$3", flexWrap: "wrap", rowGap: "$1", maxW: "100%", children: [a.draft ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: "Draft" })) : null, _jsx(SizableText, { size: "$1", color: "$soft", width: 220, numberOfLines: 1, style: { textAlign: 'left' }, children: a.draft ? 'Needs instructions' : words(a.schedule) }), _jsxs(XStack, { width: 150, items: "center", gap: "$2", children: [a.last ? _jsx(StatusDot, { status: DOT[a.last.status] ?? 'idle' }) : null, _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: a.last ? `${WORD[a.last.status] ?? a.last.status} · ${ago(a.last.at)}` : 'Never run' })] })] })] }), _jsx(XStack, { width: 56, justify: "flex-end", children: _jsx(Switch, { checked: a.enabled, disabled: a.draft, onCheckedChange: onSwitch, "aria-label": `${a.name} on` }) })] }));
|
|
150
164
|
}
|
|
151
165
|
function Empty({ says, children }) {
|
|
152
166
|
return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$4", px: "$4", py: "$8", gap: "$3", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: says }), children] }));
|
|
153
167
|
}
|
|
154
|
-
const BLANK = () => ({ name: '', instructions: '', model: null, schedule: { kind: 'manual' }, permissions: 'ask', notify: false, enabled: true });
|
|
168
|
+
const BLANK = () => ({ name: '', instructions: '', model: null, schedule: { kind: 'manual' }, permissions: 'ask', notify: false, postTo: [], enabled: true });
|
|
155
169
|
/** One automation: what it does and when, its switch, Run now, and its runs. `new` writes one. */
|
|
156
170
|
export function Editor({ id }) {
|
|
157
171
|
const host = useHost();
|
|
@@ -172,8 +186,13 @@ export function Editor({ id }) {
|
|
|
172
186
|
const [busy, setBusy] = useState('');
|
|
173
187
|
const [note, setNote] = useState('');
|
|
174
188
|
const [asking, setAsking] = useState(false);
|
|
175
|
-
|
|
176
|
-
const [
|
|
189
|
+
// Where a move away from unsaved work was headed, while it asks first; '' for nowhere.
|
|
190
|
+
const [leaving, setLeaving] = useState('');
|
|
191
|
+
const [upgrade, setUpgrade] = useState(false);
|
|
192
|
+
const social = useSocial(signed, SOCIAL.filter((p) => p.posts).map((p) => p.id));
|
|
193
|
+
const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
|
|
194
|
+
// Free is what the platform says when it names no live subscription; a read that failed says nothing.
|
|
195
|
+
const free = !subs.loading && !subs.error && !current(subs.value);
|
|
177
196
|
const nameRef = useRef(null);
|
|
178
197
|
const asksRef = useRef(null);
|
|
179
198
|
const offered = useRead(signed && fresh ? () => starters(t) : null, [], [t, signed, fresh]);
|
|
@@ -216,7 +235,7 @@ export function Editor({ id }) {
|
|
|
216
235
|
// An edit answers what the last line asked for, so the line goes with it.
|
|
217
236
|
const set = (p) => {
|
|
218
237
|
setNote('');
|
|
219
|
-
|
|
238
|
+
setUpgrade(false);
|
|
220
239
|
setD((x) => ({ ...x, ...p }));
|
|
221
240
|
};
|
|
222
241
|
const setWhen = (p) => set({ schedule: { ...d.schedule, ...p } });
|
|
@@ -238,15 +257,15 @@ export function Editor({ id }) {
|
|
|
238
257
|
const act = async (what, work) => {
|
|
239
258
|
setBusy(what);
|
|
240
259
|
setNote('');
|
|
241
|
-
|
|
260
|
+
setUpgrade(false);
|
|
242
261
|
try {
|
|
243
262
|
await work();
|
|
244
263
|
}
|
|
245
264
|
catch (e) {
|
|
246
|
-
// Wanting
|
|
247
|
-
const run = what === 'run' &&
|
|
248
|
-
setNote(run ?
|
|
249
|
-
|
|
265
|
+
// Wanting a plan is said in these words only where a run was asked for.
|
|
266
|
+
const run = what === 'run' && unplanned(e);
|
|
267
|
+
setNote(run ? PLAN : e.message);
|
|
268
|
+
setUpgrade(run);
|
|
250
269
|
}
|
|
251
270
|
finally {
|
|
252
271
|
setBusy('');
|
|
@@ -300,8 +319,17 @@ export function Editor({ id }) {
|
|
|
300
319
|
setNote('Not started: the previous run is still going.');
|
|
301
320
|
});
|
|
302
321
|
const leave = (how) => host.go(path({ kind: 'screen', screen: 'automations' }), how);
|
|
303
|
-
//
|
|
304
|
-
const
|
|
322
|
+
// A held post is answered once; its buttons go when it is.
|
|
323
|
+
const [answered, setAnswered] = useState(() => new Set());
|
|
324
|
+
const answer = (r, post) => act('review', async () => {
|
|
325
|
+
await review(t, id, r.id, post);
|
|
326
|
+
setAnswered((x) => new Set(x).add(r.id));
|
|
327
|
+
setBeat((n) => n + 1);
|
|
328
|
+
setTick((n) => n + 1);
|
|
329
|
+
});
|
|
330
|
+
// Unsaved work is not left without a word, wherever the move goes.
|
|
331
|
+
const away = (to) => (dirty ? setLeaving(to) : host.go(to));
|
|
332
|
+
const back = () => away(path({ kind: 'screen', screen: 'automations' }));
|
|
305
333
|
const begin = (x) => {
|
|
306
334
|
const tz = x.schedule.kind === 'manual' ? undefined : zone();
|
|
307
335
|
set({ name: x.name, instructions: x.instructions, schedule: tz ? { ...x.schedule, tz } : { kind: 'manual' } });
|
|
@@ -314,6 +342,8 @@ export function Editor({ id }) {
|
|
|
314
342
|
if (!ready)
|
|
315
343
|
return _jsx(Page, { title: "Automation", onBack: () => leave(), says: "Reading the automation\u2026" });
|
|
316
344
|
const s = d.schedule;
|
|
345
|
+
// An automation that posts speaks for the organization: only an admin changes it.
|
|
346
|
+
const locked = !host.admin && Boolean(a?.postTo.length);
|
|
317
347
|
// A new automation or a draft has nothing to switch on until it says what to do.
|
|
318
348
|
const can = switchesAlone || Boolean(d.instructions.trim());
|
|
319
349
|
const on = switchesAlone ? Boolean(a?.enabled) : can && d.enabled;
|
|
@@ -324,14 +354,32 @@ export function Editor({ id }) {
|
|
|
324
354
|
setBase(drafted(a));
|
|
325
355
|
setD(drafted(a));
|
|
326
356
|
setNote('');
|
|
327
|
-
}, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || !(fresh || drafty || dirty), onPress: () => complete() && void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), note ? (_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", pt: "$2", children: [_jsx(Line, { children: note }),
|
|
357
|
+
}, children: "Discard changes" })) : null, _jsx(Button, { size: "sm", disabled: Boolean(busy) || locked || !(fresh || drafty || dirty), onPress: () => complete() && void keep(), children: busy === 'save' ? 'Saving…' : fresh ? 'Create' : 'Save' })] }), note ? (_jsxs(XStack, { width: "100%", maxW: 760, mx: "auto", items: "center", gap: "$3", pt: "$2", children: [_jsx(Line, { children: note }), upgrade ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: "See plans" })) : null] })) : null] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$5", children: [_jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$5", children: [locked ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: "This automation posts to the organization\u2019s accounts, so only an org admin changes it." })) : null, free ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", rowGap: "$2", children: [_jsx(SizableText, { flex: 1, minW: 200, size: "$1", color: "$soft", children: "Paid plans include automation runs. On the Free plan each run needs $1 of credit." }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => away(path({ kind: 'screen', screen: 'plans' })), children: "See plans" })] })) : null, _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", numberOfLines: 1, children: fresh ? 'New automation' : a?.name }), _jsx(SizableText, { size: "$2", color: "$soft", children: fresh
|
|
328
358
|
? 'Name it, say what it should do, and when. It runs as you, in this organization.'
|
|
329
359
|
: a?.draft
|
|
330
360
|
? 'A draft: say what it should do and save it, and it runs.'
|
|
331
|
-
: [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), fresh && offered.value.length && !d.instructions.trim() ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Start from" }), offered.value.map((x) => (_jsx(Button, { size: "sm", variant: "outline", onPress: () => begin(x), "aria-label": `Start from ${x.name}`, children: x.name }, x.key)))] })) : null, _jsx(Field, { label: "Name", children: _jsx(Input, { ref: nameRef, value: d.name, onChangeText: (v) => set({ name: v }), onSubmitEditing: () => asksRef.current?.focus(), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh }) }), _jsx(Field, { label: "Instructions (required)", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { ref: asksRef, value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "What should it do each run? For example: summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: shaped(k.id, s) }), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(
|
|
361
|
+
: [words(a.schedule), a.next && a.enabled ? `next ${local(a.next)}` : a.schedule.kind === 'manual' ? '' : 'off'].filter(Boolean).join(' · ') })] }), fresh && offered.value.length && !d.instructions.trim() ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Start from" }), offered.value.map((x) => (_jsx(Button, { size: "sm", variant: "outline", onPress: () => begin(x), "aria-label": `Start from ${x.name}`, children: x.name }, x.key)))] })) : null, _jsx(Field, { label: "Name", children: _jsx(Input, { ref: nameRef, value: d.name, onChangeText: (v) => set({ name: v }), onSubmitEditing: () => asksRef.current?.focus(), placeholder: "Morning briefing", "aria-label": "Automation name", autoFocus: fresh, disabled: locked }) }), _jsx(Field, { label: "Instructions (required)", hint: "What the agent does each run. It works in a Dev sandbox with this organization's connectors, MCP servers and skills.", children: _jsx(Textarea, { ref: asksRef, disabled: locked, value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "What should it do each run? For example: summarize what needs my attention today across calendar, email and messages.", "aria-label": "Instructions", rows: 7 }) }), _jsx(Field, { label: "When it runs", hint: s.kind === 'cron' ? 'Five fields: minute, hour, day of month, month, day of week.' : undefined, children: _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", items: "center", children: [_jsx(ChipSelect, { name: "When it runs", label: KINDS.find((k) => k.id === s.kind).label, chosen: KINDS.find((k) => k.id === s.kind), items: KINDS, onChange: (k) => set({ schedule: shaped(k.id, s) }), placement: "bottom-start", width: 200 }), s.kind === 'weekly' ? _jsx(ChipSelect, { name: "Day", label: DAYS.find((x) => x.id === s.day)?.label ?? 'Monday', chosen: DAYS.find((x) => x.id === s.day), items: DAYS, onChange: (x) => setWhen({ day: x.id }), placement: "bottom-start", width: 180 }) : null, s.kind === 'daily' || s.kind === 'weekdays' || s.kind === 'weekly' ? (_jsx(YStack, { width: 110, children: _jsx(Input, { value: s.at ?? '', onChangeText: (v) => setWhen({ at: v.trim() }), placeholder: "09:00", "aria-label": "Time of day", inputMode: "text" }) })) : null, s.kind === 'hourly' ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "at minute" }), _jsx(YStack, { width: 70, children: _jsx(Input, { value: (s.at ?? '00:00').slice(3), onChangeText: (v) => setWhen({ at: `00:${v.replace(/\D/g, '').slice(0, 2)}` }), placeholder: "00", "aria-label": "Minute", inputMode: "numeric" }) })] })) : null, s.kind === 'cron' ? (_jsx(YStack, { width: 200, children: _jsx(Input, { value: s.cron ?? '', onChangeText: (v) => setWhen({ cron: v }), placeholder: "0 9 * * 1-5", "aria-label": "Cron", autoCapitalize: "none" }) })) : null, s.kind !== 'manual' ? _jsx(ChipSelect, { name: "Time zone", label: s.tz || 'UTC', chosen: { id: s.tz || 'UTC', label: s.tz || 'UTC' }, items: zones, onChange: (z) => setWhen({ tz: z.id }), placeholder: "Search zones\u2026", placement: "bottom-start", width: 260 }) : null] }) }), _jsx(Field, { label: "Model", children: _jsx(XStack, { children: _jsx(ModelPicker, { size: "sm", name: "Model", models: catalog.value, scope: "chat", limits: limits, value: d.model ?? ENSO, onChange: (id) => set({ model: id === ENSO ? null : id }), loading: catalog.loading, error: catalog.error?.message ?? null }) }) }), _jsx(Field, { label: "Permissions", hint: d.permissions === 'auto' ? 'Works and uses connectors without stopping.' : 'Reads and researches, changes nothing, and ends with the actions it would take for you to approve.', children: _jsx(Choice, { label: "Permissions", value: d.permissions, options: PERMISSIONS, onChange: (p) => set({ permissions: p }) }) }), _jsxs(Field, { label: "Post to", hint: !host.admin
|
|
362
|
+
? 'An org admin chooses where automations post: the accounts are the organization’s.'
|
|
363
|
+
: !d.postTo.length
|
|
364
|
+
? 'Turn an account on and each run’s answer is a post for it.'
|
|
365
|
+
: d.permissions === 'auto'
|
|
366
|
+
? 'Each run posts what the agent writes, as written.'
|
|
367
|
+
: 'Each run holds what the agent writes for you to post or discard.', children: [_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: SOCIAL.filter((p) => p.posts).map((p, i) => {
|
|
368
|
+
const c = social.value[p.id];
|
|
369
|
+
const on = d.postTo.includes(p.id);
|
|
370
|
+
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: social.error
|
|
371
|
+
? social.error.message
|
|
372
|
+
: !c
|
|
373
|
+
? 'Reading…'
|
|
374
|
+
: c.connected
|
|
375
|
+
? `Connected${c.account ? ` as ${c.account}` : ''}`
|
|
376
|
+
: `${c.available ? 'Not connected: an org admin connects it in Settings → Integrations.' : p.needs} ${on || d.permissions === 'ask'
|
|
377
|
+
? 'Runs hold the post for you to read; posting it fails until it is connected.'
|
|
378
|
+
: 'Acting on its own, it posts only to a connected account.'}` })] }), _jsx(Switch, { checked: on, disabled: !host.admin || (!on && !c?.connected && d.permissions === 'auto'), onCheckedChange: (v) => set({ postTo: v ? [...d.postTo, p.id].sort() : d.postTo.filter((x) => x !== p.id) }), "aria-label": `Post to ${p.name}` })] }, p.id));
|
|
379
|
+
}) }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => away(path({ kind: 'settings', section: 'integrations' })), children: "Connect accounts" }) })] }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Email me when a run ends" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "One line on how it went, with a link to the run." })] }), _jsx(Switch, { checked: d.notify, onCheckedChange: (v) => set({ notify: v }), "aria-label": "Email me when a run ends" })] }), dirty && wrong && note !== wrong ? _jsx(Line, { children: wrong }) : null, a ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => setAsking(true), children: "Delete" }) })) : null, a ? (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Runs" }), history.error && !history.value.length ? (_jsx(Line, { children: history.error.message })) : !history.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: history.loading ? 'Reading its runs…' : a.draft ? 'It runs once its instructions are saved.' : 'No runs yet. Run now starts one.' })) : (_jsx(YStack, { role: "list", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", "aria-label": "Run history", children: history.value.map((r, i) => (_jsxs(XStack, { role: "listitem", items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(YStack, { pt: "$1", children: _jsx(StatusDot, { status: DOT[r.status] ?? 'idle' }) }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: `${WORD[r.status] ?? r.status} · ${local(r.at)}` }), _jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: r.summary || (live(r.status) ? 'Working…' : '') }), r.draft ? (_jsx(YStack, { mt: "$1.5", px: "$3", py: "$2", rounded: "$2", bg: "$raised", role: "group", "aria-label": "The post", children: _jsx(SizableText, { size: "$2", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: r.draft }) })) : null, r.posts.map((x) => (_jsxs(XStack, { items: "center", gap: "$2", pt: "$1", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: NAME[x.to] ?? x.to }), x.url ? (_jsx(Out, { href: x.url, label: `Open the ${NAME[x.to] ?? x.to} post`, children: _jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: "Posted \u00B7 open" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: x.error }))] }, x.to))), r.status === 'review' && host.admin && !answered.has(r.id) ? (_jsxs(XStack, { gap: "$2", pt: "$2", children: [_jsx(Button, { size: "sm", disabled: Boolean(busy), onPress: () => void answer(r, true), "aria-label": `Post the post of ${local(r.at)}`, children: "Post it" }), _jsx(Button, { size: "sm", variant: "ghost", disabled: Boolean(busy), onPress: () => void answer(r, false), "aria-label": `Discard the post of ${local(r.at)}`, children: "Discard" })] })) : r.status === 'review' && !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", pt: "$1", children: "An org admin posts or discards it." })) : null] }), r.session ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go(r.session), "aria-label": `Open the Dev run of ${local(r.at)}`, children: "Open run" })) : null] }, r.id))) }))] })) : null] }), a ? (_jsx(Confirm, { what: a.name, says: "It stops running, and its schedule and its runs go with it. A run going now is stopped.", open: asking, onOpenChange: setAsking, onYes: async () => {
|
|
332
380
|
await remove(t, a.id);
|
|
333
381
|
leave({ replace: true });
|
|
334
|
-
} })) : null, _jsxs(Sheet, { title: "Leave without saving?", open: leaving, onOpenChange: setLeaving, width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: fresh ? 'This automation is not created yet.' : 'Your changes to this automation are not saved.' }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(
|
|
382
|
+
} })) : null, _jsxs(Sheet, { title: "Leave without saving?", open: Boolean(leaving), onOpenChange: (o) => !o && setLeaving(''), width: 420, children: [_jsx(SizableText, { size: "$2", color: "$soft", children: fresh ? 'This automation is not created yet.' : 'Your changes to this automation are not saved.' }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(''), children: "Keep editing" }), _jsx(Button, { size: "sm", variant: "destructive", onPress: () => host.go(leaving), children: "Leave" })] })] })] })] }));
|
|
335
383
|
}
|
|
336
384
|
function Page({ title, says, onBack }) {
|
|
337
385
|
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 760, mx: "auto", gap: "$4", children: [_jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: onBack, "aria-label": "All automations", children: [_jsx(ChevronLeft, { size: 14 }), "Automations"] }) }), _jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }) }));
|
package/lib/settings/index.js
CHANGED
|
@@ -16,6 +16,6 @@ export function Settings({ section }) {
|
|
|
16
16
|
const go = (id) => host.go(path({ kind: 'settings', section: id }));
|
|
17
17
|
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [_jsxs(YStack, { role: "navigation", "aria-label": "Settings", width: 220, shrink: 0, borderRightWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: "Settings" }), GROUPS.map((g) => {
|
|
18
18
|
const list = ENTRIES.filter((e) => e.group === g);
|
|
19
|
-
return (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g }), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
|
|
19
|
+
return (_jsxs(YStack, { gap: "$0.5", children: [g === 'Settings' ? null : (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g })), list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
|
|
20
20
|
})] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
|
|
21
21
|
}
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
import { type Connector } from '../api/provider.ts';
|
|
1
2
|
export declare function Integrations(): import("react").JSX.Element;
|
|
3
|
+
/**
|
|
4
|
+
* The named social connectors, by id: whether each can be connected here and
|
|
5
|
+
* whether it is. One that could not be read is left out, and its row reads on
|
|
6
|
+
* as "Reading…" rather than taking the others with it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useSocial(signed: boolean, ids: readonly string[]): import("../data.ts").Read<Record<string, Connector>>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Integrations: your own GitHub connection, the GitHub accounts the
|
|
4
|
-
* organization has installed the platform's App on,
|
|
4
|
+
* organization has installed the platform's App on, its Slack workspace, and
|
|
5
|
+
* the social accounts its automations post to.
|
|
5
6
|
*
|
|
6
7
|
* Connecting leaves this page once, for GitHub's or Slack's own consent screen,
|
|
7
8
|
* and the platform's callback brings the person back here with the answer
|
|
@@ -10,11 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
11
|
* the platform's rule.
|
|
11
12
|
*/
|
|
12
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
13
|
-
import { Github, Hash, Lock, Slack } from '@hanzogui/lucide-icons-2';
|
|
14
|
+
import { Github, Hash, Lock, Megaphone, Slack } from '@hanzogui/lucide-icons-2';
|
|
14
15
|
import { Button } from '@hanzo/ui';
|
|
15
16
|
import { useState } from 'react';
|
|
16
17
|
import { connect, connection, disconnect as unlinkGithub, installations } from '../api/github.js';
|
|
17
|
-
import { authorize, channels, disconnect, read } from '../api/provider.js';
|
|
18
|
+
import { authorize, channels, disconnect, read, SOCIAL } from '../api/provider.js';
|
|
18
19
|
import { here, useBack } from '../back.js';
|
|
19
20
|
import { useRead } from '../data.js';
|
|
20
21
|
import { useHost, useTarget } from '../host.js';
|
|
@@ -28,12 +29,14 @@ export function Integrations() {
|
|
|
28
29
|
const slack = useRead(signed ? () => read(t, 'slack') : null, null, [t, signed]);
|
|
29
30
|
const joined = slack.value?.connected ?? false;
|
|
30
31
|
const rooms = useRead(signed && joined ? () => channels(t) : null, { channels: [], next: '' }, [t, signed, joined]);
|
|
32
|
+
const social = useSocial(signed, SOCIAL.map((p) => p.id));
|
|
31
33
|
const [working, setWorking] = useState(false);
|
|
32
34
|
const [note, setNote] = useState('');
|
|
33
35
|
useBack(t, signed, setNote, () => {
|
|
34
36
|
mine.reload();
|
|
35
37
|
installed.reload();
|
|
36
38
|
slack.reload();
|
|
39
|
+
social.reload();
|
|
37
40
|
});
|
|
38
41
|
if (!signed) {
|
|
39
42
|
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Integrations" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s integrations." })] }));
|
|
@@ -85,5 +88,31 @@ export function Integrations() {
|
|
|
85
88
|
? 'Not connected'
|
|
86
89
|
: s.note || 'This deployment cannot connect Slack yet.', trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack', here())), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
|
|
87
90
|
? rooms.value.channels.map((c) => (_jsx(Row, { leading: c.private ? _jsx(Lock, { size: 14 }) : _jsx(Hash, { size: 14 }), title: c.name || c.id, trailing: _jsx(SizableText, { size: "$1", color: c.member ? '$ink' : '$soft', children: c.member ? 'Joined' : 'Not joined' }) }, c.id)))
|
|
88
|
-
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the Slack workspace." })) : null] }), _jsx(
|
|
91
|
+
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the Slack workspace." })) : null] }), _jsxs(Group, { title: "Social accounts", detail: "Where automations post what their agent writes.", children: [_jsx(Card, { children: SOCIAL.map((p, i) => {
|
|
92
|
+
const c = social.value[p.id];
|
|
93
|
+
return (_jsx(Row, { first: i === 0, leading: _jsx(Megaphone, { size: 16 }), title: c?.connected && c.account ? `${p.name} · ${c.account}` : p.name, detail: social.error
|
|
94
|
+
? social.error.message
|
|
95
|
+
: !c
|
|
96
|
+
? 'Reading…'
|
|
97
|
+
: c.connected
|
|
98
|
+
? `Connected${c.since ? ` since ${day(c.since)}` : ''}${p.takes ? `. ${p.takes}` : ''}`
|
|
99
|
+
: c.available
|
|
100
|
+
? p.takes || 'Not connected'
|
|
101
|
+
: p.takes ? `${p.takes} ${p.needs}` : p.needs, trailing: host.admin && c && (c.connected || c.available) ? (c.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(t, p.id), `${p.name} is disconnected`, social.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, p.id, here())), "aria-label": `Connect ${p.name}`, children: "Connect" }))) : null }, p.id));
|
|
102
|
+
}) }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the organization\u2019s social accounts." })) : null] }), _jsx(Note, { children: note })] }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The named social connectors, by id: whether each can be connected here and
|
|
106
|
+
* whether it is. One that could not be read is left out, and its row reads on
|
|
107
|
+
* as "Reading…" rather than taking the others with it.
|
|
108
|
+
*/
|
|
109
|
+
export function useSocial(signed, ids) {
|
|
110
|
+
const t = useTarget();
|
|
111
|
+
const key = ids.join(',');
|
|
112
|
+
return useRead(signed
|
|
113
|
+
? async () => {
|
|
114
|
+
const got = await Promise.allSettled(ids.map((id) => read(t, id)));
|
|
115
|
+
return Object.fromEntries(got.flatMap((r, i) => (r.status === 'fulfilled' ? [[ids[i], r.value]] : [])));
|
|
116
|
+
}
|
|
117
|
+
: null, {}, [t, signed, key]);
|
|
89
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|