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. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
@@ -17,6 +19,7 @@ type Args = {
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
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
|
||||
|
||||
@@ -5,6 +5,8 @@ import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
@@ -17,6 +19,7 @@ type Args = {
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
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
|
||||
|
||||
@@ -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. */
|
||||
import configPromise from "@payload-config";
|
||||
import "@payloadcms/next/css";
|
||||
import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* 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 = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
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 { User } from '../../../../payload-types'
|
||||
import type { User } from '../../../payload-types'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import type { User } from '../../../payload-types'
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
|
||||
import { createAccess } from './access/create'
|
||||
@@ -37,32 +37,6 @@ const Users: CollectionConfig = {
|
||||
{
|
||||
name: 'tenant',
|
||||
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,
|
||||
relationTo: 'tenants',
|
||||
required: true,
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
import type { Option } from '@payloadcms/ui/elements/ReactSelect'
|
||||
import type { OptionObject } from 'payload'
|
||||
|
||||
import { getTenantAdminTenantAccessIDs } from '@/utilities/getTenantAccessIDs'
|
||||
import { SelectInput, useAuth } from '@payloadcms/ui'
|
||||
import * as qs from 'qs-esm'
|
||||
import React from 'react'
|
||||
|
||||
import type { Tenant, User } from '../../../payload-types.js'
|
||||
import type { Tenant, User } from '../../payload-types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => {
|
||||
const { user } = useAuth<User>()
|
||||
const [options, setOptions] = React.useState<OptionObject[]>([])
|
||||
const [value, setValue] = React.useState<string | undefined>(initialCookie)
|
||||
|
||||
const isSuperAdmin = user?.roles?.includes('super-admin')
|
||||
const tenantIDs =
|
||||
@@ -28,18 +29,6 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
|
||||
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[]) => {
|
||||
if (!option) {
|
||||
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 (
|
||||
<div className="tenant-selector">
|
||||
<SelectInput
|
||||
@@ -59,7 +85,7 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
path="setTenant"
|
||||
value={value}
|
||||
value={options.find((opt) => opt.value === initialCookie)?.value}
|
||||
/>
|
||||
</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 { RelationshipField, useAuth, useFieldProps } from '@payloadcms/ui'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { cookies as getCookies, headers as getHeaders } from 'next/headers'
|
||||
import React from 'react'
|
||||
|
||||
import type { User } from '../../../../payload-types.js'
|
||||
import { TenantFieldComponentClient } from './Field.client'
|
||||
|
||||
export const TenantFieldComponent = () => {
|
||||
const { user } = useAuth<User>()
|
||||
const { path, readOnly } = useFieldProps()
|
||||
export const TenantFieldComponent: React.FC<{
|
||||
path: string
|
||||
payload: Payload
|
||||
readOnly: boolean
|
||||
}> = async (args) => {
|
||||
const cookies = getCookies()
|
||||
const headers = getHeaders()
|
||||
const { user } = await args.payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
if ((user.tenants && user.tenants.length > 1) || user?.roles?.includes('super-admin')) {
|
||||
if (
|
||||
user &&
|
||||
((Array.isArray(user.tenants) && user.tenants.length > 1) ||
|
||||
user?.roles?.includes('super-admin'))
|
||||
) {
|
||||
return (
|
||||
<RelationshipField
|
||||
label="Tenant"
|
||||
name={path}
|
||||
path={path}
|
||||
readOnly={readOnly}
|
||||
relationTo="tenants"
|
||||
required
|
||||
<TenantFieldComponentClient
|
||||
initialValue={cookies.get('payload-tenant')?.value || undefined}
|
||||
path={args.path}
|
||||
readOnly={args.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Field } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../access/isSuperAdmin'
|
||||
import { tenantFieldUpdate } from './access/update'
|
||||
import { TenantFieldComponent } from './components/Field'
|
||||
import { autofillTenant } from './hooks/autofillTenant'
|
||||
|
||||
export const tenantField: Field = {
|
||||
@@ -17,7 +16,7 @@ export const tenantField: Field = {
|
||||
},
|
||||
admin: {
|
||||
components: {
|
||||
Field: TenantFieldComponent,
|
||||
Field: '@/fields/TenantField/components/Field#TenantFieldComponent',
|
||||
},
|
||||
position: 'sidebar',
|
||||
},
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface Config {
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
@@ -26,15 +29,20 @@ export interface Config {
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
password: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
|
||||
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'url'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Tenants } from './collections/Tenants'
|
||||
import Users from './collections/Users'
|
||||
import { TenantSelectorRSC } from './components/TenantSelector'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -15,7 +14,7 @@ const dirname = path.dirname(filename)
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
afterNavLinks: [TenantSelectorRSC],
|
||||
afterNavLinks: ['@/components/TenantSelector#TenantSelectorRSC'],
|
||||
},
|
||||
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[] => {
|
||||
if (!user) return []
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user