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: {
[key in AcceptedLanguages]?: {
/**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
* Shown inside 3 dot menu on edit document view
*
* @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'
*/
'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
}

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'
import type { RelationshipFieldClientProps, StaticLabel } from 'payload'
import type { RelationshipFieldClientProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import {
ConfirmationModal,
Pill,
RelationshipField,
Translation,
useDocumentInfo,
useField,
useForm,
useFormModified,
useModal,
useTranslation,
} from '@payloadcms/ui'
import React from 'react'
import type {
PluginMultiTenantTranslationKeys,
PluginMultiTenantTranslations,
} from '../../translations/index.js'
import './index.scss'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
import {
AssignTenantFieldModal,
assignTenantModalSlug,
} from '../AssignTenantFieldModal/index.client.js'
import './index.scss'
const baseClass = 'tenantField'
@@ -30,17 +27,74 @@ type Props = {
unique?: boolean
} & RelationshipFieldClientProps
export const TenantField = (args: Props) => {
export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
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(() => {
if (!entityType) {
setEntityType(args.unique ? 'global' : 'document')
setEntityType(unique ? 'global' : 'document')
} else {
// unique documents are controlled from the global TenantSelector
if (!args.unique && value) {
if (!selectedTenantID || value !== selectedTenantID) {
if (!unique && value) {
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 })
}
}
@@ -51,137 +105,73 @@ export const TenantField = (args: Props) => {
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) {
return (
<>
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
<RelationshipField
{...args}
field={{
...args.field,
required: true,
}}
readOnly={args.readOnly || args.field.admin?.readOnly || args.unique}
/>
</div>
</div>
{args.unique ? (
<SyncFormModified />
) : (
<ConfirmTenantChange fieldLabel={args.field.label} fieldPath={args.path} />
)}
</>
)
React.useEffect(() => {
if (unique) {
return
}
if ((!isFormValid && showError && showField) || (!value && !selectedTenantID)) {
openModal(assignTenantModalSlug)
}
}, [isFormValid, showError, showField, openModal, value, docID, selectedTenantID, unique])
if (showField) {
if (debug) {
return <TenantFieldInModal debug={debug} fieldArgs={fieldArgs} unique={unique} />
}
if (!unique) {
/** Editing a non-global tenant document */
return (
<AssignTenantFieldModal
afterModalClose={afterModalClose}
afterModalOpen={afterModalOpen}
onConfirm={onConfirm}
>
<TenantFieldInModal
debug={debug}
fieldArgs={{
...fieldArgs,
field: {
...fieldArgs.field,
},
}}
unique={unique}
/>
</AssignTenantFieldModal>
)
}
return <SyncFormModified />
}
return null
}
const confirmSwitchTenantSlug = 'confirm-switch-tenant'
const ConfirmTenantChange = ({
fieldLabel,
fieldPath,
}: {
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,
])
const TenantFieldInModal: React.FC<{
debug?: boolean
fieldArgs: RelationshipFieldClientProps
unique?: boolean
}> = ({ debug, fieldArgs, unique }) => {
return (
<ConfirmationModal
body={
<Translation
elements={{
0: ({ children }) => {
return <b>{children}</b>
},
}}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-multi-tenant:confirm-modal-tenant-switch--body"
t={t}
variables={{
fromTenant: fromTenantOption?.label,
toTenant: toTenantOption?.label,
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
{debug && (
<Pill className={`${baseClass}__debug-pill`} pillStyle="success" size="small">
Multi-Tenant Debug Enabled
</Pill>
)}
<RelationshipField
{...fieldArgs}
field={{
...fieldArgs.field,
required: true,
}}
readOnly={fieldArgs.readOnly || fieldArgs.field.admin?.readOnly || unique}
/>
}
heading={t('plugin-multi-tenant:confirm-modal-tenant-switch--heading', {
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)
}}
/>
</div>
</div>
)
}

View File

@@ -43,6 +43,10 @@
margin-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 { WatchTenantCollection } from '../components/WatchTenantCollection/index.js'
export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js'

View File

@@ -339,6 +339,16 @@ export const multiTenantPlugin =
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
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const bnInTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'আপনি স্বত্বাধিকার পরিবর্তন করতে চলেছেন <0>{{fromTenant}}</0> থেকে <0>{{toTenant}}</0> এ।',
'confirm-modal-tenant-switch--heading': '{{tenantLabel}} পরিবর্তন নিশ্চিত করুন',
'assign-tenant-button-label': 'টেনেন্ট নিয়োগ করুন',
'assign-tenant-modal-title': '"{{title}}" এর দায়িত্ব দিন',
'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 = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'Està a punt de canviar la propietat de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-modal-tenant-switch--heading': 'Confirmeu el canvi de {{tenantLabel}}',
'assign-tenant-button-label': 'Assignar Tenant',
'assign-tenant-modal-title': 'Assigna "{{title}}"',
'field-assignedTenant-label': 'Llogater Assignat',
'nav-tenantSelector-label': 'Inquilí',
},

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,10 @@ import type { PluginLanguage } from '../types.js'
export const enTranslations = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>',
'confirm-modal-tenant-switch--heading': 'Confirm {{tenantLabel}} change',
'assign-tenant-button-label': 'Assign Tenant',
'assign-tenant-modal-title': 'Assign "{{title}}"',
'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 = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'Está a punto de cambiar la propiedad de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-modal-tenant-switch--heading': 'Confirme el cambio de {{tenantLabel}}',
'assign-tenant-button-label': 'Asignar Inquilino',
'assign-tenant-modal-title': 'Asignar "{{title}}"',
'field-assignedTenant-label': 'Inquilino Asignado',
'nav-tenantSelector-label': 'Inquilino',
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,10 @@ import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.j
export const idTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'Anda akan mengubah kepemilikan dari <0>{{fromTenant}}</0> ke <0>{{toTenant}}</0>',
'confirm-modal-tenant-switch--heading': 'Konfirmasi perubahan {{tenantLabel}}',
'assign-tenant-button-label': 'Tetapkan Tenant',
'assign-tenant-modal-title': 'Tetapkan "{{title}}"',
'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 = {
'plugin-multi-tenant': {
'confirm-modal-tenant-switch--body':
'Stai per cambiare il possesso da <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-modal-tenant-switch--heading': 'Conferma il cambiamento di {{tenantLabel}}',
'assign-tenant-button-label': 'Assegna Tenant',
'assign-tenant-modal-title': 'Assegna "{{title}}"',
'field-assignedTenant-label': 'Inquilino Assegnato',
'nav-tenantSelector-label': 'Inquilino',
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,21 +92,27 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
translations: {
[key in AcceptedLanguages]?: {
/**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
* Shown inside 3 dot menu on edit document view
*
* @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'
*/
'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
}

View File

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

View File

@@ -2,9 +2,6 @@ import type { BrowserContext, Page } from '@playwright/test'
import type { TypeWithID } from 'payload'
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 { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
@@ -12,8 +9,8 @@ import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
import { devUser } from '../credentials.js'
import {
closeNav,
ensureCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
@@ -21,6 +18,8 @@ import {
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.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 { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
import {

View File

@@ -7,15 +7,12 @@ import type {
} from '@playwright/test'
import type { Config } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { expect } from '@playwright/test'
import { defaults } from 'payload'
import { wait } from 'payload/shared'
import shelljs from 'shelljs'
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'
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
}
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> {
const button = page.locator('.localizer button.popup-button')
const popup = page.locator('.localizer .popup.popup--active')

View File

@@ -13,7 +13,7 @@ export async function assertToastErrors({
}): Promise<void> {
const isSingleError = errors.length === 1
const message = isSingleError
? 'The following field is invalid:'
? 'The following field is invalid: '
: `The following fields are invalid (${errors.length}):`
// 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('.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,
plugins: [
multiTenantPlugin<ConfigType>({
debug: true,
// debug: true,
userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')),
useTenantsCollectionAccess: false,
tenantField: {
@@ -52,9 +52,9 @@ export default buildConfigWithDefaults({
i18n: {
translations: {
en: {
'field-assignedTenant-label': 'Currently Assigned Site',
'nav-tenantSelector-label': 'Filter By Site',
'confirm-modal-tenant-switch--heading': 'Confirm Site Change',
'field-assignedTenant-label': 'Site',
'nav-tenantSelector-label': 'Filter by Site',
'assign-tenant-button-label': 'Assign Site',
},
},
},

View File

@@ -6,6 +6,7 @@ import * as path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config } from './payload-types.js'
import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
@@ -18,7 +19,7 @@ import {
getSelectInputValue,
selectInput,
} 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 { reInitializeDB } from '../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
@@ -37,16 +38,19 @@ test.describe('Multi Tenant', () => {
let menuItemsURL: AdminUrlUtil
let usersURL: AdminUrlUtil
let tenantsURL: AdminUrlUtil
let payload: PayloadTestSDK<Config>
test.beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
const { serverURL: serverFromInit, payload } = await initPayloadE2ENoConfig<Config>({ dirname })
const { serverURL: serverFromInit, payload: payloadFromInit } =
await initPayloadE2ENoConfig<Config>({ dirname })
serverURL = serverFromInit
globalMenuURL = new AdminUrlUtil(serverURL, menuSlug)
menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug)
usersURL = new AdminUrlUtil(serverURL, usersSlug)
tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug)
payload = payloadFromInit
autosaveGlobalURL = new AdminUrlUtil(serverURL, autosaveGlobalSlug)
const context = await browser.newContext()
@@ -72,7 +76,7 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await page.goto(tenantsURL.list)
@@ -99,8 +103,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await page.goto(tenantsURL.list)
await selectTenant({
await setTenantFilter({
urlUtil: tenantsURL,
page,
tenant: 'Blue Dog',
})
@@ -127,7 +131,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await expect(
page.locator('.collection-list .table .cell-name', {
@@ -147,8 +151,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await page.goto(menuItemsURL.list)
await selectTenant({
await setTenantFilter({
urlUtil: menuItemsURL,
page,
tenant: 'Blue Dog',
})
@@ -172,7 +176,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await expect(
page.locator('.collection-list .table .cell-name', {
@@ -188,7 +192,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await expect(
page.locator('.collection-list .table .cell-name', {
@@ -207,7 +211,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(usersURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await expect(
page.locator('.collection-list .table .cell-email', {
@@ -233,8 +237,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await page.goto(usersURL.list)
await selectTenant({
await setTenantFilter({
urlUtil: usersURL,
page,
tenant: 'Blue Dog',
})
@@ -267,7 +271,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await goToListDoc({
page,
@@ -287,7 +291,7 @@ test.describe('Multi Tenant', () => {
.toBe('Blue Dog')
})
test('should prompt for confirmation upon tenant switching', async () => {
test('should allow tenant switching cancellation', async () => {
await loginClientSide({
page,
serverURL,
@@ -295,7 +299,7 @@ test.describe('Multi Tenant', () => {
})
await page.goto(menuItemsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await goToListDoc({
page,
@@ -307,14 +311,46 @@ test.describe('Multi Tenant', () => {
await selectDocumentTenant({
page,
tenant: 'Steel Cat',
action: 'cancel',
payload,
})
const confirmationModal = page.locator('#confirm-switch-tenant')
await expect(confirmationModal).toBeVisible()
await expect(
confirmationModal.getByText('You are about to change ownership from Blue Dog to Steel Cat'),
).toBeVisible()
await expect(page.locator('#action-save')).toBeDisabled()
await page.goto(menuItemsURL.list)
await expect
.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 () => {
await loginClientSide({
page,
@@ -324,6 +360,7 @@ test.describe('Multi Tenant', () => {
await page.goto(menuItemsURL.create)
await selectDocumentTenant({
page,
payload,
tenant: 'Blue Dog',
})
const editor = page.locator('[data-lexical-editor="true"]')
@@ -364,8 +401,8 @@ test.describe('Multi Tenant', () => {
serverURL,
data: credentials.admin,
})
await page.goto(tenantsURL.list)
await selectTenant({
await setTenantFilter({
urlUtil: tenantsURL,
page,
tenant: 'Blue Dog',
})
@@ -381,8 +418,8 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await page.goto(tenantsURL.list)
await selectTenant({
await setTenantFilter({
urlUtil: tenantsURL,
page,
tenant: 'Blue Dog',
})
@@ -391,7 +428,7 @@ test.describe('Multi Tenant', () => {
// Attempt to switch tenants with unsaved changes
await page.fill('#field-title', 'New Global Menu Name')
await selectTenant({
await switchGlobalDocTenant({
page,
tenant: 'Steel Cat',
})
@@ -424,15 +461,25 @@ test.describe('Multi Tenant', () => {
data: credentials.admin,
})
await page.goto(tenantsURL.list)
await clearGlobalTenant({ page })
await clearTenantFilter({ page })
await page.goto(autosaveGlobalURL.list)
await expect(page.locator('.doc-header__title')).toBeVisible()
const globalTenant = await getGlobalTenant({ page })
await expect
.poll(async () => {
return await getDocumentTenant({ page })
})
.toBe(globalTenant)
const docID = (await page.locator('.render-title').getAttribute('data-doc-id')) as string
await expect.poll(() => docID).not.toBeUndefined()
const globalTenant = await getSelectedTenantFilterName({ page, payload })
const autosaveGlobal = await payload.find({
collection: autosaveGlobalSlug,
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 clearGlobalTenant({ page })
await clearTenantFilter({ page })
await expect(
page.locator('.collection-list .table .cell-name', {
@@ -602,24 +649,6 @@ test.describe('Multi Tenant', () => {
/**
* 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[]> {
await openNav(page)
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({
page,
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
tenant: string
}): Promise<void> {
await openNav(page)
return 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({
await selectInput({
selectLocator: page.locator('.tenant-selector'),
option: tenant,
multiSelect: false,
})
}
async function clearGlobalTenant({ page }: { page: Page }): Promise<void> {
async function clearTenantFilter({ page }: { page: Page }): Promise<void> {
await openNav(page)
return clearSelectInput({
await clearSelectInput({
selectLocator: page.locator('.tenant-selector'),
})
await closeNav(page)
}

View File

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