Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
63a0ff6
Update changelog
brianjhanson Sep 28, 2026
91b27da
Render autosuggest inputs as craft-combobox
brianjhanson Sep 30, 2026
2522a63
Save legacy screen bridge work in progress before merging 6.x
brianjhanson Oct 6, 2026
ff03c40
Merge remote-tracking branch 'origin/6.x' into feature/legacy-settings
brianjhanson Oct 6, 2026
41df2b3
Wrap the tabs in a container
brianjhanson Oct 6, 2026
040df01
Remove css.bkup
brianjhanson Oct 6, 2026
e3636d4
Remove old css
brianjhanson Oct 6, 2026
a53aabf
Add Plugin::getVersion()
brianjhanson Oct 6, 2026
4cc0f0d
Defer legacy type registration events until first read
brianjhanson Oct 6, 2026
29d02c8
Extend the Craft 6 utility base class from the legacy one
brianjhanson Oct 6, 2026
75bd744
Return non-alias values from Craft::getAlias() unchanged
brianjhanson Oct 6, 2026
d769029
Keep getVersion() out of the Craft 6 plugin API
brianjhanson Oct 6, 2026
fa78c33
Trim comments
brianjhanson Oct 6, 2026
e4f8877
Honour a legacy URL rule for a plugin's settings path
brianjhanson Oct 6, 2026
c9f7aeb
Keep a collected screen's assets for the document that renders it
brianjhanson Oct 6, 2026
6697677
Stop reading indented control panel markup as a code block
brianjhanson Oct 6, 2026
7875d36
Keep a combobox value that arrived as markup
brianjhanson Oct 6, 2026
f5847cf
Mirror the selected tab in the location hash
brianjhanson Oct 6, 2026
0ba39b9
Hold a bridged screen's JS until its markup is mounted
brianjhanson Oct 6, 2026
e54aef9
Load a legacy plugin's stored settings in full
brianjhanson Oct 6, 2026
41136dc
Gutter the tab strip with the content it sits above
brianjhanson Oct 6, 2026
eb5cdba
Colour a combobox option's indicator and icon
brianjhanson Oct 6, 2026
8b21bcd
Render selectize fields as a combobox
brianjhanson Oct 6, 2026
30529a8
Let a combobox drive field toggles
brianjhanson Oct 6, 2026
acbe81d
Move the remaining indicator options onto fill
brianjhanson Oct 6, 2026
c206a96
Initialize an editable table when it is finally laid out
brianjhanson Oct 7, 2026
5e426b2
Put a bridged element index's sources in the secondary nav
brianjhanson Oct 7, 2026
657f4aa
Merge remote-tracking branch 'origin/6.x' into feature/legacy-settings
brianjhanson Oct 7, 2026
a54073c
Drop the flavor that moved onto Markdown options
brianjhanson Oct 7, 2026
62777d7
Let plugins build a nav section from an element type's sources
brianjhanson Oct 7, 2026
cf49749
Share the CP Vue runtime and element index through an import map
brianjhanson Oct 7, 2026
4d01177
Badge a screen that renders through the bridge in dev mode
brianjhanson Oct 7, 2026
2ff556b
Add a core HtmxAsset shim so dependent bundles resolve
brianjhanson Oct 7, 2026
2d77f7a
Let a page supply its own secondary nav
brianjhanson Oct 7, 2026
d56d275
Update the changelog
brianjhanson Oct 7, 2026
4aea20a
Share the Customize Sources nav entry with plugin indexes
brianjhanson Oct 7, 2026
c784f07
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
brianjhanson Oct 7, 2026
0674512
Type the legacy admin table under TanStack v9 and translate its save …
brianjhanson Oct 7, 2026
d3280db
Rector changes
brianjhanson Oct 7, 2026
0c81aa4
Add since tags to the legacy bridge classes
brianjhanson Oct 7, 2026
426a5bc
Pair the equal-width docblock with its property
brianjhanson Oct 7, 2026
0052e45
Append through appendChild when there is no head anchor
brianjhanson Oct 7, 2026
8febe0e
Format the legacy stylesheet
brianjhanson Oct 7, 2026
fcf47d1
Fix combobox tests
brianjhanson Oct 7, 2026
c67e5a4
Actually fix the combobox
brianjhanson Oct 7, 2026
558526d
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
brianjhanson Oct 7, 2026
76c73c2
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
brianjhanson Oct 7, 2026
3159cae
Show source status dots in the secondary nav and fix ElementStatus co…
nfourtythree Oct 7, 2026
88f4f8e
Merge branch 'legacy-bridge' into feature/source-subnav-status
nfourtythree Oct 7, 2026
e98fbf7
Merge branch '6.x' into legacy-bridge
brandonkelly Oct 7, 2026
724364e
Fixed test
brandonkelly Oct 7, 2026
7174dba
Merge branch '6.x' into legacy-bridge
brandonkelly Oct 7, 2026
0e68498
Merge branch 'legacy-bridge' into feature/source-subnav-status
brandonkelly Oct 7, 2026
73babcc
Add changelog entries for #19892
brandonkelly Oct 8, 2026
72183c7
Merge pull request #19892 from craftcms/feature/source-subnav-status
brandonkelly Oct 8, 2026
c969c2f
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
github-actions[bot] Oct 8, 2026
453d5e6
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
github-actions[bot] Oct 8, 2026
6fcdb74
Merge remote-tracking branch 'origin/6.x' into legacy-bridge
github-actions[bot] Oct 8, 2026
9291cd4
Merge branch '6.x' into legacy-bridge
brandonkelly Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,39 @@
> [!IMPORTANT]
> This update contains breaking changes for plugins. See [#19882](https://github.com/craftcms/cms/pull/19882) for details.

- Control panel screens from plugins that haven’t been updated for Craft 6 now render inside the control panel shell, with their tabs, assets, and JavaScript intact.
- Element index screens from plugins that haven’t been updated for Craft 6 now use the new element index, with the element type’s sources in the secondary nav and a Customize Sources action.
- Element sources’ `status` and `badgeCount` values are now shown in the secondary nav, as they were in the Craft 5 sidebar. ([#19892](https://github.com/craftcms/cms/pull/19892))
- Tabs now mirror the selected tab in the URL hash, select the tab named by the hash on page load, and keep it after saving.
- Selectize fields are now rendered as `<craft-combobox>` web components, rather than Selectize instances.
- Autosuggest inputs are now rendered as `<craft-combobox>` web components, rather than Vue 2 `vue-autosuggest` instances.
- Autosuggest inputs now match suggestions on their hint as well as their label, and no longer cap each suggestion group at five items.
- Added `CraftCms\Cms\Cp\Navigation::sourceSubnav()`, which builds a subnav from an element type’s sources, for plugins adding their own control panel section.
- Added `CraftCms\Cms\Cp\Data\ActionItem::$status` and `status()`. ([#19892](https://github.com/craftcms/cms/pull/19892))
- Added `craft.cp.selectize()` and `CraftCms\Cms\Cp\FormFields::selectizeFromConfig()`, which render the legacy selectize variables as a combobox.
- Added `craft.cp.select()` and `CraftCms\Cms\Cp\FormFields::selectFromConfig()`, which render the legacy select variables as a `<craft-select>`.
- Added `craft.cp.autosuggest()` and `CraftCms\Cms\Cp\FormFields::autosuggestFromConfig()`, which render the legacy autosuggest variables as a combobox. Craft 5’s `suggestions` shape is still accepted.
- Added `CraftCms\Cms\Cp\Components\Combobox::labelledBy()`, for a combobox labelled by an element the caller renders itself.
- Added `CraftCms\Cms\Plugin\Concerns\HasSettings::createSettings()`, a static method which replaces `CraftCms\Cms\Contracts\PluginInterface::createSettings()`. ([#19574](https://github.com/craftcms/cms/pull/19574))
- `CraftCms\Cms\Element\Element::searchableAttributes()` is now `final`. `defineSearchableAttributes()` should be overridden instead, including by element types extending `craft\base\Element` through the Yii adapter.
- `CraftCms\Cms\Element\Element::prepareEditScreen()` now accepts a `Symfony\Component\HttpFoundation\Response|CraftCms\Cms\Http\Responses\CpScreenResponse` argument, rather than `yii\web\Response`. Overrides must update their signatures.
- `CraftCms\Cms\Element\Queries\ElementQuery::statusCondition()` now returns a `Closure` which modifies an `Illuminate\Database\Query\Builder`, rather than a Yii query condition. Overrides must update their signatures and return closures.
- `craft\base\Model::attributes()` now declares an `array` return type. Overrides must update their signatures.
- Plugin settings models must now extend `CraftCms\Cms\Plugin\PluginSettings` rather than `craft\base\Model`.
- Plugin settings are now validated using Laravel validation rules returned by `CraftCms\Cms\Plugin\PluginSettings::getRules()`, rather than Yii validation rules returned by `rules()`. Validation errors are added via the `$fail` callback or `errors()`, rather than `addError()`.
- Plugin settings are now persisted based on the settings model’s public properties, via `CraftCms\Cms\Plugin\PluginSettings::configData()`. Settings stored in private properties with getter and setter methods must be redeclared as public properties, or included by overriding `configData()`.
- Removed the `data` and `methods` blocks from the `_includes/forms/autosuggest.twig` template, along with the Vue instance they configured. Templates that overrode them should pass `suggestions` instead.
- Removed `CraftCms\Cms\Plugin\Contracts\PluginInterface::createSettingsModel()`. The static `createSettings()` must be implemented instead. (`createSettingsModel()` remains supported for plugins extending `craft\base\Plugin` through the Yii adapter.) ([#19574](https://github.com/craftcms/cms/pull/19574))
- Improved control panel styling for medium-sized viewports.
- Renamed `CraftCms\Cms\Form` to `CraftCms\Cms\Ui`, along with its `Form` classes and methods, and the JavaScript `forms` module and its definition APIs to `ui`, including payload fields and refresh endpoints. ([#19882](https://github.com/craftcms/cms/pull/19882))
- Fixed an error that could occur after upgrading to Craft 6 when Laravel queue or other supporting database tables were missing. ([#19889](https://github.com/craftcms/cms/pull/19889))
- Fixed a bug where new control panel assets weren’t getting published when updating via the control panel or `craft:update` command.
- Fixed an error that occurred when a Craft 5 plugin registered a utility, which no longer satisfied `CraftCms\Cms\Utility\UtilityTypes`’ contract. ([#19852](https://github.com/craftcms/cms/pull/19852))
- Fixed an error that occurred when registering asset bundles that depend on `CraftCms\Cms\View\LegacyAssets\HtmxAsset`. The bundle is now deprecated and does nothing.
- Fixed a bug where the tab strip wasn’t guttered with the content it sits above. ([#19861](https://github.com/craftcms/cms/pull/19861))
- Fixed a bug where an editable table that wasn’t visible when the page loaded didn’t initialize until the window was resized.
- Fixed a bug where environment variable, alias, and template suggestions weren’t sorted alphabetically.
- Fixed a bug where status indicators for disabled elements were filled in rather than outlined, and custom status colors were shown as gray. ([#19892](https://github.com/craftcms/cms/pull/19892))
- Fixed a bug where admin tables no longer had selectable rows. ([#19893](https://github.com/craftcms/cms/pull/19893))
- Fixed a bug where the license enforcement screen wasn’t showing the “Continue to the control panel” link after the countdown ended. ([#19895](https://github.com/craftcms/cms/issues/19895))

Expand Down
10 changes: 7 additions & 3 deletions packages/craftcms-legacy/cp/src/js/BaseElementIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -3928,7 +3928,11 @@ Craft.BaseElementIndex = Garnish.Base.extend(
})
.appendTo($form);

const $exportSubmit = new Garnish.MultiFunctionBtn($submitBtn);
// Guarded: the modernized Garnish has no MultiFunctionBtn. Without it
// the export button just stays a plain button rather than throwing.
const $exportSubmit = Garnish.MultiFunctionBtn
? new Garnish.MultiFunctionBtn($submitBtn)
: null;

var hud = new Garnish.HUD(this.$exportBtn, $form);

Expand All @@ -3946,7 +3950,7 @@ Craft.BaseElementIndex = Garnish.Base.extend(
}

submitting = true;
$exportSubmit.busyEvent();
$exportSubmit?.busyEvent();

var params = this.getViewParams();
delete params.baseCriteria.offset;
Expand Down Expand Up @@ -3986,7 +3990,7 @@ Craft.BaseElementIndex = Garnish.Base.extend(
})
.finally(() => {
submitting = false;
$exportSubmit.successEvent();
$exportSubmit?.successEvent();
});
});
},
Expand Down
30 changes: 18 additions & 12 deletions packages/craftcms-legacy/cp/src/js/Craft.js
Original file line number Diff line number Diff line change
Expand Up @@ -3041,7 +3041,7 @@ $.extend($.fn, {
*/
checkboxselect: function () {
return this.each(function () {
if (!$.data(this, 'checkboxSelect')) {
if (!$.data(this, 'checkboxSelect') && Garnish.CheckboxSelect) {
new Garnish.CheckboxSelect(this, {
storageKey: this.getAttribute('data-storage-key'),
});
Expand All @@ -3062,7 +3062,7 @@ $.extend($.fn, {

nicetext: function () {
return this.each(function () {
if (!$.data(this, 'nicetext')) {
if (!$.data(this, 'nicetext') && Garnish.NiceText) {
new Garnish.NiceText(this);
}
});
Expand Down Expand Up @@ -3277,16 +3277,22 @@ $.extend($.fn, {
},
});

// Override Garnish.NiceText.charsLeftHtml() to be more accessible
Garnish.NiceText.charsLeftHtml = (charsLeft) => {
return Craft.t(
'app',
'<span class="visually-hidden">Characters left:</span> {chars, number}',
{
chars: charsLeft,
}
);
};
// Override Garnish.NiceText.charsLeftHtml() to be more accessible.
// Guarded: the modernized Garnish has no NiceText — `craft-input`'s
// `showCharsLeft` covers it — and this runs at module scope, so an
// unguarded assignment aborts the rest of Craft.js on any page where the
// modern namespace owns `window.Garnish`.
if (Garnish.NiceText) {
Garnish.NiceText.charsLeftHtml = (charsLeft) => {
return Craft.t(
'app',
'<span class="visually-hidden">Characters left:</span> {chars, number}',
{
chars: charsLeft,
}
);
};
}

Garnish.$doc.ready(function () {
Craft.initUiElements();
Expand Down
10 changes: 8 additions & 2 deletions packages/craftcms-legacy/cp/src/js/UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ Craft.ui = {
)
);

if (config.placeholder || config.showCharsLeft) {
// Guarded: the modernized Garnish has no NiceText, and `craft-input`'s
// own `showCharsLeft` covers it where that namespace is in play.
if ((config.placeholder || config.showCharsLeft) && Garnish.NiceText) {
new Garnish.NiceText($input);
}

Expand Down Expand Up @@ -535,7 +537,11 @@ Craft.ui = {
if (config.includeSortActions) {
new Craft.SortableCheckboxSelect($container);
} else {
new Garnish.CheckboxSelect($container);
// Guarded: superseded by the CheckboxSelect component where the
// modernized Garnish owns the namespace.
if (Garnish.CheckboxSelect) {
new Garnish.CheckboxSelect($container);
}

if (config.sortable) {
const dragSort = new Garnish.DragSort(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {beforeEach, expect, it} from 'vite-plus/test';
import type CraftCombobox from './combobox.js';
import './combobox.js';
import '../../styles/cp.css';

beforeEach(() => {
document.body.innerHTML = '';
});

/**
* A single-choice combobox in a plain form — the shape a page that posts itself
* uses, rather than going through an Inertia form.
*/
async function fixture(
configure: (control: CraftCombobox) => void = () => {}
): Promise<{control: CraftCombobox; form: HTMLFormElement}> {
const control = document.createElement('craft-combobox') as CraftCombobox;
control.setAttribute('name', 'apiVersion');
control.options = [
{label: '2026-01', value: '2026-01'},
{label: '2026-04', value: '2026-04'},
];
configure(control);

const form = document.createElement('form');
form.append(control);
document.body.append(form);
await control.updateComplete;
// Lion adopts a requested `model-value` only once the option naming it has
// registered, so the value lands a tick after the first render.
await new Promise((resolve) => requestAnimationFrame(resolve));
await control.updateComplete;

return {control, form};
}

const entries = (form: HTMLFormElement) => [...new FormData(form).entries()];

it('posts its value under the host name', async () => {
const {control, form} = await fixture((c) =>
c.setAttribute('model-value', '2026-01')
);

expect(entries(form)).toEqual([['apiVersion', '2026-01']]);

// Changing the selection changes what the form carries, so a save writes the
// value the field is showing.
control.modelValue = '2026-04';
await control.updateComplete;

expect(entries(form)).toEqual([['apiVersion', '2026-04']]);
});

it('posts a custom value when it does not match an option', async () => {
const {control, form} = await fixture((c) => {
c.requireOptionMatch = false;
c.setAttribute('model-value', '$SHOPIFY_API_VERSION');
});

expect(entries(form)).toEqual([['apiVersion', '$SHOPIFY_API_VERSION']]);
expect(control.modelValue).toBe('$SHOPIFY_API_VERSION');
});

it('posts a value its markup carried, with the options as markup too', async () => {
// How a server-rendered page arrives: both `model-value` and `options` are
// attributes on the element, and it upgrades with them already set.
const options = JSON.stringify([
{label: '$SHOPIFY_API_VERSION', value: '$SHOPIFY_API_VERSION'},
]);
document.body.innerHTML = `<form><craft-combobox name="folderPath" model-value="@webroot/cache/blitz" allow-custom-choice options='${options}'></craft-combobox></form>`;

const form = document.querySelector('form')!;
const control = document.querySelector('craft-combobox') as CraftCombobox;
await control.updateComplete;
await new Promise((resolve) => requestAnimationFrame(resolve));
await control.updateComplete;

expect(control.modelValue).toBe('@webroot/cache/blitz');
expect(entries(form)).toEqual([['folderPath', '@webroot/cache/blitz']]);
});

it('posts nothing while disabled', async () => {
const {control, form} = await fixture((c) => {
c.setAttribute('model-value', '2026-01');
c.disabled = true;
});

await control.updateComplete;

expect(entries(form)).toEqual([]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {beforeEach, expect, it} from 'vite-plus/test';
import type CraftCombobox from './combobox.js';
import './combobox.js';

beforeEach(() => {
document.body.innerHTML = '';
});

async function fixture(
options: CraftCombobox['options']
): Promise<CraftCombobox> {
const control = document.createElement('craft-combobox') as CraftCombobox;
control.options = options;
document.body.append(control);
await control.updateComplete;
await new Promise((resolve) => requestAnimationFrame(resolve));
await control.updateComplete;

return control;
}

it('colours an option’s indicator', async () => {
const control = await fixture([
{label: 'Online', value: 'true', data: {indicator: {fill: 'success'}}},
{label: 'Offline', value: 'false', data: {indicator: {fill: 'danger'}}},
]);

const fills = [...control.querySelectorAll('craft-indicator')].map((el) =>
el.getAttribute('fill')
);

expect(fills).toEqual(['success', 'danger']);
});

it('leaves an indicator without a fill on the component’s default', async () => {
const control = await fixture([
{label: 'Plain', value: 'plain', data: {indicator: {}}},
]);

const indicator = control.querySelector('craft-indicator')!;

expect(indicator.getAttribute('fill')).toBe('var(--c-color-fill-loud)');
});

it('renders an indicator’s appearance', async () => {
const control = await fixture([
{label: 'Off', value: 'off', data: {indicator: {appearance: 'outline'}}},
]);

expect(
control.querySelector('craft-indicator')!.getAttribute('appearance')
).toBe('outline');
});

it('colours an option’s icon', async () => {
const control = await fixture([
{label: 'Red', value: 'red', data: {icon: 'circle', color: 'danger'}},
]);

expect(control.querySelector('craft-icon')!.getAttribute('style')).toContain(
'--icon-color: danger'
);
});

it('leaves an icon without a colour unstyled', async () => {
const control = await fixture([
{label: 'Plain', value: 'plain', data: {icon: 'circle'}},
]);

expect(control.querySelector('craft-icon')!.hasAttribute('style')).toBe(
false
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const countries: ComboboxItem[] = [
];

const statuses: ComboboxItem[] = [
{label: 'Online', value: 'true', data: {indicator: {variant: 'success'}}},
{label: 'Offline', value: 'false', data: {indicator: {variant: 'danger'}}},
{label: 'Online', value: 'true', data: {indicator: {fill: 'success'}}},
{label: 'Offline', value: 'false', data: {indicator: {fill: 'danger'}}},
];

const grouped: ComboboxItem[] = [
Expand Down
Loading
Loading