Skip to content

Fieldtype picker shows the wrong list after switching between form and regular blueprints (until reload) #14903

Description

@kylewalow

Bug description

The field picker (FieldtypeSelector) caches its fieldtype list in a module-level singleton that is shared across every instance and is not keyed on whether the blueprint is a form blueprint. Because the Control Panel is a single-page app, whichever blueprint type you open first populates that cache, and every subsequent picker reuses it — regardless of mode — until a full page reload clears the module state.

The two modes request different lists from the server:

  • Regular blueprint → GET /cp/fields/fieldtypes?selectable=true
  • Form blueprint → GET /cp/fields/fieldtypes?selectable=true&forms=true (a smaller list, since fieldtypes can be made unselectable in forms)

So after navigating (SPA, no reload) from one to the other, the picker offers the wrong set of fieldtypes. It is especially visible for anyone who uses Fieldtype::makeUnselectableInForms() to restrict the form builder to a custom set — the form picker ends up showing all of the regular-blueprint fieldtypes.

Expected: the picker should always reflect the current blueprint's mode (form blueprints show the forms=true list, regular blueprints show the full list) without requiring a full page reload.

Root cause

In resources/js/components/fields/FieldtypeSelector.vue:

const loadedFieldtypes = ref(null); // module scope → one shared instance for the whole session

export default {
    computed: {
        fieldtypes() {
            if (!this.fieldtypesLoaded) return;
            return loadedFieldtypes.value;
        },
        fieldtypesLoaded() {
            return Array.isArray(loadedFieldtypes.value); // "loaded" regardless of which mode it was loaded for
        },
    },

    created() {
        if (this.fieldtypesLoaded) return; // already populated by ANY earlier picker → never refetch

        let url = cp_url('fields/fieldtypes?selectable=true');
        if (this.$config.get('isFormBlueprint')) url += '&forms=true'; // mode only affects the URL, not the cache key

        this.$axios.get(url)
            .then((response) => (loadedFieldtypes.value = response.data))
            .catch(/* ... */);
    },
};

The cache has no key. fieldtypesLoaded only checks "is the cache an array?", so once any blueprint editor fills it, the created() early-return prevents every later picker from refetching — even though isFormBlueprint (and therefore the correct list) has changed. A full page reload resets the module-scoped loadedFieldtypes to null, which is why reloading fixes it.

How to reproduce

  1. Make the two lists meaningfully different — the simplest way is to mark some fieldtypes unselectable in forms, e.g. in a service provider's boot():
    use Facades\Statamic\Fields\FieldtypeRepository;
    
    FieldtypeRepository::makeUnselectableInForms('bard');
    FieldtypeRepository::makeUnselectableInForms('grid');
    // ...etc
  2. Open a regular blueprint in the CP (e.g. Collections → … → Blueprint) and click to add a field. The full fieldtype list loads.
  3. Without reloading the page, navigate to a form blueprint (Forms → … → Blueprint) and click to add a field.
  4. Observed: the picker shows the regular blueprint's fieldtypes, including the ones that should be unselectable in forms.
  5. Reload the page while on the form blueprint → the picker now shows the correct, form-filtered list.

It is symmetric: opening a form blueprint first and then navigating to a regular blueprint shows the form's (smaller) list until reload.

Environment

Statamic: 6.19.0 Solo
Laravel: 12.x
PHP: 8.4

This is a Control-Panel (frontend) issue and reproduces independently of the data/database driver.

Installation

Existing Laravel app

Additional details

Suggested fix

Key the cache on the form/non-form mode (or refetch when the cached mode doesn't match the current isFormBlueprint). For example, store the mode alongside the data:

const loadedFieldtypes = ref(null); // { forms: boolean, data: array } | null

export default {
    computed: {
        fieldtypes() {
            if (!this.fieldtypesLoaded) return;
            return loadedFieldtypes.value.data;
        },
        fieldtypesLoaded() {
            return loadedFieldtypes.value != null
                && loadedFieldtypes.value.forms === !!this.$config.get('isFormBlueprint')
                && Array.isArray(loadedFieldtypes.value.data);
        },
    },

    created() {
        if (this.fieldtypesLoaded) return;

        const forms = !!this.$config.get('isFormBlueprint');
        let url = cp_url('fields/fieldtypes?selectable=true');
        if (forms) url += '&forms=true';

        this.$axios.get(url)
            .then((response) => (loadedFieldtypes.value = { forms, data: response.data }))
            .catch(/* ... */);
    },
};

This keeps the cross-picker caching benefit within a mode, while correctly refetching when switching between a form blueprint and a regular blueprint.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions