Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
85d5589
Update donate enable option to be the same as other topLevelNav
rchlfryn Mar 23, 2026
4a650e7
Add banner description components to replace overlooked descriptions
rchlfryn Mar 23, 2026
9a74a54
Update donate button
rchlfryn Mar 23, 2026
a451a32
Replace top level automated nav items with built-in pages (minus fore…
rchlfryn Mar 23, 2026
616e1d7
Add ability for nav item to not have subnav items (purely UI change)
rchlfryn Mar 23, 2026
1c400f1
Update forecast tab to use built in pages
rchlfryn Mar 23, 2026
4503f60
Sort pages, built-in pages, and posts by title in reference dropdown
rchlfryn Mar 23, 2026
49d192c
Add migration
rchlfryn Mar 23, 2026
c134389
Add script to add built in pages on prod
rchlfryn Mar 23, 2026
b6eba56
Change read only nav items to be editable by superAdmin
rchlfryn Mar 23, 2026
1572b6b
Update nav to actually use built-in pages with hardcoded as fallback …
rchlfryn Mar 23, 2026
bab3fe3
Update one time script to use sql instead of update
rchlfryn Mar 23, 2026
4e7fda4
Add isInNav boolean to prevent users from accidentally deleting built…
rchlfryn Mar 23, 2026
a829fd1
Update migration to include isInNav
rchlfryn Mar 23, 2026
018e820
TODO - figure out why nav is incorrect after one time seed
rchlfryn Mar 23, 2026
e4e34c5
Merge branch 'main' into nav-updates
rchlfryn Mar 25, 2026
35efe60
Redo migration to fix json
rchlfryn Mar 25, 2026
60a9781
Remove unnecessary fallback for migration
rchlfryn Mar 25, 2026
6d186fc
Remove type assertions
rchlfryn Mar 25, 2026
1590fc2
Add obs fallback
rchlfryn Mar 25, 2026
4b3811b
Make tenant slug immutable after creation
rchlfryn Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion consistent-type-assertions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ src/collections/Users/components/InviteUser.tsx
src/collections/Users/components/InviteUserDrawer.tsx
src/collections/Users/components/inviteUserAction.ts
src/collections/Users/components/resendInviteActions.ts
src/components/Header/utils.ts
src/endpoints/seed/upsert.ts
src/globals/Diagnostics/actions/revalidateCache.ts
src/utilities/removeNonDeterministicKeys.ts
2 changes: 2 additions & 0 deletions src/app/(payload)/admin/importMap.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion src/collections/BuiltInPages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ export const BuiltInPages: CollectionConfig<'pages'> = {
},
read: byTenantRole('read', 'builtInPages'),
update: byGlobalRole('update', 'builtInPages'),
delete: byGlobalRole('delete', 'builtInPages'),
delete: ({ req }) => {
if (!byGlobalRole('delete', 'builtInPages')({ req })) return false
return { isInNav: { not_equals: true } }
},
},
admin: {
group: 'Content',
useAsTitle: 'title',
baseListFilter: filterByTenant,
defaultColumns: ['title', 'url', 'tenant'],
},
fields: [
titleField(),
Expand All @@ -37,6 +41,15 @@ export const BuiltInPages: CollectionConfig<'pages'> = {
type: 'text',
required: true,
},
{
name: 'isInNav',
type: 'checkbox',
defaultValue: false,
admin: {
hidden: true,
description: 'Pages used in navigation cannot be deleted to prevent broken links.',
},
},
tenantField(),
contentHashField(),
],
Expand Down
57 changes: 34 additions & 23 deletions src/collections/Navigations/fields/itemsField.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { navLink } from '@/fields/navLink'
import { merge } from 'lodash-es'
import { ArrayField, FieldHook } from 'payload'
import { ArrayField, Condition, FieldHook } from 'payload'

// Condition: show field only when item has sub-items (accordion/section mode)
const hasSubItems = (_data: unknown, siblingData: Record<string, unknown>) =>
// Conditions: show/hide fields based on whether item has sub-items (accordion/section mode)
const hasSubItems: Condition = (_, siblingData) =>
Array.isArray(siblingData?.items) && siblingData.items.length > 0

