@tightknitai/block-kitchen 0.9.10 → 0.10.0
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/README.md +86 -28
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.cjs +114 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +114 -74
- package/dist/index.js.map +1 -1
- package/dist/{palette-Duj901T6.d.cts → palette-C8sPJuXx.d.cts} +133 -60
- package/dist/{palette-Duj901T6.d.ts → palette-C8sPJuXx.d.ts} +133 -60
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,9 +110,10 @@ export function MyBuilderPage() {
|
|
|
110
110
|
| `loadSendAsUserStatus` | `() => Promise<{ canSendAsUser: boolean; oauthUrl?: string }>` | grouped\* | Whether the current user has a Slack user-token and can post as themselves. If `canSendAsUser` is false, `oauthUrl` is shown as a "Sign in with Slack" link. |
|
|
111
111
|
| `onSend` | `(payload) => Promise<{ ok: boolean; error?: string }>` | grouped\* | Called when the user submits the send dialog. Payload is `{ channelId, blocks, sendAsUser, extras? }` (`extras` only when `renderSendExtras` is wired). |
|
|
112
112
|
| `renderSendExtras` | `(ctx: SendExtrasContext) => ReactNode` | no | Renders host-defined fields inside the built-in send dialog, below the channel and identity pickers. Collect values with `ctx.setExtras(patch)`; they arrive on `onSend` as `payload.extras`. Requires the send trio. See [Extending the send dialog](#extending-the-send-dialog-custom-fields). |
|
|
113
|
-
| `primaryAction` | `{ label, onClick, disableWhenInvalid? }` | no | Compose-only mode only: a host-owned button in the toolbar slot where "Review & send" normally sits. `onClick` receives `{ blocks, validation }` — the current draft
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
113
|
+
| `primaryAction` | `{ label, onClick, disableWhenInvalid? }` | no | Compose-only mode only: a host-owned button in the toolbar slot where "Review & send" normally sits. `onClick` receives `{ blocks, validation, loadedMessage }` — the current draft, the same verdict `onValidationChange` reports, and the currently [loaded message](#loading-an-existing-message-opt-in) (`null` unless `loading` is configured and one is loaded). See [Keeping the CTA in the toolbar](#keeping-the-cta-in-the-toolbar-primaryaction). |
|
|
114
|
+
| `loading` | `{ onLoadMessage, loadRecentMessages?, loadChannels?, initialTarget?, onLoadedMessageChange? }` | no | Opt-in: [load an existing message](#loading-an-existing-message-opt-in) into the editor. Adds a "Find message" toolbar entry — the user pastes a Slack permalink (or picks a recent message) and the draft is hydrated with its blocks. Loading is a *composition* concern, so it works in **both** send mode and compose-only mode. `onLoadedMessageChange` reports the loaded target (`null` on exit) so a host-owned commitment step can stay in sync. |
|
|
115
|
+
| `onUpdate` | `(payload) => Promise<{ ok: boolean; error?: string }>` | no | Opt-in update-in-place mode (send mode only); sibling to `onSend`, but the payload carries the source `channel + ts`. With `loading` configured, a loaded message flips the primary action to a "Review & update" split button wired to it (`chat.update`). |
|
|
116
|
+
| `loadButtonLabel` | `string` | no | Label + accessible name for the toolbar button that opens the load-message dialog. Defaults to `'Find message'`. Only shown when `loading` is configured. |
|
|
116
117
|
| `updateButtonLabel` | `string` | no | Label for the toolbar's primary button while a message is loaded for editing. It's a split button: clicking it updates the message in place; the menu beside it also offers "Send as a new message" (post the current blocks as new). Defaults to `'Review & update'`. |
|
|
117
118
|
| `confirmUpdateLabel` | `string` | no | Label for the update dialog's final confirm button. Defaults to `'Update message'` (shows `'Updating…'` while in flight). |
|
|
118
119
|
| `previewHooks` | `PreviewHooks` | no | Hooks forwarded to `slack-blocks-to-jsx`'s `<Message>` for resolving user / channel / emoji directives. |
|
|
@@ -134,9 +135,10 @@ export function MyBuilderPage() {
|
|
|
134
135
|
all-or-nothing: provide all three for the built-in send flow, or omit all
|
|
135
136
|
three for [compose-only mode](#compose-only-mode-bring-your-own-send-flow)
|
|
136
137
|
(no send button; your app owns the send flow). Wiring only some of the three
|
|
137
|
-
is a type error. `
|
|
138
|
+
is a type error. `onUpdate` and `renderSendExtras` require the trio;
|
|
138
139
|
`primaryAction` requires its absence (the built-in Send/Update flow owns the
|
|
139
|
-
toolbar's primary slot otherwise).
|
|
140
|
+
toolbar's primary slot otherwise). `loading` is mode-agnostic — it works
|
|
141
|
+
with or without the trio.
|
|
140
142
|
|
|
141
143
|
## Compose-only mode (bring your own send flow)
|
|
142
144
|
|
|
@@ -186,9 +188,15 @@ Notes:
|
|
|
186
188
|
- `onValidationChange` reports the same verdict the issues sheet displays
|
|
187
189
|
(validated against the `message` surface, like Send), so your CTA and the
|
|
188
190
|
in-builder issue count can never disagree.
|
|
189
|
-
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
- [Loading an existing message](#loading-an-existing-message-opt-in) works
|
|
192
|
+
here too — pass `loading` and the "Find message" entry appears. The loaded
|
|
193
|
+
target (channel + ts + editability verdict) reaches your app via
|
|
194
|
+
`primaryAction`'s `loadedMessage` context and
|
|
195
|
+
`loading.onLoadedMessageChange`, so your own commitment step can offer
|
|
196
|
+
"update in place" alongside "post as new".
|
|
197
|
+
- The built-in *update* flow (`onUpdate`) is unavailable in compose-only
|
|
198
|
+
mode: dispatching a `chat.update` is inherently bound to a channel +
|
|
199
|
+
timestamp + token, so it only exists alongside the send integration.
|
|
192
200
|
|
|
193
201
|
### Keeping the CTA in the toolbar (`primaryAction`)
|
|
194
202
|
|
|
@@ -287,17 +295,20 @@ Notes:
|
|
|
287
295
|
(identities, channels), prefer proposing a first-class prop over routing
|
|
288
296
|
them through extras; the slot is for genuinely host-specific concerns.
|
|
289
297
|
|
|
290
|
-
##
|
|
298
|
+
## Loading an existing message (opt-in)
|
|
291
299
|
|
|
292
|
-
By default the builder
|
|
293
|
-
already-posted message
|
|
300
|
+
By default the builder starts from a blank canvas. Pass `loading` to let
|
|
301
|
+
users load an already-posted message's blocks into the editor — paste a
|
|
302
|
+
permalink, or pick from a "recent messages from this app" list. Loading is a
|
|
303
|
+
*composition* concern, so it works in **both** send mode and
|
|
304
|
+
[compose-only mode](#compose-only-mode-bring-your-own-send-flow). The
|
|
294
305
|
package stays integration-agnostic: it makes no Slack calls and computes
|
|
295
306
|
nothing about who can edit; the host does both.
|
|
296
307
|
|
|
297
308
|
```tsx
|
|
298
309
|
<BlockKitchen
|
|
299
|
-
/* …send-only
|
|
300
|
-
|
|
310
|
+
/* …send trio, or nothing (compose-only)… */
|
|
311
|
+
loading={{
|
|
301
312
|
// Host parses the pasted permalink, fetches the message, and returns a
|
|
302
313
|
// verdict. `chat.update` only edits a message authored by the calling
|
|
303
314
|
// token, so the host decides: bot message → 'bot', the user's own
|
|
@@ -315,17 +326,11 @@ nothing about who can edit; the host does both.
|
|
|
315
326
|
return { ok: true, channelId: msg.channel, channelName: msg.channelName, ts: msg.ts, blocks: msg.blocks, editableVia: 'user', ...author };
|
|
316
327
|
return { ok: false, reason: 'Only messages your app or you posted can be edited.', blocks: msg.blocks };
|
|
317
328
|
},
|
|
318
|
-
// Sibling to onSend; carries the source channel + ts. `asUser` follows
|
|
319
|
-
// the verdict's `editableVia`.
|
|
320
|
-
onUpdate: async ({ channelId, ts, blocks, asUser }) => {
|
|
321
|
-
await chatUpdate({ channel: channelId, ts, blocks, asUser }); // your code
|
|
322
|
-
return { ok: true };
|
|
323
|
-
},
|
|
324
329
|
// Optional: adds a "recent messages from this app" picker beside the paste
|
|
325
|
-
// input. The user first picks a channel
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
330
|
+
// input. The user first picks a channel, then this is called with that
|
|
331
|
+
// `channelId` so the lookup scans only one channel. These are
|
|
332
|
+
// editable-by-construction (the app authored them), so picking one loads
|
|
333
|
+
// it directly (no verdict needed).
|
|
329
334
|
loadRecentMessages: async (channelId) => {
|
|
330
335
|
const msgs = await fetchRecentAppMessages(channelId); // your code
|
|
331
336
|
return msgs.map((m) => ({
|
|
@@ -337,17 +342,68 @@ nothing about who can edit; the host does both.
|
|
|
337
342
|
label: m.preview // one-line preview shown in the picker row
|
|
338
343
|
}));
|
|
339
344
|
}
|
|
345
|
+
// In send mode the recent picker's channel list reuses the send trio's
|
|
346
|
+
// `loadChannels`. In compose-only mode there is no trio — pass
|
|
347
|
+
// `loadChannels` here too, or the picker is hidden.
|
|
340
348
|
}}
|
|
341
349
|
/>
|
|
342
350
|
```
|
|
343
351
|
|
|
344
|
-
- On `ok`, the builder hydrates with `blocks
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
in with Slack" gate).
|
|
352
|
+
- On `ok`, the builder hydrates with `blocks` and shows a loaded banner
|
|
353
|
+
("References an existing message in #channel"), with the author's
|
|
354
|
+
name/avatar in the preview header.
|
|
348
355
|
- On `{ ok: false, reason }`, the load dialog renders the reason inline and
|
|
349
356
|
offers **Open as a new message instead**. Pass `blocks` on the failure
|
|
350
357
|
result to hydrate the draft for that fallback.
|
|
358
|
+
- `loading.onLoadedMessageChange` reports the loaded target — and `null`
|
|
359
|
+
when the user exits it — so host state stays in sync (most useful in
|
|
360
|
+
compose-only mode, where your app owns what happens next). It can
|
|
361
|
+
**re-fire for the same `channelId` + `ts`** when the host's verdict or
|
|
362
|
+
metadata changed (e.g. `editableVia` flipping after a Slack sign-in), so
|
|
363
|
+
key host state on the full target, not `ts` alone.
|
|
364
|
+
- The banner's **"Switch to a new message"** exit is mode-aware: in send
|
|
365
|
+
mode it clears the canvas and reopens the load dialog (the edit-centric
|
|
366
|
+
"pick a different message" flow); in compose-only mode it only detaches
|
|
367
|
+
the reference and **keeps the draft** — loading seeded a composition, and
|
|
368
|
+
a misclick must not destroy it.
|
|
369
|
+
|
|
370
|
+
### Updating it in place (send mode)
|
|
371
|
+
|
|
372
|
+
In send mode, pair `loading` with `onUpdate` to dispatch a `chat.update` for
|
|
373
|
+
the loaded message. The primary action flips to a "Review & update" split
|
|
374
|
+
button (the menu also offers "Send as a new message"), the destination is
|
|
375
|
+
locked to the source channel, and the post-as identity is fixed to the
|
|
376
|
+
verdict's `editableVia` (the `'user'` path reuses `loadSendAsUserStatus` for
|
|
377
|
+
the "Sign in with Slack" gate).
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
<BlockKitchen
|
|
381
|
+
/* …send trio… */
|
|
382
|
+
loading={{ onLoadMessage }}
|
|
383
|
+
// Sibling to onSend; carries the source channel + ts. `asUser` follows
|
|
384
|
+
// the verdict's `editableVia`.
|
|
385
|
+
onUpdate={async ({ channelId, ts, blocks, asUser }) => {
|
|
386
|
+
await chatUpdate({ channel: channelId, ts, blocks, asUser }); // your code
|
|
387
|
+
return { ok: true };
|
|
388
|
+
}}
|
|
389
|
+
/>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
With `loading` alone (no `onUpdate`), a loaded message can still be posted
|
|
393
|
+
as a brand-new message via the regular Send flow — there's just no in-place
|
|
394
|
+
update. In compose-only mode `onUpdate` doesn't exist; implement your own
|
|
395
|
+
update against the target from `loadedMessage` / `onLoadedMessageChange`.
|
|
396
|
+
|
|
397
|
+
> **Migrating from the pre-split `editing` bundle (≤ 0.9.x):** the load
|
|
398
|
+
> fields move to `loading` unchanged, and `onUpdate` becomes a top-level
|
|
399
|
+
> prop (the `editing` wrapper is gone).
|
|
400
|
+
>
|
|
401
|
+
> ```tsx
|
|
402
|
+
> // Before
|
|
403
|
+
> <BlockKitchen editing={{ onLoadMessage, onUpdate, loadRecentMessages }} />
|
|
404
|
+
> // After
|
|
405
|
+
> <BlockKitchen loading={{ onLoadMessage, loadRecentMessages }} onUpdate={onUpdate} />
|
|
406
|
+
> ```
|
|
351
407
|
|
|
352
408
|
## Customizing the palette
|
|
353
409
|
|
|
@@ -405,7 +461,9 @@ import type {
|
|
|
405
461
|
SupportedBlockType,
|
|
406
462
|
BlockKitchenProps,
|
|
407
463
|
BlockKitchenBaseProps, // shared props
|
|
408
|
-
BlockKitchenSendProps, // the send trio + `
|
|
464
|
+
BlockKitchenSendProps, // the send trio + `onUpdate` + `renderSendExtras`
|
|
465
|
+
LoadingConfig, // load an existing message (works in both modes)
|
|
466
|
+
LoadedMessage, // the loaded target: channel + ts + verdict
|
|
409
467
|
BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
|
|
410
468
|
PaletteSection,
|
|
411
469
|
PaletteVariant,
|
package/dist/helpers.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { _ as SupportedBlock } from './palette-C8sPJuXx.cjs';
|
|
2
|
+
export { $ as SupportedBlockType } from './palette-C8sPJuXx.cjs';
|
|
3
3
|
import 'react';
|
|
4
4
|
import 'slack-web-api-client';
|
|
5
5
|
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { _ as SupportedBlock } from './palette-C8sPJuXx.js';
|
|
2
|
+
export { $ as SupportedBlockType } from './palette-C8sPJuXx.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import 'slack-web-api-client';
|
|
5
5
|
|
package/dist/index.cjs
CHANGED
|
@@ -2256,6 +2256,29 @@ function useBlockKitchenState({
|
|
|
2256
2256
|
replaceAll
|
|
2257
2257
|
};
|
|
2258
2258
|
}
|
|
2259
|
+
function useDevWarning(condition, message) {
|
|
2260
|
+
const warnedRef = React.useRef(false);
|
|
2261
|
+
React.useEffect(() => {
|
|
2262
|
+
if (condition && !warnedRef.current) {
|
|
2263
|
+
warnedRef.current = true;
|
|
2264
|
+
console.warn(message);
|
|
2265
|
+
}
|
|
2266
|
+
}, [condition, message]);
|
|
2267
|
+
}
|
|
2268
|
+
function useNotifyOnChange(value, toKey, callback) {
|
|
2269
|
+
const lastNotifiedRef = React.useRef(null);
|
|
2270
|
+
React.useEffect(() => {
|
|
2271
|
+
if (!callback) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const key = toKey(value);
|
|
2275
|
+
if (lastNotifiedRef.current === key) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
lastNotifiedRef.current = key;
|
|
2279
|
+
callback(value);
|
|
2280
|
+
}, [value, toKey, callback]);
|
|
2281
|
+
}
|
|
2259
2282
|
|
|
2260
2283
|
// src/lib/brand-theme.ts
|
|
2261
2284
|
var TOKEN_TO_CSS_VAR = {
|
|
@@ -3053,7 +3076,7 @@ function LoadMessageDialog({
|
|
|
3053
3076
|
loadRecentMessagesRef.current = loadRecentMessages;
|
|
3054
3077
|
loadChannelsRef.current = loadChannels;
|
|
3055
3078
|
});
|
|
3056
|
-
const hasRecent = !!loadRecentMessages;
|
|
3079
|
+
const hasRecent = !!loadRecentMessages && !!loadChannels;
|
|
3057
3080
|
React.useEffect(() => {
|
|
3058
3081
|
if (!open) {
|
|
3059
3082
|
return;
|
|
@@ -3064,7 +3087,10 @@ function LoadMessageDialog({
|
|
|
3064
3087
|
setRecent(null);
|
|
3065
3088
|
setRecentError(null);
|
|
3066
3089
|
setSelectedRecent(null);
|
|
3067
|
-
|
|
3090
|
+
}, [open]);
|
|
3091
|
+
React.useEffect(() => {
|
|
3092
|
+
const load = loadChannelsRef.current;
|
|
3093
|
+
if (!open || !hasRecent || !load) {
|
|
3068
3094
|
setChannels([]);
|
|
3069
3095
|
setChannelsError(null);
|
|
3070
3096
|
return;
|
|
@@ -3072,7 +3098,7 @@ function LoadMessageDialog({
|
|
|
3072
3098
|
setChannels(null);
|
|
3073
3099
|
setChannelsError(null);
|
|
3074
3100
|
let cancelled = false;
|
|
3075
|
-
|
|
3101
|
+
load().then((list) => {
|
|
3076
3102
|
if (!cancelled) {
|
|
3077
3103
|
setChannels(list);
|
|
3078
3104
|
}
|
|
@@ -3084,7 +3110,7 @@ function LoadMessageDialog({
|
|
|
3084
3110
|
return () => {
|
|
3085
3111
|
cancelled = true;
|
|
3086
3112
|
};
|
|
3087
|
-
}, [open]);
|
|
3113
|
+
}, [open, hasRecent]);
|
|
3088
3114
|
React.useEffect(() => {
|
|
3089
3115
|
if (!open || !loadRecentMessagesRef.current || !channelId) {
|
|
3090
3116
|
return;
|
|
@@ -9444,7 +9470,8 @@ function Toolbar2({
|
|
|
9444
9470
|
showSend = true,
|
|
9445
9471
|
primaryAction,
|
|
9446
9472
|
sendButtonLabel = "Review & send",
|
|
9447
|
-
|
|
9473
|
+
loadEnabled = false,
|
|
9474
|
+
updateEnabled = false,
|
|
9448
9475
|
editBadge,
|
|
9449
9476
|
onOpenLoad,
|
|
9450
9477
|
onOpenUpdate,
|
|
@@ -9464,7 +9491,7 @@ function Toolbar2({
|
|
|
9464
9491
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9465
9492
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1 border-b bg-background px-2 py-1.5 sm:px-3 sm:py-2", children: [
|
|
9466
9493
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
|
|
9467
|
-
|
|
9494
|
+
loadEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
|
|
9468
9495
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MailSearch, { className: "h-3.5 w-3.5" }),
|
|
9469
9496
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
|
|
9470
9497
|
] }) : null,
|
|
@@ -9594,9 +9621,12 @@ function Toolbar2({
|
|
|
9594
9621
|
children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryAction.label })
|
|
9595
9622
|
}
|
|
9596
9623
|
)
|
|
9597
|
-
) : null : editBadge ? (
|
|
9598
|
-
// A message is loaded: split button.
|
|
9599
|
-
// place; the menu also offers posting
|
|
9624
|
+
) : null : editBadge && updateEnabled ? (
|
|
9625
|
+
// A message is loaded and update-in-place is wired: split button.
|
|
9626
|
+
// Main action updates it in place; the menu also offers posting
|
|
9627
|
+
// the blocks as a new message. Without `updateEnabled` (loading
|
|
9628
|
+
// configured but no `editing.onUpdate`), the plain send button
|
|
9629
|
+
// below stays — the loaded message can only be posted as new.
|
|
9600
9630
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-stretch", children: [
|
|
9601
9631
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9602
9632
|
Button,
|
|
@@ -9669,7 +9699,7 @@ function Toolbar2({
|
|
|
9669
9699
|
] })
|
|
9670
9700
|
] })
|
|
9671
9701
|
] }),
|
|
9672
|
-
|
|
9702
|
+
loadEnabled && editBadge ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
|
|
9673
9703
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4 shrink-0 text-primary" }),
|
|
9674
9704
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 text-sm", children: [
|
|
9675
9705
|
"References an existing message in ",
|
|
@@ -9832,6 +9862,19 @@ function UpdateDialog({
|
|
|
9832
9862
|
] })
|
|
9833
9863
|
] }) });
|
|
9834
9864
|
}
|
|
9865
|
+
function toLoadedMessage({
|
|
9866
|
+
ok: _ok,
|
|
9867
|
+
blocks: _blocks,
|
|
9868
|
+
...loadedMessage
|
|
9869
|
+
}) {
|
|
9870
|
+
return loadedMessage;
|
|
9871
|
+
}
|
|
9872
|
+
function loadedMessageKey(message) {
|
|
9873
|
+
return message ? JSON.stringify(message) : null;
|
|
9874
|
+
}
|
|
9875
|
+
function validationSummaryKey(summary) {
|
|
9876
|
+
return JSON.stringify([summary.valid, summary.errorCount, ...summary.errors]);
|
|
9877
|
+
}
|
|
9835
9878
|
function BlockKitchen(props) {
|
|
9836
9879
|
const {
|
|
9837
9880
|
workspaceName,
|
|
@@ -9844,7 +9887,8 @@ function BlockKitchen(props) {
|
|
|
9844
9887
|
loadSendAsUserStatus,
|
|
9845
9888
|
onSend,
|
|
9846
9889
|
renderSendExtras,
|
|
9847
|
-
|
|
9890
|
+
loading,
|
|
9891
|
+
onUpdate: onUpdateProp,
|
|
9848
9892
|
primaryAction,
|
|
9849
9893
|
loadButtonLabel,
|
|
9850
9894
|
updateButtonLabel,
|
|
@@ -9888,36 +9932,39 @@ function BlockKitchen(props) {
|
|
|
9888
9932
|
const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
|
|
9889
9933
|
const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
|
|
9890
9934
|
const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
|
|
9891
|
-
const
|
|
9892
|
-
const
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9935
|
+
const loadEnabled = Boolean(loading);
|
|
9936
|
+
const recentChannelSource = loading?.loadChannels ?? (sendEnabled ? loadChannels : void 0);
|
|
9937
|
+
const onUpdate = sendEnabled ? onUpdateProp : void 0;
|
|
9938
|
+
const seededBlocks = loading?.initialTarget?.blocks ?? initialBlocks;
|
|
9939
|
+
const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
|
|
9940
|
+
useDevWarning(
|
|
9941
|
+
Boolean(loading?.initialTarget && initialBlocks),
|
|
9942
|
+
"[BlockKitchen] Both `initialBlocks` and an `initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
|
|
9943
|
+
);
|
|
9944
|
+
useDevWarning(
|
|
9945
|
+
sendPropsProvided > 0 && sendPropsProvided < 3,
|
|
9946
|
+
"[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
|
|
9947
|
+
);
|
|
9948
|
+
useDevWarning(
|
|
9949
|
+
Boolean(onUpdateProp && !sendEnabled),
|
|
9950
|
+
"[BlockKitchen] `onUpdate` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `onUpdate`. To load an existing message without the send integration, use the `loading` prop instead."
|
|
9951
|
+
);
|
|
9952
|
+
useDevWarning(
|
|
9953
|
+
Boolean(sendEnabled && onUpdateProp && !loading),
|
|
9954
|
+
"[BlockKitchen] `onUpdate` needs a loaded message, but no load source is configured \u2014 provide the `loading` prop. The update flow is unreachable."
|
|
9955
|
+
);
|
|
9956
|
+
useDevWarning(
|
|
9957
|
+
Boolean(loading?.loadRecentMessages && !recentChannelSource),
|
|
9958
|
+
"[BlockKitchen] `loading.loadRecentMessages` needs a channel list to scope its lookup \u2014 provide `loading.loadChannels` (compose-only mode has no send integration to reuse). Hiding the recent-messages picker."
|
|
9959
|
+
);
|
|
9960
|
+
useDevWarning(
|
|
9961
|
+
Boolean(renderSendExtras && !sendEnabled),
|
|
9962
|
+
"[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
|
|
9963
|
+
);
|
|
9964
|
+
useDevWarning(
|
|
9965
|
+
Boolean(primaryAction && sendEnabled),
|
|
9966
|
+
"[BlockKitchen] `primaryAction` is only available in compose-only mode \u2014 with the send integration wired, the built-in Send/Update flow owns the toolbar\u2019s primary action; ignoring `primaryAction`."
|
|
9967
|
+
);
|
|
9921
9968
|
const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
|
|
9922
9969
|
const childParentById = React.useMemo(() => {
|
|
9923
9970
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -9934,8 +9981,11 @@ function BlockKitchen(props) {
|
|
|
9934
9981
|
const [sendOpen, setSendOpen] = React.useState(false);
|
|
9935
9982
|
const [updateOpen, setUpdateOpen] = React.useState(false);
|
|
9936
9983
|
const [loadOpen, setLoadOpen] = React.useState(false);
|
|
9937
|
-
const [editTarget, setEditTarget] = React.useState(
|
|
9938
|
-
|
|
9984
|
+
const [editTarget, setEditTarget] = React.useState(
|
|
9985
|
+
() => loading?.initialTarget ? toLoadedMessage(loading.initialTarget) : null
|
|
9986
|
+
);
|
|
9987
|
+
const activeEditTarget = loadEnabled ? editTarget : null;
|
|
9988
|
+
useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
|
|
9939
9989
|
const [issuesOpen, setIssuesOpen] = React.useState(false);
|
|
9940
9990
|
const [paletteOpen, setPaletteOpen] = React.useState(false);
|
|
9941
9991
|
const [openBlockId, setOpenBlockId] = React.useState(null);
|
|
@@ -9957,18 +10007,7 @@ function BlockKitchen(props) {
|
|
|
9957
10007
|
() => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
|
|
9958
10008
|
[validation]
|
|
9959
10009
|
);
|
|
9960
|
-
|
|
9961
|
-
React.useEffect(() => {
|
|
9962
|
-
if (!onValidationChange) {
|
|
9963
|
-
return;
|
|
9964
|
-
}
|
|
9965
|
-
const key = JSON.stringify([validationSummary.valid, validationSummary.errorCount, ...validationSummary.errors]);
|
|
9966
|
-
if (lastNotifiedValidationRef.current === key) {
|
|
9967
|
-
return;
|
|
9968
|
-
}
|
|
9969
|
-
lastNotifiedValidationRef.current = key;
|
|
9970
|
-
onValidationChange(validationSummary);
|
|
9971
|
-
}, [validationSummary, onValidationChange]);
|
|
10010
|
+
useNotifyOnChange(validationSummary, validationSummaryKey, onValidationChange);
|
|
9972
10011
|
const sensors = core.useSensors(
|
|
9973
10012
|
core.useSensor(core.PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
9974
10013
|
core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
|
|
@@ -10075,11 +10114,16 @@ function BlockKitchen(props) {
|
|
|
10075
10114
|
showSend: sendEnabled,
|
|
10076
10115
|
primaryAction: !sendEnabled && primaryAction ? {
|
|
10077
10116
|
label: primaryAction.label,
|
|
10078
|
-
onClick: () => primaryAction.onClick({
|
|
10117
|
+
onClick: () => primaryAction.onClick({
|
|
10118
|
+
blocks: blockPayloads,
|
|
10119
|
+
validation: validationSummary,
|
|
10120
|
+
loadedMessage: activeEditTarget
|
|
10121
|
+
}),
|
|
10079
10122
|
disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
|
|
10080
10123
|
} : null,
|
|
10081
10124
|
sendButtonLabel,
|
|
10082
|
-
|
|
10125
|
+
loadEnabled,
|
|
10126
|
+
updateEnabled: Boolean(onUpdate),
|
|
10083
10127
|
editBadge: activeEditTarget ? {
|
|
10084
10128
|
channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
|
|
10085
10129
|
ts: activeEditTarget.ts
|
|
@@ -10087,9 +10131,13 @@ function BlockKitchen(props) {
|
|
|
10087
10131
|
onOpenLoad: () => setLoadOpen(true),
|
|
10088
10132
|
onOpenUpdate: () => setUpdateOpen(true),
|
|
10089
10133
|
onExitEdit: () => {
|
|
10134
|
+
if (sendEnabled) {
|
|
10135
|
+
setEditTarget(null);
|
|
10136
|
+
replaceAll([]);
|
|
10137
|
+
setLoadOpen(true);
|
|
10138
|
+
return;
|
|
10139
|
+
}
|
|
10090
10140
|
setEditTarget(null);
|
|
10091
|
-
replaceAll([]);
|
|
10092
|
-
setLoadOpen(true);
|
|
10093
10141
|
},
|
|
10094
10142
|
loadButtonLabel,
|
|
10095
10143
|
updateButtonLabel
|
|
@@ -10179,7 +10227,7 @@ function BlockKitchen(props) {
|
|
|
10179
10227
|
}
|
|
10180
10228
|
}
|
|
10181
10229
|
) : null,
|
|
10182
|
-
activeEditTarget &&
|
|
10230
|
+
activeEditTarget && onUpdate && loadSendAsUserStatus ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
10183
10231
|
UpdateDialog,
|
|
10184
10232
|
{
|
|
10185
10233
|
open: updateOpen,
|
|
@@ -10187,7 +10235,7 @@ function BlockKitchen(props) {
|
|
|
10187
10235
|
target: activeEditTarget,
|
|
10188
10236
|
blocks: blockPayloads,
|
|
10189
10237
|
loadSendAsUserStatus,
|
|
10190
|
-
onUpdate
|
|
10238
|
+
onUpdate,
|
|
10191
10239
|
confirmUpdateLabel,
|
|
10192
10240
|
errorCount: validation.total,
|
|
10193
10241
|
onShowIssues: () => {
|
|
@@ -10196,25 +10244,17 @@ function BlockKitchen(props) {
|
|
|
10196
10244
|
}
|
|
10197
10245
|
}
|
|
10198
10246
|
) : null,
|
|
10199
|
-
|
|
10247
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
10200
10248
|
LoadMessageDialog,
|
|
10201
10249
|
{
|
|
10202
10250
|
open: loadOpen,
|
|
10203
10251
|
onOpenChange: setLoadOpen,
|
|
10204
|
-
onLoadMessage:
|
|
10205
|
-
loadRecentMessages:
|
|
10206
|
-
loadChannels,
|
|
10252
|
+
onLoadMessage: loading.onLoadMessage,
|
|
10253
|
+
loadRecentMessages: loading.loadRecentMessages,
|
|
10254
|
+
loadChannels: recentChannelSource,
|
|
10207
10255
|
onLoaded: (result) => {
|
|
10208
10256
|
replaceAll(result.blocks);
|
|
10209
|
-
setEditTarget(
|
|
10210
|
-
channelId: result.channelId,
|
|
10211
|
-
channelName: result.channelName,
|
|
10212
|
-
ts: result.ts,
|
|
10213
|
-
editableVia: result.editableVia,
|
|
10214
|
-
workspaceName: result.workspaceName,
|
|
10215
|
-
username: result.username,
|
|
10216
|
-
iconUrl: result.iconUrl
|
|
10217
|
-
});
|
|
10257
|
+
setEditTarget(toLoadedMessage(result));
|
|
10218
10258
|
setLoadOpen(false);
|
|
10219
10259
|
},
|
|
10220
10260
|
onOpenAsNew: (loadedBlocks) => {
|