@growth-labs/cms 0.8.7 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/fronts-publish.d.ts.map +1 -1
- package/dist/engine/fronts-publish.js +44 -8
- package/dist/engine/fronts-publish.js.map +1 -1
- package/dist/engine/publisher.d.ts +3 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +21 -11
- package/dist/engine/publisher.js.map +1 -1
- package/dist/migration-vendor.d.ts.map +1 -1
- package/dist/migration-vendor.js +129 -12
- package/dist/migration-vendor.js.map +1 -1
- package/dist/routes/content.d.ts +1 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +166 -10
- package/dist/routes/content.js.map +1 -1
- package/dist/ui/api-error.d.ts +17 -0
- package/dist/ui/api-error.d.ts.map +1 -0
- package/dist/ui/api-error.js +46 -0
- package/dist/ui/api-error.js.map +1 -0
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +5 -4
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/editor-media-upload.d.ts.map +1 -1
- package/dist/ui/editor/editor-media-upload.js +2 -1
- package/dist/ui/editor/editor-media-upload.js.map +1 -1
- package/dist/ui/inspector/PublishTab.d.ts.map +1 -1
- package/dist/ui/inspector/PublishTab.js +2 -1
- package/dist/ui/inspector/PublishTab.js.map +1 -1
- package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
- package/dist/ui/screens/LibraryScreen.js +4 -3
- package/dist/ui/screens/LibraryScreen.js.map +1 -1
- package/dist/ui/screens/SettingsScreen.d.ts.map +1 -1
- package/dist/ui/screens/SettingsScreen.js +14 -13
- package/dist/ui/screens/SettingsScreen.js.map +1 -1
- package/dist/ui/screens/TopicsScreen.d.ts.map +1 -1
- package/dist/ui/screens/TopicsScreen.js +3 -2
- package/dist/ui/screens/TopicsScreen.js.map +1 -1
- package/dist/ui/screens/author-recovery.d.ts.map +1 -1
- package/dist/ui/screens/author-recovery.js +2 -1
- package/dist/ui/screens/author-recovery.js.map +1 -1
- package/dist/ui/screens/media-upload.d.ts.map +1 -1
- package/dist/ui/screens/media-upload.js +2 -1
- package/dist/ui/screens/media-upload.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/fronts-publish.ts +45 -6
- package/src/engine/publisher.ts +24 -11
- package/src/migration-vendor.ts +146 -14
- package/src/routes/content.ts +188 -18
- package/src/ui/api-error.ts +55 -0
- package/src/ui/editor/ContentForm.tsx +8 -13
- package/src/ui/editor/editor-media-upload.ts +2 -1
- package/src/ui/inspector/PublishTab.tsx +2 -1
- package/src/ui/screens/LibraryScreen.tsx +4 -3
- package/src/ui/screens/SettingsScreen.tsx +14 -13
- package/src/ui/screens/TopicsScreen.tsx +3 -2
- package/src/ui/screens/author-recovery.ts +3 -1
- package/src/ui/screens/media-upload.ts +3 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Every admin route answers a failed zod parse the same way:
|
|
2
|
+
//
|
|
3
|
+
// { error: 'Invalid request', details: { fieldErrors: { field: ['why'] }, formErrors: [...] } }
|
|
4
|
+
//
|
|
5
|
+
// The screens used to render only `error`, so an editor saw a bare
|
|
6
|
+
// "Invalid request" and had no way to learn which field was wrong or why. That
|
|
7
|
+
// stranded every video upload for over two hours when a source-URL rule started
|
|
8
|
+
// refusing the share links editors had always pasted: the message that said
|
|
9
|
+
// exactly what to change was in the response the whole time, unrendered.
|
|
10
|
+
//
|
|
11
|
+
// Read the field messages here, once, so every screen surfaces them.
|
|
12
|
+
|
|
13
|
+
export interface ApiErrorBody {
|
|
14
|
+
error?: string
|
|
15
|
+
details?: {
|
|
16
|
+
fieldErrors?: Record<string, string[] | undefined>
|
|
17
|
+
formErrors?: string[]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MAX_RENDERED_FIELD_MESSAGES = 4
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the message an editor should see from a failed admin-API response.
|
|
25
|
+
*
|
|
26
|
+
* Prefers the specific over the generic: field messages first (prefixed with
|
|
27
|
+
* their field so the editor knows where to look), then form-level messages,
|
|
28
|
+
* then the top-level `error`, then the caller's fallback. Never returns an
|
|
29
|
+
* empty string.
|
|
30
|
+
*/
|
|
31
|
+
export function apiErrorMessage(body: ApiErrorBody | null | undefined, fallback: string): string {
|
|
32
|
+
const top = typeof body?.error === 'string' ? body.error.trim() : ''
|
|
33
|
+
const details = body?.details
|
|
34
|
+
|
|
35
|
+
const fieldMessages: string[] = []
|
|
36
|
+
for (const [field, messages] of Object.entries(details?.fieldErrors ?? {})) {
|
|
37
|
+
for (const message of messages ?? []) {
|
|
38
|
+
const text = typeof message === 'string' ? message.trim() : ''
|
|
39
|
+
if (text) fieldMessages.push(`${field}: ${text}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const message of details?.formErrors ?? []) {
|
|
43
|
+
const text = typeof message === 'string' ? message.trim() : ''
|
|
44
|
+
if (text) fieldMessages.push(text)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (fieldMessages.length === 0) return top || fallback
|
|
48
|
+
|
|
49
|
+
const shown = fieldMessages.slice(0, MAX_RENDERED_FIELD_MESSAGES)
|
|
50
|
+
const hidden = fieldMessages.length - shown.length
|
|
51
|
+
const detail = shown.join('; ') + (hidden > 0 ? ` (+${hidden} more)` : '')
|
|
52
|
+
// Keep the top-level text when it adds something beyond the generic prefix
|
|
53
|
+
// every zod failure shares.
|
|
54
|
+
return top && top !== 'Invalid request' ? `${top}: ${detail}` : detail
|
|
55
|
+
}
|
|
@@ -23,6 +23,7 @@ import { type ContentMetadata, parseContentMetadata } from '../../engine/content
|
|
|
23
23
|
import { countWords, estimateReadTime } from '../../engine/publisher.js'
|
|
24
24
|
import type { ContentMetadataSelectField, PrimaryTopicControl } from '../../integration/options.js'
|
|
25
25
|
import type { ContentStatus, ContentType, FrontsPublishStatus } from '../../schema/types.js'
|
|
26
|
+
import { type ApiErrorBody, apiErrorMessage } from '../api-error.js'
|
|
26
27
|
import { Icon } from '../icons.js'
|
|
27
28
|
import { uploadMediaAsset } from '../screens/media-upload.js'
|
|
28
29
|
import { AiAssistPanel } from './AiAssistPanel.js'
|
|
@@ -312,10 +313,8 @@ export function ContentForm({
|
|
|
312
313
|
try {
|
|
313
314
|
const res = await fetch(`/admin/api/content/${docId}`)
|
|
314
315
|
if (!res.ok) {
|
|
315
|
-
const err = (await res.json().catch(() => ({ error: res.statusText }))) as
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
throw new Error(err.error ?? 'Could not load content')
|
|
316
|
+
const err = (await res.json().catch(() => ({ error: res.statusText }))) as ApiErrorBody
|
|
317
|
+
throw new Error(apiErrorMessage(err, 'Could not load content'))
|
|
319
318
|
}
|
|
320
319
|
const data = (await res.json()) as ExistingContentResponse
|
|
321
320
|
const next = contentResponseToFormState(data, contentType, docId)
|
|
@@ -381,10 +380,8 @@ export function ContentForm({
|
|
|
381
380
|
method: 'POST',
|
|
382
381
|
})
|
|
383
382
|
if (!res.ok) {
|
|
384
|
-
const err = (await res.json().catch(() => ({ error: res.statusText }))) as
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
throw new Error(err.error ?? 'Foundry dispatch failed')
|
|
383
|
+
const err = (await res.json().catch(() => ({ error: res.statusText }))) as ApiErrorBody
|
|
384
|
+
throw new Error(apiErrorMessage(err, 'Foundry dispatch failed'))
|
|
388
385
|
}
|
|
389
386
|
queuedVideoSources.current.add(queueKey)
|
|
390
387
|
setForm((prev) => ({
|
|
@@ -434,13 +431,11 @@ export function ContentForm({
|
|
|
434
431
|
body: JSON.stringify(buildContentCreatePayload(contentType, draft)),
|
|
435
432
|
})
|
|
436
433
|
if (!res.ok) {
|
|
437
|
-
const err = (await res.json().catch(() => ({ error: res.statusText }))) as
|
|
438
|
-
error?: string
|
|
439
|
-
}
|
|
434
|
+
const err = (await res.json().catch(() => ({ error: res.statusText }))) as ApiErrorBody
|
|
440
435
|
setForm((prev) => ({
|
|
441
436
|
...prev,
|
|
442
437
|
saving: false,
|
|
443
|
-
saveError: err
|
|
438
|
+
saveError: apiErrorMessage(err, 'Save failed'),
|
|
444
439
|
}))
|
|
445
440
|
return
|
|
446
441
|
}
|
|
@@ -490,7 +485,7 @@ export function ContentForm({
|
|
|
490
485
|
setForm((prev) => ({
|
|
491
486
|
...prev,
|
|
492
487
|
saving: false,
|
|
493
|
-
saveError: err
|
|
488
|
+
saveError: apiErrorMessage(err, 'Save failed'),
|
|
494
489
|
}))
|
|
495
490
|
return
|
|
496
491
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
1
2
|
import { slugifyTitle } from './content-payload.js'
|
|
2
3
|
|
|
3
4
|
type FetchLike = (url: string, init?: RequestInit) => Promise<Response>
|
|
@@ -17,7 +18,7 @@ export async function uploadEditorImage(
|
|
|
17
18
|
})
|
|
18
19
|
if (!res.ok) {
|
|
19
20
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as { error?: string }
|
|
20
|
-
throw new Error(err
|
|
21
|
+
throw new Error(apiErrorMessage(err, 'Image upload failed'))
|
|
21
22
|
}
|
|
22
23
|
const body = (await res.json()) as { url?: string; publicUrl?: string }
|
|
23
24
|
const url = body.url || body.publicUrl
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useCallback, useEffect, useState } from 'react'
|
|
14
14
|
import type { ContentStatus } from '../../schema/types.js'
|
|
15
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
15
16
|
import { Icon } from '../icons.js'
|
|
16
17
|
import { Field } from './Field.js'
|
|
17
18
|
import {
|
|
@@ -246,7 +247,7 @@ export function PublishTab({
|
|
|
246
247
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
|
|
247
248
|
error?: string
|
|
248
249
|
}
|
|
249
|
-
throw new Error(err
|
|
250
|
+
throw new Error(apiErrorMessage(err, 'Could not add author'))
|
|
250
251
|
}
|
|
251
252
|
const created = (await res.json()) as CreatedAuthorResponse
|
|
252
253
|
const nextAuthor = { id: created.id, name }
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
|
14
14
|
import type { ContentStatus, ContentType, FrontsPublishStatus } from '../../schema/types.js'
|
|
15
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
15
16
|
import { buildSharePrefill, ShareModal } from '../components/ShareModal.js'
|
|
16
17
|
import { Icon } from '../icons.js'
|
|
17
18
|
import { useWorkspace } from '../workspace-context.js'
|
|
@@ -1139,7 +1140,7 @@ function BulkBar({
|
|
|
1139
1140
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
|
|
1140
1141
|
error?: string
|
|
1141
1142
|
}
|
|
1142
|
-
alert(`Bulk ${op} failed: ${err
|
|
1143
|
+
alert(`Bulk ${op} failed: ${apiErrorMessage(err, res.statusText)}`)
|
|
1143
1144
|
setBusy(false)
|
|
1144
1145
|
return
|
|
1145
1146
|
}
|
|
@@ -1241,7 +1242,7 @@ function ImportModal({
|
|
|
1241
1242
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
|
|
1242
1243
|
error?: string
|
|
1243
1244
|
}
|
|
1244
|
-
setError(err
|
|
1245
|
+
setError(apiErrorMessage(err, 'Parse failed'))
|
|
1245
1246
|
return
|
|
1246
1247
|
}
|
|
1247
1248
|
const data = (await res.json()) as ParseResult
|
|
@@ -1266,7 +1267,7 @@ function ImportModal({
|
|
|
1266
1267
|
const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
|
|
1267
1268
|
error?: string
|
|
1268
1269
|
}
|
|
1269
|
-
setError(err
|
|
1270
|
+
setError(apiErrorMessage(err, 'Confirm failed'))
|
|
1270
1271
|
setStep('preview')
|
|
1271
1272
|
return
|
|
1272
1273
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { useCallback, useEffect, useReducer, useState } from 'react'
|
|
25
25
|
import type { ApiKeyListEntry } from '../../engine/api-keys.js'
|
|
26
26
|
import { WEBHOOK_EVENTS, type WebhookRecord } from '../../engine/webhooks.js'
|
|
27
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
27
28
|
import { Icon } from '../icons.js'
|
|
28
29
|
import { useWorkspace } from '../workspace-context.js'
|
|
29
30
|
import {
|
|
@@ -552,7 +553,7 @@ function TeamSettings({ currentSubject }: { currentSubject: string | null }) {
|
|
|
552
553
|
const res = await fetch('/admin/api/settings/members')
|
|
553
554
|
if (!res.ok) {
|
|
554
555
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
555
|
-
dispatch({ type: 'fetch-error', error: body
|
|
556
|
+
dispatch({ type: 'fetch-error', error: apiErrorMessage(body, `HTTP ${res.status}`) })
|
|
556
557
|
return
|
|
557
558
|
}
|
|
558
559
|
const body = (await res.json()) as {
|
|
@@ -600,7 +601,7 @@ function TeamSettings({ currentSubject }: { currentSubject: string | null }) {
|
|
|
600
601
|
})
|
|
601
602
|
if (!res.ok) {
|
|
602
603
|
const body = (await res.json().catch(() => ({}))) as { error?: string; reason?: string }
|
|
603
|
-
alert(body.reason ?? body
|
|
604
|
+
alert(body.reason ?? apiErrorMessage(body, `Failed to change role (HTTP ${res.status})`))
|
|
604
605
|
return
|
|
605
606
|
}
|
|
606
607
|
dispatch({ type: 'update-role', userId, role: newRole })
|
|
@@ -629,7 +630,7 @@ function TeamSettings({ currentSubject }: { currentSubject: string | null }) {
|
|
|
629
630
|
error?: string
|
|
630
631
|
}
|
|
631
632
|
if (!res.ok) {
|
|
632
|
-
setInviteError(body
|
|
633
|
+
setInviteError(apiErrorMessage(body, `HTTP ${res.status}`))
|
|
633
634
|
return
|
|
634
635
|
}
|
|
635
636
|
setInviteSuccess(
|
|
@@ -905,7 +906,7 @@ function ApiSettings() {
|
|
|
905
906
|
const res = await fetch('/admin/api/settings/api-keys')
|
|
906
907
|
if (!res.ok) {
|
|
907
908
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
908
|
-
dispatch({ type: 'fetch-error', error: body
|
|
909
|
+
dispatch({ type: 'fetch-error', error: apiErrorMessage(body, `HTTP ${res.status}`) })
|
|
909
910
|
return
|
|
910
911
|
}
|
|
911
912
|
const body = (await res.json()) as { apiKeys: ApiKeyListEntry[] }
|
|
@@ -937,7 +938,7 @@ function ApiSettings() {
|
|
|
937
938
|
error?: string
|
|
938
939
|
}
|
|
939
940
|
if (!res.ok) {
|
|
940
|
-
setCreateError(body
|
|
941
|
+
setCreateError(apiErrorMessage(body, `HTTP ${res.status}`))
|
|
941
942
|
return
|
|
942
943
|
}
|
|
943
944
|
// Show the one-time reveal modal
|
|
@@ -965,7 +966,7 @@ function ApiSettings() {
|
|
|
965
966
|
dispatch({ type: 'revoked', id })
|
|
966
967
|
} else {
|
|
967
968
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
968
|
-
alert(body
|
|
969
|
+
alert(apiErrorMessage(body, `Failed to revoke (HTTP ${res.status})`))
|
|
969
970
|
}
|
|
970
971
|
} catch (err) {
|
|
971
972
|
alert(String(err))
|
|
@@ -1158,7 +1159,7 @@ function WebhooksSettings() {
|
|
|
1158
1159
|
const res = await fetch('/admin/api/settings/webhooks')
|
|
1159
1160
|
if (!res.ok) {
|
|
1160
1161
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1161
|
-
dispatch({ type: 'fetch-error', error: body
|
|
1162
|
+
dispatch({ type: 'fetch-error', error: apiErrorMessage(body, `HTTP ${res.status}`) })
|
|
1162
1163
|
return
|
|
1163
1164
|
}
|
|
1164
1165
|
const body = (await res.json()) as { webhooks: WebhookRecord[] }
|
|
@@ -1185,7 +1186,7 @@ function WebhooksSettings() {
|
|
|
1185
1186
|
})
|
|
1186
1187
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1187
1188
|
if (!res.ok) {
|
|
1188
|
-
setCreateError(body
|
|
1189
|
+
setCreateError(apiErrorMessage(body, `HTTP ${res.status}`))
|
|
1189
1190
|
return
|
|
1190
1191
|
}
|
|
1191
1192
|
setNewUrl('')
|
|
@@ -1206,7 +1207,7 @@ function WebhooksSettings() {
|
|
|
1206
1207
|
})
|
|
1207
1208
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1208
1209
|
if (!res.ok) {
|
|
1209
|
-
alert(body
|
|
1210
|
+
alert(apiErrorMessage(body, `Test failed (HTTP ${res.status})`))
|
|
1210
1211
|
return
|
|
1211
1212
|
}
|
|
1212
1213
|
dispatch({ type: 'update-status', id, status: 'test' })
|
|
@@ -1228,7 +1229,7 @@ function WebhooksSettings() {
|
|
|
1228
1229
|
dispatch({ type: 'deleted', id })
|
|
1229
1230
|
} else {
|
|
1230
1231
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1231
|
-
alert(body
|
|
1232
|
+
alert(apiErrorMessage(body, `Failed to delete (HTTP ${res.status})`))
|
|
1232
1233
|
}
|
|
1233
1234
|
} catch (err) {
|
|
1234
1235
|
alert(String(err))
|
|
@@ -1481,7 +1482,7 @@ function IntegrationsSettings() {
|
|
|
1481
1482
|
.then(async (res) => {
|
|
1482
1483
|
if (!res.ok) {
|
|
1483
1484
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1484
|
-
setError(body
|
|
1485
|
+
setError(apiErrorMessage(body, `HTTP ${res.status}`))
|
|
1485
1486
|
return
|
|
1486
1487
|
}
|
|
1487
1488
|
const body = (await res.json()) as IntegrationsData
|
|
@@ -1600,7 +1601,7 @@ function DomainsSettings() {
|
|
|
1600
1601
|
.then(async (res) => {
|
|
1601
1602
|
if (!res.ok) {
|
|
1602
1603
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1603
|
-
setError(body
|
|
1604
|
+
setError(apiErrorMessage(body, `HTTP ${res.status}`))
|
|
1604
1605
|
return
|
|
1605
1606
|
}
|
|
1606
1607
|
const body = (await res.json()) as DomainsData
|
|
@@ -1728,7 +1729,7 @@ export function SettingsScreen() {
|
|
|
1728
1729
|
})
|
|
1729
1730
|
if (!res.ok) {
|
|
1730
1731
|
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
1731
|
-
alert(body
|
|
1732
|
+
alert(apiErrorMessage(body, `Failed to save settings (HTTP ${res.status})`))
|
|
1732
1733
|
return
|
|
1733
1734
|
}
|
|
1734
1735
|
// Re-fetch to ensure the UI is in sync with server state
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// confirm dialog. NO classes outside the broadsheet vocabulary.
|
|
31
31
|
|
|
32
32
|
import { useCallback, useEffect, useReducer, useState } from 'react'
|
|
33
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
33
34
|
import { Icon } from '../icons.js'
|
|
34
35
|
import {
|
|
35
36
|
buildTopicCreateBody,
|
|
@@ -191,7 +192,7 @@ export function TopicsScreen() {
|
|
|
191
192
|
error?: string
|
|
192
193
|
}
|
|
193
194
|
if (!res.ok || !body.topic) {
|
|
194
|
-
setCreateError(body
|
|
195
|
+
setCreateError(apiErrorMessage(body, `${res.status} ${res.statusText}`))
|
|
195
196
|
return
|
|
196
197
|
}
|
|
197
198
|
dispatchTopics({ type: 'create-ok', topic: body.topic })
|
|
@@ -219,7 +220,7 @@ export function TopicsScreen() {
|
|
|
219
220
|
error?: string
|
|
220
221
|
}
|
|
221
222
|
if (!res.ok || !body.topic) {
|
|
222
|
-
setRenameError(body
|
|
223
|
+
setRenameError(apiErrorMessage(body, `${res.status} ${res.statusText}`))
|
|
223
224
|
return
|
|
224
225
|
}
|
|
225
226
|
dispatchTopics({ type: 'rename-ok', fromSlug: pendingRename.slug, topic: body.topic })
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { apiErrorMessage } from '../api-error.js'
|
|
2
|
+
|
|
1
3
|
interface AuthorWriteFailureBody {
|
|
2
4
|
error?: string
|
|
3
5
|
errorClass?: string
|
|
@@ -17,7 +19,7 @@ export function formatAuthorWriteFailure(
|
|
|
17
19
|
body.author?.id ? `author id: ${body.author.id}` : '',
|
|
18
20
|
body.author?.slug ? `slug: ${body.author.slug}` : '',
|
|
19
21
|
].filter(Boolean)
|
|
20
|
-
const message = body
|
|
22
|
+
const message = apiErrorMessage(body, 'Author synchronization failed')
|
|
21
23
|
const classifiedMessage = message.includes(body.errorClass)
|
|
22
24
|
? message
|
|
23
25
|
: `${body.errorClass}: ${message}`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ApiErrorBody, apiErrorMessage } from '../api-error.js'
|
|
1
2
|
export type MediaUploadKind = 'image' | 'video' | 'podcast'
|
|
2
3
|
|
|
3
4
|
export interface MediaUploadResult {
|
|
@@ -97,9 +98,9 @@ async function readJson<T>(response: Response): Promise<T> {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
async function assertOk<T>(response: Response, fallback: string): Promise<T> {
|
|
100
|
-
const body = await readJson<T &
|
|
101
|
+
const body = await readJson<T & ApiErrorBody>(response)
|
|
101
102
|
if (!response.ok) {
|
|
102
|
-
throw new Error(body
|
|
103
|
+
throw new Error(apiErrorMessage(body, fallback))
|
|
103
104
|
}
|
|
104
105
|
return body
|
|
105
106
|
}
|