Conditionally-Available Block Types #7064
Replies: 6 comments
-
I have a need for conditional blocks also. One of my blocks is a list of tabs that each accept blocks as content. If a user wants to use this, other blocks need to be unavailable at the top level, as all content should be under a tab in this pattern. My use case is a single pages collection that allows selecting from a list of templates, and conditionally modifying the templates. That includes using the template with tabs or without tabs. Having conditional blocks would clean up the implementation significantly. |
Beta Was this translation helpful? Give feedback.
-
@politicoder @AshtarCodes Did either of you find a solution? |
Beta Was this translation helpful? Give feedback.
-
I'm trying to find an answer on this as well. Hopefully a solution pops up soon. |
Beta Was this translation helpful? Give feedback.
-
Edit: Ok no, after experimenting with it I can see the problems, I have an idea for a solution though, testing it at the moment. |
Beta Was this translation helpful? Give feedback.
-
So, after some experimenting I've come up with a possibility, it's currently not the cleanest and could use some finetuning as well as some more abstract / generic implementations because in the way I've set it up now it could quickly become messy, but nothing that can't be solved with some more code splitting. This solution assumes there's a The main takeaway here is to have multiple So, for example, in your pages collection you can add: {
name: 'layout',
type: 'blocks',
blocks: AllBlocks,
admin: {
disabled: true,
},
},
{
name: 'collexiLayout',
type: 'blocks',
required: true,
blocks: getBlocksForTenant('Collexi'),
admin: {
condition: (data, siblingData, { user }) => {
return user.tenant === 'Collexi';
},
},
hooks: {
afterChange: [saveLayoutBlocksAfterChange],
},
},
{
name: 'filteredLayout',
type: 'blocks',
required: true,
blocks: getFilteredBlocks(),
admin: {
condition: (data, siblingData, { user }) => {
return user.tenant !== 'Collexi';
},
},
hooks: {
afterChange: [saveLayoutBlocksAfterChange],
},
}, The The other layouts blocks should only contain the blocks available for that tenant, you can choose how you want to define these. export function getCollexiBlocks() {
return [BasicMessageContactFormBlock];
}
export function getFilteredBlocks() {
return [MediaBlock, SpacerBlock];
} Or you could extend export interface TenantBlock extends Block {
allowedTenants: string[];
} Use export const BasicMessageContactFormBlock: TenantBlock = {
allowedTenants: ['Collexi'],
...
}; Then you could use: export function getBlocksForTenant(tenant: string): Block[] {
return AllBlocks.filter((block) => {
return block.allowedTenants?.includes(tenant);
});
} And call it The import {
filterInvalidBlocks,
getBlocksForTenant,
} from '../pageFields/getBlocks';
import { FieldHook } from 'payload';
export const saveLayoutBlocksAfterChange: FieldHook = async ({
data,
originalDoc,
req,
}) => {
if (!req.user || !req.user.tenant) {
console.log(
'Skipping filtering as no authenticated user with an assigned tenant is found.'
);
return data;
}
const tenant = req.user.tenant;
console.log('Processing layout for tenant:', tenant);
const allowedBlocks = getBlocksForTenant(tenant);
if (tenant === 'Collexi') {
data.builderContent.layout = filterInvalidBlocks(
originalDoc.builderContent.collexiLayout,
allowedBlocks
);
}
if (tenant === 'Filtered') {
data.builderContent.layout = filterInvalidBlocks(
originalDoc.builderContent.filteredLayout,
allowedBlocks
);
}
return data;
}; I've added a export function filterInvalidBlocks(blocks: any[], allowedBlocks: Block[]) {
return blocks.filter((block) =>
allowedBlocks.some((allowedBlock) => allowedBlock.slug === block.blockType)
);
} I also have a collection And then I also added a collection To improve maintainability for tens or hundreds of tenants you would probably want to add a helper function that iterates over all available tenants and build the Does this offer a solution to what you were looking for? |
Beta Was this translation helpful? Give feedback.
-
I will soon be facing the same challenge. I have implemented something similar elsewhere, but with a SelectField. Depending on the current tenant, different options should be available. My solution there was to overwrite the field component. I think that worked quite well. My hope so far is that it will work the same way with the block field or the editor field (we only use the editor to make blocks available). This is my code for the "tenant aware select field", maybe it can serve as an inspiration for your implementation. templateField.ts const t = createTranslator('fields:templateField');
export const templateField = <Group extends TemplateGroupKey>(args: TemplateFieldArgs<Group>): TextField => {
const {defaultValue, label, hidden, templateGroupKey, templateGroupEntryKey} = args;
return {
name: 'template',
label: label ?? t('label'),
type: 'text',
hidden,
required: true,
defaultValue: defaultValue ?? 'default',
admin: {
components: {
Field: {
path: '@/backend/fields/templateField/components/TenantAwareTemplateField',
serverProps: {
templateGroupKey,
templateGroupEntryKey,
},
},
},
},
};
}; TenantAwareTemplateField.tsx import {SelectField} from '@payloadcms/ui';
import {SelectFieldServerComponent} from 'payload';
import {findTenantByRequest} from '@/backend/api/local/findTenantByRequest';
import {getTenantAwareTemplateOptions} from '@/backend/utils/getTenantAwareTemplateOptions';
const TenantAwareTemplateField: SelectFieldServerComponent = async (props) => {
const {schemaPath, clientField, req, templateGroupKey, templateGroupEntryKey} = props;
const path = (props?.path || props?.field?.name || '') as string;
const tenant = await findTenantByRequest({req});
const options = getTenantAwareTemplateOptions({
group: templateGroupKey as any,
entry: templateGroupEntryKey as any,
companyNumber: tenant?.companyNumber,
});
const field = {
...clientField,
options,
};
return clientField.hidden || options.length <= 1 ? (
<></>
) : (
<SelectField field={field} path={path} schemaPath={schemaPath} />
);
};
export default TenantAwareTemplateField; |
Beta Was this translation helpful? Give feedback.
-
To my knowledge what I'm attempting to do is not currently possible in Payload, but I would love to be told otherwise.
My agency is pretty much the exact use-case described in the Multi-Tenant Docs; we currently host hundreds of custom WordPress sites that follow similar, but not identical, design patterns. For user-editable page layouts we use Advanced Custom Fields' "Flexible Content" field, which is functionally very similar to Payload's Blocks field.
I am sold on Payload and want very much to break free of WordPress on new sites going forward. Hosting each Payload site as a fully independent repo like we do with WordPress seems impractical from a server configuration standpoint; I would rather take advantage of Payload's multi-tenant capabilities and give each client a login to one giant Payload app as described in the above-linked docs.
The trouble is that while every site in the app has a Pages collection with a Blocks field for its layout, every client should NOT have access to every type of Block. There are a handful of universal blocks that every site will have, but in general, a big part of our design process is custom-designing blocks for individual websites. I need a way to designate any given Block as only being visible to certain Users. How that's implemented, I don't really care; setting available blocks as a hasMany multiselect field on the Sites collection, doing it all in code, either way is fine. But if every tenant always has access to every block in the codebase, I'm not sure Payload is viable for our use case.
Any insight is appreciated, thanks!
Beta Was this translation helpful? Give feedback.
All reactions