Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7554dae
Ability to register custom link types
duncanmcclean Jul 14, 2026
326d6b6
Support custom link types in bard links
duncanmcclean Jul 14, 2026
f7c13df
formatting
duncanmcclean Jul 14, 2026
107ffcc
update phpstan baseline
duncanmcclean Jul 14, 2026
02064eb
prevent link types leaking into other tests
duncanmcclean Jul 14, 2026
fb3a870
translate link type labels
duncanmcclean Jul 14, 2026
ef280ba
actually avoid translating on the frontend. we do it in php
duncanmcclean Jul 14, 2026
2986fba
make Bard's link button use the same picker components as the Link fi…
duncanmcclean Jul 14, 2026
58197cd
fix TypeError when checking visibility of an unregistered link type
duncanmcclean Jul 14, 2026
be6ea12
also reset extraConfigFields between link type tests
duncanmcclean Jul 14, 2026
32bba9d
open the picker straight away when selecting a link type in Bard
duncanmcclean Jul 14, 2026
42cf3fa
update phpstan baseline
duncanmcclean Jul 14, 2026
84df859
enable adaptive width to account for longer link type labels
duncanmcclean Jul 16, 2026
8eef902
Merge remote-tracking branch 'origin/6.x' into link-types
jasonvarga Jul 20, 2026
46fe339
Stop emitting unused link type icons and standardise the default icon
jasonvarga Jul 20, 2026
28bf3b6
Memoize Link::types() to avoid redundant link type instantiation
jasonvarga Jul 20, 2026
531079a
Add Vitest coverage for Link fieldtype and Bard link toolbar
jasonvarga Jul 20, 2026
34808c9
fix baseline
jasonvarga Jul 20, 2026
d1a6178
fix js tests
jasonvarga Jul 20, 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
2 changes: 1 addition & 1 deletion .phpstan/baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ parameters:
path: ../src/Facades/Endpoint/Parse.php

-
message: '#^Method Illuminate\\Contracts\\Validation\\DataAwareRule@anonymous/Fieldtypes/Bard\.php\:898\:\:setData\(\) should return \$this\(Illuminate\\Contracts\\Validation\\DataAwareRule@anonymous/Fieldtypes/Bard\.php\:898\) but return statement is missing\.$#'
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
Expand Down
134 changes: 68 additions & 66 deletions resources/js/components/fieldtypes/LinkFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,22 @@
<div class="flex gap-2 sm:gap-3">
<!-- Link type selector -->
<div class="w-fit">
<Select :options v-model="option" />
<Select :options v-model="option" :adaptive-width="true" />
</div>

<div class="flex-1 flex">
<!-- URL text input -->
<Input v-if="option === 'url'" :read-only="isReadOnly" v-model="urlValue" />

<!-- Entry select -->
<relationship-fieldtype
:config="meta.entry.config"
:meta="meta.entry.meta"
:value="selectedEntries"
@update:meta="meta.entry.meta = $event"
@update:value="entriesSelected"
button-size="base"
handle="entry"
ref="entries"
v-if="option === 'entry'"
/>

<!-- Asset select -->
<assets-fieldtype
v-if="option === 'asset'"
ref="assets"
handle="asset"
:value="selectedAssets"
:config="meta.asset.config"
:meta="meta.asset.meta"
@update:value="assetsSelected"
@update:meta="meta.asset.meta = $event"
<component
v-else-if="matchedType"
:is="matchedTypeComponent"
ref="typeField"
:config="matchedType.config"
:meta="matchedType.meta"
:value="selectedByType[option]"
:handle="option"
@update:value="typeSelected"
@update:meta="updateTypeMeta"
/>
</div>
</div>
Expand All @@ -45,7 +31,7 @@ import { markRaw } from 'vue';
import { UPDATE_DEBOUNCE_MS } from './constants';

