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
- 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
- Open a regular blueprint in the CP (e.g. Collections → … → Blueprint) and click to add a field. The full fieldtype list loads.
- Without reloading the page, navigate to a form blueprint (Forms → … → Blueprint) and click to add a field.
- Observed: the picker shows the regular blueprint's fieldtypes, including the ones that should be unselectable in forms.
- 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.
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:
GET /cp/fields/fieldtypes?selectable=trueGET /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=truelist, regular blueprints show the full list) without requiring a full page reload.Root cause
In
resources/js/components/fields/FieldtypeSelector.vue:The cache has no key.
fieldtypesLoadedonly checks "is the cache an array?", so once any blueprint editor fills it, thecreated()early-return prevents every later picker from refetching — even thoughisFormBlueprint(and therefore the correct list) has changed. A full page reload resets the module-scopedloadedFieldtypestonull, which is why reloading fixes it.How to reproduce
boot():It is symmetric: opening a form blueprint first and then navigating to a regular blueprint shows the form's (smaller) list until reload.
Environment
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:This keeps the cross-picker caching benefit within a mode, while correctly refetching when switching between a form blueprint and a regular blueprint.