@hanzo/build 0.2.85 → 0.2.87

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.
@@ -78,6 +78,13 @@ export interface Installation {
78
78
  }
79
79
  /** The accounts this org has bound the App on. An org admin adds one through `/v1/provider/github/connect`. */
80
80
  export declare function installations(t: Target): Promise<Installation[]>;
81
+ /**
82
+ * Whether this page reaches GitHub at all, and as whom: the person's own
83
+ * connection first, else the accounts the org has bound the App on. An org that
84
+ * installed the App (onboarding's Connect GitHub) has its repositories to sync
85
+ * without the person connecting their own, so it reads as connected too.
86
+ */
87
+ export declare function linked(me: Connection, installed: Installation[]): Connection;
81
88
  /** One repository the GitHub connection grants this organization. */
82
89
  export interface Grant {
83
90
  owner: string;
package/lib/api/github.js CHANGED
@@ -98,6 +98,18 @@ export async function installations(t) {
98
98
  .map((i) => ({ login: str(i.login), type: str(i.type), grant: str(i.grant), connected: i.connected === true }))
99
99
  .filter((i) => i.login);
100
100
  }
101
+ /**
102
+ * Whether this page reaches GitHub at all, and as whom: the person's own
103
+ * connection first, else the accounts the org has bound the App on. An org that
104
+ * installed the App (onboarding's Connect GitHub) has its repositories to sync
105
+ * without the person connecting their own, so it reads as connected too.
106
+ */
107
+ export function linked(me, installed) {
108
+ if (me.connected)
109
+ return me;
110
+ const bound = installed.filter((i) => i.connected).map((i) => i.login);
111
+ return bound.length ? { configured: me.configured, connected: true, login: bound.join(', @') } : me;
112
+ }
101
113
  /**
102
114
  * One row of GET /v1/provider/github/repos (apps/provider github_index.go
103
115
  * repoView): `owner`, `name`, `full_name`, `private`, `default_branch`,
@@ -29,7 +29,12 @@ export declare const SOCIAL: readonly {
29
29
  }[];
30
30
  /** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
31
31
  export declare function authorize(t: Target, id: string, back?: string): Promise<string>;
32
- export declare function disconnect(t: Target, id: string): Promise<void>;
32
+ /**
33
+ * Forget the org's connection to a provider: every account, or the one `account`
34
+ * names (a GitHub login). For GitHub the platform also removes its App from an
35
+ * account no other org uses, so connecting that account again is a fresh install.
36
+ */
37
+ export declare function disconnect(t: Target, id: string, account?: string): Promise<void>;
33
38
  export interface Channel {
34
39
  id: string;
35
40
  name: string;
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * GET /v1/provider/{id} one connector and this org's connection to it
7
7
  * POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
8
- * POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
8
+ * POST /v1/provider/{id}/disconnect {account?} → forget the org's connection, or one account of it (org admin)
9
9
  * GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
10
10
  *
11
11
  * Connecting leaves this page once, for the provider's consent screen, and only
@@ -76,8 +76,13 @@ export async function authorize(t, id, back) {
76
76
  throw new Sentence(phrase('The platform did not name a {provider} address to connect at'), { provider: id });
77
77
  return url;
78
78
  }
79
- export async function disconnect(t, id) {
80
- await call(t, 'POST', `/v1/provider/${seg(id)}/disconnect`, {});
79
+ /**
80
+ * Forget the org's connection to a provider: every account, or the one `account`
81
+ * names (a GitHub login). For GitHub the platform also removes its App from an
82
+ * account no other org uses, so connecting that account again is a fresh install.
83
+ */
84
+ export async function disconnect(t, id, account) {
85
+ await call(t, 'POST', `/v1/provider/${seg(id)}/disconnect`, account ? { account } : {});
81
86
  }
82
87
  export async function channels(t, cursor = '') {
83
88
  const raw = obj(await call(t, 'GET', `/v1/provider/slack/channels${query({ cursor })}`));
package/lib/forge.js CHANGED
@@ -30,7 +30,8 @@ import { useHost, useTarget } from './host.js';
30
30
  import { path } from './route.js';
31
31
  import { Customize } from './customize/index.js';
32
32
  import { Sync } from './sync.js';
33
- import { connect, connection, grants, syncIssues } from './api/github.js';
33
+ import { connect, connection, disconnect as unlink, grants, installations, linked, syncIssues } from './api/github.js';
34
+ import { disconnect } from './api/provider.js';
34
35
  import { here, useBack } from './back.js';
35
36
  import { Out } from './out.js';
36
37
  import { ask, assemble, attach, closes, facets, handle, home, keyOf, matches, pickProjects, place } from './merge.js';
@@ -145,19 +146,31 @@ function Choice({ label, icon, says, onPress }) {
145
146
  return (_jsxs(YStack, { render: "button", "aria-label": label, onPress: onPress, flex: 1, minW: 240, gap: "$2", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", items: "flex-start", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon, _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(SizableText, { size: "$4", color: "$ink", fontWeight: "500", children: label }), _jsx(ArrowRight, { size: 14, 'data-i18n-mirror': '' })] }), _jsx(SizableText, { size: "$2", color: "$soft", text: "start", children: says })] }));
146
147
  }
147
148
  /**
148
- * The person's GitHub connection, in one line: who it is connected as and the
149
- * way to sync, or the one press that connects it. GitHub returns here, and this
150
- * page finishes the connect (../back.ts).
149
+ * The GitHub connection, in one line: who it is connected as and the way to
150
+ * sync, or the one press that connects it. The person's own connection, or else
151
+ * the accounts the org has bound the App on (`linked`), so an org that installed
152
+ * the App in onboarding does not read as unconnected here. GitHub returns here,
153
+ * and this page finishes the connect (../back.ts).
154
+ *
155
+ * Disconnect lets go of it, so another account can be connected: the person's own
156
+ * link, or (for an org admin) the org's App accounts, which the platform also
157
+ * uninstalls on GitHub. It asks once before doing either.
151
158
  */
152
159
  function GitHubLink({ onSync }) {
153
160
  const host = useHost();
154
161
  const target = useTarget();
155
162
  const { t } = useI18n();
156
163
  const me = useRead(() => connection(target), null, [target]);
164
+ // An org whose installations cannot be read falls back to the person's own connection.
165
+ const installed = useRead(() => installations(target).catch(() => []), [], [target]);
157
166
  const [note, setNote] = useState('');
158
167
  const [busy, setBusy] = useState(false);
159
- useBack(target, true, setNote, me.reload);
160
- const c = me.value;
168
+ const [asking, setAsking] = useState(false);
169
+ useBack(target, true, setNote, () => {
170
+ me.reload();
171
+ installed.reload();
172
+ });
173
+ const c = me.value ? linked(me.value, installed.value) : null;
161
174
  // A connection that cannot be read says nothing here: Sync from GitHub above still works.
162
175
  if (me.error || !c || !c.configured)
163
176
  return note ? _jsx(Note, { children: note }) : null;
@@ -172,7 +185,31 @@ function GitHubLink({ onSync }) {
172
185
  setBusy(false);
173
186
  }
174
187
  };
175
- return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", children: [_jsx(Github, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 200, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: c.connected ? t('GitHub connected as {login}', { login: `@${c.login}` }) : t('Connect GitHub to bring your repositories here') }), _jsx(SizableText, { size: "$1", color: "$soft", children: note || (c.connected ? t('Choose repositories to mirror; each stays in step with GitHub.') : t('One step on GitHub, then pick the repositories to sync.')) })] }), c.connected ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: onSync, children: [_jsx(RefreshCw, { size: 14 }), t('Sync repositories')] })) : (_jsxs(Button, { size: "sm", variant: "primary", disabled: busy, onPress: () => void link(), children: [_jsx(Github, { size: 14 }), busy ? t('Opening GitHub…') : t('Connect GitHub')] }))] }));
188
+ // The person's own link is theirs to drop; the org's App accounts are an org admin's.
189
+ const own = me.value?.connected === true;
190
+ const leave = c.connected && (own || host.admin);
191
+ const drop = async () => {
192
+ setBusy(true);
193
+ setNote('');
194
+ try {
195
+ await (own ? unlink(target) : disconnect(target, 'github'));
196
+ setAsking(false);
197
+ setNote(t('GitHub is disconnected. Connect it again to choose another account.'));
198
+ me.reload();
199
+ installed.reload();
200
+ }
201
+ catch (e) {
202
+ setNote(e.message);
203
+ }
204
+ finally {
205
+ setBusy(false);
206
+ }
207
+ };
208
+ return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flexWrap: "wrap", children: [_jsx(Github, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 200, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: c.connected ? t('GitHub connected as {login}', { login: `@${c.login}` }) : t('Connect GitHub to bring your repositories here') }), _jsx(SizableText, { size: "$1", color: "$soft", children: asking
209
+ ? own
210
+ ? t('Disconnect your GitHub from Hanzo?')
211
+ : t('Disconnect @{login}? Hanzo’s GitHub App is removed from it too, unless another organization uses it.', { login: c.login })
212
+ : note || (c.connected ? t('Choose repositories to mirror; each stays in step with GitHub.') : t('One step on GitHub, then pick the repositories to sync.')) })] }), c.connected && asking ? (_jsxs(XStack, { gap: "$2", children: [_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => setAsking(false), children: t('Cancel') }), _jsx(Button, { size: "sm", variant: "primary", disabled: busy, onPress: () => void drop(), children: busy ? t('Disconnecting…') : t('Disconnect') })] })) : c.connected ? (_jsxs(XStack, { gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "outline", onPress: onSync, children: [_jsx(RefreshCw, { size: 14 }), t('Sync repositories')] }), leave ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => setAsking(true), children: t('Disconnect') })) : null] })) : (_jsxs(Button, { size: "sm", variant: "primary", disabled: busy, onPress: () => void link(), children: [_jsx(Github, { size: 14 }), busy ? t('Opening GitHub…') : t('Connect GitHub')] }))] }));
176
213
  }