export default {
components: { Input, Text, Select },
components: { Input, Select },
mixins: [Fieldtype],

provide: {
Expand All @@ -57,8 +43,7 @@ export default {
option: this.meta.initialOption,
options: this.initialOptions(),
urlValue: this.meta.initialUrl,
selectedEntries: this.meta.initialSelectedEntries,
selectedAssets: this.meta.initialSelectedAssets,
selectedByType: this.initialSelectedByType(this.meta),
metaChanging: false,
};
},
Expand All @@ -71,26 +56,29 @@ export default {
},

computed: {
entryValue() {
return this.selectedEntries.length ? `entry::${this.selectedEntries[0]}` : null;
matchedType() {
return this.meta.types[this.option] ?? null;
},

matchedTypeComponent() {
return `${this.matchedType.component}-fieldtype`;
},

assetValue() {
return this.selectedAssets.length ? `asset::${this.selectedAssets[0]}` : null;
typeValue() {
const selected = this.selectedByType[this.option];
return selected && selected.length ? `${this.option}::${selected[0]}` : null;
},

replicatorPreview() {
if (!this.showFieldPreviews) return;

switch (this.option) {
case 'url':
return this.urlValue;
case 'first-child':
return __('First Child');
case 'entry':
return data_get(this.meta, 'entry.meta.data.0.title', this.entryValue);
case 'asset':
return data_get(this.meta, 'asset.meta.data.0.basename', this.assetValue);
if (this.option === 'url') return this.urlValue;
if (this.option === 'first-child') return __('First Child');

if (this.matchedType) {
return data_get(this.meta, `types.${this.option}.meta.data.0.title`)
?? data_get(this.meta, `types.${this.option}.meta.data.0.basename`)
?? this.typeValue;
}

return this.value;
Expand All @@ -107,18 +95,10 @@ export default {
this.updateDebounced(this.urlValue);
} else if (option === 'first-child') {
this.update('@child');
} else if (option === 'entry') {
if (this.entryValue) {
this.update(this.entryValue);
} else {
setTimeout(() => this.$refs.entries.linkExistingItem(), 0);
}
} else if (option === 'asset') {
if (this.assetValue) {
this.update(this.assetValue);
} else {
setTimeout(() => this.$refs.assets.openSelector(), 0);
}
} else if (this.matchedType) {
this.typeValue
? this.update(this.typeValue)
: this.$nextTick(() => this.openTypeSelector());
}

this.updateMeta({ ...this.meta, initialOption: option });
Expand All @@ -136,8 +116,7 @@ export default {
this.metaChanging = true;
this.urlValue = meta.initialUrl;
this.option = meta.initialOption;
this.selectedEntries = meta.initialSelectedEntries;
this.selectedAssets = meta.initialSelectedAssets;
this.selectedByType = this.initialSelectedByType(meta);
this.$nextTick(() => (this.metaChanging = false));
},
},
Expand All @@ -151,22 +130,45 @@ export default {

this.meta.showFirstChildOption ? { label: __('First Child'), value: 'first-child' } : null,

{ label: __('Entry'), value: 'entry' },

this.meta.showAssetOption ? { label: __('Asset'), value: 'asset', maxFiles: 1 } : null,
...Object.entries(this.meta.types).map(([handle, type]) => ({ label: type.title, value: handle })),
].filter((option) => option);
},

entriesSelected(entries) {
this.selectedEntries = entries;
this.update(this.entryValue);
this.updateMeta({ ...this.meta, initialSelectedEntries: entries });
initialSelectedByType(meta) {
return Object.fromEntries(
Object.entries(meta.types).map(([handle, type]) => [handle, type.selected])
);
},

openTypeSelector() {
const field = this.$refs.typeField;

if (!field) return;

if (typeof field.linkExistingItem === 'function') field.linkExistingItem();
else if (typeof field.openSelector === 'function') field.openSelector();
},

typeSelected(selected) {
this.selectedByType = { ...this.selectedByType, [this.option]: selected };
this.update(this.typeValue);
this.updateMeta({
...this.meta,
types: {
...this.meta.types,
[this.option]: { ...this.meta.types[this.option], selected },
},
});
},

assetsSelected(assets) {
this.selectedAssets = assets;
this.update(this.assetValue);
this.updateMeta({ ...this.meta, initialSelectedAssets: assets });
updateTypeMeta(typeMeta) {
this.updateMeta({
...this.meta,
types: {
...this.meta.types,
[this.option]: { ...this.meta.types[this.option], meta: typeMeta },
},
});
},
},
};
Expand Down
5 changes: 1 addition & 4 deletions resources/js/components/fieldtypes/LinkIndexFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const props = defineProps(IndexFieldtype.props);

<template>
<a v-if="value" :key="value.url" :href="value.url" target="_blank" rel="noopener noreferrer" class="flex items-center space-x-2 text-ellipsis">
<Icon v-if="value.type === 'asset'" name="assets" class="size-3 flex-shrink-0" />
<Icon v-else-if="value.type === 'entry'" name="collections" class="size-3 flex-shrink-0" />
<Icon v-else-if="value.type === 'child'" name="page" class="size-3 flex-shrink-0" />
<Icon v-else name="external-link" class="size-3 flex-shrink-0" />
<Icon :name="value.icon" class="size-3 flex-shrink-0" />
<span v-text="value.url" />
</a>
</template>
Loading
Loading