Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
1789d66
wip admin table updates
nfourtythree Sep 9, 2026
8b99c43
Merge branch '6.x' into feature/6.x-admintable-updates
nfourtythree Sep 9, 2026
e2b129d
fix card designer element type
nfourtythree Sep 9, 2026
a1a302b
table and control compatibility changes
nfourtythree Sep 15, 2026
095ec44
Merge branch '6.x' into feature/6.x-admintable-updates
nfourtythree Sep 15, 2026
58b1531
php stuff for select colour component
nfourtythree Sep 15, 2026
bd88505
Missing suffix for text control
nfourtythree Sep 16, 2026
96b63f3
added new combocreate control
nfourtythree Sep 16, 2026
272b482
Merge branch '6.x' into feature/6.x-admintable-updates
nfourtythree Sep 16, 2026
f204747
fix change event bug
nfourtythree Sep 16, 2026
be18ea9
Add bulk actions to table, allow money field in table and allow group…
nfourtythree Sep 17, 2026
4d48c69
Allow hidden tables rows and reactivity
nfourtythree Sep 17, 2026
97d3c81
Update create combo to allow multi select
nfourtythree Sep 17, 2026
368dc5b
further table node updates
nfourtythree Sep 21, 2026
26e85fb
Table node reorder success/fail notices
nfourtythree Sep 22, 2026
b542c22
Remove leftover
nfourtythree Sep 22, 2026
5f3826e
for third party components to interact with inertia
nfourtythree Sep 22, 2026
43f2f92
Support pagination, search, data end points, move to page, footer act…
nfourtythree Sep 22, 2026
529b700
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 22, 2026
d34c3b4
fix action menu bug causing stuck layouts
nfourtythree Sep 22, 2026
0d90385
matching table styling
nfourtythree Sep 22, 2026
ddc5f70
Merge remote-tracking branch 'origin/6.x' into feature/6.x-form-compo…
riasvdv Sep 23, 2026
dd02f15
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 23, 2026
f49471a
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Sep 23, 2026
ef55725
Move combobox creation into reusable options
riasvdv Sep 23, 2026
e10c733
Fix a bug that would persist edit screens when navigating
nfourtythree Sep 23, 2026
d6e6bdc
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Sep 23, 2026
ef1400d
Cleanup Claude's overly verbose comments
riasvdv Sep 23, 2026
d62e345
Trim remaining verbose comments
riasvdv Sep 23, 2026
bd3b99f
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Sep 23, 2026
f3218a4
Make bulk actions bar in the table node more generic for greater flex…
nfourtythree Sep 23, 2026
e360ac8
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 23, 2026
8ed595f
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 24, 2026
5fb0a2c
formatting (standardise later)
nfourtythree Sep 24, 2026
8bc087a
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 24, 2026
7fc9110
Make sure status actions can have a fill colour
nfourtythree Sep 24, 2026
472b87e
Fix move to page styling
nfourtythree Sep 24, 2026
e23681f
Make “+ New X” buttons consistent with the rest of the system
nfourtythree Sep 24, 2026
09c6cd1
Fix icon picker field
nfourtythree Sep 24, 2026
fc6c113
Tweak icon and thumbnail in chip html
nfourtythree Sep 24, 2026
5f77c29
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 24, 2026
cb180b4
Add page size selector to admin table for data endpoint tables
nfourtythree Sep 24, 2026
02de564
Store page size preference for tables in local storage
nfourtythree Sep 24, 2026
182f806
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 24, 2026
dc0e3e2
Introduce (or re-introduce) admin table toolbar with ability to butto…
nfourtythree Sep 24, 2026
7a47c1b
Add status filtering dropdown feature to admin tables
nfourtythree Sep 24, 2026
0fc0cce
Add in sorting capabilities to admin tables for both upfront and data…
nfourtythree Sep 24, 2026
cbc6e82
Admin tables have view button and toggleable columns
nfourtythree Sep 24, 2026
da8f124
Missing search icon in search fiel on tables
nfourtythree Sep 24, 2026
7c9079f
Fix reloading of tables
nfourtythree Sep 24, 2026
b293334
Fix missing . on class definition
nfourtythree Sep 25, 2026
4685705
Remove double padding on bare tables
nfourtythree Sep 25, 2026
6148c61
Fix validation errors not showing. Use inertia `’errors’`
nfourtythree Sep 25, 2026
5bea10b
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 25, 2026
c24764b
Remaining part of merege
nfourtythree Sep 25, 2026
ff0303a
Fix double border bottom in table
nfourtythree Sep 25, 2026
b5e300d
expose vue for plugin developers so they can use Craft’s version
nfourtythree Sep 29, 2026
d7fc28b
add info tool tip to column headings
nfourtythree Sep 29, 2026
8d56c89
Allow add row button text customisation
nfourtythree Sep 29, 2026
b1dfdea
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 29, 2026
147176d
Fix icon alignment
nfourtythree Sep 29, 2026
73796e0
ask before deleting modal in admin table
nfourtythree Sep 29, 2026
940da64
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 30, 2026
433f195
Fix entries view actions styling
nfourtythree Sep 30, 2026
dc87805
expose element index to for plugins to be able to implement
nfourtythree Sep 30, 2026
430aa44
Expose element editor
nfourtythree Sep 30, 2026
cc7b6f8
add refresh to element editor for reactivity
nfourtythree Sep 30, 2026
c9d3640
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Sep 30, 2026
5ef4722
added form modal
nfourtythree Sep 30, 2026
0cf08a4
Update changelog
nfourtythree Sep 30, 2026
8a15b14
Fix metadata display on non-element from screens
nfourtythree Sep 30, 2026
8d02b67
Merge origin/6.x and adapt admin table bulk actions
riasvdv Sep 30, 2026
b0b0cd5
Merge fresh origin/6.x into PR 19665
riasvdv Sep 30, 2026
b474185
Allow metadetails side bars for non-elements have form components
nfourtythree Sep 30, 2026
bb0f157
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Sep 30, 2026
28d0086
Merge fresh origin/6.x into form component compatibility branch
riasvdv Sep 30, 2026
d7a542b
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Oct 1, 2026
4ad9164
Merge origin/6.x into form component compatibility
riasvdv Oct 1, 2026
36d831a
Export action menu
nfourtythree Oct 1, 2026
859270e
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Oct 1, 2026
a222095
Merge 6.x and reconcile extracted form actions
riasvdv Oct 1, 2026
9fe9a66
ability to redirect from action
nfourtythree Oct 1, 2026
90bed81
Merge branch 'feature/6.x-form-component-compatibility-and-usage-upda…
nfourtythree Oct 1, 2026
c72352b
Allow plugins to access the sourceSubnav method
nfourtythree Oct 1, 2026
6eb17c1
Allow further crumb subnav controller (useful for plugins with elemen…
nfourtythree Oct 1, 2026
b771c72
fix color select attribute
nfourtythree Oct 1, 2026
5151bfb
missing import
nfourtythree Oct 2, 2026
f5650bb
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Oct 2, 2026
853fc6c
group as field
nfourtythree Oct 3, 2026
b0f65f5
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Oct 3, 2026
32a252c
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Oct 4, 2026
949a595
Fixed missing internal element card padding
nfourtythree Oct 4, 2026
f2ed3c8
Merge branch '6.x' into feature/6.x-form-component-compatibility-and-…
nfourtythree Oct 4, 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
4 changes: 4 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/
- Removed `Cp.$axios`.
- Removed the `Craft.Accordion` and `Craft.EnvVarGenerator` control panel JavaScript classes. ([#19323](https://github.com/craftcms/cms/pull/19323))
- Deprecated the `Craft.LightSwitch`, `Craft.InfoIcon`, `Craft.ColorInput`, `Craft.PasswordInput`, `Craft.IconPicker`, `Craft.SlidePicker`, `Craft.SlideRuleInput`, and `Craft.Tooltip` control panel JavaScript classes, along with the `.infoicon` jQuery plugin. The corresponding `@craftcms/ui` web components should be used instead.
- Plugin bundles can now import the control panel’s element index and element editor components (`ElementIndexPage`, `ElementEditor`, `CpButtonLink`, `ActionMenu`) from the `@craftcms/cms/elements` import-map module, so a plugin’s own element types can have Inertia index and edit pages.
- Reactive controls in an element’s field layout now refresh the layout from the server when they change, the same as in settings forms.
- Element action menu items can now use a `formModal` behavior (`modalUrl`, `actionUrl`, `params`), which opens a server-built form in a modal, posts its values to `actionUrl`, and reloads the page once it’s submitted.
- Element actions run from the element index’s bulk actions bar can now redirect the browser by setting a redirect response with `setResponse()`.

### Drafts

Expand Down
5 changes: 5 additions & 0 deletions resources/js/bootstrap/cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ const Cp = {
return cpComponentRegistry;
},

// Plugin bundles must use the mounted app's router, not their own module copy.
get $router() {
return router;
},

get $elementDetailsTabs() {
return elementDetailsTabRegistry;
},
Expand Down
4 changes: 4 additions & 0 deletions resources/js/common/components/ActionList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
href?: string;
external?: boolean;
label?: string;
/** A colored status dot before the label — `craft-indicator`'s own `fill` values. */
fill?: string;
onClick?: (event: Event) => void;
/**
* Everything optional, with the unset keys left out entirely. Binding an
Expand Down Expand Up @@ -172,6 +174,7 @@
{
kind: 'button',
label: action.label,
fill: action.fill,
onClick: action.onClick,
attrs: defined({
...attrs,
Expand Down Expand Up @@ -476,6 +479,7 @@
:is="as"
@click="action.onClick"
>
<craft-indicator v-if="action.fill" :fill="action.fill"></craft-indicator>
{{ action.label }}
</component>
</template>
Expand Down
44 changes: 44 additions & 0 deletions resources/js/common/components/MetadataDetails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,47 @@ it('renders nothing without metadata', async () => {

expect(container!.querySelector('craft-tabs')).toBeNull();
});

it('shows sidebar controls above the metadata', async () => {
container = document.createElement('div');
document.body.append(container);
app = createApp({
render: () =>
h(
MetadataDetails,
{html: '<dl>ID 1</dl>'},
{default: () => h('div', {class: 'controls'})}
),
});
app.mount(container);
await nextTick();

const controls = container.querySelector('.controls');
const details = container.querySelector('.details-html');

expect(controls).not.toBeNull();
expect(container.querySelector('hr')).not.toBeNull();
expect(
controls!.compareDocumentPosition(details!) &
Node.DOCUMENT_POSITION_FOLLOWING
).toBeTruthy();
});

it('shows sidebar controls without metadata', async () => {
container = document.createElement('div');
document.body.append(container);
app = createApp({
render: () =>
h(
MetadataDetails,
{html: null},
{default: () => h('div', {class: 'controls'})}
),
});
app.mount(container);
await nextTick();

expect(container.querySelector('.controls')).not.toBeNull();
expect(container.querySelector('hr')).toBeNull();
expect(container.querySelector('.details-html')).toBeNull();
});
114 changes: 114 additions & 0 deletions resources/js/common/composables/subnavCrumbs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
withSubnavCrumbs,
} from './subnavCrumbs';

import type {BreadcrumbItem} from '@/common/types';

type NavItem = CraftCms.Cms.Cp.Data.NavItem;

function navCrumbItem(config: Partial<NavItem> & {label: string}): NavItem {
Expand Down Expand Up @@ -315,4 +317,116 @@ describe('withNavCrumbMenus', () => {

expect(crumb!.items).toBe(items);
});

it('leaves a section that is nothing but its sources to the source crumb', () => {
const [entries, all] = withNavCrumbMenus(
[
{label: 'Entries', href: '/admin/content/entries'},
{
label: 'All entries',
href: '/admin/content/entries',
items: [{type: 'link', label: 'All entries', href: '/x'}],
},
],
NAV,
'/admin/content/entries'
);

expect(entries!.items).toBeUndefined();
expect(all!.items).toHaveLength(3);
});

describe('in a plugin’s section', () => {
const COMMERCE = navCrumbItem({
label: 'Commerce',
href: '/admin/commerce',
subnav: [
navCrumbItem({label: 'Orders', href: '/admin/commerce/orders'}),
navCrumbItem({label: 'All products', href: '/admin/commerce/products'}),
navCrumbItem({
label: 'Product Types',
href: null,
group: true,
subnav: [
navCrumbItem({
label: 'Widgets',
href: '/admin/commerce/products/widgets',
}),
navCrumbItem({
label: 'Gizmos',
href: '/admin/commerce/products/gizmos',
}),
],
}),
navCrumbItem({label: 'Inventory', href: '/admin/commerce/inventory'}),
],
});
const PLUGIN_NAV = [navCrumbItem({label: 'Dashboard'}), COMMERCE];
const SOURCES = [
{type: 'link' as const, label: 'All products', href: '/x'},
];

const productCrumbs = (current: string): Array<BreadcrumbItem> => [
{label: 'Commerce', href: '/admin/commerce'},
{label: 'Products', href: '/admin/commerce/products'},
{label: 'Current', href: current, items: SOURCES},
];

it('gives the section’s crumb the whole section', () => {
const [commerce] = withNavCrumbMenus(
productCrumbs('/admin/commerce/products/gizmos'),
PLUGIN_NAV,
'/admin/commerce/products/gizmos'
);

expect(commerce!.items).toMatchObject([
{type: 'link', label: 'Orders', selected: false},
{type: 'link', label: 'All products', selected: false},
{
type: 'group',
heading: 'Product Types',
items: [
{type: 'link', label: 'Widgets', selected: false},
{type: 'link', label: 'Gizmos', selected: true},
],
},
{type: 'link', label: 'Inventory', selected: false},
]);
});

it('limits a source crumb to what sits under its index', () => {
const [, products, source] = withNavCrumbMenus(
productCrumbs('/admin/commerce/products'),
PLUGIN_NAV,
'/admin/commerce/products'
);

expect(products!.items).toBeUndefined();
expect(source!.items).toMatchObject([
{type: 'link', label: 'All products', selected: true},
{
type: 'group',
heading: 'Product Types',
items: [
{type: 'link', label: 'Widgets', selected: false},
{type: 'link', label: 'Gizmos', selected: false},
],
},
]);
});

it('gives the section’s crumb its menu on a screen with no switchers', () => {
const [commerce, inventory] = withNavCrumbMenus(
[
{label: 'Commerce', href: '/admin/commerce'},
{label: 'Inventory', href: '/admin/commerce/inventory'},
],
PLUGIN_NAV,
'/admin/commerce/inventory'
);

expect(commerce!.items).toHaveLength(4);
expect(inventory!.items).toBeUndefined();
});
});
});
113 changes: 108 additions & 5 deletions resources/js/common/composables/subnavCrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,27 +134,130 @@ function navLevelOf(
}

/**
* Gives each crumb that has a switcher the menu the nav draws for its level.
* The part of a level that lives under `href`, keeping the headings over
* whatever survives.
*
* A plugin lists an index's sources in its own nav alongside everything else
* it has, so the level a source sits in is the whole plugin. The index crumb
* above the source is what says which of those belong to it.
*/
function levelWithin(level: Array<NavItem>, href: string): Array<NavItem> {
return level.flatMap((item): Array<NavItem> => {
if (item.group) {
const children = levelWithin(
Array.isArray(item.subnav) ? item.subnav : [],
href
);

return children.length > 0 ? [{...item, subnav: children}] : [];
}

return item.href && navItemContains(href, item.href) ? [item] : [];
});
}

/** What a level offers, for telling whether two crumbs would offer the same. */
function levelSignature(level: Array<NavItem>): string {
return level
.map((item) =>
item.group && Array.isArray(item.subnav)
? `[${levelSignature(item.subnav)}]`
: (item.href ?? '')
)
.join(',');
}

function offersChoice(level: Array<NavItem>): boolean {
return (
level.filter((item) => !item.group).length > 1 ||
level.some((item) => item.group)
);
}

/**
* Gives each crumb that has a switcher the menu the nav draws for its level,
* and a section's crumb the menu of the section.
*
* An index screen's crumbs come from its secondary nav, while a screen deeper
* in (an entry's edit page, say) gets its crumbs from the server. Taking the
* menu from the nav for both keeps a source's switcher the same wherever it
* appears. A crumb without a menu keeps not having one, and one the nav
* doesn't know keeps the server's.
*
* The exception is a section — a main nav item with a subnav, which is what a
* plugin's nav item is. Its crumb switches between everything in the section,
* unless a crumb further along already offers exactly that, as an element
* index's source crumb does when the section is nothing but its sources.
*
* @param url The page being shown, which the section's menu marks.
*/
export function withNavCrumbMenus(
crumbs: Array<BreadcrumbItem>,
nav: Array<NavItem>
nav: Array<NavItem>,
url?: string
): Array<BreadcrumbItem> {
return crumbs.map((crumb) => {
const href = crumb.href ?? crumb.url;
const hrefs = crumbs.map((crumb) => crumb.href ?? crumb.url ?? null);

const levels = crumbs.map((crumb, index): Array<NavItem> | null => {
const href = hrefs[index];

if (!crumb.items?.length || !href) {
return crumb;
return null;
}

const level = navLevelOf(withNavSelection(nav, href), href);

if (!level) {
return null;
}

const parent = hrefs
.slice(0, index)
.reverse()
.find((other): other is string => other !== null);

if (!parent || !navItemContains(parent, href)) {
return level;
}

const within = levelWithin(level, parent);

return within.length > 0 ? within : level;
});

const current =
url ?? [...hrefs].reverse().find((href) => href !== null) ?? '';
const selectedNav = withNavSelection(nav, current);

const sections = crumbs.map((crumb, index): Array<NavItem> | null => {
const href = hrefs[index];

if (crumb.items?.length || !href) {
return null;
}

const section = selectedNav.find(
(item) =>
samePath(item.href, href) &&
Array.isArray(item.subnav) &&
offersChoice(item.subnav)
);

if (!section || !Array.isArray(section.subnav)) {
return null;
}

const signature = levelSignature(section.subnav);
const offeredLater = levels
.slice(index + 1)
.some((level) => level !== null && levelSignature(level) === signature);

return offeredLater ? null : section.subnav;
});

return crumbs.map((crumb, index) => {
const level = levels[index] ?? sections[index];

return level ? {...crumb, items: navItemActions(level)} : crumb;
});
}
2 changes: 1 addition & 1 deletion resources/js/common/layouts/screens/PageScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
const {nav, siteCrumb} = useCraftData();
const crumbs = computed<Array<BreadcrumbItem> | null>(() => {
const merged = withSubnavCrumbs(
withNavCrumbMenus(page.props.crumbs ?? [], nav.value ?? []),
withNavCrumbMenus(page.props.crumbs ?? [], nav.value ?? [], page.url),
subnav.value
);

Expand Down
2 changes: 2 additions & 0 deletions resources/js/common/types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {QueueService} from '@/modules/queue/queue';
import type {CpComponentRegistry} from '@/bootstrap/components';
import type {ElementDetailsTabRegistry} from '@/bootstrap/element-details-tabs';
import type {InertiaPageRegistry} from '@/bootstrap/inertia-pages';
import type {Router} from '@inertiajs/core';
import type {HttpResponse} from '@craftcms/ui/utilities/api/http';
import type {LegacyRequestOptions} from '@craftcms/ui/utilities/api/legacyRequest';

Expand Down Expand Up @@ -75,6 +76,7 @@ interface CpStatic extends CpServices {
$components: CpComponentRegistry;
$elementDetailsTabs: ElementDetailsTabRegistry;
$inertia: InertiaPageRegistry;
$router: Router;
}

interface CpNotificationSettings {
Expand Down
Loading
Loading