Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b912f7b
performance testing instrumentation
jackmcdade Aug 11, 2026
3e3e684
Remove redundant full-tree clones from publish Values
jackmcdade Aug 11, 2026
3f9b95a
Cache SelectField options and stop double-fetching on mount
jackmcdade Aug 11, 2026
4197c08
Speed up visibleValues and stop deep-watching it
jackmcdade Aug 11, 2026
6d437c0
Cheapen field-condition evaluation hot paths
jackmcdade Aug 11, 2026
64ee663
Dedupe in-flight assets-fieldtype requests
jackmcdade Aug 11, 2026
cb6faf6
Keep settled RelationshipInput responses for the page view
jackmcdade Aug 11, 2026
402492a
Cache Template fieldtype API list per page view
jackmcdade Aug 11, 2026
f83cc23
Fast-path UTF-8 check in relationship fieldtype controller
jackmcdade Aug 11, 2026
e29ddb8
Avoid wasted field resolution in Fields::newInstance
jackmcdade Aug 11, 2026
616a2e8
Memoize relationship item finds per fieldtype instance
jackmcdade Aug 11, 2026
7ae4c59
Speed up Bard/Replicator PHP preload and auto-collapse large fields
jackmcdade Aug 11, 2026
be478ce
Compute collapsed-set preview text from raw values
jackmcdade Aug 11, 2026
3ac919f
Defer collapsed set bodies and lazy-init Bard TipTap
jackmcdade Aug 11, 2026
148ce1c
Watch only condition-referenced handles in Publish Field
jackmcdade Aug 11, 2026
a95eb9b
Fix CI failures from perf branch side effects
jackmcdade Aug 11, 2026
3c9e3de
Prevent Live Preview refresh on progressive Bard mount
jackmcdade Aug 12, 2026
79dfbc6
Guard escapeHtml against non-string values
jackmcdade Aug 12, 2026
6bc1c0c
Harden collapsed preview text against non-string HTML
jackmcdade Aug 12, 2026
55842aa
Ignore Reveal.mount when the element ref is null
jackmcdade Aug 12, 2026
638f658
Make $perf.copy resilient when clipboard write is denied
jackmcdade Aug 12, 2026
a7e889b
Escape backslashes before pipes in $perf markdown cells
jackmcdade Aug 12, 2026
83f2068
Merge branch '6.x' into bard-replicator-performance
jackmcdade Aug 12, 2026
43f8a67
Render lazily-mounted set bodies in a single expand step
jackmcdade Aug 13, 2026
a3d331e
Collapse replicator set bodies while dragging
jackmcdade Aug 13, 2026
e35b9f3
Auto-collapse sets on drag. Better performance, better UX, Papa Johns.
jackmcdade Aug 13, 2026
3b38b5f
Better drop state indicator
jackmcdade Aug 13, 2026
6092e39
Skip Bard getHTML serialization unless reading time is enabled.
jackmcdade Aug 13, 2026
f30cdb3
Only deep-watch Live Preview payloads while preview is open.
jackmcdade Aug 13, 2026
f6a4411
Remove the Vitest browser benchmark suite
jasonvarga Aug 13, 2026
d7af319
Remove the $perf instrumentation
jasonvarga Aug 13, 2026
5f367e8
Keep the grabbed set under the pointer when collapsing siblings on drag.
jackmcdade Aug 13, 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
6 changes: 0 additions & 6 deletions .phpstan/baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,6 @@ parameters:
count: 1
path: ../src/Facades/Endpoint/Parse.php

-
message: '#^Method Illuminate\\Contracts\\Validation\\DataAwareRule@anonymous/Fieldtypes/Bard\.php\:934\:\:setData\(\) should return \$this\(Illuminate\\Contracts\\Validation\\DataAwareRule@anonymous/Fieldtypes/Bard\.php\:934\) but return statement is missing\.$#'
identifier: return.missing
count: 1
path: ../src/Fieldtypes/Bard.php

-
message: '#^Access to an undefined property Statamic\\Filesystem\\AbstractAdapter\:\:\$filesystem\.$#'
identifier: property.notFound
Expand Down
42 changes: 42 additions & 0 deletions resources/css/components/fieldtypes/bard.css
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,48 @@
}
}
}

/* BARD / DRAGGING
=================================================== */
/* Hide set bodies synchronously (classList, not waiting on Vue) so collapse-on-drag
doesn't leave the pointer over empty space where the expanded set used to be. */
.bard-dragging [data-set-body] {
display: none;
}

/* BARD / DROP + GAP CURSORS
=================================================== */
/* Unlayered so we beat ProseMirror's injected 1px black defaults. */
.bard-dropcursor {
border-radius: 999px;
background-color: var(--focus-outline-color, var(--color-blue-400));
}

