@prenta/admin 1.6.1 → 1.7.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 (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/__tests__/components/seo-editor-pane.render.test.js +2 -1
  3. package/dist/__tests__/components/seo-editor-pane.render.test.js.map +1 -1
  4. package/dist/__tests__/components/seo-issue-fix-panel.render.test.d.ts +2 -0
  5. package/dist/__tests__/components/seo-issue-fix-panel.render.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/seo-issue-fix-panel.render.test.js +60 -0
  7. package/dist/__tests__/components/seo-issue-fix-panel.render.test.js.map +1 -0
  8. package/dist/__tests__/components/seo-linking-section.render.test.d.ts +2 -0
  9. package/dist/__tests__/components/seo-linking-section.render.test.d.ts.map +1 -0
  10. package/dist/__tests__/components/seo-linking-section.render.test.js +94 -0
  11. package/dist/__tests__/components/seo-linking-section.render.test.js.map +1 -0
  12. package/dist/__tests__/lib/collection-routes.test.js +8 -1
  13. package/dist/__tests__/lib/collection-routes.test.js.map +1 -1
  14. package/dist/__tests__/lib/page-editor-service.test.js +7 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  16. package/dist/__tests__/lib/seo-service-errors.test.js +24 -1
  17. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
  18. package/dist/__tests__/lib/workflow-chrome.test.js +4 -0
  19. package/dist/__tests__/lib/workflow-chrome.test.js.map +1 -1
  20. package/dist/__tests__/views/editor-top-bar.render.test.js +5 -0
  21. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  22. package/dist/__tests__/views/seo-content-pane.render.test.js +1 -0
  23. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
  24. package/dist/components/seo/SeoEditorPane.d.ts +2 -1
  25. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  26. package/dist/components/seo/SeoEditorPane.js +2 -2
  27. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  28. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  29. package/dist/components/seo/SeoIssueFixPanel.js +32 -8
  30. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  31. package/dist/components/seo/SeoLinkingSection.d.ts +5 -1
  32. package/dist/components/seo/SeoLinkingSection.d.ts.map +1 -1
  33. package/dist/components/seo/SeoLinkingSection.js +50 -3
  34. package/dist/components/seo/SeoLinkingSection.js.map +1 -1
  35. package/dist/lib/collection-routes.d.ts +2 -0
  36. package/dist/lib/collection-routes.d.ts.map +1 -1
  37. package/dist/lib/collection-routes.js +5 -0
  38. package/dist/lib/collection-routes.js.map +1 -1
  39. package/dist/lib/seo-service.d.ts +12 -1
  40. package/dist/lib/seo-service.d.ts.map +1 -1
  41. package/dist/lib/seo-service.js +10 -0
  42. package/dist/lib/seo-service.js.map +1 -1
  43. package/dist/lib/workflow-chrome.d.ts +3 -0
  44. package/dist/lib/workflow-chrome.d.ts.map +1 -1
  45. package/dist/lib/workflow-chrome.js +9 -0
  46. package/dist/lib/workflow-chrome.js.map +1 -1
  47. package/dist/views/SEO.d.ts.map +1 -1
  48. package/dist/views/SEO.js +1 -1
  49. package/dist/views/SEO.js.map +1 -1
  50. package/dist/views/page-editor/EditorTopBar.js +1 -1
  51. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  52. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  53. package/dist/views/page-editor/PageSectionEditor.js +1 -1
  54. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  55. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  56. package/dist/views/post-editor/PostSectionEditor.js +1 -1
  57. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  58. package/dist/views/seo/ContentTab.d.ts +2 -1
  59. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  60. package/dist/views/seo/ContentTab.js +2 -2
  61. package/dist/views/seo/ContentTab.js.map +1 -1
  62. package/dist/views/seo/LinksTab.d.ts +3 -1
  63. package/dist/views/seo/LinksTab.d.ts.map +1 -1
  64. package/dist/views/seo/LinksTab.js +36 -6
  65. package/dist/views/seo/LinksTab.js.map +1 -1
  66. package/package.json +4 -4
  67. package/src/__tests__/components/seo-editor-pane.render.test.tsx +4 -1
  68. package/src/__tests__/components/seo-issue-fix-panel.render.test.tsx +65 -0
  69. package/src/__tests__/components/seo-linking-section.render.test.tsx +146 -0
  70. package/src/__tests__/lib/collection-routes.test.ts +13 -1
  71. package/src/__tests__/lib/page-editor-service.test.ts +8 -0
  72. package/src/__tests__/lib/seo-service-errors.test.ts +29 -0
  73. package/src/__tests__/lib/workflow-chrome.test.ts +5 -0
  74. package/src/__tests__/views/editor-top-bar.render.test.tsx +6 -0
  75. package/src/__tests__/views/seo-content-pane.render.test.tsx +1 -0
  76. package/src/components/seo/SeoEditorPane.tsx +3 -0
  77. package/src/components/seo/SeoIssueFixPanel.tsx +34 -10
  78. package/src/components/seo/SeoLinkingSection.tsx +116 -18
  79. package/src/lib/collection-routes.ts +13 -0
  80. package/src/lib/seo-service.ts +19 -1
  81. package/src/lib/workflow-chrome.ts +10 -0
  82. package/src/views/SEO.tsx +2 -1
  83. package/src/views/page-editor/EditorTopBar.tsx +1 -1
  84. package/src/views/page-editor/PageSectionEditor.tsx +1 -0
  85. package/src/views/post-editor/PostSectionEditor.tsx +1 -0
  86. package/src/views/seo/ContentTab.tsx +3 -0
  87. package/src/views/seo/LinksTab.tsx +108 -19
@@ -17,6 +17,7 @@ import {
17
17
  } from 'lucide-react'
18
18
  import { toast } from 'sonner'
19
19
  import {
20
+ applySeoInternalLink,
20
21
  fetchLinkGraphSnapshot,
21
22
  fetchLinkHealthIssues,
22
23
  fetchLinkSuggestions,
@@ -24,7 +25,9 @@ import {
24
25
  type LinkGraphEntityRef,
25
26
  type LinkSuggestion,
26
27
  } from '../../lib/seo-service.js'
27
- import { editPathForDoc } from '../../lib/collection-routes.js'
28
+ import { editPathForDoc, sourceEditPath } from '../../lib/collection-routes.js'
29
+ import { hasPlanFeature, type PlanInfo } from '../../lib/plan.js'
30
+ import { PlanUpgradeCallout } from '../../components/PlanUpgradeCallout.js'
28
31
  import {
29
32
  SectionCard,
30
33
  SeoLoading,
@@ -88,10 +91,39 @@ function ActionCard({
88
91
  function SuggestionList({
89
92
  suggestions,
90
93
  loading,
94
+ targetEntityId,
95
+ onNavigate,
96
+ onApplied,
97
+ planInfo,
91
98
  }: {
92
99
  suggestions: LinkSuggestion[]
93
100
  loading: boolean
101
+ targetEntityId: string
102
+ onNavigate?: (path: string) => void
103
+ onApplied?: (suggestion: LinkSuggestion) => void
104
+ planInfo?: PlanInfo | null
94
105
  }) {
106
+ const [applyingKey, setApplyingKey] = useState<string | null>(null)
107
+ const inlineApplyEnabled = hasPlanFeature(planInfo, 'seo.inlineApply')
108
+
109
+ async function handleApprove(suggestion: LinkSuggestion) {
110
+ const key = `${suggestion.sourceEntityId}-${suggestion.fingerprint}`
111
+ setApplyingKey(key)
112
+ const res = await applySeoInternalLink({
113
+ sourceEntityId: suggestion.sourceEntityId,
114
+ targetEntityId,
115
+ anchorText: suggestion.anchorText,
116
+ fingerprint: suggestion.fingerprint,
117
+ })
118
+ setApplyingKey(null)
119
+ if (res.error) {
120
+ toast.error(res.error)
121
+ return
122
+ }
123
+ toast.success('Link added.')
124
+ onApplied?.(suggestion)
125
+ }
126
+
95
127
  if (loading) {
96
128
  return (
97
129
  <p className="text-muted-foreground flex items-center gap-2 text-sm">
@@ -108,26 +140,64 @@ function SuggestionList({
108
140
  )
109
141
  }
110
142
  return (
111
- <ul className="divide-border divide-y rounded-lg border">
112
- {suggestions.map((s) => (
113
- <li key={`${s.sourceEntityId}-${s.anchorText}`} className="px-4 py-3">
114
- <p className="text-foreground text-sm font-medium">{s.sourceTitle}</p>
115
- <p className="text-muted-foreground mt-0.5 text-xs">
116
- Link &ldquo;{s.anchorText}&rdquo; → {s.sourceUrl || 'target page'}{' '}
117
- <span className="text-muted-foreground/80 tabular-nums">
118
- ({Math.round(s.confidence * 100)}% match)
119
- </span>
120
- </p>
121
- {s.excerpt && (
122
- <p className="text-muted-foreground mt-1 line-clamp-2 text-xs">{s.excerpt}</p>
123
- )}
124
- </li>
125
- ))}
126
- </ul>
143
+ <div className="space-y-3">
144
+ {!inlineApplyEnabled && suggestions.some((s) => s.wrappable) ? (
145
+ <PlanUpgradeCallout feature="seo.inlineApply" upgradeUrl={planInfo?.upgradeUrl} />
146
+ ) : null}
147
+ <ul className="divide-border divide-y rounded-lg border">
148
+ {suggestions.map((s) => {
149
+ const key = `${s.sourceEntityId}-${s.fingerprint || s.anchorText}`
150
+ const busy = applyingKey === key
151
+ return (
152
+ <li key={key} className="px-4 py-3">
153
+ <p className="text-foreground text-sm font-medium">{s.sourceTitle}</p>
154
+ <p className="text-muted-foreground mt-0.5 text-xs">
155
+ Link &ldquo;{s.anchorText}&rdquo; → {s.sourceUrl || 'target page'}{' '}
156
+ <span className="text-muted-foreground/80 tabular-nums">
157
+ ({Math.round(s.confidence * 100)}% match)
158
+ </span>
159
+ </p>
160
+ {s.excerpt && (
161
+ <p className="text-muted-foreground mt-1 line-clamp-2 text-xs">{s.excerpt}</p>
162
+ )}
163
+ {s.wrappable && inlineApplyEnabled ? (
164
+ <div className="mt-2">
165
+ <button
166
+ type="button"
167
+ className={btnPrimary}
168
+ disabled={busy}
169
+ onClick={() => void handleApprove(s)}
170
+ >
171
+ {busy ? <Loader2 className="h-4 w-4 animate-spin" aria-hidden /> : null}
172
+ Approve link
173
+ </button>
174
+ </div>
175
+ ) : onNavigate ? (
176
+ <div className="mt-2">
177
+ <button
178
+ type="button"
179
+ className={btnSecondary}
180
+ onClick={() => onNavigate(sourceEditPath(s.sourceCollection, s.sourceEntityId))}
181
+ >
182
+ Open in editor
183
+ </button>
184
+ </div>
185
+ ) : null}
186
+ </li>
187
+ )
188
+ })}
189
+ </ul>
190
+ </div>
127
191
  )
128
192
  }
129
193
 
130
- export function LinksTab({ onNavigate }: { onNavigate?: (path: string) => void }) {
194
+ export function LinksTab({
195
+ onNavigate,
196
+ planInfo,
197
+ }: {
198
+ onNavigate?: (path: string) => void
199
+ planInfo?: PlanInfo | null
200
+ }) {
131
201
  const { data, loading, error, refetch } = useSeoResource(fetchLinkGraphSnapshot, [])
132
202
  const [indexing, setIndexing] = useState(false)
133
203
  const [healthLoading, setHealthLoading] = useState(false)
@@ -379,7 +449,26 @@ export function LinksTab({ onNavigate }: { onNavigate?: (path: string) => void }
379
449
  <p className="text-foreground mb-2 text-sm font-medium">
380
450
  AI suggestions for &ldquo;{selectedOrphan.title}&rdquo;
381
451
  </p>
382
- <SuggestionList suggestions={suggestions} loading={suggestLoading} />
452
+ <SuggestionList
453
+ suggestions={suggestions}
454
+ loading={suggestLoading}
455
+ targetEntityId={selectedOrphan.id}
456
+ onNavigate={onNavigate}
457
+ planInfo={planInfo}
458
+ onApplied={(applied) => {
459
+ setSuggestions((prev) =>
460
+ prev.filter(
461
+ (row) =>
462
+ !(
463
+ row.fingerprint === applied.fingerprint &&
464
+ row.sourceEntityId === applied.sourceEntityId
465
+ ),
466
+ ),
467
+ )
468
+ refetch()
469
+ void loadSuggestions(selectedOrphan)
470
+ }}
471
+ />
383
472
  </div>
384
473
  )}
385
474
  </div>