@prenta/admin 2.7.0 → 2.8.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/design-system/tokens.css +17 -0
  3. package/design-system/tokens.json +35 -1
  4. package/design-system/tokens.scss +15 -0
  5. package/design-system/tokens.tailwind.cjs +14 -0
  6. package/dist/AdminRoot.d.ts.map +1 -1
  7. package/dist/AdminRoot.js +2 -2
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/lib/batch-wizard.test.d.ts +2 -0
  10. package/dist/__tests__/lib/batch-wizard.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/batch-wizard.test.js +144 -0
  12. package/dist/__tests__/lib/batch-wizard.test.js.map +1 -0
  13. package/dist/__tests__/lib/job-drawer.test.js +2 -0
  14. package/dist/__tests__/lib/job-drawer.test.js.map +1 -1
  15. package/dist/__tests__/lib/job-view.test.js +100 -3
  16. package/dist/__tests__/lib/job-view.test.js.map +1 -1
  17. package/dist/__tests__/lib/jobs-service.test.js +56 -2
  18. package/dist/__tests__/lib/jobs-service.test.js.map +1 -1
  19. package/dist/__tests__/views/appearance-chrome.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/appearance-chrome.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/appearance-chrome.render.test.js +688 -0
  22. package/dist/__tests__/views/appearance-chrome.render.test.js.map +1 -0
  23. package/dist/__tests__/views/header-footer-form.test.d.ts +2 -0
  24. package/dist/__tests__/views/header-footer-form.test.d.ts.map +1 -0
  25. package/dist/__tests__/views/header-footer-form.test.js +266 -0
  26. package/dist/__tests__/views/header-footer-form.test.js.map +1 -0
  27. package/dist/__tests__/views/jobs-drawer.render.test.js +442 -2
  28. package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -1
  29. package/dist/__tests__/views/jobs-list.render.test.js +7 -5
  30. package/dist/__tests__/views/jobs-list.render.test.js.map +1 -1
  31. package/dist/__tests__/views/jobs-wizard.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/jobs-wizard.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/jobs-wizard.render.test.js +588 -0
  34. package/dist/__tests__/views/jobs-wizard.render.test.js.map +1 -0
  35. package/dist/lib/api.d.ts +2 -0
  36. package/dist/lib/api.d.ts.map +1 -1
  37. package/dist/lib/api.js +8 -1
  38. package/dist/lib/api.js.map +1 -1
  39. package/dist/lib/batch-wizard.d.ts +84 -0
  40. package/dist/lib/batch-wizard.d.ts.map +1 -0
  41. package/dist/lib/batch-wizard.js +122 -0
  42. package/dist/lib/batch-wizard.js.map +1 -0
  43. package/dist/lib/chrome-service.d.ts +69 -0
  44. package/dist/lib/chrome-service.d.ts.map +1 -0
  45. package/dist/lib/chrome-service.js +108 -0
  46. package/dist/lib/chrome-service.js.map +1 -0
  47. package/dist/lib/job-view.d.ts +43 -0
  48. package/dist/lib/job-view.d.ts.map +1 -1
  49. package/dist/lib/job-view.js +57 -1
  50. package/dist/lib/job-view.js.map +1 -1
  51. package/dist/lib/jobs-service.d.ts +19 -1
  52. package/dist/lib/jobs-service.d.ts.map +1 -1
  53. package/dist/lib/jobs-service.js +41 -0
  54. package/dist/lib/jobs-service.js.map +1 -1
  55. package/dist/prenta-admin.css +1 -1
  56. package/dist/views/AppearanceChrome.d.ts +22 -4
  57. package/dist/views/AppearanceChrome.d.ts.map +1 -1
  58. package/dist/views/AppearanceChrome.js +451 -15
  59. package/dist/views/AppearanceChrome.js.map +1 -1
  60. package/dist/views/Jobs/CancelJobDialog.d.ts +7 -1
  61. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -1
  62. package/dist/views/Jobs/CancelJobDialog.js +13 -3
  63. package/dist/views/Jobs/CancelJobDialog.js.map +1 -1
  64. package/dist/views/Jobs/DrawerReview.d.ts +7 -0
  65. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -1
  66. package/dist/views/Jobs/DrawerReview.js +1 -1
  67. package/dist/views/Jobs/DrawerReview.js.map +1 -1
  68. package/dist/views/Jobs/JobDrawer.d.ts +23 -3
  69. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -1
  70. package/dist/views/Jobs/JobDrawer.js +30 -5
  71. package/dist/views/Jobs/JobDrawer.js.map +1 -1
  72. package/dist/views/Jobs/JobsTable.d.ts +4 -2
  73. package/dist/views/Jobs/JobsTable.d.ts.map +1 -1
  74. package/dist/views/Jobs/JobsTable.js +6 -5
  75. package/dist/views/Jobs/JobsTable.js.map +1 -1
  76. package/dist/views/Jobs/NewBatchJob.d.ts +34 -0
  77. package/dist/views/Jobs/NewBatchJob.d.ts.map +1 -0
  78. package/dist/views/Jobs/NewBatchJob.js +282 -0
  79. package/dist/views/Jobs/NewBatchJob.js.map +1 -0
  80. package/dist/views/Jobs/WorkerStrip.d.ts +3 -1
  81. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -1
  82. package/dist/views/Jobs/WorkerStrip.js +2 -2
  83. package/dist/views/Jobs/WorkerStrip.js.map +1 -1
  84. package/dist/views/Jobs/focus.d.ts +6 -0
  85. package/dist/views/Jobs/focus.d.ts.map +1 -1
  86. package/dist/views/Jobs/focus.js +10 -0
  87. package/dist/views/Jobs/focus.js.map +1 -1
  88. package/dist/views/Jobs.d.ts +12 -3
  89. package/dist/views/Jobs.d.ts.map +1 -1
  90. package/dist/views/Jobs.js +109 -31
  91. package/dist/views/Jobs.js.map +1 -1
  92. package/dist/views/header-footer/AnnouncementTab.d.ts +15 -0
  93. package/dist/views/header-footer/AnnouncementTab.d.ts.map +1 -0
  94. package/dist/views/header-footer/AnnouncementTab.js +43 -0
  95. package/dist/views/header-footer/AnnouncementTab.js.map +1 -0
  96. package/dist/views/header-footer/ChromeField.d.ts +25 -0
  97. package/dist/views/header-footer/ChromeField.d.ts.map +1 -0
  98. package/dist/views/header-footer/ChromeField.js +99 -0
  99. package/dist/views/header-footer/ChromeField.js.map +1 -0
  100. package/dist/views/header-footer/ChromePreview.d.ts +31 -0
  101. package/dist/views/header-footer/ChromePreview.d.ts.map +1 -0
  102. package/dist/views/header-footer/ChromePreview.js +195 -0
  103. package/dist/views/header-footer/ChromePreview.js.map +1 -0
  104. package/dist/views/header-footer/DeveloperTab.d.ts +17 -0
  105. package/dist/views/header-footer/DeveloperTab.d.ts.map +1 -0
  106. package/dist/views/header-footer/DeveloperTab.js +98 -0
  107. package/dist/views/header-footer/DeveloperTab.js.map +1 -0
  108. package/dist/views/header-footer/RegionTab.d.ts +27 -0
  109. package/dist/views/header-footer/RegionTab.d.ts.map +1 -0
  110. package/dist/views/header-footer/RegionTab.js +80 -0
  111. package/dist/views/header-footer/RegionTab.js.map +1 -0
  112. package/dist/views/header-footer/chrome-form.d.ts +137 -0
  113. package/dist/views/header-footer/chrome-form.d.ts.map +1 -0
  114. package/dist/views/header-footer/chrome-form.js +518 -0
  115. package/dist/views/header-footer/chrome-form.js.map +1 -0
  116. package/dist/views/header-footer/code-samples.d.ts +30 -0
  117. package/dist/views/header-footer/code-samples.d.ts.map +1 -0
  118. package/dist/views/header-footer/code-samples.js +127 -0
  119. package/dist/views/header-footer/code-samples.js.map +1 -0
  120. package/dist/views/header-footer/controls.d.ts +57 -0
  121. package/dist/views/header-footer/controls.d.ts.map +1 -0
  122. package/dist/views/header-footer/controls.js +43 -0
  123. package/dist/views/header-footer/controls.js.map +1 -0
  124. package/package.json +3 -3
  125. package/src/AdminRoot.tsx +16 -2
  126. package/src/__tests__/lib/batch-wizard.test.ts +199 -0
  127. package/src/__tests__/lib/job-drawer.test.ts +2 -0
  128. package/src/__tests__/lib/job-view.test.ts +123 -2
  129. package/src/__tests__/lib/jobs-service.test.ts +72 -3
  130. package/src/__tests__/views/appearance-chrome.render.test.tsx +818 -0
  131. package/src/__tests__/views/header-footer-form.test.ts +333 -0
  132. package/src/__tests__/views/jobs-drawer.render.test.tsx +508 -2
  133. package/src/__tests__/views/jobs-list.render.test.tsx +8 -5
  134. package/src/__tests__/views/jobs-wizard.render.test.tsx +707 -0
  135. package/src/lib/api.ts +12 -1
  136. package/src/lib/batch-wizard.ts +188 -0
  137. package/src/lib/chrome-service.ts +200 -0
  138. package/src/lib/job-view.ts +95 -1
  139. package/src/lib/jobs-service.ts +56 -1
  140. package/src/styles/prenta-tokens.css +17 -0
  141. package/src/styles/theme.css +15 -0
  142. package/src/views/AppearanceChrome.tsx +704 -46
  143. package/src/views/Jobs/CancelJobDialog.tsx +20 -2
  144. package/src/views/Jobs/DrawerReview.tsx +1 -1
  145. package/src/views/Jobs/JobDrawer.tsx +83 -3
  146. package/src/views/Jobs/JobsTable.tsx +11 -4
  147. package/src/views/Jobs/NewBatchJob.tsx +857 -0
  148. package/src/views/Jobs/WorkerStrip.tsx +16 -11
  149. package/src/views/Jobs/focus.ts +11 -0
  150. package/src/views/Jobs.tsx +163 -49
  151. package/src/views/header-footer/AnnouncementTab.tsx +231 -0
  152. package/src/views/header-footer/ChromeField.tsx +358 -0
  153. package/src/views/header-footer/ChromePreview.tsx +497 -0
  154. package/src/views/header-footer/DeveloperTab.tsx +331 -0
  155. package/src/views/header-footer/RegionTab.tsx +398 -0
  156. package/src/views/header-footer/chrome-form.ts +603 -0
  157. package/src/views/header-footer/code-samples.ts +146 -0
  158. package/src/views/header-footer/controls.tsx +166 -0
  159. package/dist/views/Jobs/placeholders.d.ts +0 -6
  160. package/dist/views/Jobs/placeholders.d.ts.map +0 -1
  161. package/dist/views/Jobs/placeholders.js +0 -20
  162. package/dist/views/Jobs/placeholders.js.map +0 -1
  163. package/src/views/Jobs/placeholders.tsx +0 -66