/* Leading disc on block (horizontal) drops — Notion-style slot marker.
Inline (vertical) carets stay a rounded pill so they don't grow a blob. */
.bard-dropcursor.prosemirror-dropcursor-block::before {
content: '';
position: absolute;
inset-inline-start: 0;
top: 50%;
width: 6px;
height: 6px;
border-radius: 999px;
background-color: inherit;
translate: -30% -50%;
}

[dir='rtl'] .bard-dropcursor.prosemirror-dropcursor-block::before {
translate: 30% -50%;
}

/* Gapcursor: click-between-blocks caret. Same token as dropcursor. */
.ProseMirror-gapcursor:after {
border-top: 2px solid var(--focus-outline-color, var(--color-blue-400));
border-radius: 999px;
width: 1.5rem;
}

/* BARD / FULL SCREEN
=================================================== */
@layer ui-states {
Expand Down
6 changes: 6 additions & 0 deletions resources/css/components/fieldtypes/replicator.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/* ==========================================================================
REPLICATOR FIELDTYPE
========================================================================== */

/* Collapse set bodies to header bars while dragging so swaps only relayout
header-height rows instead of full expanded field trees. */
.replicator-dragging [data-replicator-set] > [data-set-body] {
display: none;
}
5 changes: 5 additions & 0 deletions resources/css/core/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,8 @@ main.nav-closed {
body > .draggable-mirror {
z-index: var(--z-index-draggable);
}

/* Don't clone expanded set bodies into the drag mirror. */
body > .draggable-mirror [data-set-body] {
display: none;
}
1 change: 1 addition & 0 deletions resources/css/cp.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@import './components/fieldtypes/markdown.css';
@import './components/fieldtypes/partial.css';
@import './components/fieldtypes/relationship.css';
@import './components/fieldtypes/replicator.css';
@import './components/fieldtypes/section.css';
@import './components/fieldtypes/table.css';
@import './components/fieldtypes/width.css';
2 changes: 2 additions & 0 deletions resources/js/bootstrap/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export function truncate(string, length, ending = '...') {
}

export function escapeHtml(string) {
if (typeof string !== 'string') return string;

return string
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
Expand Down
3 changes: 3 additions & 0 deletions resources/js/components/Reveal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Reveal {
}

mount(el, callback) {
// Progressive set mounting can call this before the template ref exists.
if (!el) return;

registry.set(el, callback);

onBeforeUnmount(() => registry.delete(el));
Expand Down
19 changes: 17 additions & 2 deletions resources/js/components/field-conditions/ShowField.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { nextTick } from 'vue';
export default class {
constructor(values, extraValues, rootValues, revealerValues, hiddenFields, setHiddenField, extraPayload) {
this.values = values;
// Merge once per instance — reused across showField() calls when Sections/Tabs
// construct a single ShowField for a filter loop.
this.extraValues = { ...extraValues, ...revealerValues };
this.mergedValues = { ...values, ...this.extraValues };
this.rootValues = rootValues;
this.revealerValues = revealerValues;
this.hiddenFields = hiddenFields;
Expand All @@ -31,7 +34,14 @@ export default class {
}

// Use validation to determine whether field should be shown.
let validator = new Validator(field, { ...this.values, ...this.extraValues }, this.rootValues, dottedFieldPath, Object.keys(this.revealerValues), this.extraPayload);
let validator = new Validator(
field,
this.mergedValues,
this.rootValues,
dottedFieldPath,
Object.keys(this.revealerValues),
this.extraPayload,
);
let passes = validator.passesConditions();

// If the field is configured to always save, never omit value.
Expand All @@ -45,12 +55,17 @@ export default class {
return passes;
}

// With no revealers registered, passesNonRevealerConditions === passesConditions.
const hasRevealers = Object.keys(this.revealerValues).length > 0;

// Ensure DOM is updated to ensure all revealers are properly loaded and tracked before committing to store.
nextTick(() => {
this.setHiddenFieldState({
dottedKey: dottedFieldPath,
hidden: !passes,
omitValue: field.type === 'revealer' || !validator.passesNonRevealerConditions(dottedPrefix),
omitValue:
field.type === 'revealer' ||
(hasRevealers ? !validator.passesNonRevealerConditions(dottedPrefix) : !passes),
});
});

Expand Down
31 changes: 26 additions & 5 deletions resources/js/components/field-conditions/Validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { data_get } from '../../bootstrap/globals.js';
import { isObject, intersection } from 'lodash-es';

const NUMBER_SPECIFIC_COMPARISONS = ['>', '>=', '<', '<='];
const CUSTOM_PREFIX_RE = /^custom /;
const ROOT_PREFIX_RE = /^\$?root\./;
const TRAILING_FIELD_RE = /\.[^.]+$/;

const isEmpty = (value) => {
if (value === null || value === undefined) return true;
Expand All @@ -25,6 +28,7 @@ export default class {
this.passOnAny = false;
this.showOnPass = true;
this.converter = new Converter();
this._conditionsResolved = false;
}

usingRootValues() {
Expand Down Expand Up @@ -56,25 +60,42 @@ export default class {
}

getConditions() {
// Memoized per Validator instance — field config is static for the evaluation cycle.
// Side-effect flags (passOnAny / showOnPass) are restored on subsequent calls.
if (this._conditionsResolved) {
this.passOnAny = this._passOnAny;
this.showOnPass = this._showOnPass;
return this._conditions;
}

this._conditionsResolved = true;
this._passOnAny = false;
this._showOnPass = true;

let key = KEYS.filter((key) => this.field[key])[0];

if (!key) {
this._conditions = undefined;
return undefined;
}

if (key.includes('any')) {
this.passOnAny = true;
this._passOnAny = true;
}

if (key.includes('unless') || key.includes('hide_when')) {
this.showOnPass = false;
this._showOnPass = false;
}

let conditions = this.field[key];

return this.isCustomConditionWithoutTarget(conditions)
this._conditions = this.isCustomConditionWithoutTarget(conditions)
? conditions
: this.converter.fromBlueprint(conditions, this.field.prefix);

return this._conditions;
}

isCustomConditionWithoutTarget(conditions) {
Expand Down Expand Up @@ -189,7 +210,7 @@ export default class {
}

prepareFunctionName(condition) {
return condition.replace(new RegExp('^custom '), '').split(':')[0];
return condition.replace(CUSTOM_PREFIX_RE, '').split(':')[0];
}

prepareParams(condition) {
Expand All @@ -204,7 +225,7 @@ export default class {
}

if (field.startsWith('$root.') || field.startsWith('root.')) {
return data_get(this.rootValues, field.replace(new RegExp('^\\$?root\\.'), ''));
return data_get(this.rootValues, field.replace(ROOT_PREFIX_RE, ''));
}

return data_get(this.values, field);
Expand Down Expand Up @@ -293,14 +314,14 @@ export default class {
}

if (lhs.startsWith('$root.') || lhs.startsWith('root.')) {
return lhs.replace(new RegExp('^\\$?root\\.'), '');
return lhs.replace(ROOT_PREFIX_RE, '');
}

return dottedPrefix ? dottedPrefix + '.' + lhs : lhs;
}

scopeValuesToParent() {
let scope = this.currentFieldPath.replace(new RegExp('\.[^\.]+$'), '');
let scope = this.currentFieldPath.replace(TRAILING_FIELD_RE, '');

this.values = data_get(this.rootValues, scope);

Expand Down
30 changes: 28 additions & 2 deletions resources/js/components/fieldtypes/TemplateFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@
<script>
import Fieldtype from './Fieldtype.vue';
import { Combobox } from '@/components/ui';
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';

// Raw template list shared across instances for the page-view lifetime.
const templatesCache = ref(null);
let navigationListenerAttached = false;

function ensureCacheClearedOnNavigation() {
if (navigationListenerAttached) return;
navigationListenerAttached = true;
router.on('before', () => {
templatesCache.value = null;
});
}

export default {
components: { Combobox },
Expand All @@ -35,9 +49,21 @@ export default {
},

mounted() {
ensureCacheClearedOnNavigation();

if (templatesCache.value) {
this.applyTemplates(templatesCache.value);
return;
}

this.$axios.get(cp_url('api/templates')).then((response) => {
var templates = response.data;
templatesCache.value = response.data;
this.applyTemplates(response.data);
});
},

methods: {
applyTemplates(templates) {
// Filter out partials
if (this.config.hide_partials) {
templates = templates.filter((template) => {
Expand Down Expand Up @@ -74,7 +100,7 @@ export default {

this.options = options;
this.loading = false;
});
},
},
};
</script>
19 changes: 11 additions & 8 deletions resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ import { isEqual } from 'lodash-es';
import { Button, Dropdown, DropdownMenu, DropdownItem, Stack } from '@/components/ui';
import ItemActions from '@/components/actions/ItemActions.vue';
import useCheckerboard from '@/composables/checkerboard.js';
import { dedupeInFlight } from '@/util/dedupeInFlight.js';

export default {
components: {
Expand Down Expand Up @@ -496,14 +497,16 @@ export default {

this.loading = true;

this.$axios
.post(cp_url('assets-fieldtype'), {
assets,
})
.then((response) => {
this.assets = response.data;
this.loading = false;
});
const cacheKey = JSON.stringify([...assets].slice().sort());

dedupeInFlight('assets-fieldtype', cacheKey, () =>
this.$axios.post(cp_url('assets-fieldtype'), { assets }),
).then((response) => {
// Clone so mutations on one field's asset rows don't bleed into others
// sharing the same in-flight response.
this.assets = clone(response.data);
this.loading = false;
});
},

/**
Expand Down
Loading
Loading