payload-nested-docs-page-tree 1.5.1 → 1.6.1

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 (35) hide show
  1. package/README.md +150 -7
  2. package/dist/components/PageTreeListView.client.js +9 -4
  3. package/dist/components/PageTreeListView.client.js.map +1 -1
  4. package/dist/components/PageTreeListView.module.css +27 -0
  5. package/dist/components/PageTreeListView.server.js +30 -14
  6. package/dist/components/PageTreeListView.server.js.map +1 -1
  7. package/dist/components/PageTreeLocaleStatusBadges.d.ts +7 -0
  8. package/dist/components/PageTreeLocaleStatusBadges.js +80 -0
  9. package/dist/components/PageTreeLocaleStatusBadges.js.map +1 -0
  10. package/dist/components/PageTreeStatusBadge.d.ts +2 -1
  11. package/dist/components/PageTreeStatusBadge.js +10 -1
  12. package/dist/components/PageTreeStatusBadge.js.map +1 -1
  13. package/dist/components/PageTreeTitleCell.js +8 -8
  14. package/dist/components/PageTreeTitleCell.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +8 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/types.d.ts +51 -8
  19. package/dist/types.js.map +1 -1
  20. package/dist/utilities/badgeConfig.js +3 -0
  21. package/dist/utilities/badgeConfig.js.map +1 -1
  22. package/dist/utilities/badgeLinks.d.ts +1 -1
  23. package/dist/utilities/badgeLinks.js +13 -3
  24. package/dist/utilities/badgeLinks.js.map +1 -1
  25. package/dist/utilities/localeStatus.d.ts +11 -0
  26. package/dist/utilities/localeStatus.js +88 -0
  27. package/dist/utilities/localeStatus.js.map +1 -0
  28. package/dist/utilities/pageTree.js +1 -1
  29. package/dist/utilities/pageTree.js.map +1 -1
  30. package/dist/utilities/pageTreeConfig.d.ts +1 -1
  31. package/dist/utilities/pageTreeConfig.js +2 -0
  32. package/dist/utilities/pageTreeConfig.js.map +1 -1
  33. package/dist/utilities/status.d.ts +2 -2
  34. package/dist/utilities/status.js.map +1 -1
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,7 @@ Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-do
5
5
  - **Page tree UI** with hierarchy and page URL paths.
6
6
  - **Intuitive drag and drop** to reorder siblings or move pages between parents.
7
7
  - **Status badges** for published pages, drafts, and drafts with unpublished changes.
8
+ - **Localized status badges** to see each language's publication status and open the page editor in that language with one click.
8
9
  - **Live and preview links** from badges, with separate destinations for changed pages.
9
10
  - **Customizable badges** with label and color overrides for light and dark themes.
10
11
  - **Homepage icon** to identify the root `home` page.
@@ -14,7 +15,7 @@ Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-do
14
15
 
15
16
  **Coming soon:** custom badges for any cell.
16
17
 
