Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
13 changes: 13 additions & 0 deletions packages/craftcms-ui/src/components/callout/callout.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export default css`
align-items: start;
padding: var(--_callout-padding-block) var(--_callout-padding-inline);
border: 1px solid transparent;
/*
Spans the full width of a surrounding <craft-field-group> grid. A
block-level Form Node can rely on that grid's own \`craft-field-group >
*\` default (or a \`width-*\` class) for this, but \`:host\` here is
\`display: contents\` — grid placement given to the *host* has no box to
apply to, so the grid falls back to auto-placing this shadow-rendered
box on its own, one column wide, unless it claims its own span here.
Every current Callout usage is full-width (no narrower \`width-*\`
variant exists yet), so this is unconditional rather than keyed off a
host class. Harmless outside a grid parent — \`grid-column\` is simply
inert there.
*/
grid-column: 1 / -1;
}

.callout--hide-icon {
Expand Down
7 changes: 7 additions & 0 deletions packages/craftcms-ui/src/components/callout/callout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import {styleMap} from 'lit/directives/style-map.js';
* @summary A boxed message: an optional icon, an optional title, body content,
* and an optional trailing action.
*
* The host is `display: contents` (see `callout.styles`) so it never adds an
* unstyled wrapper box of its own — but that means a surrounding CSS Grid
* (e.g. `<craft-field-group>`'s `width-*` classes) can't size the *host*: with
* no box of its own, the host's own grid placement is inert, and the grid
* instead auto-places whatever the shadow root renders one column at a time.
* `.callout` spans the full grid itself to compensate — see its own rule.
*
* @attr size - `small` steps the box down to `--c-text-sm` and tightens the gap
* between the icon and the text. Defaults to `auto`, which leaves the callout
* at the surrounding text size. Note this is type only — the padding below is
Expand Down
10 changes: 9 additions & 1 deletion packages/craftcms-ui/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,15 @@ export default class CraftCombobox extends LionCombobox {
}
}

private changeValues(values: string[]) {
/**
* Commits a new set of selected values for a multi-select combobox — resets the free-text
* query, closes the listbox, re-renders options, syncs the hidden native inputs used for real
* form submission, and announces the change. `protected` (not `private`) so a subclass that
* needs to inject a value outside the normal option-click/keyboard paths (e.g.
* {@link CraftComboboxCreate} appending a freshly-created record's option) can reuse this
* exact path instead of re-deriving its side effects, which would drift out of sync over time.
*/
protected changeValues(values: string[]) {
if (this.inactive) {
return;
}
Expand Down
49 changes: 44 additions & 5 deletions packages/craftcms-ui/src/components/select-color/select-color.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {html, LitElement} from 'lit';
import {property} from 'lit/decorators.js';
import {colors} from '@src/constants/colors';
import type {Validator} from '@lion/ui/form-core.js';
import {colors as paletteColors} from '@src/constants/colors';
import {t} from '@src/utilities/translate';
import styles from './select-color.styles.js';
import '../select-rich/select-rich.js';
Expand All @@ -15,7 +16,8 @@ function titleCase(value: string): string {

/**
* @summary A color picker built on top of the rich select. Renders one option
* per color from `constants/colors`, with an optional "transparent" option.
* per color from `constants/colors` (or {@link CraftSelectColor.colors}, when
* narrowed), with an optional "transparent" option.
*
* @since 1.0
*/
Expand All @@ -41,11 +43,44 @@ export default class CraftSelectColor extends LitElement {
modelValue: string | null = null;

/**
* When enabled, a "Transparent" option is prepended to the list of colors.
* When enabled, a blank option (labelled {@link blankLabel}) is prepended
* to the list of colors.
*/
@property({type: Boolean, reflect: true, attribute: 'allow-transparent'})
allowTransparent = false;

/**
* Overrides the blank option's label (default "Transparent"). Use this
* when the value being picked isn't a background/opacity concept — e.g.
* "No color" for a category swatch.
*/
@property({attribute: 'blank-label'})
blankLabel: string | null = null;

/**
* The colors offered, in order. Defaults to every color in the shared
* palette ({@link paletteColors}) — pass a subset to restrict the choices
* to whatever set a particular caller's values are actually drawn from.
*/
@property({type: Array})
colors: string[] = [...paletteColors];

/** Forwarded to the underlying rich select. */
@property({type: Boolean, reflect: true})
disabled = false;

/** Forwarded to the underlying rich select. */
@property({type: Boolean, reflect: true, attribute: 'readonly'})
readOnly = false;

/** Forwarded to the underlying rich select. */
@property({type: Boolean, reflect: true})
required = false;

/** Forwarded to the underlying rich select. */
@property({attribute: false})
validators: Validator[] = [];

/**
* Renders a color swatch for the given color value. The special `__blank__`
* value (the "Transparent" option) reuses the checkerboard treatment so it
Expand Down Expand Up @@ -137,12 +172,16 @@ export default class CraftSelectColor extends LitElement {
label=${this.label}
name=${this.name}
.modelValue=${this.modelValue}
.disabled=${this.disabled}
.readOnly=${this.readOnly}
.required=${this.required}
.validators=${this.validators}
@model-value-changed=${this._handleModelValueChanged}
>
${this.allowTransparent
? this._optionTemplate('__blank__', t('Transparent'))
? this._optionTemplate('__blank__', this.blankLabel ?? t('Transparent'))
: ''}
${colors.map((color) =>
${this.colors.map((color) =>
this._optionTemplate(color, t(titleCase(color)))
)}
</craft-select-rich>
Expand Down
10 changes: 10 additions & 0 deletions resources/js/bootstrap/cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ const Cp = {
return cpComponentRegistry;
},

// The real, mounted-app `router` singleton from `@inertiajs/vue3` — exposed
// for the same reason as `$components`/`$axios`: a plugin's own,
// independently-bundled script gets its own disconnected copy of the
// `@inertiajs/vue3` module (and hence a `router` object with no page/app
// actually wired to it) if it imports the package directly, so it needs a
// way to reach the one that's actually driving this page instead.
get $router() {
return router;
},

get $elementDetailsTabs() {
return elementDetailsTabRegistry;
},
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 {AxiosRequestConfig, AxiosResponse} from 'axios';

type LegacySettingValue =
Expand Down Expand Up @@ -90,6 +91,7 @@ interface CpStatic extends CpServices {
$components: CpComponentRegistry;
$elementDetailsTabs: ElementDetailsTabRegistry;
$inertia: InertiaPageRegistry;
$router: Router;
}

interface CpNotificationSettings {
Expand Down
30 changes: 30 additions & 0 deletions resources/js/common/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ActionFeedback, BaseAction, VariantKey} from '@craftcms/ui';
import type {ComboboxOptionData} from '@craftcms/ui/components/combobox/combobox';
import type {UrlMethodPair} from '@inertiajs/core';
import type {Component} from 'vue';
import type {FormValues} from '@/modules/forms/types';

Expand Down Expand Up @@ -210,10 +211,38 @@ export type ActionItem =

export type ActionItems = Array<ActionItem>;

/**
* A server-described alternate form action — the `Form` page's counterpart to
* `\CraftCms\Cms\Http\Responses\CpScreenResponse::addAltAction()` ("Save as a
* new X", "Delete", …). Unlike a plain {@link ActionItemButton}, this carries
* no client-side `onClick` (it's plain JSON from the server) — `Form.vue`
* translates each one into a real action that resubmits the form's *current*
* in-progress values to `action` (default POST to the screen's own submit
* method) instead of firing an isolated request.
*/
export interface FormAltAction {
label: string;
/** Renders the action as destructive (e.g. a "Delete"). */
destructive?: boolean;
/** Defaults to the screen's own submit action when omitted. */
action?: string;
/** Extra values merged into the posted payload alongside the form's own. */
params?: FormValues;
/** A confirmation message shown (native `confirm()`) before submitting. */
confirm?: string;
}

export interface FormSaveOptions {
redirect?: boolean;
data?: FormValues;
preserveState?: boolean;
/**
* Submits to a different destination than the screen's own default
* `submit` target — e.g. an alternate form action posting the same
* in-progress values elsewhere ("Save as a new X", "Delete"). Omit to use
* the screen's own action.
*/
action?: UrlMethodPair;
}

export interface EntryType {
Expand Down Expand Up @@ -280,6 +309,7 @@ export type EditableTableCellType =
| 'autosuggest'
| 'template'
| 'number'
| 'money'
| 'singleline'
| 'multiline'
| 'heading'
Expand Down
98 changes: 98 additions & 0 deletions resources/js/modules/admin-table/components/MoveToPageButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import {t} from '@craftcms/ui/utilities/translate';
import {computed, ref} from 'vue';
import Select from '@/common/form/Select.vue';

/**
* A "Move to page…" control for a {@see Table} Form Node in `dataUrl()` mode — the
* within-page drag-and-drop reorder `AdminTableNode.vue` already offers can't reach a
* page that isn't loaded, so this is the only way to move a row to a different one.
* Mirrors the legacy `Craft.VueAdminTable` widget's own `AdminTableMoveToPageHud` (a
* small "choose a page" popover, not literal drag-across-pages) — same interaction,
* rebuilt on `<craft-popover>` instead of a `Garnish.HUD`.
*
* Lives once in the selection footer (`AdminTableNode.vue`), not per row — matching
* this codebase's own established convention (see the real Entries index: uncommon
* per-selection actions like "Move to…" live in a footer "Actions" menu once
* something's selected, not in a disclosure control on every row) rather than
* inventing a new per-row menu. `AdminTableNode.vue` only mounts this component while
* exactly one row is selected, so there's no separate `disabled` state to model here.
*/
const props = defineProps<{
currentPage: number;
lastPage: number;
loading?: boolean;
}>();

const emit = defineEmits<{
(e: 'move', page: number): void;
}>();

interface PopoverElement extends HTMLElement {
hide(): Promise<void>;
}

const popover = ref<PopoverElement>();
const targetPageRaw = ref<string | number>(props.currentPage);

// `Select`'s v-model round-trips through a native <select>, whose value is
// always a string — coerce back to a number the same way `BaseElementIndex.
// vue`'s own page-size/page-index proxies already do.
const targetPage = computed(() => parseInt(String(targetPageRaw.value), 10));