// Copy link.label to standalone label when sub-items are added,
Expand Down Expand Up @@ -35,10 +35,12 @@ const clearLinkWhenHasSubItems: FieldHook = ({ siblingData, value }) => {
export const itemsField = ({
label,
description,
hasSubNavItems = true,
overrides = {},
}: {
label: string
description?: string
hasSubNavItems?: boolean
overrides?: Partial<ArrayField>
}): ArrayField =>
merge(
Expand All @@ -50,36 +52,45 @@ export const itemsField = ({
description,
},
fields: [
{
name: 'label',
type: 'text',
required: true,
admin: {
description: 'Label for this nav section (shown when item has sub-items)',
condition: hasSubItems,
},
hooks: {
beforeChange: [clearLabelWhenItemHasNoSubItems],
},
},
...(hasSubNavItems
? [
{
name: 'label',
type: 'text',
required: true,
admin: {
description: 'Label for this nav section (shown when item has sub-items)',
condition: hasSubItems,
},
hooks: {
beforeChange: [clearLabelWhenItemHasNoSubItems],
},
},
]
: []),
{
...navLink,
admin: {
...navLink.admin,
condition: (data: unknown, siblingData: Record<string, unknown>) =>
!hasSubItems(data, siblingData),
condition: hasSubNavItems
? (...args: Parameters<Condition>) => !hasSubItems(...args)
: undefined,
},
hooks: {
// navLink.hooks contains clearIrrelevantLinkValues; we add our cleanup hook
beforeChange: [...(navLink.hooks?.beforeChange ?? []), clearLinkWhenHasSubItems],
},
},
{
name: 'items',
type: 'array',
label: 'Sub Nav Items',
fields: [navLink],
},
...(hasSubNavItems
? [
{
name: 'items',
type: 'array',
label: 'Sub Nav Items',
fields: [navLink],
},
]
: []),
],
},
overrides,
Expand Down
45 changes: 42 additions & 3 deletions src/collections/Navigations/fields/topLevelNavTab.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,70 @@
import { hasSuperAdminPermissions } from '@/access/hasSuperAdminPermissions'
import { navLink } from '@/fields/navLink'
import { Field, Tab, toWords } from 'payload'
import { itemsField } from './itemsField'

export const topLevelNavTab = ({
name,
description,
hasConfigurableNavItems = true,
hasReadOnlyLink = false,
hasReadOnlyNavItems = false,
hasEnabledToggle = true,
enabledToggleDescription = 'If hidden, pages with links in this nav item will not be accessible at their navigation-nested URLs.',
}: {
name: string
description?: string
hasConfigurableNavItems?: boolean
hasReadOnlyLink?: boolean
hasReadOnlyNavItems?: boolean
hasEnabledToggle?: boolean
enabledToggleDescription?: string
}): Tab => {
let fields: Field[] = [
itemsField({
label: `${toWords(name)} Nav Items`,
description: `Dropdown items under ${toWords(name)}`,
hasSubNavItems: !hasReadOnlyNavItems,
overrides: {
...(hasReadOnlyNavItems ? { access: { update: hasSuperAdminPermissions } } : {}),
admin: {
hidden: !hasConfigurableNavItems,
hidden: !hasConfigurableNavItems && !hasReadOnlyNavItems,
},
},
}),
]

if (hasReadOnlyLink) {
fields = [
{
...navLink,
access: {
update: hasSuperAdminPermissions,
},
},
...fields,
]
}

if (description) {
const descriptionField: Field = {
type: 'ui',
name: `${name}Description`,
admin: {
components: {
Field: {
path: '@/components/BannerDescription#BannerDescription',
clientProps: {
message: description,
type: 'info',
},
},
},
},
}
fields = [descriptionField, ...fields]
}

if (hasEnabledToggle) {
const enabledToggleField: Field = {
type: 'group',
Expand All @@ -48,8 +87,8 @@ export const topLevelNavTab = ({

return {
name,
description,
virtual: !hasConfigurableNavItems && !hasEnabledToggle,
virtual:
!hasConfigurableNavItems && !hasReadOnlyNavItems && !hasEnabledToggle && !hasReadOnlyLink,
fields,
}
}
29 changes: 27 additions & 2 deletions src/collections/Navigations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ export const Navigations: CollectionConfig = {
name: 'forecasts',
description: 'This nav dropdown is autofilled with your forecast zones.',
hasConfigurableNavItems: false,
hasReadOnlyLink: true,
hasReadOnlyNavItems: true,
hasEnabledToggle: false,
}),
topLevelNavTab({
name: 'observations',
description: 'This nav dropdown is autofilled with the default observations links.',
hasConfigurableNavItems: false,
hasReadOnlyNavItems: true,
hasEnabledToggle: false,
}),
topLevelNavTab({
Expand All @@ -60,6 +63,7 @@ export const Navigations: CollectionConfig = {
description:
'This nav item navigates to your blog landing page and does not have any dropdown items.',
hasConfigurableNavItems: false,
hasReadOnlyLink: true,
enabledToggleDescription:
'If hidden from the nav, the blog landing page will still be accessible to visitors for filtered blog lists.',
}),
Expand All @@ -68,27 +72,48 @@ export const Navigations: CollectionConfig = {
description:
'This nav item navigates to your events landing page and does not have any dropdown items.',
hasConfigurableNavItems: false,
hasReadOnlyLink: true,
enabledToggleDescription:
'If hidden from the nav, the events landing page will still be accessible to visitors for filtered event lists.',
}),
topLevelNavTab({ name: 'about' }),
topLevelNavTab({ name: 'support' }),
{
name: 'donate',
description: 'This nav item is styled as a button.',
fields: [
{
type: 'group',
name: 'options',
label: '',
fields: [
{
type: 'checkbox',
defaultValue: true,
name: 'enabled',
label: 'Show Button in Navigation',
admin: {
description: 'If hidden, the button will not appear in the nav.',
},
},
],
},
navLink,
{
...navLink,
label: '',
admin: {
...navLink.admin,
components: {
...navLink.admin?.components,
Description: {
path: '@/components/BannerDescription#BannerDescription',
clientProps: {
message: 'This nav item is styled as a button.',
type: 'info',
},
},
},
},
},
],
},
],
Expand Down
19 changes: 18 additions & 1 deletion src/collections/Tenants/components/TenantSlugField.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import type { SelectFieldServerComponent } from 'payload'

import { SelectField } from '@payloadcms/ui'
import { SelectField, SelectInput } from '@payloadcms/ui'

export const TenantSlugField: SelectFieldServerComponent = async ({
clientField,
data,
field,
payload,
}) => {
// Slug is immutable after creation
if (data?.id) {
return (
<SelectInput
name={field.name}
path={field.name}
label={clientField.label}
options={(clientField.options ?? []).map((o) =>
typeof o === 'string' ? { label: o, value: o } : o,
)}
value={data.slug}
readOnly
/>
)
}

const { docs } = await payload.find({
collection: 'tenants',
limit: 0,
Expand Down
27 changes: 27 additions & 0 deletions src/components/BannerDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import { Banner } from '@payloadcms/ui'
import { AlertCircle, CheckCircle, Info, MessageSquare } from 'lucide-react'

type BannerDescriptionProps = {
message: string
type?: 'default' | 'error' | 'info' | 'success'
[key: string]: unknown
}

const icons: Record<NonNullable<BannerDescriptionProps['type']>, React.ReactNode> = {
default: <MessageSquare size={18} />,
error: <AlertCircle size={18} />,
info: <Info size={18} />,
success: <CheckCircle size={18} />,
}

export const BannerDescription = ({ message, type = 'default' }: BannerDescriptionProps) => {
if (!message) return null

return (
<Banner alignIcon="left" icon={icons[type]} type={type} className="w-fit gap-2 items-center">
{message}
</Banner>
)
}
Loading
Loading