feat(plugin-multi-tenant): improves tenant assignment flow (#13881)

### Improved tenant assignment flow
This PR improves the tenant assignment flow. I know a lot of users liked
the previous flow where the field was not injected into the document.
But the original flow, confused many of users because the tenant filter
(top left) was being used to set the tenant on the document _and_ filter
the list view.

This change shown below is aiming to solve both of those groups with a
slightly different approach. As always, feedback is welcome while we try
to really make this plugin work for everyone.


https://github.com/user-attachments/assets/ceee8b3a-c5f5-40e9-8648-f583e2412199

Added 2 new localization strings:

```
// shown in the 3 dot menu
'assign-tenant-button-label': 'Assign Tenant',

// shown when needing to assign a tenant to a NEW document
'assign-tenant-modal-title': 'Assign "{{title}}"',
```

Removed 2 localization strings:
```
'confirm-modal-tenant-switch--body',
'confirm-modal-tenant-switch--heading'
```
This commit is contained in:
Jarrod Flesch
2025-09-24 13:19:33 -04:00
committed by GitHub
parent 3f5c989954
commit fcb8b5a066
61 changed files with 699 additions and 378 deletions

View File

@@ -136,21 +136,27 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
translations: { translations: {
[key in AcceptedLanguages]?: { [key in AcceptedLanguages]?: {
/** /**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>' * Shown inside 3 dot menu on edit document view
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
* *
* @default 'Confirm {{tenantLabel}} change' * @default 'Assign Tenant'
*/ */
'confirm-modal-tenant-switch--heading'?: string 'assign-tenant-button-label'?: string
/** /**
* Shown as the title of the assign tenant modal
*
* @default 'Assign "{{title}}"'
*/
'assign-tenant-modal-title'?: string
/**
* Shown as the label for the assigned tenant field in the assign tenant modal
*
* @default 'Assigned Tenant' * @default 'Assigned Tenant'
*/ */
'field-assignedTenant-label'?: string 'field-assignedTenant-label'?: string
/** /**
* @default 'Tenant' * Shown as the label for the global tenant selector in the admin UI
*
* @default 'Filter by Tenant'
*/ */
'nav-tenantSelector-label'?: string 'nav-tenantSelector-label'?: string
} }

View File

@@ -0,0 +1,132 @@
'use client'
import type { ClientCollectionConfig } from 'payload'
import {
Button,
Modal,
Pill,
PopupList,
useConfig,
useDocumentInfo,
useDocumentTitle,
useModal,
useTranslation,
} from '@payloadcms/ui'
import { drawerZBase, useDrawerDepth } from '@payloadcms/ui/elements/Drawer'
import React from 'react'
import type {
PluginMultiTenantTranslationKeys,
PluginMultiTenantTranslations,
} from '../../translations/index.js'
import './index.scss'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
export const assignTenantModalSlug = 'assign-tenant-field-modal'
const baseClass = 'assign-tenant-field-modal'
export const AssignTenantFieldTrigger: React.FC = () => {
const { openModal } = useModal()
const { t } = useTranslation<PluginMultiTenantTranslations, PluginMultiTenantTranslationKeys>()
const { options } = useTenantSelection()
if (options.length <= 1) {
return null
}
return (
<>
<PopupList.Button onClick={() => openModal(assignTenantModalSlug)}>
{t('plugin-multi-tenant:assign-tenant-button-label')}
</PopupList.Button>
</>
)
}
export const AssignTenantFieldModal: React.FC<{
afterModalClose: () => void
afterModalOpen: () => void
children: React.ReactNode
onCancel?: () => void
onConfirm?: () => void
}> = ({ afterModalClose, afterModalOpen, children, onCancel, onConfirm }) => {
const editDepth = useDrawerDepth()
const { t } = useTranslation<PluginMultiTenantTranslations, PluginMultiTenantTranslationKeys>()
const { collectionSlug } = useDocumentInfo()
const { title } = useDocumentTitle()
const { getEntityConfig } = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const { closeModal, isModalOpen: isModalOpenFn } = useModal()
const isModalOpen = isModalOpenFn(assignTenantModalSlug)
const wasModalOpenRef = React.useRef<boolean>(isModalOpen)
const onModalConfirm = React.useCallback(() => {
if (typeof onConfirm === 'function') {
onConfirm()
}
closeModal(assignTenantModalSlug)
}, [onConfirm, closeModal])
const onModalCancel = React.useCallback(() => {
if (typeof onCancel === 'function') {
onCancel()
}
closeModal(assignTenantModalSlug)
}, [onCancel, closeModal])
React.useEffect(() => {
if (wasModalOpenRef.current && !isModalOpen) {
// modal was open, and now is closed
if (typeof afterModalClose === 'function') {
afterModalClose()
}
}
if (!wasModalOpenRef.current && isModalOpen) {
// modal was closed, and now is open
if (typeof afterModalOpen === 'function') {
afterModalOpen()
}
}
wasModalOpenRef.current = isModalOpen
}, [isModalOpen, onCancel, afterModalClose, afterModalOpen])
if (!collectionConfig) {
return null
}
return (
<Modal
className={baseClass}
slug={assignTenantModalSlug}
style={{
zIndex: drawerZBase + editDepth,
}}
>
<div className={`${baseClass}__bg`} />
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__header`}>
<h3>
{t('plugin-multi-tenant:assign-tenant-modal-title', {
title,
})}
</h3>
<Pill className={`${baseClass}__collection-pill`} size="small">
<>{collectionConfig.labels.singular}</>
</Pill>
</div>
<div className={`${baseClass}__content`}>{children}</div>
<div className={`${baseClass}__actions`}>
<Button buttonStyle="secondary" margin={false} onClick={onModalCancel}>
{t('general:cancel')}
</Button>
<Button margin={false} onClick={onModalConfirm}>
{t('general:confirm')}
</Button>
</div>
</div>
</Modal>
)
}

View File

@@ -0,0 +1,78 @@
@layer payload-default {
.assign-tenant-field-modal {
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&[open] {
pointer-events: auto;
.assign-tenant-field-modal__wrapper {
display: flex;
}
}
&__bg {
z-index: -1;
&:before {
content: ' ';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: var(--theme-bg);
opacity: 0.8;
}
}
&__wrapper {
z-index: 1;
position: relative;
display: none;
flex-direction: column;
max-width: calc(var(--base) * 30);
min-width: min(500px, calc(100% - (var(--base) * 2)));
border-radius: var(--style-radius-m);
border: 1px solid var(--theme-elevation-100);
background-color: var(--theme-bg);
justify-content: center;
}
&__header {
padding: calc(var(--base) * 0.75) var(--base);
border-bottom: 1px solid var(--theme-elevation-100);
display: flex;
flex-direction: row;
justify-content: space-between;
gap: calc(var(--base) * 2);
}
&__collection-pill {
align-self: flex-start;
flex-shrink: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: calc(var(--base) * 0.5);
padding: var(--base) var(--base) 0 var(--base);
}
&__controls {
display: flex;
gap: calc(var(--base) * 0.5);
}
&__actions {
display: flex;
justify-content: flex-end;
gap: calc(var(--base) * 0.4);
padding: var(--base);
}
}
}

View File

@@ -1,27 +1,24 @@
'use client' 'use client'
import type { RelationshipFieldClientProps, StaticLabel } from 'payload' import type { RelationshipFieldClientProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { import {
ConfirmationModal, Pill,
RelationshipField, RelationshipField,
Translation, useDocumentInfo,
useField, useField,
useForm, useForm,
useFormModified, useFormModified,
useModal, useModal,
useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import React from 'react' import React from 'react'
import type {
PluginMultiTenantTranslationKeys,
PluginMultiTenantTranslations,
} from '../../translations/index.js'
import './index.scss'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js' import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
import {
AssignTenantFieldModal,
assignTenantModalSlug,
} from '../AssignTenantFieldModal/index.client.js'
import './index.scss'
const baseClass = 'tenantField' const baseClass = 'tenantField'
@@ -30,17 +27,74 @@ type Props = {
unique?: boolean unique?: boolean
} & RelationshipFieldClientProps } & RelationshipFieldClientProps
export const TenantField = (args: Props) => { export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
const { entityType, options, selectedTenantID, setEntityType, setTenant } = useTenantSelection() const { entityType, options, selectedTenantID, setEntityType, setTenant } = useTenantSelection()
const { value } = useField<number | string>() const { setValue, showError, value } = useField<(number | string)[] | (number | string)>()
const modified = useFormModified()
const { isValid: isFormValid, setModified } = useForm()
const { id: docID } = useDocumentInfo()
const { openModal } = useModal()
const isConfirmingRef = React.useRef<boolean>(false)
const prevModified = React.useRef(modified)
const prevValue = React.useRef<typeof value>(value)
const showField =
(options.length > 1 && !fieldArgs.field.admin?.hidden && !fieldArgs.field.hidden) || debug
const onConfirm = React.useCallback(() => {
isConfirmingRef.current = true
}, [])
const afterModalOpen = React.useCallback(() => {
prevModified.current = modified
prevValue.current = value
}, [modified, value])
const afterModalClose = React.useCallback(() => {
let didChange = true
if (isConfirmingRef.current) {
// did the values actually change?
if (fieldArgs.field.hasMany) {
const prev = (prevValue.current || []) as (number | string)[]
const newValue = (value || []) as (number | string)[]
if (prev.length !== newValue.length) {
didChange = true
} else {
const allMatch = newValue.every((val) => prev.includes(val))
if (allMatch) {
didChange = false
}
}
} else if (value === prevValue.current) {
didChange = false
}
if (didChange) {
prevModified.current = true
prevValue.current = value
}
}
setValue(prevValue.current, true)
setModified(prevModified.current)
isConfirmingRef.current = false
}, [setValue, setModified, value, fieldArgs.field.hasMany])
React.useEffect(() => { React.useEffect(() => {
if (!entityType) { if (!entityType) {
setEntityType(args.unique ? 'global' : 'document') setEntityType(unique ? 'global' : 'document')
} else { } else {
// unique documents are controlled from the global TenantSelector // unique documents are controlled from the global TenantSelector
if (!args.unique && value) { if (!unique && value) {
if (!selectedTenantID || value !== selectedTenantID) { if (Array.isArray(value)) {
if (value.length) {
if (!selectedTenantID) {
setTenant({ id: value[0], refresh: false })
} else if (!value.includes(selectedTenantID)) {
setTenant({ id: value[0], refresh: false })
}
}
} else if (selectedTenantID !== value) {
setTenant({ id: value, refresh: false }) setTenant({ id: value, refresh: false })
} }
} }
@@ -51,137 +105,73 @@ export const TenantField = (args: Props) => {
setEntityType(undefined) setEntityType(undefined)
} }
} }
}, [args.unique, options, selectedTenantID, setTenant, value, setEntityType, entityType]) }, [unique, options, selectedTenantID, setTenant, value, setEntityType, entityType])
if (options.length > 1 && !args.field.admin?.hidden && !args.field.hidden) { React.useEffect(() => {
return ( if (unique) {
<> return
<div className={baseClass}> }
<div className={`${baseClass}__wrapper`}> if ((!isFormValid && showError && showField) || (!value && !selectedTenantID)) {
<RelationshipField openModal(assignTenantModalSlug)
{...args} }
field={{ }, [isFormValid, showError, showField, openModal, value, docID, selectedTenantID, unique])
...args.field,
required: true, if (showField) {
}} if (debug) {
readOnly={args.readOnly || args.field.admin?.readOnly || args.unique} return <TenantFieldInModal debug={debug} fieldArgs={fieldArgs} unique={unique} />
/> }
</div>
</div> if (!unique) {
{args.unique ? ( /** Editing a non-global tenant document */
<SyncFormModified /> return (
) : ( <AssignTenantFieldModal
<ConfirmTenantChange fieldLabel={args.field.label} fieldPath={args.path} /> afterModalClose={afterModalClose}
)} afterModalOpen={afterModalOpen}
</> onConfirm={onConfirm}
) >
<TenantFieldInModal
debug={debug}
fieldArgs={{
...fieldArgs,
field: {
...fieldArgs.field,
},
}}
unique={unique}
/>
</AssignTenantFieldModal>
)
}
return <SyncFormModified />
} }
return null return null
} }
const confirmSwitchTenantSlug = 'confirm-switch-tenant' const TenantFieldInModal: React.FC<{
debug?: boolean
const ConfirmTenantChange = ({ fieldArgs: RelationshipFieldClientProps
fieldLabel, unique?: boolean
fieldPath, }> = ({ debug, fieldArgs, unique }) => {
}: {
fieldLabel?: StaticLabel
fieldPath: string
}) => {
const { options, selectedTenantID, setTenant } = useTenantSelection()
const { setValue: setTenantFormValue, value: tenantFormValue } = useField<null | number | string>(
{ path: fieldPath },
)
const { setModified } = useForm()
const modified = useFormModified()
const { i18n, t } = useTranslation<
PluginMultiTenantTranslations,
PluginMultiTenantTranslationKeys
>()
const { isModalOpen, openModal } = useModal()
const prevTenantValueRef = React.useRef<null | number | string>(tenantFormValue || null)
const [tenantToConfirm, setTenantToConfirm] = React.useState<null | number | string>(
tenantFormValue || null,
)
const fromTenantOption = React.useMemo(() => {
if (tenantFormValue) {
return options.find((option) => option.value === tenantFormValue)
}
return undefined
}, [options, tenantFormValue])
const toTenantOption = React.useMemo(() => {
if (tenantToConfirm) {
return options.find((option) => option.value === tenantToConfirm)
}
return undefined
}, [options, tenantToConfirm])
const modalIsOpen = isModalOpen(confirmSwitchTenantSlug)
const testRef = React.useRef<boolean>(false)
React.useEffect(() => {
// the form value changed
if (
!modalIsOpen &&
tenantFormValue &&
prevTenantValueRef.current &&
tenantFormValue !== prevTenantValueRef.current
) {
// revert the form value change temporarily
setTenantFormValue(prevTenantValueRef.current, true)
// save the tenant to confirm in modal
setTenantToConfirm(tenantFormValue)
// open confirmation modal
openModal(confirmSwitchTenantSlug)
}
}, [
tenantFormValue,
setTenantFormValue,
openModal,
setTenant,
selectedTenantID,
modalIsOpen,
modified,
])
return ( return (
<ConfirmationModal <div className={baseClass}>
body={ <div className={`${baseClass}__wrapper`}>
<Translation {debug && (
elements={{ <Pill className={`${baseClass}__debug-pill`} pillStyle="success" size="small">
0: ({ children }) => { Multi-Tenant Debug Enabled
return <b>{children}</b> </Pill>
}, )}
}} <RelationshipField
// eslint-disable-next-line @typescript-eslint/ban-ts-comment {...fieldArgs}
// @ts-expect-error field={{
i18nKey="plugin-multi-tenant:confirm-modal-tenant-switch--body" ...fieldArgs.field,
t={t} required: true,
variables={{
fromTenant: fromTenantOption?.label,
toTenant: toTenantOption?.label,
}} }}
readOnly={fieldArgs.readOnly || fieldArgs.field.admin?.readOnly || unique}
/> />
} </div>
heading={t('plugin-multi-tenant:confirm-modal-tenant-switch--heading', { </div>
tenantLabel: fieldLabel
? getTranslation(fieldLabel, i18n)
: t('plugin-multi-tenant:nav-tenantSelector-label'),
})}
modalSlug={confirmSwitchTenantSlug}
onCancel={() => {
setModified(testRef.current)
}}
onConfirm={() => {
// set the form value to the tenant to confirm
prevTenantValueRef.current = tenantToConfirm
setTenantFormValue(tenantToConfirm)
}}
/>
) )
} }

View File

@@ -43,6 +43,10 @@
margin-top: calc(var(--base) * -1.5); margin-top: calc(var(--base) * -1.5);
padding-top: calc(var(--base) * 1.5); padding-top: calc(var(--base) * 1.5);
} }
&__debug-pill {
margin-bottom: calc(var(--base) * 0.5);
}
} }
} }
} }

View File

@@ -1,3 +1,4 @@
export { AssignTenantFieldTrigger } from '../components/AssignTenantFieldModal/index.client.js'
export { TenantField } from '../components/TenantField/index.client.js' export { TenantField } from '../components/TenantField/index.client.js'
export { WatchTenantCollection } from '../components/WatchTenantCollection/index.js' export { WatchTenantCollection } from '../components/WatchTenantCollection/index.js'
export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js' export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js'

View File

@@ -339,6 +339,16 @@ export const multiTenantPlugin =
collection.disableDuplicate = true collection.disableDuplicate = true
} }
if (!pluginConfig.debug && !isGlobal) {
collection.admin ??= {}
collection.admin.components ??= {}
collection.admin.components.edit ??= {}
collection.admin.components.edit.editMenuItems ??= []
collection.admin.components.edit.editMenuItems.push({
path: '@payloadcms/plugin-multi-tenant/client#AssignTenantFieldTrigger',
})
}
/** /**
* Add filter options to all relationship fields * Add filter options to all relationship fields
*/ */

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const arTranslations: PluginDefaultTranslationsObject = { export const arTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'تعيين المستأجر',
'أنت على وشك تغيير الملكية من <0>{{fromTenant}}</0> إلى <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'قم بتعيين "{{title}}"',
'confirm-modal-tenant-switch--heading': 'تأكيد تغيير {{tenantLabel}}',
'field-assignedTenant-label': 'المستأجر المعين', 'field-assignedTenant-label': 'المستأجر المعين',
'nav-tenantSelector-label': 'المستأجر', 'nav-tenantSelector-label': 'المستأجر',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const azTranslations: PluginDefaultTranslationsObject = { export const azTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Kirayəçiyə təyin et',
'Siz <0>{{fromTenant}}</0>-dən <0>{{toTenant}}</0>-a mülkiyyəti dəyişməyə hazırlaşırsınız', 'assign-tenant-modal-title': '"{{title}}" təyin edin',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} dəyişikliyini təsdiqləyin',
'field-assignedTenant-label': 'Təyin edilmiş İcarəçi', 'field-assignedTenant-label': 'Təyin edilmiş İcarəçi',
'nav-tenantSelector-label': 'Kirayəçi', 'nav-tenantSelector-label': 'Kirayəçi',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const bgTranslations: PluginDefaultTranslationsObject = { export const bgTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Назначаване на Tenant',
'Предстои да промените собствеността от <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Назначете "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Потвърждаване на промяна в {{tenantLabel}}',
'field-assignedTenant-label': 'Назначен наемател', 'field-assignedTenant-label': 'Назначен наемател',
'nav-tenantSelector-label': 'Потребител', 'nav-tenantSelector-label': 'Потребител',
}, },

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const bnBdTranslations: PluginDefaultTranslationsObject = { export const bnBdTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'টেনেন্ট নির্ধারণ করুন',
'আপনি <0>{{fromTenant}}</0> থেকে <0>{{toTenant}}</0> তে মালিকানা পরিবর্তন করতে চলেছেন।', 'assign-tenant-modal-title': '"{{title}}" নিয়োগ করুন',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} পরিবর্তন নিশ্চিত করুন', 'field-assignedTenant-label': 'নিযুক্ত টেনেন্ট',
'field-assignedTenant-label': 'নির্ধারিত টেনেন্ট', 'nav-tenantSelector-label': 'টেনেন্ট অনুসারে ফিল্টার করুন',
'nav-tenantSelector-label': 'ভাড়াটিয়া',
}, },
} }

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const bnInTranslations: PluginDefaultTranslationsObject = { export const bnInTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'টেনেন্ট নিয়োগ করুন',
'আপনি স্বত্বাধিকার পরিবর্তন করতে চলেছেন <0>{{fromTenant}}</0> থেকে <0>{{toTenant}}</0> এ।', 'assign-tenant-modal-title': '"{{title}}" এর দায়িত্ব দিন',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} পরিবর্তন নিশ্চিত করুন',
'field-assignedTenant-label': 'নির্ধারিত টেনেন্ট', 'field-assignedTenant-label': 'নির্ধারিত টেনেন্ট',
'nav-tenantSelector-label': 'ভাড়াটিয়া', 'nav-tenantSelector-label': 'টেনেন্ট অনুসারে ফিল্টার করুন',
}, },
} }

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const caTranslations: PluginDefaultTranslationsObject = { export const caTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Assignar Tenant',
'Està a punt de canviar la propietat de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Assigna "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirmeu el canvi de {{tenantLabel}}',
'field-assignedTenant-label': 'Llogater Assignat', 'field-assignedTenant-label': 'Llogater Assignat',
'nav-tenantSelector-label': 'Inquilí', 'nav-tenantSelector-label': 'Inquilí',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const csTranslations: PluginDefaultTranslationsObject = { export const csTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Přiřadit nájemce',
'Chystáte se změnit vlastnictví z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Přiřadit "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potvrďte změnu {{tenantLabel}}',
'field-assignedTenant-label': 'Přiřazený nájemce', 'field-assignedTenant-label': 'Přiřazený nájemce',
'nav-tenantSelector-label': 'Nájemce', 'nav-tenantSelector-label': 'Nájemce',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const daTranslations: PluginDefaultTranslationsObject = { export const daTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Tildel Tenant',
'Du er ved at skifte ejerskab fra <0>{{fromTenant}}</0> til <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Tildel "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Bekræft ændring af {{tenantLabel}}',
'field-assignedTenant-label': 'Tildelt Lejer', 'field-assignedTenant-label': 'Tildelt Lejer',
'nav-tenantSelector-label': 'Lejer', 'nav-tenantSelector-label': 'Lejer',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const deTranslations: PluginDefaultTranslationsObject = { export const deTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Mieter zuweisen',
'Sie sind dabei, den Besitz von <0>{{fromTenant}}</0> zu <0>{{toTenant}}</0> zu ändern.', 'assign-tenant-modal-title': 'Weisen Sie "{{title}}" zu',
'confirm-modal-tenant-switch--heading': 'Bestätigung der Änderung von {{tenantLabel}}',
'field-assignedTenant-label': 'Zugewiesener Mandant', 'field-assignedTenant-label': 'Zugewiesener Mandant',
'nav-tenantSelector-label': 'Mieter', 'nav-tenantSelector-label': 'Mieter',
}, },

View File

@@ -2,11 +2,10 @@ import type { PluginLanguage } from '../types.js'
export const enTranslations = { export const enTranslations = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Assign Tenant',
'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Assign "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirm {{tenantLabel}} change',
'field-assignedTenant-label': 'Assigned Tenant', 'field-assignedTenant-label': 'Assigned Tenant',
'nav-tenantSelector-label': 'Tenant', 'nav-tenantSelector-label': 'Filter by Tenant',
}, },
} }

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const esTranslations: PluginDefaultTranslationsObject = { export const esTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Asignar Inquilino',
'Está a punto de cambiar la propiedad de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Asignar "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirme el cambio de {{tenantLabel}}',
'field-assignedTenant-label': 'Inquilino Asignado', 'field-assignedTenant-label': 'Inquilino Asignado',
'nav-tenantSelector-label': 'Inquilino', 'nav-tenantSelector-label': 'Inquilino',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const etTranslations: PluginDefaultTranslationsObject = { export const etTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Määra Tenant',
'Te olete just muutmas omandiõigust <0>{{fromTenant}}</0> -lt <0>{{toTenant}}</0> -le.', 'assign-tenant-modal-title': 'Määra "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Kinnita {{tenantLabel}} muutus',
'field-assignedTenant-label': 'Määratud üürnik', 'field-assignedTenant-label': 'Määratud üürnik',
'nav-tenantSelector-label': 'Üürnik', 'nav-tenantSelector-label': 'Üürnik',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const faTranslations: PluginDefaultTranslationsObject = { export const faTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'اختصاص Tenant',
'شما در حال تغییر مالکیت از <0>{{fromTenant}}</0> به <0>{{toTenant}}</0> هستید.', 'assign-tenant-modal-title': 'اختصاص "{{title}}"',
'confirm-modal-tenant-switch--heading': 'تأیید تغییر {{tenantLabel}}',
'field-assignedTenant-label': 'مستاجر اختصاص یافته', 'field-assignedTenant-label': 'مستاجر اختصاص یافته',
'nav-tenantSelector-label': 'مستاجر', 'nav-tenantSelector-label': 'مستاجر',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const frTranslations: PluginDefaultTranslationsObject = { export const frTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Attribuer un Locataire',
'Vous êtes sur le point de changer la propriété de <0>{{fromTenant}}</0> à <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Attribuer "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirmer le changement de {{tenantLabel}}',
'field-assignedTenant-label': 'Locataire Attribué', 'field-assignedTenant-label': 'Locataire Attribué',
'nav-tenantSelector-label': 'Locataire', 'nav-tenantSelector-label': 'Locataire',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const heTranslations: PluginDefaultTranslationsObject = { export const heTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'הקצה Tenant',
'אתה עומד לשנות בעלות מ- <0>{{fromTenant}}</0> ל- <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'הקצה "{{title}}"',
'confirm-modal-tenant-switch--heading': 'אשר שינוי {{tenantLabel}}',
'field-assignedTenant-label': 'דייר מוקצה', 'field-assignedTenant-label': 'דייר מוקצה',
'nav-tenantSelector-label': 'דייר', 'nav-tenantSelector-label': 'דייר',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const hrTranslations: PluginDefaultTranslationsObject = { export const hrTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Dodijeli Najmoprimca',
'Na rubu ste promjene vlasništva iz <0>{{fromTenant}}</0> u <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Dodijeli "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potvrdite promjenu {{tenantLabel}}',
'field-assignedTenant-label': 'Dodijeljeni stanar', 'field-assignedTenant-label': 'Dodijeljeni stanar',
'nav-tenantSelector-label': 'Podstanar', 'nav-tenantSelector-label': 'Podstanar',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const huTranslations: PluginDefaultTranslationsObject = { export const huTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Hozzárendelési bérlő',
'Közel áll ahhoz, hogy megváltoztassa a tulajdonságot <0>{{fromTenant}}</0> -ból <0>{{toTenant}}</0> -ba.', 'assign-tenant-modal-title': 'Rendelje hozzá a "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Erősítse meg a {{tenantLabel}} változást',
'field-assignedTenant-label': 'Kijelölt Bérlő', 'field-assignedTenant-label': 'Kijelölt Bérlő',
'nav-tenantSelector-label': 'Bérlő', 'nav-tenantSelector-label': 'Bérlő',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const hyTranslations: PluginDefaultTranslationsObject = { export const hyTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Տեղադրել Tenant',
'Դուք պատրաստվում եք փոխել սեփականությունը <0>{{fromTenant}}</0>-ից <0>{{toTenant}}</0>-ին:', 'assign-tenant-modal-title': 'Հանձնել "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Հաստատեք {{tenantLabel}}֊ի փոփոխությունը',
'field-assignedTenant-label': 'Հանձնարարված վարձակալ', 'field-assignedTenant-label': 'Հանձնարարված վարձակալ',
'nav-tenantSelector-label': 'Տենանտ', 'nav-tenantSelector-label': 'Տենանտ',
}, },

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const idTranslations: PluginDefaultTranslationsObject = { export const idTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Tetapkan Tenant',
'Anda akan mengubah kepemilikan dari <0>{{fromTenant}}</0> ke <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Tetapkan "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Konfirmasi perubahan {{tenantLabel}}',
'field-assignedTenant-label': 'Penyewa yang Ditugaskan', 'field-assignedTenant-label': 'Penyewa yang Ditugaskan',
'nav-tenantSelector-label': 'Penyewa', 'nav-tenantSelector-label': 'Filter berdasarkan Tenant',
}, },
} }

View File

@@ -0,0 +1,15 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const isTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'assign-tenant-button-label': 'Úthluta leigjanda',
'assign-tenant-modal-title': 'Úthluta "{{title}}"',
'field-assignedTenant-label': 'Úthlutaður leigjandi',
'nav-tenantSelector-label': 'Síaðu eftir leigjanda',
},
}
export const is: PluginLanguage = {
dateFNSKey: 'is',
translations: isTranslations,
}

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const itTranslations: PluginDefaultTranslationsObject = { export const itTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Assegna Tenant',
'Stai per cambiare il possesso da <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Assegna "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Conferma il cambiamento di {{tenantLabel}}',
'field-assignedTenant-label': 'Inquilino Assegnato', 'field-assignedTenant-label': 'Inquilino Assegnato',
'nav-tenantSelector-label': 'Inquilino', 'nav-tenantSelector-label': 'Inquilino',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const jaTranslations: PluginDefaultTranslationsObject = { export const jaTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'テナントを割り当てる',
'あなたは、<0>{{fromTenant}}</0>から<0>{{toTenant}}</0>への所有権を変更しようとしています。', 'assign-tenant-modal-title': '"{{title}}"を割り当てる',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}}の変更を確認します',
'field-assignedTenant-label': '割り当てられたテナント', 'field-assignedTenant-label': '割り当てられたテナント',
'nav-tenantSelector-label': 'テナント', 'nav-tenantSelector-label': 'テナント',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const koTranslations: PluginDefaultTranslationsObject = { export const koTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': '테넌트 지정',
'<0>{{fromTenant}}</0>에서 <0>{{toTenant}}</0>로 소유권을 변경하려고 합니다.', 'assign-tenant-modal-title': '"{{title}}"를 지정하십시오.',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} 변경 확인',
'field-assignedTenant-label': '지정된 세입자', 'field-assignedTenant-label': '지정된 세입자',
'nav-tenantSelector-label': '세입자', 'nav-tenantSelector-label': '세입자',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const ltTranslations: PluginDefaultTranslationsObject = { export const ltTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Priskirkite nuomininką',
'Jūs ketinate pakeisti nuosavybę iš <0>{{fromTenant}}</0> į <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Paskirkite "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Patvirtinkite {{tenantLabel}} pakeitimą',
'field-assignedTenant-label': 'Paskirtas nuomininkas', 'field-assignedTenant-label': 'Paskirtas nuomininkas',
'nav-tenantSelector-label': 'Nuomininkas', 'nav-tenantSelector-label': 'Nuomininkas',
}, },

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const lvTranslations: PluginDefaultTranslationsObject = { export const lvTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Piešķirt Tenant',
'Jūs gatavojaties mainīt īpašumtiesības no <0>{{fromTenant}}</0> uz <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Piešķirt "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Apstipriniet {{tenantLabel}} izmaiņu', 'field-assignedTenant-label': 'Piešķirtais tenants',
'field-assignedTenant-label': 'Piešķirts nomnieks', 'nav-tenantSelector-label': 'Filtrēt pēc Nomnieka',
'nav-tenantSelector-label': 'Nomnieks',
}, },
} }

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const myTranslations: PluginDefaultTranslationsObject = { export const myTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'အသစ်ထည့်သည့် Tenant',
'Anda akan menukar pemilikan dari <0>{{fromTenant}}</0> kepada <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Tetapkan "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Sahkan perubahan {{tenantLabel}}',
'field-assignedTenant-label': 'ခွဲစိုက်ထားသော အငှားယူသူ', 'field-assignedTenant-label': 'ခွဲစိုက်ထားသော အငှားယူသူ',
'nav-tenantSelector-label': 'Penyewa', 'nav-tenantSelector-label': 'Penyewa',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const nbTranslations: PluginDefaultTranslationsObject = { export const nbTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Tildel Leietaker',
'Du er i ferd med å endre eierskap fra <0>{{fromTenant}}</0> til <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Tildel "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Bekreft endring av {{tenantLabel}}',
'field-assignedTenant-label': 'Tildelt leietaker', 'field-assignedTenant-label': 'Tildelt leietaker',
'nav-tenantSelector-label': 'Leietaker', 'nav-tenantSelector-label': 'Leietaker',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const nlTranslations: PluginDefaultTranslationsObject = { export const nlTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Toewijzen Tenant',
'U staat op het punt om eigenaarschap te wijzigen van <0>{{fromTenant}}</0> naar <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Wijs "{{title}}" toe',
'confirm-modal-tenant-switch--heading': 'Bevestig wijziging van {{tenantLabel}}',
'field-assignedTenant-label': 'Toegewezen Huurder', 'field-assignedTenant-label': 'Toegewezen Huurder',
'nav-tenantSelector-label': 'Huurder', 'nav-tenantSelector-label': 'Huurder',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const plTranslations: PluginDefaultTranslationsObject = { export const plTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Przypisz Najemcę',
'Za chwilę nastąpi zmiana właściciela z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Przypisz "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potwierdź zmianę {{tenantLabel}}',
'field-assignedTenant-label': 'Przypisany Najemca', 'field-assignedTenant-label': 'Przypisany Najemca',
'nav-tenantSelector-label': 'Najemca', 'nav-tenantSelector-label': 'Najemca',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const ptTranslations: PluginDefaultTranslationsObject = { export const ptTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Atribuir Inquilino',
'Está prestes a mudar a propriedade de <0>{{fromTenant}}</0> para <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Atribuir "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirme a alteração do {{tenantLabel}}',
'field-assignedTenant-label': 'Inquilino Atribuído', 'field-assignedTenant-label': 'Inquilino Atribuído',
'nav-tenantSelector-label': 'Inquilino', 'nav-tenantSelector-label': 'Inquilino',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const roTranslations: PluginDefaultTranslationsObject = { export const roTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Alocați Tenant',
'Sunteți pe cale să schimbați proprietatea de la <0>{{fromTenant}}</0> la <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Atribuiți "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Confirmați modificarea {{tenantLabel}}',
'field-assignedTenant-label': 'Locatar Atribuit', 'field-assignedTenant-label': 'Locatar Atribuit',
'nav-tenantSelector-label': 'Locatar', 'nav-tenantSelector-label': 'Locatar',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const rsTranslations: PluginDefaultTranslationsObject = { export const rsTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Dodeli Tenant',
'Na putu ste da promenite vlasništvo od <0>{{fromTenant}}</0> do <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Dodelite "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potvrdite promenu {{tenantLabel}}',
'field-assignedTenant-label': 'Dodeljen stanar', 'field-assignedTenant-label': 'Dodeljen stanar',
'nav-tenantSelector-label': 'Podstanar', 'nav-tenantSelector-label': 'Podstanar',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const rsLatinTranslations: PluginDefaultTranslationsObject = { export const rsLatinTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Dodeli Tenant',
'Uskoro ćete promeniti vlasništvo sa <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Dodeli "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potvrdite promenu {{tenantLabel}}',
'field-assignedTenant-label': 'Dodeljen stanar', 'field-assignedTenant-label': 'Dodeljen stanar',
'nav-tenantSelector-label': 'Podstanar', 'nav-tenantSelector-label': 'Podstanar',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const ruTranslations: PluginDefaultTranslationsObject = { export const ruTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Назначить Арендатора',
'Вы собираетесь изменить владельца с <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Назначить "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Подтвердите изменение {{tenantLabel}}',
'field-assignedTenant-label': 'Назначенный Арендатор', 'field-assignedTenant-label': 'Назначенный Арендатор',
'nav-tenantSelector-label': 'Арендатор', 'nav-tenantSelector-label': 'Арендатор',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const skTranslations: PluginDefaultTranslationsObject = { export const skTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Priradiť nájomcu',
'Chystáte sa zmeniť vlastníctvo z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Priradiť "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potvrďte zmenu {{tenantLabel}}',
'field-assignedTenant-label': 'Pridelený nájomca', 'field-assignedTenant-label': 'Pridelený nájomca',
'nav-tenantSelector-label': 'Nájomca', 'nav-tenantSelector-label': 'Nájomca',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const slTranslations: PluginDefaultTranslationsObject = { export const slTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Dodeli najemnika',
'Pravkar ste na točki, da spremenite lastništvo iz <0>{{fromTenant}}</0> v <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Dodeli "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Potrdite spremembo {{tenantLabel}}',
'field-assignedTenant-label': 'Dodeljen najemnik', 'field-assignedTenant-label': 'Dodeljen najemnik',
'nav-tenantSelector-label': 'Najemnik', 'nav-tenantSelector-label': 'Najemnik',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const svTranslations: PluginDefaultTranslationsObject = { export const svTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Tilldela Hyresgäst',
'Du är på väg att ändra ägande från <0>{{fromTenant}}</0> till <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Tilldela "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Bekräfta ändring av {{tenantLabel}}',
'field-assignedTenant-label': 'Tilldelad hyresgäst', 'field-assignedTenant-label': 'Tilldelad hyresgäst',
'nav-tenantSelector-label': 'Hyresgäst', 'nav-tenantSelector-label': 'Hyresgäst',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const taTranslations: PluginDefaultTranslationsObject = { export const taTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'டெனன்டை ஒதுக்குக',
'நீங்கள் உரிமையைக் <0>{{fromTenant}}</0> இலிருந்து <0>{{toTenant}}</0> க்கு மாற்ற உள்ளீர்கள்', 'assign-tenant-modal-title': '"{{title}}"ஐ ஒதுக்கி வைக்கவும்.',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} மாற்றத்தை உறுதிப்படுத்தவும்',
'field-assignedTenant-label': 'ஒதுக்கப்பட்ட Tenant', 'field-assignedTenant-label': 'ஒதுக்கப்பட்ட Tenant',
'nav-tenantSelector-label': 'Tenant', 'nav-tenantSelector-label': 'Tenant',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const thTranslations: PluginDefaultTranslationsObject = { export const thTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'กำหนดผู้เช่า',
'คุณกำลังจะเปลี่ยนสิทธิ์การเป็นเจ้าของจาก <0>{{fromTenant}}</0> ไปยัง <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'มอบหมาย "{{title}}"',
'confirm-modal-tenant-switch--heading': 'ยืนยันการเปลี่ยนแปลง {{tenantLabel}}',
'field-assignedTenant-label': 'ผู้เช่าที่ได้รับการกำหนด', 'field-assignedTenant-label': 'ผู้เช่าที่ได้รับการกำหนด',
'nav-tenantSelector-label': 'ผู้เช่า', 'nav-tenantSelector-label': 'ผู้เช่า',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const trTranslations: PluginDefaultTranslationsObject = { export const trTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Kiracı Ata',
"<0>{{fromTenant}}</0>'den <0>{{toTenant}}</0>'ye sahipliği değiştirmek üzeresiniz.", 'assign-tenant-modal-title': '"{{title}}" atayın.',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} değişikliğini onayla',
'field-assignedTenant-label': 'Atanan Kiracı', 'field-assignedTenant-label': 'Atanan Kiracı',
'nav-tenantSelector-label': 'Kiracı', 'nav-tenantSelector-label': 'Kiracı',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const ukTranslations: PluginDefaultTranslationsObject = { export const ukTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Призначити орендаря',
'Ви збираєтеся змінити власність з <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Призначте "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Підтвердіть зміну {{tenantLabel}}',
'field-assignedTenant-label': 'Призначений орендар', 'field-assignedTenant-label': 'Призначений орендар',
'nav-tenantSelector-label': 'Орендар', 'nav-tenantSelector-label': 'Орендар',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const viTranslations: PluginDefaultTranslationsObject = { export const viTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': 'Giao Tenant',
'Bạn sắp chuyển quyền sở hữu từ <0>{{fromTenant}}</0> đến <0>{{toTenant}}</0>', 'assign-tenant-modal-title': 'Gán "{{title}}"',
'confirm-modal-tenant-switch--heading': 'Xác nhận thay đổi {{tenantLabel}}',
'field-assignedTenant-label': 'Người thuê đã được chỉ định', 'field-assignedTenant-label': 'Người thuê đã được chỉ định',
'nav-tenantSelector-label': 'Người thuê', 'nav-tenantSelector-label': 'Người thuê',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const zhTranslations: PluginDefaultTranslationsObject = { export const zhTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': '分配租户',
'您即将从<0>{{fromTenant}}</0>更改为<0>{{toTenant}}</0>的所有权', 'assign-tenant-modal-title': '分配"{{title}}"',
'confirm-modal-tenant-switch--heading': '确认更改{{tenantLabel}}',
'field-assignedTenant-label': '指定租户', 'field-assignedTenant-label': '指定租户',
'nav-tenantSelector-label': '租户', 'nav-tenantSelector-label': '租户',
}, },

View File

@@ -2,9 +2,8 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const zhTwTranslations: PluginDefaultTranslationsObject = { export const zhTwTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': 'assign-tenant-button-label': '指派租戶',
'您即將變更擁有者,從 <0>{{fromTenant}}</0> 切換為 <0>{{toTenant}}</0>', 'assign-tenant-modal-title': '將 "{{title}}"',
'confirm-modal-tenant-switch--heading': '確認變更 {{tenantLabel}}',
'field-assignedTenant-label': '指派的租用戶', 'field-assignedTenant-label': '指派的租用戶',
'nav-tenantSelector-label': '租戶', 'nav-tenantSelector-label': '租戶',
}, },

View File

@@ -4,8 +4,8 @@ import type { enTranslations } from './languages/en.js'
export type PluginLanguage = Language<{ export type PluginLanguage = Language<{
'plugin-multi-tenant': { 'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body': string 'assign-tenant-button-label': string
'confirm-modal-tenant-switch--heading': string 'assign-tenant-modal-title': string
'field-assignedTenant-label': string 'field-assignedTenant-label': string
'nav-tenantSelector-label': string 'nav-tenantSelector-label': string
} }

View File

@@ -92,21 +92,27 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
translations: { translations: {
[key in AcceptedLanguages]?: { [key in AcceptedLanguages]?: {
/** /**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>' * Shown inside 3 dot menu on edit document view
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
* *
* @default 'Confirm {{tenantLabel}} change' * @default 'Assign Tenant'
*/ */
'confirm-modal-tenant-switch--heading'?: string 'assign-tenant-button-label'?: string
/** /**
* Shown as the title of the assign tenant modal
*
* @default 'Assign "{{title}}"'
*/
'assign-tenant-modal-title'?: string
/**
* Shown as the label for the assigned tenant field in the assign tenant modal
*
* @default 'Assigned Tenant' * @default 'Assigned Tenant'
*/ */
'field-assignedTenant-label'?: string 'field-assignedTenant-label'?: string
/** /**
* @default 'Tenant' * Shown as the label for the global tenant selector in the admin UI
*
* @default 'Filter by Tenant'
*/ */
'nav-tenantSelector-label'?: string 'nav-tenantSelector-label'?: string
} }

View File

@@ -46,7 +46,7 @@ function createErrorsFromMessage(message: string): {
if (errors.length === 1) { if (errors.length === 1) {
return { return {
errors, errors,
message: `${intro}:`, message: `${intro}: `,
} }
} }

View File

@@ -2,9 +2,6 @@ import type { BrowserContext, Page } from '@playwright/test'
import type { TypeWithID } from 'payload' import type { TypeWithID } from 'payload'
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import { openDocControls } from 'helpers/e2e/openDocControls.js'
import { openNav } from 'helpers/e2e/toggleNav.js'
import path from 'path' import path from 'path'
import { wait } from 'payload/shared' import { wait } from 'payload/shared'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -12,8 +9,8 @@ import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js' import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js' import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
import { devUser } from '../credentials.js'
import { import {
closeNav,
ensureCompilationIsDone, ensureCompilationIsDone,
exactText, exactText,
initPageConsoleErrorCatch, initPageConsoleErrorCatch,
@@ -21,6 +18,8 @@ import {
} from '../helpers.js' } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { login } from '../helpers/e2e/auth/login.js' import { login } from '../helpers/e2e/auth/login.js'
import { openDocControls } from '../helpers/e2e/openDocControls.js'
import { closeNav, openNav } from '../helpers/e2e/toggleNav.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { import {

View File

@@ -7,15 +7,12 @@ import type {
} from '@playwright/test' } from '@playwright/test'
import type { Config } from 'payload' import type { Config } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { defaults } from 'payload' import { defaults } from 'payload'
import { wait } from 'payload/shared' import { wait } from 'payload/shared'
import shelljs from 'shelljs' import shelljs from 'shelljs'
import { setTimeout } from 'timers/promises' import { setTimeout } from 'timers/promises'
import { devUser } from './credentials.js'
import { openNav } from './helpers/e2e/toggleNav.js'
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js' import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
export type AdminRoutes = NonNullable<Config['admin']>['routes'] export type AdminRoutes = NonNullable<Config['admin']>['routes']
@@ -220,14 +217,6 @@ export async function openCreateDocDrawer(page: Page, fieldSelector: string): Pr
await wait(500) // wait for drawer form state to initialize await wait(500) // wait for drawer form state to initialize
} }
export async function closeNav(page: Page): Promise<void> {
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) {
return
}
await page.locator('.nav-toggler >> visible=true').click()
await expect(page.locator('.template-default.template-default--nav-open')).toBeHidden()
}
export async function openLocaleSelector(page: Page): Promise<void> { export async function openLocaleSelector(page: Page): Promise<void> {
const button = page.locator('.localizer button.popup-button') const button = page.locator('.localizer button.popup-button')
const popup = page.locator('.localizer .popup.popup--active') const popup = page.locator('.localizer .popup.popup--active')

View File

@@ -13,7 +13,7 @@ export async function assertToastErrors({
}): Promise<void> { }): Promise<void> {
const isSingleError = errors.length === 1 const isSingleError = errors.length === 1
const message = isSingleError const message = isSingleError
? 'The following field is invalid:' ? 'The following field is invalid: '
: `The following fields are invalid (${errors.length}):` : `The following fields are invalid (${errors.length}):`
// Check the intro message text // Check the intro message text

View File

@@ -24,3 +24,17 @@ export async function openNav(page: Page): Promise<void> {
await expect(page.locator('.nav--nav-animate[inert], .nav--nav-hydrated[inert]')).toBeHidden() await expect(page.locator('.nav--nav-animate[inert], .nav--nav-hydrated[inert]')).toBeHidden()
await expect(page.locator('.template-default.template-default--nav-open')).toBeVisible() await expect(page.locator('.template-default.template-default--nav-open')).toBeVisible()
} }
export async function closeNav(page: Page): Promise<void> {
// wait for the preferences/media queries to either open or close the nav
await expect(page.locator('.template-default--nav-hydrated')).toBeVisible()
// check to see if the nav is already closed and if so, return early
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) {
return
}
// playwright: get first element with .nav-toggler which is VISIBLE (not hidden), could be 2 elements with .nav-toggler on mobile and desktop but only one is visible
await page.locator('.nav-toggler >> visible=true').click()
await expect(page.locator('.template-default.template-default--nav-open')).toBeHidden()
}

View File

@@ -32,7 +32,7 @@ export default buildConfigWithDefaults({
onInit: seed, onInit: seed,
plugins: [ plugins: [
multiTenantPlugin<ConfigType>({ multiTenantPlugin<ConfigType>({
debug: true, // debug: true,
userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')), userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')),
useTenantsCollectionAccess: false, useTenantsCollectionAccess: false,
tenantField: { tenantField: {
@@ -52,9 +52,9 @@ export default buildConfigWithDefaults({
i18n: { i18n: {
translations: { translations: {
en: { en: {
'field-assignedTenant-label': 'Currently Assigned Site', 'field-assignedTenant-label': 'Site',
'nav-tenantSelector-label': 'Filter By Site', 'nav-tenantSelector-label': 'Filter by Site',
'confirm-modal-tenant-switch--heading': 'Confirm Site Change', 'assign-tenant-button-label': 'Assign Site',
}, },
}, },
}, },

View File

@@ -6,6 +6,7 @@ import * as path from 'path'
import { wait } from 'payload/shared' import { wait } from 'payload/shared'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config } from './payload-types.js' import type { Config } from './payload-types.js'
import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js' import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
@@ -18,7 +19,7 @@ import {
getSelectInputValue, getSelectInputValue,
selectInput, selectInput,
} from '../helpers/e2e/selectInput.js' } from '../helpers/e2e/selectInput.js'
import { openNav } from '../helpers/e2e/toggleNav.js' import { closeNav, openNav } from '../helpers/e2e/toggleNav.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js' import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
@@ -37,16 +38,19 @@ test.describe('Multi Tenant', () => {
let menuItemsURL: AdminUrlUtil let menuItemsURL: AdminUrlUtil
let usersURL: AdminUrlUtil let usersURL: AdminUrlUtil
let tenantsURL: AdminUrlUtil let tenantsURL: AdminUrlUtil
let payload: PayloadTestSDK<Config>
test.beforeAll(async ({ browser }, testInfo) => { test.beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG) testInfo.setTimeout(TEST_TIMEOUT_LONG)
const { serverURL: serverFromInit, payload } = await initPayloadE2ENoConfig<Config>({ dirname }) const { serverURL: serverFromInit, payload: payloadFromInit } =
await initPayloadE2ENoConfig<Config>({ dirname })
serverURL = serverFromInit serverURL = serverFromInit
globalMenuURL = new AdminUrlUtil(serverURL, menuSlug) globalMenuURL = new AdminUrlUtil(serverURL, menuSlug)
menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug) menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug)
usersURL = new AdminUrlUtil(serverURL, usersSlug) usersURL = new AdminUrlUtil(serverURL, usersSlug)
tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug) tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug)
payload = payloadFromInit
autosaveGlobalURL = new AdminUrlUtil(serverURL, autosaveGlobalSlug) autosaveGlobalURL = new AdminUrlUtil(serverURL, autosaveGlobalSlug)
const context = await browser.newContext() const context = await browser.newContext()
@@ -72,7 +76,7 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await page.goto(tenantsURL.list) await page.goto(tenantsURL.list)
@@ -99,8 +103,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(tenantsURL.list) await setTenantFilter({
await selectTenant({ urlUtil: tenantsURL,
page, page,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
@@ -127,7 +131,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(menuItemsURL.list) await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await expect( await expect(
page.locator('.collection-list .table .cell-name', { page.locator('.collection-list .table .cell-name', {
@@ -147,8 +151,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(menuItemsURL.list) await setTenantFilter({
await selectTenant({ urlUtil: menuItemsURL,
page, page,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
@@ -172,7 +176,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(menuItemsURL.list) await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await expect( await expect(
page.locator('.collection-list .table .cell-name', { page.locator('.collection-list .table .cell-name', {
@@ -188,7 +192,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(menuItemsURL.list) await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await expect( await expect(
page.locator('.collection-list .table .cell-name', { page.locator('.collection-list .table .cell-name', {
@@ -207,7 +211,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(usersURL.list) await page.goto(usersURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await expect( await expect(
page.locator('.collection-list .table .cell-email', { page.locator('.collection-list .table .cell-email', {
@@ -233,8 +237,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(usersURL.list) await setTenantFilter({
await selectTenant({ urlUtil: usersURL,
page, page,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
@@ -267,7 +271,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(menuItemsURL.list) await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await goToListDoc({ await goToListDoc({
page, page,
@@ -287,7 +291,7 @@ test.describe('Multi Tenant', () => {
.toBe('Blue Dog') .toBe('Blue Dog')
}) })
test('should prompt for confirmation upon tenant switching', async () => { test('should allow tenant switching cancellation', async () => {
await loginClientSide({ await loginClientSide({
page, page,
serverURL, serverURL,
@@ -295,7 +299,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(menuItemsURL.list) await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await goToListDoc({ await goToListDoc({
page, page,
@@ -307,14 +311,46 @@ test.describe('Multi Tenant', () => {
await selectDocumentTenant({ await selectDocumentTenant({
page, page,
tenant: 'Steel Cat', tenant: 'Steel Cat',
action: 'cancel',
payload,
}) })
const confirmationModal = page.locator('#confirm-switch-tenant') await expect(page.locator('#action-save')).toBeDisabled()
await expect(confirmationModal).toBeVisible()
await expect( await page.goto(menuItemsURL.list)
confirmationModal.getByText('You are about to change ownership from Blue Dog to Steel Cat'), await expect
).toBeVisible() .poll(async () => {
return await getSelectedTenantFilterName({ page, payload })
})
.toBe('Blue Dog')
}) })
test('should allow tenant switching confirmation', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await page.goto(menuItemsURL.list)
await clearTenantFilter({ page })
await goToListDoc({
page,
cellClass: '.cell-name',
textToMatch: 'Spicy Mac',
urlUtil: menuItemsURL,
})
await selectDocumentTenant({
page,
payload,
tenant: 'Steel Cat',
})
await saveDocAndAssert(page)
})
test('should filter internal links in Lexical editor', async () => { test('should filter internal links in Lexical editor', async () => {
await loginClientSide({ await loginClientSide({
page, page,
@@ -324,6 +360,7 @@ test.describe('Multi Tenant', () => {
await page.goto(menuItemsURL.create) await page.goto(menuItemsURL.create)
await selectDocumentTenant({ await selectDocumentTenant({
page, page,
payload,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
const editor = page.locator('[data-lexical-editor="true"]') const editor = page.locator('[data-lexical-editor="true"]')
@@ -364,8 +401,8 @@ test.describe('Multi Tenant', () => {
serverURL, serverURL,
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(tenantsURL.list) await setTenantFilter({
await selectTenant({ urlUtil: tenantsURL,
page, page,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
@@ -381,8 +418,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(tenantsURL.list) await setTenantFilter({
await selectTenant({ urlUtil: tenantsURL,
page, page,
tenant: 'Blue Dog', tenant: 'Blue Dog',
}) })
@@ -391,7 +428,7 @@ test.describe('Multi Tenant', () => {
// Attempt to switch tenants with unsaved changes // Attempt to switch tenants with unsaved changes
await page.fill('#field-title', 'New Global Menu Name') await page.fill('#field-title', 'New Global Menu Name')
await selectTenant({ await switchGlobalDocTenant({
page, page,
tenant: 'Steel Cat', tenant: 'Steel Cat',
}) })
@@ -424,15 +461,25 @@ test.describe('Multi Tenant', () => {
data: credentials.admin, data: credentials.admin,
}) })
await page.goto(tenantsURL.list) await page.goto(tenantsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await page.goto(autosaveGlobalURL.list) await page.goto(autosaveGlobalURL.list)
await expect(page.locator('.doc-header__title')).toBeVisible() await expect(page.locator('.doc-header__title')).toBeVisible()
const globalTenant = await getGlobalTenant({ page }) const docID = (await page.locator('.render-title').getAttribute('data-doc-id')) as string
await expect await expect.poll(() => docID).not.toBeUndefined()
.poll(async () => { const globalTenant = await getSelectedTenantFilterName({ page, payload })
return await getDocumentTenant({ page }) const autosaveGlobal = await payload.find({
}) collection: autosaveGlobalSlug,
.toBe(globalTenant) where: {
id: {
equals: docID,
},
'tenant.name': {
equals: globalTenant,
},
},
})
await expect.poll(() => autosaveGlobal?.totalDocs).toBe(1)
await expect.poll(() => autosaveGlobal?.docs?.[0]?.tenant).toBeDefined()
}) })
}) })
@@ -515,7 +562,7 @@ test.describe('Multi Tenant', () => {
}) })
await page.goto(tenantsURL.list) await page.goto(tenantsURL.list)
await clearGlobalTenant({ page }) await clearTenantFilter({ page })
await expect( await expect(
page.locator('.collection-list .table .cell-name', { page.locator('.collection-list .table .cell-name', {
@@ -602,24 +649,6 @@ test.describe('Multi Tenant', () => {
/** /**
* Helper Functions * Helper Functions
*/ */
async function getGlobalTenant({ page }: { page: Page }): Promise<string | undefined> {
await openNav(page)
return await getSelectInputValue<false>({
selectLocator: page.locator('.tenant-selector'),
multiSelect: false,
})
}
async function getDocumentTenant({ page }: { page: Page }): Promise<string | undefined> {
await openNav(page)
return await getSelectInputValue<false>({
selectLocator: page.locator('#field-tenant'),
multiSelect: false,
valueLabelClass: '.relationship--single-value',
})
}
async function getTenantOptions({ page }: { page: Page }): Promise<string[]> { async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
await openNav(page) await openNav(page)
return await getSelectInputOptions({ return await getSelectInputOptions({
@@ -627,33 +656,124 @@ async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
}) })
} }
async function openAssignTenantModal({
page,
payload,
}: {
page: Page
payload: PayloadTestSDK<Config>
}): Promise<void> {
const assignTenantModal = page.locator('#assign-tenant-field-modal')
const globalTenant = await getSelectedTenantFilterName({ page, payload })
if (!globalTenant) {
await expect(assignTenantModal).toBeVisible()
return
}
// Open the assign tenant modal
const docControlsPopup = page.locator('.doc-controls__popup')
const docControlsButton = docControlsPopup.locator('.popup-button')
await expect(docControlsButton).toBeVisible()
await docControlsButton.click()
const assignTenantButtonLocator = docControlsPopup.locator('button', { hasText: 'Assign Site' })
await expect(assignTenantButtonLocator).toBeVisible()
await assignTenantButtonLocator.click()
await expect(assignTenantModal).toBeVisible()
}
async function selectDocumentTenant({ async function selectDocumentTenant({
page, page,
tenant, tenant,
action = 'confirm',
payload,
}: {
action?: 'cancel' | 'confirm'
page: Page
payload: PayloadTestSDK<Config>
tenant: string
}): Promise<void> {
await closeNav(page)
await openAssignTenantModal({ page, payload })
await selectInput({
selectLocator: page.locator('.tenantField'),
option: tenant,
multiSelect: false,
})
const assignTenantModal = page.locator('#assign-tenant-field-modal')
if (action === 'confirm') {
await assignTenantModal.locator('button', { hasText: 'Confirm' }).click()
await expect(assignTenantModal).toBeHidden()
} else {
await assignTenantModal.locator('button', { hasText: 'Cancel' }).click()
await expect(assignTenantModal).toBeHidden()
}
}
async function getSelectedTenantFilterName({
page,
payload,
}: {
page: Page
payload: PayloadTestSDK<Config>
}): Promise<string | undefined> {
const cookies = await page.context().cookies()
const tenantIDFromCookie = cookies.find((c) => c.name === 'payload-tenant')?.value
if (tenantIDFromCookie) {
const tenant = await payload.find({
collection: 'tenants',
where: {
id: {
equals: tenantIDFromCookie,
},
},
})
return tenant?.docs?.[0]?.name || undefined
}
return undefined
}
async function setTenantFilter({
page,
tenant,
urlUtil,
}: {
page: Page
tenant: string
urlUtil: AdminUrlUtil
}): Promise<void> {
await page.goto(urlUtil.list)
await openNav(page)
await selectInput({
selectLocator: page.locator('.tenant-selector'),
option: tenant,
multiSelect: false,
})
}
async function switchGlobalDocTenant({
page,
tenant,
}: { }: {
page: Page page: Page
tenant: string tenant: string
}): Promise<void> { }): Promise<void> {
await openNav(page) await openNav(page)
return selectInput({ await selectInput({
selectLocator: page.locator('.tenantField'),
option: tenant,
multiSelect: false,
})
}
async function selectTenant({ page, tenant }: { page: Page; tenant: string }): Promise<void> {
await openNav(page)
return selectInput({
selectLocator: page.locator('.tenant-selector'), selectLocator: page.locator('.tenant-selector'),
option: tenant, option: tenant,
multiSelect: false, multiSelect: false,
}) })
} }
async function clearGlobalTenant({ page }: { page: Page }): Promise<void> { async function clearTenantFilter({ page }: { page: Page }): Promise<void> {
await openNav(page) await openNav(page)
return clearSelectInput({ await clearSelectInput({
selectLocator: page.locator('.tenant-selector'), selectLocator: page.locator('.tenant-selector'),
}) })
await closeNav(page)
} }

View File

@@ -183,7 +183,7 @@ export interface FoodItem {
root: { root: {
type: string; type: string;
children: { children: {
type: string; type: any;
version: number; version: number;
[k: string]: unknown; [k: string]: unknown;
}[]; }[];