const pages = Array.from({length: props.lastPage}, (_, i) => i + 1);

function handleMove(): void {
if (targetPage.value === props.currentPage) {
return;
}

emit('move', targetPage.value);
popover.value?.hide();
}

function onOpenedChanged(event: CustomEvent<boolean>): void {
if (event.detail) {
targetPageRaw.value = props.currentPage;
}
}
</script>

<template>
<craft-popover
ref="popover"
placement="bottom-end"
@opened-changed="onOpenedChanged"
>
<!--
`v-once` is load-bearing, not an optimization — see `ActionMenu.vue`'s own
identical comment. `craft-popover` extends the same Lion `OverlayMixin` as
`craft-action-menu` and relocates/restructures its slotted invoker once
mounted. This component stays mounted for as long as exactly one row is
selected, and `AdminTableNode.vue` passes it a shared `loading` prop that
flips during that same window (the move's own `fetchPage()` refetch);
without `v-once`, that re-render patches the invoker against DOM the
overlay has already moved. The invoker is static text, so freezing it
costs nothing.
-->
<craft-button type="button" slot="invoker" size="small" v-once>{{
t('Move to page…')
}}</craft-button>

<div slot="content-body" class="flex flex-nowrap items-end gap-2">
<Select
:label="t('Choose a page')"
v-model="targetPageRaw"
:options="pages.map((p) => ({label: String(p), value: String(p)}))"
/>
<craft-button
type="button"
variant="primary"
size="small"
:loading="loading"
@click="handleMove"
>{{ t('Move') }}</craft-button
>
</div>
</craft-popover>
</template>
Loading