@@ -2,17 +2,22 @@ import { describe, expect, it } from 'vitest'
2
2
  import type { Job, JobWorkerStatus } from '@prenta/core'
3
3
 
4
4
  import {
5
+ canActOnJobs,
6
+ canRetryJob,
5
7
  countJobs,
6
8
  formatCreated,
7
9
  formatDuration,
8
10
  formatNextRun,
9
11
  formatSince,
10
12
  hasReviewAction,
13
+ jobActions,
11
14
  matchJob,
15
+ retryPoint,
12
16
  scheduleLabel,
13
17
  toJobRows,
14
18
  view,
15
19
  type JobRow,
20
+ type JobViewer,
16
21
  } from '../../lib/job-view.js'
17
22
 
18
23
  function row(overrides: Partial<JobRow>): JobRow {
@@ -35,8 +40,10 @@ function row(overrides: Partial<JobRow>): JobRow {
35
40
  pending: 0,
36
41
  items: false,
37
42
  review: null,
43
+ adminRetry: false,
38
44
  took: null,
39
45
  by: 'Brandon West',
46
+ byId: null,
40
47
  created: 'now',
41
48
  ...overrides,
42
49
  }
@@ -386,11 +393,125 @@ describe('toJobRows()', () => {
386
393
  review: null,
387
394
  created: 'Today 12:00',
388
395
  })
389
- expect(cron).toMatchObject({ cancelledBy: 'Dana Cole', by: 'Dana Cole' })
390
- expect(nightly).toMatchObject({ by: 'Nightly cron', created: 'Yesterday 12:00' })
396
+ expect(cron).toMatchObject({ cancelledBy: 'Dana Cole', by: 'Dana Cole', byId: 'u2' })
397
+ expect(nightly).toMatchObject({ by: 'Nightly cron', byId: null, created: 'Yesterday 12:00' })
391
398
  expect(review).toMatchObject({ pending: 2, items: true })
392
399
  expect(review && view(review).l).toBe('Needs review')
393
400
  })
