chore(examples): multi tenant single domain fixes (#7922)
This commit is contained in:
@@ -5,6 +5,8 @@ import config from '@payload-config'
|
|||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
|
||||||
|
import { importMap } from '../importMap.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
params: {
|
params: {
|
||||||
segments: string[]
|
segments: string[]
|
||||||
@@ -17,6 +19,7 @@ type Args = {
|
|||||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
generatePageMetadata({ config, params, searchParams })
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
const NotFound = ({ params, searchParams }: Args) =>
|
||||||
|
NotFoundPage({ config, importMap, params, searchParams })
|
||||||
|
|
||||||
export default NotFound
|
export default NotFound
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import config from '@payload-config'
|
|||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
|
||||||
|
import { importMap } from '../importMap.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
params: {
|
params: {
|
||||||
segments: string[]
|
segments: string[]
|
||||||
@@ -17,6 +19,7 @@ type Args = {
|
|||||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
generatePageMetadata({ config, params, searchParams })
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config, importMap, params, searchParams })
|
||||||
|
|
||||||
export default Page
|
export default Page
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { TenantFieldComponent as TenantFieldComponent_0 } from '@/fields/TenantField/components/Field'
|
||||||
|
import { TenantSelectorRSC as TenantSelectorRSC_1 } from '@/components/TenantSelector'
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
'@/fields/TenantField/components/Field#TenantFieldComponent': TenantFieldComponent_0,
|
||||||
|
'@/components/TenantSelector#TenantSelectorRSC': TenantSelectorRSC_1,
|
||||||
|
}
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
import configPromise from "@payload-config";
|
import configPromise from '@payload-config'
|
||||||
import "@payloadcms/next/css";
|
import '@payloadcms/next/css'
|
||||||
import { RootLayout } from "@payloadcms/next/layouts";
|
import { RootLayout } from '@payloadcms/next/layouts'
|
||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import "./custom.scss";
|
import { importMap } from './admin/importMap.js'
|
||||||
|
import './custom.scss'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
};
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: Args) => (
|
const Layout = ({ children }: Args) => (
|
||||||
<RootLayout config={configPromise}>{children}</RootLayout>
|
<RootLayout config={configPromise} importMap={importMap}>
|
||||||
);
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
export default Layout;
|
export default Layout
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Access } from 'payload'
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
import type { User } from '../../../../payload-types'
|
import type { User } from '../../../payload-types'
|
||||||
|
|
||||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
import type { User } from '../../../payload-types'
|
import type { User } from '../../payload-types'
|
||||||
|
|
||||||
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
|
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
|
||||||
import { createAccess } from './access/create'
|
import { createAccess } from './access/create'
|
||||||
@@ -37,32 +37,6 @@ const Users: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'tenant',
|
name: 'tenant',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
filterOptions: ({ user }) => {
|
|
||||||
if (!user) {
|
|
||||||
// Would like to query where exists true on id
|
|
||||||
// but that is not working
|
|
||||||
return {
|
|
||||||
id: {
|
|
||||||
like: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (user?.roles?.includes('super-admin')) {
|
|
||||||
// Would like to query where exists true on id
|
|
||||||
// but that is not working
|
|
||||||
return {
|
|
||||||
id: {
|
|
||||||
like: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(user as User)
|
|
||||||
return {
|
|
||||||
id: {
|
|
||||||
in: adminTenantAccessIDs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
index: true,
|
index: true,
|
||||||
relationTo: 'tenants',
|
relationTo: 'tenants',
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
import type { Option } from '@payloadcms/ui/elements/ReactSelect'
|
import type { Option } from '@payloadcms/ui/elements/ReactSelect'
|
||||||
import type { OptionObject } from 'payload'
|
import type { OptionObject } from 'payload'
|
||||||
|
|
||||||
|
import { getTenantAdminTenantAccessIDs } from '@/utilities/getTenantAccessIDs'
|
||||||
import { SelectInput, useAuth } from '@payloadcms/ui'
|
import { SelectInput, useAuth } from '@payloadcms/ui'
|
||||||
|
import * as qs from 'qs-esm'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { Tenant, User } from '../../../payload-types.js'
|
import type { Tenant, User } from '../../payload-types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => {
|
export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => {
|
||||||
const { user } = useAuth<User>()
|
const { user } = useAuth<User>()
|
||||||
const [options, setOptions] = React.useState<OptionObject[]>([])
|
const [options, setOptions] = React.useState<OptionObject[]>([])
|
||||||
const [value, setValue] = React.useState<string | undefined>(initialCookie)
|
|
||||||
|
|
||||||
const isSuperAdmin = user?.roles?.includes('super-admin')
|
const isSuperAdmin = user?.roles?.includes('super-admin')
|
||||||
const tenantIDs =
|
const tenantIDs =
|
||||||
@@ -28,18 +29,6 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
|
|||||||
document.cookie = name + '=' + (value || '') + expires + '; path=/'
|
document.cookie = name + '=' + (value || '') + expires + '; path=/'
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const fetchTenants = async () => {
|
|
||||||
const res = await fetch(`/api/tenants?depth=0&limit=100&sort=name`, {
|
|
||||||
credentials: 'include',
|
|
||||||
}).then((res) => res.json())
|
|
||||||
|
|
||||||
setOptions(res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id })))
|
|
||||||
}
|
|
||||||
|
|
||||||
void fetchTenants()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleChange = React.useCallback((option: Option | Option[]) => {
|
const handleChange = React.useCallback((option: Option | Option[]) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
setCookie('payload-tenant', undefined)
|
setCookie('payload-tenant', undefined)
|
||||||
@@ -50,7 +39,44 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (isSuperAdmin || tenantIDs.length > 1) {
|
React.useEffect(() => {
|
||||||
|
const fetchTenants = async () => {
|
||||||
|
const adminOfTenants = getTenantAdminTenantAccessIDs(user ?? null)
|
||||||
|
|
||||||
|
const queryString = qs.stringify(
|
||||||
|
{
|
||||||
|
depth: 0,
|
||||||
|
limit: 100,
|
||||||
|
sort: 'name',
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: adminOfTenants,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addQueryPrefix: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await fetch(`/api/tenants${queryString}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
}).then((res) => res.json())
|
||||||
|
|
||||||
|
const optionsToSet = res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id }))
|
||||||
|
|
||||||
|
if (optionsToSet.length === 1) {
|
||||||
|
setCookie('payload-tenant', optionsToSet[0].value)
|
||||||
|
}
|
||||||
|
setOptions(optionsToSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
void fetchTenants()
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
if ((isSuperAdmin || tenantIDs.length > 1) && options.length > 1) {
|
||||||
return (
|
return (
|
||||||
<div className="tenant-selector">
|
<div className="tenant-selector">
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -59,7 +85,7 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
options={options}
|
options={options}
|
||||||
path="setTenant"
|
path="setTenant"
|
||||||
value={value}
|
value={options.find((opt) => opt.value === initialCookie)?.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use client'
|
||||||
|
import { RelationshipField, useField } from '@payloadcms/ui'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValue?: string
|
||||||
|
path: string
|
||||||
|
readOnly: boolean
|
||||||
|
}
|
||||||
|
export function TenantFieldComponentClient({ initialValue, path, readOnly }: Props) {
|
||||||
|
const { formInitializing, setValue } = useField({ path })
|
||||||
|
const hasSetInitialValue = React.useRef(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!hasSetInitialValue.current && !formInitializing && initialValue) {
|
||||||
|
setValue(initialValue)
|
||||||
|
hasSetInitialValue.current = true
|
||||||
|
}
|
||||||
|
}, [initialValue, setValue, formInitializing])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RelationshipField
|
||||||
|
field={{
|
||||||
|
name: path,
|
||||||
|
type: 'relationship',
|
||||||
|
_path: path,
|
||||||
|
label: 'Tenant',
|
||||||
|
relationTo: 'tenants',
|
||||||
|
required: true,
|
||||||
|
}}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,26 +1,32 @@
|
|||||||
'use client'
|
import type { Payload } from 'payload'
|
||||||
import { RelationshipField, useAuth, useFieldProps } from '@payloadcms/ui'
|
|
||||||
|
import { cookies as getCookies, headers as getHeaders } from 'next/headers'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { User } from '../../../../payload-types.js'
|
import { TenantFieldComponentClient } from './Field.client'
|
||||||
|
|
||||||
export const TenantFieldComponent = () => {
|
export const TenantFieldComponent: React.FC<{
|
||||||
const { user } = useAuth<User>()
|
path: string
|
||||||
const { path, readOnly } = useFieldProps()
|
payload: Payload
|
||||||
|
readOnly: boolean
|
||||||
|
}> = async (args) => {
|
||||||
|
const cookies = getCookies()
|
||||||
|
const headers = getHeaders()
|
||||||
|
const { user } = await args.payload.auth({ headers })
|
||||||
|
|
||||||
if (user) {
|
if (
|
||||||
if ((user.tenants && user.tenants.length > 1) || user?.roles?.includes('super-admin')) {
|
user &&
|
||||||
return (
|
((Array.isArray(user.tenants) && user.tenants.length > 1) ||
|
||||||
<RelationshipField
|
user?.roles?.includes('super-admin'))
|
||||||
label="Tenant"
|
) {
|
||||||
name={path}
|
return (
|
||||||
path={path}
|
<TenantFieldComponentClient
|
||||||
readOnly={readOnly}
|
initialValue={cookies.get('payload-tenant')?.value || undefined}
|
||||||
relationTo="tenants"
|
path={args.path}
|
||||||
required
|
readOnly={args.readOnly}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Field } from 'payload'
|
|||||||
|
|
||||||
import { isSuperAdmin } from '../../access/isSuperAdmin'
|
import { isSuperAdmin } from '../../access/isSuperAdmin'
|
||||||
import { tenantFieldUpdate } from './access/update'
|
import { tenantFieldUpdate } from './access/update'
|
||||||
import { TenantFieldComponent } from './components/Field'
|
|
||||||
import { autofillTenant } from './hooks/autofillTenant'
|
import { autofillTenant } from './hooks/autofillTenant'
|
||||||
|
|
||||||
export const tenantField: Field = {
|
export const tenantField: Field = {
|
||||||
@@ -17,7 +16,7 @@ export const tenantField: Field = {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
Field: TenantFieldComponent,
|
Field: '@/fields/TenantField/components/Field#TenantFieldComponent',
|
||||||
},
|
},
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export interface Config {
|
|||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: string;
|
||||||
|
};
|
||||||
globals: {};
|
globals: {};
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User & {
|
user: User & {
|
||||||
@@ -26,15 +29,20 @@ export interface Config {
|
|||||||
export interface UserAuthOperations {
|
export interface UserAuthOperations {
|
||||||
forgotPassword: {
|
forgotPassword: {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
login: {
|
login: {
|
||||||
password: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
registerFirstUser: {
|
registerFirstUser: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'url'
|
|||||||
import { Pages } from './collections/Pages'
|
import { Pages } from './collections/Pages'
|
||||||
import { Tenants } from './collections/Tenants'
|
import { Tenants } from './collections/Tenants'
|
||||||
import Users from './collections/Users'
|
import Users from './collections/Users'
|
||||||
import { TenantSelectorRSC } from './components/TenantSelector'
|
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -15,7 +14,7 @@ const dirname = path.dirname(filename)
|
|||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
afterNavLinks: [TenantSelectorRSC],
|
afterNavLinks: ['@/components/TenantSelector#TenantSelectorRSC'],
|
||||||
},
|
},
|
||||||
user: 'users',
|
user: 'users',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { User } from '../../payload-types'
|
import type { User } from '../payload-types'
|
||||||
|
|
||||||
export const getTenantAccessIDs = (user: User | null): string[] => {
|
export const getTenantAccessIDs = (user: User | null): string[] => {
|
||||||
if (!user) return []
|
if (!user) return []
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user