17
- [Page tree](#page-tree-ui) · [Drag and drop](#drag-and-drop) · [Badges](#badges) · [Live and preview links](#live-and-preview-links) · [Homepage icon](#homepage-icon) · [Diagnostics](#diagnostics) · [Full setup](#setup) · [Configuration](#configuration)
18
+ [Page tree](#page-tree-ui) · [Drag and drop](#drag-and-drop) · [Badges](#badges) · [Localized status](#locale-status-badges) · [Live and preview links](#live-and-preview-links) · [Homepage icon](#homepage-icon) · [Diagnostics](#diagnostics) · [Full setup](#setup) · [Configuration](#configuration)
18
19
 
19
20
  ## Page tree UI
20
21
 
@@ -107,6 +108,81 @@ See [the playground deploy hook](dev/lib/rebuild.ts) for a complete example. The
107
108
 
108
109
  Override any labels or colors with `badges`. Unspecified values use Payload defaults; custom colors adapt to light and dark themes.
109
110
 
111
+ ### Locale status badges
112
+
113
+ ![Localized page status badges for English, French, and German, with French selected](assets/localized-status.png)
114
+
115
+ See each page's publication status across languages directly in the page tree. Each language has its own color-coded badge showing whether the page is published, a draft, or has unpublished changes. Click a language badge to open that page's editor in the selected language, in the same tab.
116
+
117
+ The active language also shows its status label, such as **FR · Published** or **FR · Changed**. Other languages show their codes, such as **EN** and **DE**, with their own status colors. Badges line up across rows so editors can quickly scan for translations that need attention.
118
+
119
+ Set `badges.locales: true` to show one badge for each available locale:
120
+
121
+ ```ts
122
+ nestedDocsPageTreePlugin({
123
+ collections: ['pages'],
124
+ badges: {
125
+ locales: true,
126
+ // Existing colors and labels apply independently to each status.
127
+ labels: { changed: 'Unpublished edits' },
128
+ },
129
+ })
130
+ ```
131
+
132
+ For an editor using French, the badges appear side by side: **EN** | **FR · Unpublished edits** | **DE**.
133
+
134
+ Locale codes display in uppercase and follow the order in Payload's localization configuration, respecting `filterAvailableLocales`. Existing badge label and color overrides apply to each locale's status.
135
+
136
+ Badge links support keyboard navigation, include the language and status in their accessible names, and respect your configured admin route. Hidden badges preserve their aligned slots and cannot be clicked. Locale badges link to the editor independently of `badgesLinks`, which controls live and preview links on single status badges.
137
+
138
+ This requires Payload's native localized status, configured separately:
139
+
140
+ ```ts
141
+ // Main Payload config
142
+ experimental: { localizeStatus: true },
143
+ localization: {
144
+ defaultLocale: 'en',
145
+ locales: ['en', 'fr', 'de'],
146
+ },
147
+
148
+ // Target collection
149
+ versions: {
150
+ drafts: { localizeStatus: true },
151
+ },
152
+ ```
153
+
154
+ Localized status is experimental in the supported Payload versions. Follow [Payload's status-localization and migration guidance](https://payloadcms.com/docs/configuration/localization#status-localization) before enabling it for existing data. The plugin does not enable it or migrate your database. Collections without localized status, or with `badges.locales` omitted, retain the existing single badge.
155
+
156
+ Statuses are read from the latest draft and current document, with locale fallback disabled:
157
+
158
+ | Latest locale status | Current locale status | Badge |
159
+ | --- | --- | --- |
160
+ | Published | Any | Published |
161
+ | Draft | Published | Unpublished edits |
162
+ | Draft | Draft or missing | Draft |
163
+ | Missing or unsupported | Any | Unknown (neutral) |
164
+
165
+ The plugin batches the two status reads across the tree result, respects read permissions, and does not fetch content separately for every badge.
166
+
167
+ #### Custom appearance
168
+
169
+ The plugin keeps the status calculation and layout reusable. Use these styling hooks to apply a project-specific appearance, such as seasonal colors and status icons:
170
+
171
+ - `.pages-hierarchy-locale-statuses`: the badge group.
172
+ - `.pages-hierarchy-locale-status-badge`: a badge, with `data-locale`, `data-status`, and `data-active`.
173
+ - `.pages-hierarchy-locale-status-badge__code`, `__separator`, and `__label`: the visible pieces.
174
+
175
+ For example, local admin CSS can assign a season color:
176
+
177
+ ```css
178
+ .pages-hierarchy-locale-statuses .pages-hierarchy-locale-status-badge[data-locale='summer'] {
179
+ background: #fff0a8;
180
+ color: #594600;
181
+ }
182
+ ```
183
+
184
+ A reskin can hide `__code` and `__separator` and add a status icon using the badge's `::before` and `data-status`, keeping the active status label and accessible name. The default plugin supplies no icons.
185
+
110
186
  ### Live and preview links
111
187
 
112
188
 
@@ -122,19 +198,42 @@ Published badges open live with a right-side link icon; draft-only badges open p
122
198
  | `'preview'` | Whole badge, including the right-side eye icon, opens preview |
123
199
  | `'both'` (default) | Body with link icon opens live; eye icon after a separator opens preview |
124
200
 
125
- - **Live:** Uses the published document's last breadcrumb URL and `liveURL`. Breadcrumbs must match your frontend routes. Omit `liveURL` for preview links only.
201
+ - **Live:** A string `liveURL` is the base URL for the published document's last breadcrumb path. Alternatively, pass a sync or async callback returning the final URL for tenant domains or localized routes. Omit `liveURL` for preview links only.
126
202
  - **Preview:** Uses the collection's [`admin.preview`](https://payloadcms.com/docs/admin/preview) callback. Your frontend must serve draft content.
127
203
 
128
- All links open in new tabs. Unlinked badges have no icon. Omit `badgesLinks` to keep badges unlinked, even with preview configured.
204
+ All live and preview links open in new tabs. Unlinked badges have no icon. Omit `badgesLinks` to keep single status badges unlinked, even with preview configured. Locale badges still link to the editor.
129
205
 
130
206
  `badgesLinks.showIcons` defaults to `true`: live links use Payload's chain-link `LinkIcon`, and preview links use `EyeIcon`. Set it to `false` to hide inline badge icons; in `'both'` mode, the separate preview action still shows `ExternalLinkIcon` after the separator.
131
207
 
208
+ For example, with a `tenant` relationship on pages and a `domain` field on tenants:
209
+
210
+ ```ts
211
+ badgesLinks: {
212
+ liveURL: async ({ doc, locale, path, req }) => {
213
+ if (!path || (typeof doc.tenant !== 'string' && typeof doc.tenant !== 'number')) {
214
+ return null
215
+ }
216
+
217
+ const tenant = await req.payload.findByID({
218
+ collection: 'tenants',
219
+ id: doc.tenant,
220
+ overrideAccess: false,
221
+ req,
222
+ })
223
+
224
+ return new URL(`${locale ? `/${locale}` : ''}${path}`, `https://${tenant.domain}`).href
225
+ },
226
+ },
227
+ ```
228
+
229
+ The callback receives `{ collectionSlug, doc, locale, path, req }`. `doc` is the readable published document at depth `0`, so relationships contain IDs. `locale` is the selected admin locale, or `undefined` without localization. `path` is the last published breadcrumb URL from `breadcrumbsFieldSlug`, or `undefined` when unavailable. The callback can build its own route without breadcrumbs. It runs only when a live destination is needed and a readable published document exists, and stays on the server. The `PageTreeLiveURL` type is exported for standalone callbacks.
230
+
132
231
  <details>
133
232
  <summary>URL resolution and unavailable links</summary>
134
233
 
135
- Live links use `breadcrumbsFieldSlug` and the published path, unaffected by draft slug or parent changes. Preview receives the latest saved draft, locale, request, and user token. Unsaved changes and `admin.livePreview.url` are not used.
234
+ String live links use `breadcrumbsFieldSlug` and the published path, unaffected by draft slug or parent changes. Callback results are used as final URLs without appending the breadcrumb path. Relative callback URLs resolve against the CMS request URL; return an absolute URL for a separate frontend. Only HTTP and HTTPS links are accepted. Preview receives the latest saved draft, locale, request, and user token. Unsaved changes and `admin.livePreview.url` are not used.
136
235
 
137
- Missing breadcrumbs, missing or invalid URLs, or a failing preview callback disable only that link. In `'both'` mode, no live URL leaves the body as text; no preview URL hides the icon. Single-link modes never switch destinations. The selected mode is independent of the badge label.
236
+ Missing breadcrumbs for string live URLs, missing or invalid URLs, or a failing live or preview callback disable only that link. Return `null`, `undefined`, or an empty string from the live callback to leave the live destination unlinked. In `'both'` mode, no live URL leaves the body as text; no preview URL hides the icon. Single-link modes never switch destinations. The selected mode is independent of the badge label.
138
237
 
139
238
  </details>
140
239
 
@@ -221,7 +320,7 @@ export const plugins = [
221
320
  ]
222
321
  ```
223
322
 
224
- Only `collections` is required in the page-tree config. Omit `badges` for default styling and `badgesLinks` to disable links. Preview links require the collection's `admin.preview` callback.
323
+ Only `collections` is required in the page-tree config. Omit `badges` for default styling and `badgesLinks` to disable live and preview links. Preview links require the collection's `admin.preview` callback.
225
324
 
226
325
  Each target collection needs parent, breadcrumbs, and `admin.useAsTitle` fields stored at the document's top level. Presentational tabs, rows, collapsibles, and unnamed groups are supported; fields inside named tabs or groups are not.
227
326
 
@@ -241,7 +340,7 @@ pnpm exec payload generate:importmap
241
340
  | `defaultLimit` | `100` | List page size |
242
341
  | `hideBreadcrumbs` | `true` | Hide the read-only breadcrumbs field |
243
342
  | `homeIndicator` | `{ collections: ['pages'] }` | Collections showing a home icon; `false` disables it |
244
- | `badges` | Payload defaults | Status label and color overrides |
343
+ | `badges` | Payload defaults | Status labels, colors, and optional locale badges |
245
344
  | `badgesLinks` | Disabled | Live and preview links |
246
345
  | `publishOnMove` | `false` | Publish moves for pages without pending edits |
247
346
  | `diagnostics` | `false` | Structured operation logs |
@@ -261,6 +360,18 @@ pnpm dev
261
360
 
262
361
  Open [localhost:3000/admin](http://localhost:3000/admin). The playground creates `admin@email.com` / `password` on startup. Use **seed the database** on the dashboard to add sample pages.
263
362
 
363
+ The Seed button populates three collections: **Pages** and **Tabbed Pages** use shared publication status; **Localized Pages** uses independent English/French/German status (`en`, `fr`, `de`, in that order). All three use the same 30-page tree, titles, slugs, and ordering in every language. Localized Pages includes mixed published, changed, and draft statuses for testing. Restart the playground after changing locale configuration, then click **seed the database** to populate all three languages. **Reseeding replaces all content and version history in these three playground collections**, removing stale pages and restoring the shared demo hierarchy. Users are preserved.
364
+
365
+ For a fresh in-memory playground database on Windows:
366
+
367
+ ```powershell
368
+ $env:PAYLOAD_TEST_DATABASE = 'true'
369
+ pnpm.cmd dev
370
+ ```
371
+
372
+ Without that flag, normal development uses `DATABASE_URL` from `dev/.env`. The two status modes can coexist in either database setup; no migration is needed for the new Localized Pages collection.
373
+
374
+
264
375
  ```bash
265
376
  pnpm generate:types
266
377
  pnpm generate:importmap
@@ -278,3 +389,35 @@ pnpm add /path/payload-nested-docs-page-tree-*.tgz
278
389
  ```
279
390
 
280
391
  </details>
392
+
393
+ ### Locale badge visibility
394
+
395
+ With localized status badges enabled, the optional top-level
396
+ `localeBadgeVisibility: ({ doc, publishedDoc, locale, req }) => boolean` callback decides which
397
+ badges are visible. It runs only on the server and receives the latest draft as
398
+ `doc` and the current document as `publishedDoc` when readable. Both contain all
399
+ locales and respect the current user's read access. Check each locale's status
400
+ before treating current content as published. This lets visibility follow published
401
+ content independently of pending draft changes. Returning false hides the badge
402
+ from both sight and assistive technology while preserving its aligned slot,
403
+ including when that locale is selected. Publication status is unchanged.
404
+
405
+ Without a callback, all locale badges remain visible. When configured, the existing
406
+ two batched queries read the draft and current document content needed by the
407
+ callback; no per-row queries are added. The callback and document content are not passed to the client.
408
+ Project-specific inheritance or detachment rules belong in the consuming app.
409
+
410
+ The optional top-level
411
+ `localeBadgeStatus: ({ doc, publishedDoc, locale, req, status }) => status`
412
+ callback overrides the displayed status. It receives the latest draft, the current
413
+ document (when readable), and the default computed status. Both documents contain
414
+ all locales. Check the current document's locale status before treating its content
415
+ as published. This callback loads content in the same two access-controlled batched
416
+ queries; no document content or callbacks are passed to the client. It never writes
417
+ publication state. Without it, the plugin's normal status behavior is unchanged.
418
+
419
+ The status callback can also return `{ status, label }` to override one locale's
420
+ display text while retaining its publication status and status color. The label is
421
+ used in the badge's accessible name and, for the active locale, its visible text.
422
+ The resolved label is also exposed as `data-label` for custom styling. Existing
423
+ callbacks returning a status string continue to work.
@@ -10,9 +10,9 @@ import { buildInsertDropTargets, getDropTargetParentDoc } from '../utilities/dro
10
10
  import { CANCEL_DRAG_MESSAGE, getDropValidation } from '../utilities/moveValidation.js';
11
11
  import { buildDocSlugPath, buildPageTreeDocs, buildProspectiveDocSlugPath, getDocDisplayLabel, getVisibleTreeDocs } from '../utilities/pageTree.js';
12
12
  import { pageTreeCollisionDetectionStrategy } from '../utilities/pageTreeCollision.js';
13
- import { PageTreeStatusBadge } from './PageTreeStatusBadge.js';
14
13
  import { PageTreeProvider, PageTreeRowDndProvider } from './PageTreeContext.js';
15
14
  import styles from './PageTreeListView.module.css';
15
+ import { PageTreeStatusBadge } from './PageTreeStatusBadge.js';
16
16
  import { PageTreeTitleCell } from './PageTreeTitleCell.js';
17
17
  const SILENT_MOVE_MESSAGES = new Set([
18
18
  CANCEL_DRAG_MESSAGE
@@ -194,7 +194,8 @@ function sliceColumnState(columnState, docs, renderedCellIndexByDocID) {
194
194
  function getSelectableRowData(doc) {
195
195
  const record = doc;
196
196
  return {
197
- id: String(doc.id ?? doc.__pageTreeID),
197
+ // Payload's selection Map preserves numeric IDs despite SelectRow's string-only type.
198
+ id: doc.id ?? doc.__pageTreeID,
198
199
  _isLocked: Boolean(record._isLocked),
199
200
  _userEditing: record._userEditing
200
201
  };
@@ -388,10 +389,11 @@ function getDropTargetValidation(args) {
388
389
  };
389
390
  }
390
391
  function renderStatusBadge(args) {
391
- const { badgeConfig, badgesLinks, doc, index } = args;
392
+ const { badgeConfig, badgesLinks, collectionSlug, doc, index } = args;
392
393
  return /*#__PURE__*/ _jsx(PageTreeStatusBadge, {
393
394
  badgeConfig: badgeConfig,
394
395
  badgesLinks: badgesLinks,
396
+ collectionSlug: collectionSlug,
395
397
  doc: doc
396
398
  }, doc.__pageTreeID ?? index);
397
399
  }
@@ -425,7 +427,7 @@ function ParentMoveToggle({ collectionSlug, enabled, onToggle }) {
425
427
  return toggle;
426
428
  }
427
429
  function buildTableColumns(args) {
428
- const { badgeConfig, badgesLinks, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
430
+ const { badgeConfig, badgesLinks, collectionSlug, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
429
431
  const columnsToUse = columnState.map((column)=>{
430
432
  if (column.accessor === useAsTitle) {
431
433
  return {
@@ -444,6 +446,7 @@ function buildTableColumns(args) {
444
446
  renderedCells: docs.map((doc, index)=>renderStatusBadge({
445
447
  badgeConfig,
446
448
  badgesLinks,
449
+ collectionSlug,
447
450
  doc,
448
451
  index,
449
452
  t
@@ -892,6 +895,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLin
892
895
  const tableColumns = React.useMemo(()=>buildTableColumns({
893
896
  badgeConfig,
894
897
  badgesLinks,
898
+ collectionSlug: props.collectionSlug,
895
899
  columnState: paginatedColumnState,
896
900
  docs: displayedPaginatedDocs,
897
901
  enableRowSelections: props.enableRowSelections,
@@ -908,6 +912,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLin
908
912
  homeIndicatorEnabled,
909
913
  orderableFieldName,
910
914
  parentFieldSlug,
915
+ props.collectionSlug,
911
916
  props.enableRowSelections,
912
917
  i18n.t,
913
918
  useAsTitle