Skip to content

Commit 7292e8f

Browse files
edalzellclaudejasonvarga
authored
[6.x] Append to Bard Entry links (#11468)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent df5474d commit 7292e8f

4 files changed

Lines changed: 152 additions & 9 deletions

File tree

resources/js/components/fieldtypes/bard/LinkToolbar.vue

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@
8080
<ui-separator :text="__('Advanced Options')" />
8181

8282
<section class="space-y-5">
83+
<!-- Append attribute -->
84+
<ui-input
85+
v-if="linkType === 'entry'"
86+
type="text"
87+
v-model="appends"
88+
:prepend="__('Append')"
89+
:placeholder="__('?query=params#anchor')"
90+
/>
91+
8392
<!-- Title attribute -->
8493
<ui-input
8594
type="text"
@@ -196,6 +205,7 @@ export default {
196205
url: {},
197206
urlData: {},
198207
itemData: {},
208+
appends: null,
199209
title: null,
200210
rel: null,
201211
targetBlank: false,
@@ -237,6 +247,13 @@ export default {
237247
return this.sanitizeLink(this.url[this.linkType]);
238248
},
239249
250+
normalizedAppends() {
251+
const value = this.appends;
252+
if (!value) return '';
253+
if (value.startsWith('?') || value.startsWith('#')) return value;
254+
return value.includes('=') ? `?${value}` : `#${value}`;
255+
},
256+
240257
defaultRel() {
241258
let rel = [];
242259
if (this.config.link_noopener) rel.push('noopener');
@@ -307,7 +324,11 @@ export default {
307324
},
308325
309326
watch: {
310-
linkType() {
327+
linkType(type) {
328+
if (type != 'entry') {
329+
this.appends = null;
330+
}
331+
311332
this.autofocus();
312333
},
313334
@@ -349,8 +370,8 @@ export default {
349370
methods: {
350371
applyAttrs(attrs) {
351372
this.linkType = this.getLinkTypeForUrl(attrs.href);
352-
353-
this.url = { [this.linkType]: attrs.href };
373+
this.appends = this.getAppendsForUrl(attrs.href);
374+
this.url = { [this.linkType]: this.appends ? attrs.href?.replace(this.appends, '') : attrs.href };
354375
this.urlData = { [this.linkType]: this.getUrlDataForUrl(attrs.href) };
355376
this.itemData = { [this.linkType]: this.getItemDataForUrl(attrs.href) };
356377
@@ -393,7 +414,7 @@ export default {
393414
}
394415
395416
this.$emit('updated', {
396-
href: this.href,
417+
href: this.href + this.normalizedAppends,
397418
rel: this.rel,
398419
target: this.canHaveTarget && this.targetBlank ? '_blank' : null,
399420
title: this.title,
@@ -494,14 +515,24 @@ export default {
494515
return this.bard.meta.linkData[ref];
495516
},
496517
518+
getAppendsForUrl(urlString) {
519+
// appends is only relevant to entry links
520+
if (! urlString?.includes('statamic://entry::')) {
521+
return null;
522+
}
523+
524+
return urlString.replace(urlString.split(/[?#]/)[0], '') || null;
525+
},
526+
497527
parseDataUrl(url) {
498528
if (!url) {
499529
return {};
500530
}
501531
532+
const appends = this.getAppendsForUrl(url);
502533
const regex = /^statamic:\/\/((.*?)::(.*))$/;
503534
504-
const matches = url.match(regex);
535+
const matches = (appends ? url.replace(appends, '') : url).match(regex);
505536
if (!matches) {
506537
return {};
507538
}

src/Fieldtypes/Bard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ private function extractLinkDataFromNode($node)
788788

789789
private function getLinkDataForUrl($url)
790790
{
791-
$ref = Str::after($url, 'statamic://');
791+
$ref = str($url)->after('statamic://')->before('?')->before('#')->toString();
792792
[$type, $id] = explode('::', $ref, 2);
793793

794794
$data = null;

src/Fieldtypes/Bard/LinkMark.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,21 @@ protected function convertHref($href)
6262
return $href;
6363
}
6464

65-
$ref = Str::after($href, 'statamic://');
65+
$ref = str($href)->after('statamic://')->before('?')->before('#')->toString();
6666

6767
if (! $item = Data::find($ref)) {
6868
return '';
6969
}
7070

7171
$selectAcrossSites = Augmentor::$currentBardConfig['select_across_sites'] ?? false;
7272

73+
$extras = Str::after($href, $ref);
74+
7375
if (! $selectAcrossSites && ! $this->isApi() && $item instanceof Entry) {
74-
return ($item->in(Site::current()->handle()) ?? $item)->url();
76+
return ($item->in(Site::current()->handle()) ?? $item)->url().$extras;
7577
}
7678

77-
return $selectAcrossSites ? $item->absoluteUrl() : $item->url();
79+
return $selectAcrossSites ? $item->absoluteUrl().$extras : $item->url().$extras;
7880
}
7981

8082
private function isApi()

tests/Fieldtypes/BardTest.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,116 @@ public function it_doesnt_localize_when_select_across_sites_setting_is_enabled()
13511351
$this->assertEquals('<a href="http://localhost/fr/blog/one-fr">The One</a>', $augmented);
13521352
}
13531353

1354+
#[Test]
1355+
public function it_preserves_query_params_on_entry_links()
1356+
{
1357+
tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->save();
1358+
EntryFactory::collection('blog')->id('123')->slug('my-post')->data(['title' => 'My Post'])->create();
1359+
1360+
$field = (new Bard)->setField(new Field('test', ['type' => 'bard']));
1361+
1362+
$augmented = $field->augment([
1363+
['type' => 'text', 'marks' => [['type' => 'link', 'attrs' => ['href' => 'statamic://entry::123?foo=bar']]], 'text' => 'Link'],
1364+
]);
1365+
1366+
$this->assertEquals('<a href="/blog/my-post?foo=bar">Link</a>', $augmented);
1367+
}
1368+
1369+
#[Test]
1370+
public function it_preserves_anchors_on_entry_links()
1371+
{
1372+
tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->save();
1373+
EntryFactory::collection('blog')->id('123')->slug('my-post')->data(['title' => 'My Post'])->create();
1374+
1375+
$field = (new Bard)->setField(new Field('test', ['type' => 'bard']));
1376+
1377+
$augmented = $field->augment([
1378+
['type' => 'text', 'marks' => [['type' => 'link', 'attrs' => ['href' => 'statamic://entry::123#section']]], 'text' => 'Link'],
1379+
]);
1380+
1381+
$this->assertEquals('<a href="/blog/my-post#section">Link</a>', $augmented);
1382+
}
1383+
1384+
#[Test]
1385+
public function it_preserves_query_params_and_anchors_on_entry_links()
1386+
{
1387+
tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->save();
1388+
EntryFactory::collection('blog')->id('123')->slug('my-post')->data(['title' => 'My Post'])->create();
1389+
1390+
$field = (new Bard)->setField(new Field('test', ['type' => 'bard']));
1391+
1392+
$augmented = $field->augment([
1393+
['type' => 'text', 'marks' => [['type' => 'link', 'attrs' => ['href' => 'statamic://entry::123?foo=bar#section']]], 'text' => 'Link'],
1394+
]);
1395+
1396+
$this->assertEquals('<a href="/blog/my-post?foo=bar#section">Link</a>', $augmented);
1397+
}
1398+
1399+
#[Test]
1400+
public function it_preserves_appends_on_localized_entry_links()
1401+
{
1402+
$this->setSites([
1403+
'en' => ['url' => 'http://localhost/', 'locale' => 'en'],
1404+
'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'],
1405+
]);
1406+
1407+
Facades\Site::setCurrent('fr');
1408+
1409+
tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->sites(['en', 'fr'])->save();
1410+
1411+
EntryFactory::id('parent')->collection('blog')->slug('theparent')->id(123)->locale('en')->create();
1412+
EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->create();
1413+
1414+
$field = (new Bard)->setField(new Field('test', array_merge(['type' => 'bard'], ['select_across_sites' => false])));
1415+
1416+
$augmented = $field->augment([
1417+
['type' => 'text', 'marks' => [['type' => 'link', 'attrs' => ['href' => 'statamic://entry::123-fr?foo=bar#section']]], 'text' => 'The One'],
1418+
]);
1419+
1420+
$this->assertEquals('<a href="/fr/blog/one-fr?foo=bar#section">The One</a>', $augmented);
1421+
}
1422+
1423+
#[Test]
1424+
public function it_preserves_appends_on_entry_links_with_select_across_sites()
1425+
{
1426+
$this->setSites([
1427+
'en' => ['url' => 'http://localhost/', 'locale' => 'en'],
1428+
'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'],
1429+
]);
1430+
1431+
Facades\Site::setCurrent('en');
1432+
1433+
tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->sites(['en', 'fr'])->save();
1434+
1435+
EntryFactory::id('parent')->collection('blog')->slug('theparent')->id(123)->locale('en')->create();
1436+
EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->create();
1437+
1438+
$field = (new Bard)->setField(new Field('test', array_merge(['type' => 'bard'], ['select_across_sites' => true])));
1439+
1440+
$augmented = $field->augment([
1441+
['type' => 'text', 'marks' => [['type' => 'link', 'attrs' => ['href' => 'statamic://entry::123-fr?foo=bar#section']]], 'text' => 'The One'],
1442+
]);
1443+
1444+
$this->assertEquals('<a href="http://localhost/fr/blog/one-fr?foo=bar#section">The One</a>', $augmented);
1445+
}
1446+
1447+
#[Test]
1448+
public function it_gets_link_data_with_appends()
1449+
{
1450+
tap(Facades\Collection::make('pages')->routes('/{slug}'))->save();
1451+
EntryFactory::collection('pages')->id('1')->slug('about')->data(['title' => 'About'])->create();
1452+
1453+
$bard = $this->bard(['save_html' => true, 'sets' => null]);
1454+
1455+
$html = '<p><a href="statamic://entry::1?foo=bar#section">Link with appends</a></p>';
1456+
1457+
$prosemirror = (new Augmentor($this))->renderHtmlToProsemirror($html)['content'];
1458+
1459+
$this->assertEquals([
1460+
'entry::1' => ['title' => 'About', 'permalink' => 'http://localhost/about'],
1461+
], $bard->getLinkData($prosemirror));
1462+
}
1463+
13541464
private function bard($config = [])
13551465
{
13561466
return (new Bard)->setField(new Field('test', array_merge(['type' => 'bard', 'sets' => ['one' => []]], $config)));

0 commit comments

Comments
 (0)