From ed4560e52352316e3dee1cda398059419a7c5bb3 Mon Sep 17 00:00:00 2001 From: "Roger (DatoCMS)" Date: Tue, 3 Nov 2026 13:01:56 -0800 Subject: [PATCH 1/5] fix(record-bin): return from onBeforeItemsDestroy without awaiting notices The `ctx.notice()` promise settles when the toast closes, about six seconds after it opens. Both deletion-blocking paths awaited it, which held the hook open after the decision was made. The CMS then reported Record Bin as slow with its "taking longer than usual" warning. Show the notices without awaiting them and log display failures in debug mode. Refs #152 Co-Authored-By: Claude Opus 5.5 --- record-bin/src/index.test.tsx | 2 ++ record-bin/src/index.tsx | 3 ++- record-bin/src/utils/lambdaLessCapture.test.ts | 7 +++++++ record-bin/src/utils/lambdaLessCapture.ts | 11 ++++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/record-bin/src/index.test.tsx b/record-bin/src/index.test.tsx index a52314f1..058692ea 100644 --- a/record-bin/src/index.test.tsx +++ b/record-bin/src/index.test.tsx @@ -66,6 +66,8 @@ describe('delete hook decisions', () => { expect(captureDeletedItemsWithoutLambda).toHaveBeenCalledTimes(1); ctx.notice.mockRejectedValue(new Error('notice failed')); expect(await hook()(items, ctx)).toBe(false); + ctx.notice.mockReturnValue(new Promise(() => {})); + expect(await hook()(items, ctx)).toBe(false); }); it('preserves the separately deployed Lambda runtime contract', async () => { expect(await hook()([{ id: 'one' }], context('lambda'))).toBe(true); diff --git a/record-bin/src/index.tsx b/record-bin/src/index.tsx index 78657bc2..9ee51625 100644 --- a/record-bin/src/index.tsx +++ b/record-bin/src/index.tsx @@ -25,7 +25,8 @@ connect({ // The host currently sends one bulk request, whose CMA limit is 200 IDs. // Performing deletion here would bypass other plugins' concurrent vetoes. if (items.length > 200) { - await ctx + // The notice promise settles when the toast closes; awaiting it holds the hook open. + void ctx .notice( 'Record Bin stopped this deletion: the dashboard must support bounded bulk operations before a selection above 200 records can be deleted safely. The original records have been kept.', ) diff --git a/record-bin/src/utils/lambdaLessCapture.test.ts b/record-bin/src/utils/lambdaLessCapture.test.ts index b8c05943..b1a36b7d 100644 --- a/record-bin/src/utils/lambdaLessCapture.test.ts +++ b/record-bin/src/utils/lambdaLessCapture.test.ts @@ -193,6 +193,13 @@ describe('pre-deletion capture', () => { }); expect(mock.writes()).toBe(0); }); + it('returns a blocked result without waiting for the notice to close', async () => { + setup({ read: () => undefined }); + const ctx = context(); + ctx.notice.mockReturnValue(new Promise(() => {})); + expect((await run(['one'], ctx)).allowDeletion).toBe(false); + expect(ctx.notice).toHaveBeenCalledTimes(1); + }); it('blocks deletion without an access token', async () => { const ctx = context(); ctx.currentUserAccessToken = undefined; diff --git a/record-bin/src/utils/lambdaLessCapture.ts b/record-bin/src/utils/lambdaLessCapture.ts index f46bc930..136d51f1 100644 --- a/record-bin/src/utils/lambdaLessCapture.ts +++ b/record-bin/src/utils/lambdaLessCapture.ts @@ -303,10 +303,15 @@ class CaptureOperation { } catch (error) { this.handleFailure(error); } + // The notice promise settles when the toast closes; awaiting it holds the hook open. if (!this.result.allowDeletion) { - await this.ctx.notice( - 'Record Bin could not verify a complete archive. Deletion was cancelled; the original records have been kept.', - ); + void this.ctx + .notice( + 'Record Bin could not verify a complete archive. Deletion was cancelled; the original records have been kept.', + ) + .catch((error) => + this.debugLogger.warn('Could not display archive notice', error), + ); } return this.result; } From 5f1563a5f4a7f9d3e854490ca16df1a87a6fbc11 Mon Sep 17 00:00:00 2001 From: "Roger (DatoCMS)" Date: Tue, 3 Nov 2026 13:01:56 -0800 Subject: [PATCH 2/5] fix(record-bin): show blocked deletions as error toasts Both deletion-blocking messages used `ctx.notice()`, which the SDK renders as a green success toast. Use `ctx.alert()`, the error toast, as the rest of the plugin already does for failures. Refs #152 Co-Authored-By: Claude Opus 5.5 --- record-bin/src/index.test.tsx | 8 ++++---- record-bin/src/index.tsx | 6 +++--- record-bin/src/utils/lambdaLessCapture.test.ts | 8 ++++---- record-bin/src/utils/lambdaLessCapture.ts | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/record-bin/src/index.test.tsx b/record-bin/src/index.test.tsx index 058692ea..4be730d6 100644 --- a/record-bin/src/index.test.tsx +++ b/record-bin/src/index.test.tsx @@ -26,7 +26,7 @@ const hook = () => { const context = (runtimeMode = 'lambdaless') => ({ plugin: { attributes: { parameters: { runtimeMode } } }, openModal: vi.fn().mockResolvedValue(true), - notice: vi.fn().mockResolvedValue(undefined), + alert: vi.fn().mockResolvedValue(undefined), }); beforeEach(() => { vi.mocked(captureDeletedItemsWithoutLambda).mockReset(); @@ -60,13 +60,13 @@ describe('delete hook decisions', () => { })); expect(await hook()(items, ctx)).toBe(false); expect(ctx.openModal).not.toHaveBeenCalled(); - expect(ctx.notice).toHaveBeenCalledWith( + expect(ctx.alert).toHaveBeenCalledWith( expect.stringContaining('above 200'), ); expect(captureDeletedItemsWithoutLambda).toHaveBeenCalledTimes(1); - ctx.notice.mockRejectedValue(new Error('notice failed')); + ctx.alert.mockRejectedValue(new Error('alert failed')); expect(await hook()(items, ctx)).toBe(false); - ctx.notice.mockReturnValue(new Promise(() => {})); + ctx.alert.mockReturnValue(new Promise(() => {})); expect(await hook()(items, ctx)).toBe(false); }); it('preserves the separately deployed Lambda runtime contract', async () => { diff --git a/record-bin/src/index.tsx b/record-bin/src/index.tsx index 9ee51625..5c32d4bc 100644 --- a/record-bin/src/index.tsx +++ b/record-bin/src/index.tsx @@ -25,13 +25,13 @@ connect({ // The host currently sends one bulk request, whose CMA limit is 200 IDs. // Performing deletion here would bypass other plugins' concurrent vetoes. if (items.length > 200) { - // The notice promise settles when the toast closes; awaiting it holds the hook open. + // The alert promise settles when the toast closes; awaiting it holds the hook open. void ctx - .notice( + .alert( 'Record Bin stopped this deletion: the dashboard must support bounded bulk operations before a selection above 200 records can be deleted safely. The original records have been kept.', ) .catch((error) => - debugLogger.warn('Could not display deletion limit notice', error), + debugLogger.warn('Could not display deletion limit alert', error), ); return false; } diff --git a/record-bin/src/utils/lambdaLessCapture.test.ts b/record-bin/src/utils/lambdaLessCapture.test.ts index b1a36b7d..00393f6f 100644 --- a/record-bin/src/utils/lambdaLessCapture.test.ts +++ b/record-bin/src/utils/lambdaLessCapture.test.ts @@ -37,7 +37,7 @@ const context = (token: string | undefined = 'token') => ({ environment: 'main', cmaBaseUrl: 'https://example.invalid', plugin: { attributes: { parameters: {} } }, - notice: vi.fn().mockResolvedValue(undefined), + alert: vi.fn().mockResolvedValue(undefined), }); type Request = { method: string; @@ -193,12 +193,12 @@ describe('pre-deletion capture', () => { }); expect(mock.writes()).toBe(0); }); - it('returns a blocked result without waiting for the notice to close', async () => { + it('returns a blocked result without waiting for the alert to close', async () => { setup({ read: () => undefined }); const ctx = context(); - ctx.notice.mockReturnValue(new Promise(() => {})); + ctx.alert.mockReturnValue(new Promise(() => {})); expect((await run(['one'], ctx)).allowDeletion).toBe(false); - expect(ctx.notice).toHaveBeenCalledTimes(1); + expect(ctx.alert).toHaveBeenCalledTimes(1); }); it('blocks deletion without an access token', async () => { const ctx = context(); diff --git a/record-bin/src/utils/lambdaLessCapture.ts b/record-bin/src/utils/lambdaLessCapture.ts index 136d51f1..2f87448b 100644 --- a/record-bin/src/utils/lambdaLessCapture.ts +++ b/record-bin/src/utils/lambdaLessCapture.ts @@ -108,7 +108,7 @@ export type LambdaLessCaptureResult = { export type CaptureContext = Pick< OnBeforeItemsDestroyCtx, - 'plugin' | 'currentUserAccessToken' | 'environment' | 'cmaBaseUrl' | 'notice' + 'plugin' | 'currentUserAccessToken' | 'environment' | 'cmaBaseUrl' | 'alert' >; async function archiveRecord( @@ -303,14 +303,14 @@ class CaptureOperation { } catch (error) { this.handleFailure(error); } - // The notice promise settles when the toast closes; awaiting it holds the hook open. + // The alert promise settles when the toast closes; awaiting it holds the hook open. if (!this.result.allowDeletion) { void this.ctx - .notice( + .alert( 'Record Bin could not verify a complete archive. Deletion was cancelled; the original records have been kept.', ) .catch((error) => - this.debugLogger.warn('Could not display archive notice', error), + this.debugLogger.warn('Could not display archive alert', error), ); } return this.result; From d3b5cef28955a545e2f4c0d99c1c12ff31891184 Mon Sep 17 00:00:00 2001 From: "Roger (DatoCMS)" Date: Tue, 3 Nov 2026 13:01:56 -0800 Subject: [PATCH 3/5] fix(record-bin): stop awaiting alerts in restore and settings flows `ctx.alert()` resolves only when the user closes the error toast. Code that awaited it kept the Restore, Save and Disconnect buttons busy until then, and delayed opening a restored record whose archive cleanup failed. Show those alerts without awaiting them. Settings save also reported a webhook cleanup failure as a green success notice and awaited it for six seconds; use a non-blocking alert instead. Refs #152 Co-Authored-By: Claude Opus 5.5 --- record-bin/src/entrypoints/BinOutlet.test.tsx | 68 +++++++++++++++++++ record-bin/src/entrypoints/BinOutlet.tsx | 31 +++++---- record-bin/src/entrypoints/ConfigScreen.tsx | 6 +- 3 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 record-bin/src/entrypoints/BinOutlet.test.tsx diff --git a/record-bin/src/entrypoints/BinOutlet.test.tsx b/record-bin/src/entrypoints/BinOutlet.test.tsx new file mode 100644 index 00000000..3ce7c904 --- /dev/null +++ b/record-bin/src/entrypoints/BinOutlet.test.tsx @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { errorObject } from '../types/types'; +import { createDebugLogger } from '../utils/debugLogger'; +import { + LambdaLessRestoreError, + restoreRecordWithoutLambda, +} from '../utils/lambdaLessRestore'; +import { executeRestoreWithoutLambda } from './BinOutlet'; + +vi.mock('datocms-react-ui', () => ({})); +vi.mock('../utils/lambdaLessRestore', async (importOriginal) => ({ + ...(await importOriginal()), + restoreRecordWithoutLambda: vi.fn(), +})); + +const restorationError = { + simplifiedError: { details: {} }, + fullErrorPayload: '{}', +} as unknown as errorObject; +// Error toasts stay open until the user closes them, so their promise never settles here. +const callbacks = () => ({ + alertFn: vi.fn(() => new Promise(() => {})), + noticeFn: vi.fn(), + navigateTo: vi.fn(), + setError: vi.fn(), +}); +const restore = (handlers: ReturnType) => + executeRestoreWithoutLambda({ + trashRecordId: 'archive-id', + parsedRecordBody: {}, + currentUserAccessToken: 'token', + environment: 'sandbox', + debugLogger: createDebugLogger(false, 'test'), + isEnvironmentPrimary: false, + ...handlers, + }); +afterEach(() => { + vi.mocked(restoreRecordWithoutLambda).mockReset(); +}); + +describe('Lambda-less restore flow', () => { + it('finishes a failed restore without waiting for the alert to close', async () => { + vi.mocked(restoreRecordWithoutLambda).mockRejectedValue( + new LambdaLessRestoreError('rejected', restorationError), + ); + const handlers = callbacks(); + await restore(handlers); + expect(handlers.setError).toHaveBeenCalledWith(restorationError); + expect(handlers.alertFn).toHaveBeenCalledWith( + 'The record could not be restored!', + ); + expect(handlers.navigateTo).not.toHaveBeenCalled(); + }); + it('opens the restored record while the cleanup alert is still open', async () => { + vi.mocked(restoreRecordWithoutLambda).mockResolvedValue({ + restoredRecord: { id: 'record-id', modelID: 'model-id' }, + cleanupError: restorationError, + }); + const handlers = callbacks(); + await restore(handlers); + expect(handlers.alertFn).toHaveBeenCalledWith( + 'The record was restored, but its archive could not be removed.', + ); + expect(handlers.navigateTo).toHaveBeenCalledWith( + '/environments/sandbox/editor/item_types/model-id/items/record-id', + ); + }); +}); diff --git a/record-bin/src/entrypoints/BinOutlet.tsx b/record-bin/src/entrypoints/BinOutlet.tsx index d3d904f0..d7532a0a 100644 --- a/record-bin/src/entrypoints/BinOutlet.tsx +++ b/record-bin/src/entrypoints/BinOutlet.tsx @@ -34,7 +34,7 @@ function buildRecordEditPath( const parseLambdaRecordBody = async ( rawRecordBody: unknown, debugLogger: DebugLogger, - alertFn: (message: string) => Promise, + alertFn: (message: string) => void, ): Promise<{ success: false } | { success: true; parsedBody: unknown }> => { try { const parsedBody = @@ -44,7 +44,7 @@ const parseLambdaRecordBody = async ( return { success: true, parsedBody }; } catch (error) { debugLogger.warn('Could not parse record_body JSON before restore', error); - await alertFn('The record body is not valid JSON.'); + alertFn('The record body is not valid JSON.'); return { success: false }; } }; @@ -65,7 +65,7 @@ const executeRestoreViaLambda = async ({ parsedRecordBody: Record; trashRecordId: string; debugLogger: DebugLogger; - alertFn: (message: string) => Promise; + alertFn: (message: string) => void; noticeFn: (message: string) => void; navigateTo: (path: string) => void; environment: string; @@ -95,7 +95,7 @@ const executeRestoreViaLambda = async ({ 'Restoration request failed before receiving response', restorationError, ); - await alertFn( + alertFn( 'The restore API did not confirm the result. Check whether the record was restored before trying again.', ); return false; @@ -126,7 +126,7 @@ const executeRestoreViaLambda = async ({ status: restoreResponse.status, restorationError, }); - await alertFn('The record could not be restored!'); + alertFn('The record could not be restored!'); return false; } @@ -146,7 +146,7 @@ const executeRestoreViaLambda = async ({ return true; }; -const executeRestoreWithoutLambda = async ({ +export const executeRestoreWithoutLambda = async ({ trashRecordId, parsedRecordBody, currentUserAccessToken, @@ -165,7 +165,7 @@ const executeRestoreWithoutLambda = async ({ cmaBaseUrl?: string; environment: string; debugLogger: DebugLogger; - alertFn: (message: string) => Promise; + alertFn: (message: string) => void; noticeFn: (message: string) => void; navigateTo: (path: string) => void; isEnvironmentPrimary: boolean; @@ -191,7 +191,7 @@ const executeRestoreWithoutLambda = async ({ 'Lambda-less restoration request failed', error.restorationError, ); - await alertFn('The record could not be restored!'); + alertFn('The record could not be restored!'); return; } throw error; @@ -210,7 +210,7 @@ const executeRestoreWithoutLambda = async ({ 'Restored record archive cleanup failed', parsedResponse.cleanupError, ); - await alertFn( + alertFn( 'The record was restored, but its archive could not be removed.', ); } @@ -281,7 +281,7 @@ const runLambdaModeRestore = async ({ pluginParameters: Record; trashRecordId: string | undefined; debugLogger: DebugLogger; - alertFn: (message: string) => Promise; + alertFn: (message: string) => void; noticeFn: (message: string) => void; navigateTo: (path: string) => void; environment: string; @@ -292,7 +292,7 @@ const runLambdaModeRestore = async ({ if (!deploymentURL) { debugLogger.warn('Missing deployment URL while restoring record'); - await alertFn('The plugin deployment URL is missing.'); + alertFn('The plugin deployment URL is missing.'); return; } @@ -302,13 +302,13 @@ const runLambdaModeRestore = async ({ Array.isArray(parsedRecordBody); if (isInvalidBody) { debugLogger.warn('record_body must be a JSON object for lambda restore'); - await alertFn('The record body is not a valid restore payload.'); + alertFn('The record body is not a valid restore payload.'); return; } if (!trashRecordId) { debugLogger.warn('Missing trash record id in lambda restore'); - await alertFn('Could not determine the trash record to remove.'); + alertFn('Could not determine the trash record to remove.'); return; } @@ -356,6 +356,7 @@ const BinOutlet = ({ ctx }: { ctx: RenderItemFormOutletCtx }) => { const runtimeMode = getRuntimeMode(ctx.plugin.attributes.parameters); const rawRecordBody = ctx.formValues.record_body; + // Alerts stay open until the user closes them; awaiting one would stall the restore flow. const alertFn = ctx.alert.bind(ctx); const noticeFn = ctx.notice.bind(ctx); const navigateTo = ctx.navigateTo.bind(ctx); @@ -399,7 +400,7 @@ const BinOutlet = ({ ctx }: { ctx: RenderItemFormOutletCtx }) => { if (!ctx.item?.id) { debugLogger.warn('Missing trash record id in lambda-less restore'); - await alertFn('Could not determine the trash record to remove.'); + alertFn('Could not determine the trash record to remove.'); return; } @@ -419,7 +420,7 @@ const BinOutlet = ({ ctx }: { ctx: RenderItemFormOutletCtx }) => { } catch (err) { setError(buildRestoreErrorPayload(err)); debugLogger.error('Restoration flow failed', err); - await alertFn('The record could not be restored!'); + alertFn('The record could not be restored!'); } finally { restorationInProgress.current = false; setLoading(false); diff --git a/record-bin/src/entrypoints/ConfigScreen.tsx b/record-bin/src/entrypoints/ConfigScreen.tsx index 511c361c..775eb28f 100644 --- a/record-bin/src/entrypoints/ConfigScreen.tsx +++ b/record-bin/src/entrypoints/ConfigScreen.tsx @@ -425,7 +425,7 @@ export default function ConfigScreen({ ctx }: { ctx: RenderConfigScreenCtx }) { webhookRemovalError, ); if (notifyOnFailure) { - await ctx.notice( + ctx.alert( "Runtime was saved as Lambda-less, but one or more managed 'đŸ—‘ Record Bin' webhooks could not be removed automatically. Remove them manually if needed.", ); } @@ -799,7 +799,7 @@ export default function ConfigScreen({ ctx }: { ctx: RenderConfigScreenCtx }) { setShowConnectionDetails(false); } - await ctx.alert('Could not disconnect the current lambda function.'); + ctx.alert('Could not disconnect the current lambda function.'); }, [applyWebhookSyncErrorState, canManageWebhooks, ctx, debugLogger], ); @@ -942,7 +942,7 @@ export default function ConfigScreen({ ctx }: { ctx: RenderConfigScreenCtx }) { }); } catch (saveError) { debugLogger.warn('Failed to save plugin settings', saveError); - await ctx.alert('Could not save plugin settings.'); + ctx.alert('Could not save plugin settings.'); } finally { setLoading(false); } From 7c573e10f32594729572f81a63007b1df30b862e Mon Sep 17 00:00:00 2001 From: "Roger (DatoCMS)" Date: Tue, 3 Nov 2026 13:01:56 -0800 Subject: [PATCH 4/5] feat(record-bin): announce the backup while a deletion waits for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting a record now shows "Record Bin is saving a copy of this record before it is deleted…" right away, so the few-second wait reads as work in progress. The toast is fired, not awaited: its promise settles only when it closes, and awaiting it would hold onBeforeItemsDestroy open. It closes on page change (a successful delete leaves the record) or after 15 s. Co-Authored-By: Claude Opus 5.5 --- .../src/utils/lambdaLessCapture.test.ts | 15 +++++++++ record-bin/src/utils/lambdaLessCapture.ts | 31 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/record-bin/src/utils/lambdaLessCapture.test.ts b/record-bin/src/utils/lambdaLessCapture.test.ts index 00393f6f..f680fc3a 100644 --- a/record-bin/src/utils/lambdaLessCapture.test.ts +++ b/record-bin/src/utils/lambdaLessCapture.test.ts @@ -38,6 +38,7 @@ const context = (token: string | undefined = 'token') => ({ cmaBaseUrl: 'https://example.invalid', plugin: { attributes: { parameters: {} } }, alert: vi.fn().mockResolvedValue(undefined), + customToast: vi.fn().mockResolvedValue(null), }); type Request = { method: string; @@ -200,6 +201,20 @@ describe('pre-deletion capture', () => { expect((await run(['one'], ctx)).allowDeletion).toBe(false); expect(ctx.alert).toHaveBeenCalledTimes(1); }); + it('announces the backup without waiting for the toast to close', async () => { + setup(); + const ctx = context(); + ctx.customToast.mockReturnValue(new Promise(() => {})); + expect((await run(['one', 'two'], ctx)).allowDeletion).toBe(true); + expect(ctx.customToast).toHaveBeenCalledTimes(1); + expect(ctx.customToast).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'notice', + message: expect.stringContaining('2 records'), + dismissOnPageChange: true, + }), + ); + }); it('blocks deletion without an access token', async () => { const ctx = context(); ctx.currentUserAccessToken = undefined; diff --git a/record-bin/src/utils/lambdaLessCapture.ts b/record-bin/src/utils/lambdaLessCapture.ts index 2f87448b..5c39142e 100644 --- a/record-bin/src/utils/lambdaLessCapture.ts +++ b/record-bin/src/utils/lambdaLessCapture.ts @@ -108,7 +108,12 @@ export type LambdaLessCaptureResult = { export type CaptureContext = Pick< OnBeforeItemsDestroyCtx, - 'plugin' | 'currentUserAccessToken' | 'environment' | 'cmaBaseUrl' | 'alert' + | 'plugin' + | 'currentUserAccessToken' + | 'environment' + | 'cmaBaseUrl' + | 'alert' + | 'customToast' >; async function archiveRecord( @@ -291,6 +296,7 @@ class CaptureOperation { this.ids.some((id) => typeof id !== 'string' || !id || id.includes(',')) ) throw new Error('Invalid deletion selection.'); + this.announceBackup(); const client = buildCaptureClient(this.ctx); const bin = await ensureRecordBinModel(client); await this.capture(client, bin.id); @@ -315,6 +321,29 @@ class CaptureOperation { } return this.result; } + + /** + * Tells the user why the deletion takes a few seconds. Fire-and-forget: the toast promise + * settles only when the toast closes, so awaiting it would hold the hook open. + */ + private announceBackup() { + const message = + this.ids.length === 1 + ? 'Record Bin is saving a copy of this record before it is deleted. This takes a few seconds.' + : `Record Bin is saving copies of ${this.ids.length} records before they are deleted. This takes a few seconds.`; + void this.ctx + .customToast({ + type: 'notice', + message, + // A successful deletion leaves the record page, which closes the toast. The timeout + // covers failures (which show their own alert) and bulk deletions from a list. + dismissOnPageChange: true, + dismissAfterTimeout: 15000, + }) + .catch((error) => + this.debugLogger.warn('Could not display backup notice', error), + ); + } } export async function captureDeletedItemsWithoutLambda( From 39277a5e0ab5ed16cdf266630a635b4b84d8f4e2 Mon Sep 17 00:00:00 2001 From: "Roger (DatoCMS)" Date: Tue, 3 Nov 2026 13:13:52 -0800 Subject: [PATCH 5/5] style(record-bin): apply Biome formatting to BinOutlet Co-Authored-By: Claude Opus 5.5 --- record-bin/src/entrypoints/BinOutlet.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/record-bin/src/entrypoints/BinOutlet.tsx b/record-bin/src/entrypoints/BinOutlet.tsx index d7532a0a..3683eb33 100644 --- a/record-bin/src/entrypoints/BinOutlet.tsx +++ b/record-bin/src/entrypoints/BinOutlet.tsx @@ -210,9 +210,7 @@ export const executeRestoreWithoutLambda = async ({ 'Restored record archive cleanup failed', parsedResponse.cleanupError, ); - alertFn( - 'The record was restored, but its archive could not be removed.', - ); + alertFn('The record was restored, but its archive could not be removed.'); } noticeFn('The record has been successfully restored!'); navigateTo(