401
+
402
+ it('counts a batch that hasn’t started by its frozen documents', () => {
403
+ const fresh = (type: string, ids: string[]) =>
404
+ apiJob({
405
+ type,
406
+ progress: { done: 0, total: 0, unit: 'documents', cursor: null, completed: 0 },
407
+ payload: { ...(apiJob({}).payload as object), documentIds: ids } as Job['payload'],
408
+ })
409
+ const [batch, scan, started] = toJobRows(
410
+ [
411
+ fresh('batch-ai', ['a', 'b', 'c']),
412
+ fresh('seo-scan', ['a']),
413
+ apiJob({ progress: { done: 2, total: 5, unit: 'documents', cursor: '2', completed: 2 } }),
414
+ ],
415
+ NOW,
416
+ null,
417
+ )
418
+ expect([batch?.total, scan?.total, started?.total]).toEqual([3, 0, 5])
419
+ })
420
+
421
+ it('makes only a stored "Apply automatically" admin-only to retry, as cms-core does', () => {
422
+ const batch = (review: 'review' | 'auto', origin?: 'ai-batch') =>
423
+ apiJob({
424
+ payload: {
425
+ ...(apiJob({}).payload as object),
426
+ review,
427
+ ...(origin ? { origin } : {}),
428
+ } as Job['payload'],
429
+ })
430
+ const [review, auto, bulk] = toJobRows(
431
+ [batch('review'), batch('auto'), batch('auto', 'ai-batch')],
432
+ NOW,
433
+ null,
434
+ )
435
+ expect([review?.adminRetry, auto?.adminRetry, bulk?.adminRetry]).toEqual([false, true, false])
436
+ // The bulk action still reads as applying directly, for the cancel copy.
437
+ expect(bulk?.review).toBe('auto')
438
+ })
439
+ })
440
+
441
+ const as = (role: string | null, userId: string | null = 'u1'): JobViewer => ({ role, userId })
442
+
443
+ describe('job actions (P-19)', () => {
444
+ it('lets the roles core calls writers run and cancel, and nobody else', () => {
445
+ for (const role of ['OWNER', 'ADMIN', 'EDITOR', 'AUTHOR', 'editor'])
446
+ expect(canActOnJobs(as(role))).toBe(true)
447
+ for (const role of ['VIEWER', 'CLIENT', 'SOMETHING_NEW', null])
448
+ expect(canActOnJobs(as(role))).toBe(false)
449
+ })
450
+
451
+ it('retries a batch for an admin, or for the editor who queued it unless it was "Apply automatically"', () => {
452
+ const mine = row({ review: 'review', byId: 'u1' })
453
+ const theirs = row({ review: 'review', byId: 'u2' })
454
+ const auto = row({ review: 'auto', adminRetry: true, byId: 'u1' })
455
+ // A list's bulk action (`/ai/batch`): it applies directly, but any editor may queue it.
456
+ const legacy = row({ review: 'auto', adminRetry: false, byId: 'u1' })
457
+ const scan = row({ type: 'seo-scan', review: null, byId: null })
458
+ expect([mine, theirs, auto, legacy, scan].map((j) => canRetryJob(j, as('ADMIN')))).toEqual([
459
+ true,
460
+ true,
461
+ true,
462
+ true,
463
+ true,
464
+ ])
465
+ expect([mine, theirs, auto, legacy, scan].map((j) => canRetryJob(j, as('EDITOR')))).toEqual([
466
+ true,
467
+ false,
468
+ false,
469
+ true,
470
+ true,
471
+ ])
472
+ expect(canRetryJob(mine, as('VIEWER'))).toBe(false)
473
+ expect(canRetryJob(row({ byId: null }), as('EDITOR', null))).toBe(false)
474
+ })
475
+
476
+ it('offers the drawer footer’s actions per state (jobs-drawer.jsx `.jd-f`)', () => {
477
+ const admin = as('ADMIN')
478
+ const actions = (overrides: Partial<JobRow>) => jobActions(row(overrides), admin)
479
+ expect(actions({ state: 'running' })).toEqual({ cancel: true, retry: false, retryNow: false })
480
+ expect(actions({ state: 'running', cancelling: true })).toEqual({
481
+ cancel: false,
482
+ retry: false,
483
+ retryNow: false,
484
+ })
485
+ expect(actions({ state: 'queued', error: 'Boom' })).toEqual({
486
+ cancel: true,
487
+ retry: false,
488
+ retryNow: true,
489
+ })
490
+ // A retry clears a cancel, so a cancelling job doesn't offer it.
491
+ expect(actions({ state: 'queued', error: 'Boom', cancelling: true }).retryNow).toBe(false)
492
+ // A stale lock: the server still holds it as running and would refuse a retry.
493
+ expect(
494
+ actions({ state: 'queued', status: 'running', error: 'Worker stopped mid-slice' }),
495
+ ).toEqual({ cancel: true, retry: false, retryNow: false })
496
+ expect(actions({ state: 'dead', error: 'Boom' })).toEqual({
497
+ cancel: false,
498
+ retry: true,
499
+ retryNow: false,
500
+ })
501
+ for (const state of ['succeeded', 'cancelled'] as const)
502
+ expect(actions({ state })).toEqual({ cancel: false, retry: false, retryNow: false })
503
+ expect(jobActions(row({ state: 'dead' }), as('VIEWER'))).toEqual({
504
+ cancel: false,
505
+ retry: false,
506
+ retryNow: false,
507
+ })
508
+ })
509
+
510
+ it('says where a retry picks up, in pages for an SEO scan', () => {
511
+ expect(retryPoint({ type: 'batch-ai', done: 7 })).toBe('document 8')
512
+ expect(retryPoint({ type: 'content-health', done: 0 })).toBe('document 1')
513
+ expect(retryPoint({ type: 'seo-scan', done: 100 })).toBe('page 101')
514
+ })
394
515
  })
