Skip to content

Commit 80a75c2

Browse files
committed
Merge remote-tracking branch 'origin/6.x' into fix-site-switch-field-sync
# Conflicts: # resources/js/components/globals/PublishForm.vue
2 parents 940e56a + 7a6794a commit 80a75c2

33 files changed

Lines changed: 587 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
# Release Notes
22

3+
## 6.29.0 (2026-08-25)
4+
5+
### What's new
6+
- Add a way to opt out of popper z-index control [#15227](https://github.com/statamic/cms/issues/15227) by @jaygeorge
7+
- Add a "compact mode" for the array fieldtype [#15219](https://github.com/statamic/cms/issues/15219) by @jaygeorge
8+
- Let sites check in from CLI, scheduler, and front-end requests [#15208](https://github.com/statamic/cms/issues/15208) by @jackmcdade
9+
- Add safe option to to_json modifier [#15260](https://github.com/statamic/cms/issues/15260) by @edalzell
10+
11+
### What's fixed
12+
- Fix Antlers identifiers erroring before the parser has been resolved [#15221](https://github.com/statamic/cms/issues/15221) by @lazerg
13+
- Support taggable comboboxes when `multiple` is false [#15064](https://github.com/statamic/cms/issues/15064) by @duncanmcclean
14+
- Improve tooltips [#15211](https://github.com/statamic/cms/issues/15211) by @jaygeorge
15+
- For links in tooltips, open them in a new tab if external [#15213](https://github.com/statamic/cms/issues/15213) by @jaygeorge
16+
- Fix nocache TypeError when rendered outside a matched route [#15215](https://github.com/statamic/cms/issues/15215) by @lazerg
17+
- Fix localized dates leaking into later template usages [#15224](https://github.com/statamic/cms/issues/15224) by @duncanmcclean
18+
- Add missing tooltips to global header [#15234](https://github.com/statamic/cms/issues/15234) by @heidkaemper
19+
- Augment nav blueprint fields onto manual-URL nav items [#15235](https://github.com/statamic/cms/issues/15235) by @lazerg
20+
- Fix reordering a paginated orderable collection corrupting the tree [#15238](https://github.com/statamic/cms/issues/15238) by @marcorieser
21+
- Fix nav entry links across sites losing their domain [#15239](https://github.com/statamic/cms/issues/15239) by @lazerg
22+
- Ensure REST API authentication errors return consistent JSON [#15077](https://github.com/statamic/cms/issues/15077) by @daun
23+
- Fix Link field's Asset mode height [#15242](https://github.com/statamic/cms/issues/15242) by @jackmcdade
24+
- Encode asset URLs [#15146](https://github.com/statamic/cms/issues/15146) by @duncanmcclean
25+
- Improve Asset fieldtype UI in smaller spaces [#15243](https://github.com/statamic/cms/issues/15243) by @jackmcdade
26+
- Fix is_external for anchor, mailto and tel links in navs [#15082](https://github.com/statamic/cms/issues/15082) by @duncanmcclean
27+
- Reduce publish form and Live Preview overhead [#15251](https://github.com/statamic/cms/issues/15251) by @jasonvarga
28+
- Dedupe concurrent Control Panel asset requests [#15252](https://github.com/statamic/cms/issues/15252) by @jasonvarga
29+
- Small PHP CP performance improvements and UTF-8 fix [#15254](https://github.com/statamic/cms/issues/15254) by @jasonvarga
30+
- Bard server-side performance improvements [#15258](https://github.com/statamic/cms/issues/15258) by @jasonvarga
31+
- Memoize the Replicator flattened sets config Blink key [#15259](https://github.com/statamic/cms/issues/15259) by @jasonvarga
32+
- Resolve NoCache session via app() in StaticCaching hydrated callback [#15256](https://github.com/statamic/cms/issues/15256) by @petemolinero
33+
- Memoize resolved field conditions [#15262](https://github.com/statamic/cms/issues/15262) by @jasonvarga
34+
- Only generate Bard's HTML when reading time is enabled [#15263](https://github.com/statamic/cms/issues/15263) by @jasonvarga
35+
- Avoid layout thrashing from Reka's aria-label fallback [#15264](https://github.com/statamic/cms/issues/15264) by @jasonvarga
36+
37+
38+
339
## 6.28.0 (2026-08-19)
440

541
### What's new

lang/en/fieldtypes.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@
114114
'html.config.html_instruct' => 'Manage the HTML to be displayed in the publish form. This is for display purposes only, the HTML will not be saved.',
115115
'html.config.sanitize_instruct' => 'Whether the HTML should be sanitized before being displayed. Only disable this if you have a good reason.',
116116
'html.title' => 'HTML',
117-
'icon.config.set' => 'The name of a custom icon set.',
117+
'icon.config.mode' => 'Choose a dropdown or compact picker.',
118+
'icon.config.set' => 'Default CP icons, or a set registered with `Icons::register()`.',
118119
'icon.title' => 'Icon',
119120
'integer.config.max' => 'The maximum allowed value.',
120121
'integer.config.min' => 'The minimum allowed value.',

resources/js/components/entries/PublishForm.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -662,10 +662,6 @@ export default {
662662
} else {
663663
this.createLocalization(localization);
664664
}
665-
666-
if (this.isBase) {
667-
window.history.replaceState({}, '', localization.url + window.location.hash);
668-
}
669665
},
670666
671667
editLocalization(localization) {
@@ -694,6 +690,10 @@ export default {
694690
this.initialPublished = data.values.published;
695691
this.readOnly = data.readOnly;
696692
693+
if (this.isBase && localization.url) {
694+
window.history.replaceState({}, '', localization.url + window.location.hash);
695+
}
696+
697697
this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 500); // after any fieldtypes do a debounced update
698698
});
699699
},

resources/js/components/entries/calendar/Calendar.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Month from './Month.vue';
77
import Week from './Week.vue';
88
import { Listing, StatusIndicator } from '@/components/ui';
99
import DateFormatter from '@/components/DateFormatter.js';
10-
import { getWeekDates, getCurrentDateRange } from './calendar.js';
10+
import { getWeekDates, getCurrentDateRange, getEntryDate } from './calendar.js';
1111
import { Link } from '@inertiajs/vue3';
1212
import { ToggleGroup, ToggleItem, Button, Popover, Label, Select, Heading } from '@ui';
1313
import { preferences } from '@api';
@@ -122,8 +122,7 @@ const selectedDateEntries = computed(() => {
122122
const dateStr = selectedDate.value.toString();
123123
124124
return entries.value.filter(entry => {
125-
const entryDate = new Date(entry.date?.date || entry.date);
126-
return entryDate.toISOString().split('T')[0] === dateStr;
125+
return getEntryDate(entry).toString().split('T')[0] === dateStr;
127126
});
128127
});
129128

resources/js/components/entries/calendar/Month.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, Cale
33
import CalendarEntry from './MonthEntry.vue';
44
import CreateEntryButton from '../CreateEntryButton.vue';
55
import { Button } from '@ui';
6-
import { isToday, getCreateUrlDateParam } from './calendar.js';
6+
import { isToday, getCreateUrlDateParam, getEntryDate } from './calendar.js';
77
import DateFormatter from '@/components/DateFormatter.js';
8-
import { parseAbsoluteToLocal } from '@internationalized/date';
98
109
const props = defineProps({
1110
weekDays: { type: Array, required: true },
@@ -32,8 +31,7 @@ const isCurrentDay = (dayIndex) => {
3231
const getEntriesForDate = (date) => {
3332
const dateStr = date.toString();
3433
return props.entries.filter(entry => {
35-
const entryDate = parseAbsoluteToLocal(entry.date?.date || entry.date);
36-
return entryDate.toString().split('T')[0] === dateStr;
34+
return getEntryDate(entry).toString().split('T')[0] === dateStr;
3735
});
3836
};
3937

resources/js/components/entries/calendar/MonthEntry.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ const entryClasses = computed(() => ({
1313
'border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900': props.entry.status === 'scheduled'
1414
}));
1515
16-
const time = computed(() => DateFormatter.format(props.entry.date?.date || props.entry.date, 'time'))
16+
const time = computed(() => {
17+
if (props.entry.date?.format_has_time === false) return null;
18+
19+
return DateFormatter.format(props.entry.date?.date || props.entry.date, 'time');
20+
});
1721
</script>
1822
1923
<template>
@@ -25,6 +29,7 @@ const time = computed(() => DateFormatter.format(props.entry.date?.date || props
2529
>
2630
<span class="line-clamp-2" v-text="entry.title" />
2731
<span
32+
v-if="time"
2833
class="hidden @4xl:block text-2xs text-gray-400 dark:text-gray-400 group-hover/entry:dark:text-gray-300"
2934
v-text="time"
3035
/>

resources/js/components/entries/calendar/Week.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { Button } from '@ui';
66
import {
77
isToday,
88
getCreateUrlDateParam,
9+
getEntryDate,
910
} from './calendar.js';
1011
import DateFormatter from '@/components/DateFormatter.js';
11-
import { parseAbsoluteToLocal } from '@internationalized/date';
1212
1313
const props = defineProps({
1414
weekDates: { type: Array, required: true },
@@ -27,12 +27,11 @@ const visibleHours = Array.from({ length: 24 }, (_, i) => i);
2727
function getEntriesForHour(date, hour) {
2828
const dateStr = date.toString();
2929
return props.entries.filter(entry => {
30-
const entryDate = parseAbsoluteToLocal(entry.date?.date || entry.date);
30+
const entryDate = getEntryDate(entry);
3131
const entryDateStr = entryDate.toString().split('T')[0];
3232
if (entryDateStr !== dateStr) return false;
3333
34-
const entryHour = entryDate.hour;
35-
return entryHour === hour;
34+
return (entryDate.hour ?? 0) === hour;
3635
});
3736
}
3837

resources/js/components/entries/calendar/calendar.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { CalendarDate, CalendarDateTime, fromDate, getLocalTimeZone, startOfWeek, endOfWeek } from '@internationalized/date';
1+
import { CalendarDate, CalendarDateTime, fromDate, getLocalTimeZone, parseAbsoluteToLocal, parseDate, startOfWeek, endOfWeek } from '@internationalized/date';
22
import DateFormatter from '@/components/DateFormatter.js';
33

4+
export function getEntryDate(entry) {
5+
const date = entry.date?.date || entry.date;
6+
7+
// Fields using a format without a time save a plain date string, which can't be parsed as an absolute datetime.
8+
return date.includes('T') ? parseAbsoluteToLocal(date) : parseDate(date);
9+
}
10+
411
export function getWeekDates(currentDate) {
512
if (!currentDate) {
613
throw new Error('getWeekDates called with undefined currentDate');

resources/js/components/field-conditions/Validator.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const ROOT_PREFIX_RE = /^\$?root\./;
1111
const isEmpty = (value) => {
1212
if (value === null || value === undefined) return true;
1313

14+
// Object.keys() would consider numbers empty.
15+
if (typeof value === 'number') return false;
16+
1417
return Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0;
1518
};
1619

resources/js/components/fieldtypes/IconFieldtype.vue

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script setup>
22
import axios from 'axios';
3+
import fuzzysort from 'fuzzysort';
34
import Fieldtype from '@/components/fieldtypes/fieldtype.js';
45
import { computed, ref, watch } from 'vue';
5-
import { Combobox, Icon } from '@/components/ui';
6+
import { Button, Combobox, Icon, Input, Modal } from '@/components/ui';
67
78
const emit = defineEmits(Fieldtype.emits);
89
const props = defineProps(Fieldtype.props);
@@ -13,11 +14,14 @@ const loading = ref(true);
1314
const loaders = ref({});
1415
const iconsCache = ref({});
1516
const cacheKey = computed(() => props.config.set ?? '__default__');
17+
const isCompact = computed(() => props.config.mode === 'compact');
18+
const isOpen = ref(false);
19+
const search = ref('');
1620
1721
const options = computed(() => {
1822
let options = [];
1923
20-
for (let [name, html] of Object.entries(icons.value)) {
24+
for (let [name, html] of Object.entries(icons.value || {})) {
2125
options.push({
2226
value: name,
2327
label: name,
@@ -28,6 +32,23 @@ const options = computed(() => {
2832
return options;
2933
});
3034
35+
const selectedOption = computed(() => {
36+
return options.value.find((option) => option.value === props.value) ?? null;
37+
});
38+
39+
const noneOption = { value: null, label: 'No Icon', none: true };
40+
41+
const filteredOptions = computed(() => {
42+
const matches = fuzzysort
43+
.go(search.value, options.value, {
44+
all: true,
45+
key: 'label',
46+
})
47+
.map((result) => result.obj);
48+
49+
return search.value ? matches : [noneOption, ...matches];
50+
});
51+
3152
function request() {
3253
if (loaders.value[cacheKey.value]) return;
3354
@@ -50,6 +71,20 @@ function comboboxUpdated(value) {
5071
update(value || null);
5172
}
5273
74+
function openPicker() {
75+
if (isReadOnly.value || props.config.disabled) return;
76+
77+
isOpen.value = true;
78+
}
79+
80+
function selectIcon(name) {
81+
if (isReadOnly.value || props.config.disabled) return;
82+
83+
update(name);
84+
isOpen.value = false;
85+
search.value = '';
86+
}
87+
5388
watch(
5489
() => loaders.value[cacheKey.value],
5590
(loadingState) => {
@@ -58,12 +93,16 @@ watch(
5893
}
5994
);
6095
96+
watch(isOpen, (open) => {
97+
if (!open) search.value = '';
98+
});
99+
61100
request();
62101
</script>
63102
64103
<template>
65104
<Combobox
66-
v-if="!loading"
105+
v-if="!loading && !isCompact"
67106
clearable
68107
:disabled="config.disabled"
69108
:model-value="value"
@@ -95,4 +134,65 @@ request();
95134
</div>
96135
</template>
97136
</Combobox>
137+
138+
<div v-else-if="!loading" class="flex items-center">
139+
<Button
140+
icon-only
141+
size="base"
142+
:aria-label="value || __('Select Icon')"
143+
:disabled="config.disabled"
144+
:read-only="isReadOnly"
145+
:title="value || undefined"
146+
@click="openPicker"
147+
>
148+
<Icon v-if="selectedOption && !selectedOption.html" :name="selectedOption.label" />
149+
<div
150+
v-else-if="selectedOption?.html"
151+
v-html="selectedOption.html"
152+
class="flex items-center justify-center [&>svg]:size-4.5"
153+
/>
154+
<Icon v-else name="plus" class="opacity-40" />
155+
</Button>
156+
157+
<Modal
158+
v-model:open="isOpen"
159+
:blur="false"
160+
:title="__('Select Icon')"
161+
class="xl:max-w-3xl 2xl:max-w-page"
162+
>
163+
<Input
164+
v-model="search"
165+
:placeholder="__('Search...')"
166+
icon-prepend="magnifying-glass"
167+
size="sm"
168+
type="text"
169+
/>
170+
171+
<div class="st-custom-scrollbar max-h-[40vh] overflow-auto">
172+
<div class="grid grid-cols-6 gap-1 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12">
173+
<button
174+
v-for="option in filteredOptions"
175+
:key="option.none ? '__none__' : option.value"
176+
type="button"
177+
class="flex size-9 items-center justify-center rounded-lg"
178+
:aria-label="__(option.label)"
179+
:aria-pressed="option.value === value"
180+
:class="{
181+
'bg-gray-100 dark:bg-gray-900': option.value === value,
182+
'hover:bg-gray-100 dark:hover:bg-gray-900': option.value !== value,
183+
}"
184+
:title="__(option.label)"
185+
@click="selectIcon(option.value)"
186+
>
187+
<Icon v-if="option.none" name="x" class="size-3.5 opacity-40" />
188+
<Icon v-else-if="!option.html" :name="option.label" class="size-5" />
189+
<div v-else v-html="option.html" class="[&>svg]:size-5" />
190+
</button>
191+
</div>
192+
<div v-if="filteredOptions.length === 0" class="p-3 text-center text-xs text-gray-600">
193+
{{ search ? __('No results') : __('No icons available') }}
194+
</div>
195+
</div>
196+
</Modal>
197+
</div>
98198
</template>

0 commit comments

Comments
 (0)