177
214
  /** The menus' words, marked (`phrase`) and drawn through `t`; GitHub and Hanzo are names, drawn as given. */
178
215
  const KINDS = { all: phrase('Issues and pull requests'), issue: phrase('Issues'), pr: phrase('Pull requests') };
@@ -85,7 +85,7 @@ export function Integrations() {
85
85
  ? t('This deployment cannot connect GitHub yet.')
86
86
  : me.connected
87
87
  ? t('Connected as @{login}', { login: me.login })
88
- : t('Not connected'), trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(target), t('Your GitHub is disconnected'), mine.reload), children: t('Disconnect') })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(target, here())), children: t('Connect') }))) : null }), installed.error ? (_jsx(Row, { title: t('App installations'), detail: told(installed.error, t) })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? t('every repository') : i.grant === 'selected' ? t('selected repositories') : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? t('Installed') : t('Removed on GitHub') }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: t('No GitHub accounts installed'), detail: t('Install the App on a GitHub account to bring its repositories here.') })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(target, 'github', here())), children: t('Install on a GitHub account') }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin installs the App on a GitHub account.') }))] }), _jsxs(Group, { title: "Slack", detail: t('Talk to Hanzo from your workspace’s channels.'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
88
+ : t('Not connected'), trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(target), t('Your GitHub is disconnected'), mine.reload), children: t('Disconnect') })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(target, here())), children: t('Connect') }))) : null }), installed.error ? (_jsx(Row, { title: t('App installations'), detail: told(installed.error, t) })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? t('every repository') : i.grant === 'selected' ? t('selected repositories') : ''].filter(Boolean).join(' · '), trailing: _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? t('Installed') : t('Removed on GitHub') }), host.admin ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(target, 'github', i.login), t('@{login} is disconnected', { login: i.login }), installed.reload), children: t('Disconnect') })) : null] }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: t('No GitHub accounts installed'), detail: t('Install the App on a GitHub account to bring its repositories here.') })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(target, 'github', here())), children: t('Install on a GitHub account') }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin installs the App on a GitHub account.') }))] }), _jsxs(Group, { title: "Slack", detail: t('Talk to Hanzo from your workspace’s channels.'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
89
89
  ? told(slack.error, t)
90
90
  : !s
91
91
  ? t('Reading…')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
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.",