395
516
 
396
517
  describe('formatting', () => {
@@ -3,17 +3,86 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
  /*
4
4
  * The drawer's calls in the Jobs service: the review page query, the
5
5
  * decision envelope's `documentStatus` and `notice`, and reading why a
6
- * decision was refused.
6
+ * decision was refused. And the batch wizard's: estimate, preview, create.
7
7
  */
8
8
 
9
9
  const cmsApi = vi.hoisted(() => vi.fn())
10
10
  vi.mock('../../lib/api.js', () => ({ cmsApi, getApiBase: () => '/api/cms' }))
11
11
 
12
- const { decideReviewItem, listReviewItems, reviewConflict } =
13
- await import('../../lib/jobs-service.js')
12
+ const {
13
+ createBatchAiJob,
14
+ decideReviewItem,
15
+ estimateBatchAi,
16
+ listReviewItems,
17
+ previewBatchAi,
18
+ reviewConflict,
19
+ } = await import('../../lib/jobs-service.js')
14
20
 
15
21
  beforeEach(() => cmsApi.mockReset())
16
22
 
23
+ describe('batch AI', () => {
24
+ const request = {
25
+ operation: 'custom' as const,
26
+ source: 'posts',
27
+ statusFilter: 'all' as const,
28
+ review: 'review' as const,
29
+ onApprove: 'draft' as const,
30
+ instruction: 'Translate it & keep names',
31
+ field: 'excerpt' as const,
32
+ filter: { documentIds: ['a', 'b'] },
33
+ }
34
+
35
+ it('estimates with the request as the query, ids comma-separated', async () => {
36
+ const value = { count: 2, sample: [], tokens: 1800, minutes: 2, pctOfMonthlyLimit: null }
37
+ cmsApi.mockResolvedValue({ data: value, status: 200 })
38
+ expect(await estimateBatchAi(request)).toEqual({ ok: true, value })
39
+ const [endpoint] = cmsApi.mock.calls[0]!
40
+ expect(endpoint).toBe(
41
+ '/jobs/batch-ai/estimate?operation=custom&source=posts&statusFilter=all&field=excerpt&instruction=Translate+it+%26+keep+names&documentIds=a%2Cb',
42
+ )
43
+ cmsApi.mockClear()
44
+ await estimateBatchAi({
45
+ ...request,
46
+ operation: 'meta',
47
+ field: undefined,
48
+ instruction: null,
49
+ filter: undefined,
50
+ })
51
+ expect(cmsApi.mock.calls[0]![0]).toBe(
52
+ '/jobs/batch-ai/estimate?operation=meta&source=posts&statusFilter=all',
53
+ )
54
+ })
55
+
56
+ it('previews with a POST, reading the items', async () => {
57
+ const items = [{ documentId: 'a', title: 'A', after: 'New text.' }]
58
+ cmsApi.mockResolvedValue({ data: { items }, status: 200 })
59
+ expect(await previewBatchAi(request)).toEqual({ ok: true, value: items })
60
+ expect(cmsApi.mock.calls[0]![0]).toBe('/jobs/batch-ai/preview')
61
+ expect(cmsApi.mock.calls[0]![1]).toMatchObject({
62
+ method: 'POST',
63
+ body: JSON.stringify(request),
64
+ })
65
+ })
66
+
67
+ it('passes a spent cap on', async () => {
68
+ cmsApi.mockResolvedValue({ error: 'AI limit reached', status: 429 })
69
+ expect(await previewBatchAi(request)).toEqual({
70
+ ok: false,
71
+ error: 'AI limit reached',
72
+ status: 429,
73
+ })
74
+ })
75
+
76
+ it('creates with a POST and returns the job', async () => {
77
+ cmsApi.mockResolvedValue({ data: { id: 'job_420' }, status: 201 })
78
+ expect(await createBatchAiJob(request)).toEqual({ ok: true, value: { id: 'job_420' } })
79
+ expect(cmsApi).toHaveBeenCalledWith('/jobs/batch-ai', {
80
+ method: 'POST',
81
+ body: JSON.stringify(request),
82
+ })
83
+ })
84
+ })
85
+
17
86
  describe('listReviewItems', () => {
18
87
  it('asks for 20 at a time, from a cursor', async () => {
19
88
  cmsApi.mockResolvedValue({ data: { items: [], nextCursor: null }, status: 200 })