diff --git a/config/bolt/contenttypes.yaml b/config/bolt/contenttypes.yaml
index e0714f32c..fe0d074b3 100644
--- a/config/bolt/contenttypes.yaml
+++ b/config/bolt/contenttypes.yaml
@@ -163,6 +163,7 @@ showcases:
slug: showcases
singular_name: Showcase
singular_slug: showcase
+ title_format: '{id}: {title}'
description: The 'Showcases' is not particularly useful in most cases, but it does a good job of showcasing most of the available fieldtypes.
fields:
title:
@@ -326,6 +327,7 @@ blocks:
tests:
name: Tests
singular_name: Test
+ title_format: "{id}: {title}"
fields:
title:
type: text
diff --git a/public/theme/skeleton/custom/test.twig b/public/theme/skeleton/custom/test.twig
index 7f18586ec..afac08634 100644
--- a/public/theme/skeleton/custom/test.twig
+++ b/public/theme/skeleton/custom/test.twig
@@ -13,11 +13,7 @@
- {% if record.hasField('title') %}
-
{{ record.title }}
- {% else %}
- {{ record|title }}
- {% endif %}
+ {{ record|title }}
diff --git a/src/Configuration/Parser/ContentTypesParser.php b/src/Configuration/Parser/ContentTypesParser.php
index 45990b47a..68fbb472d 100644
--- a/src/Configuration/Parser/ContentTypesParser.php
+++ b/src/Configuration/Parser/ContentTypesParser.php
@@ -184,7 +184,8 @@ protected function parseContentType($key, array $contentType): ?ContentType
if (isset($contentType['title_format'])) {
$contentType['title_format'] = $contentType['title_format'];
} elseif (isset($contentType['fields']['slug']['uses'])) {
- $contentType['title_format'] = (array) $contentType['fields']['slug']['uses'];
+ $fields = (array) $contentType['fields']['slug']['uses'];
+ $contentType['title_format'] = '{' . implode('} {', $fields) . '}';
} else {
$contentType['title_format'] = null;
}
diff --git a/src/Storage/Directive/OrderDirective.php b/src/Storage/Directive/OrderDirective.php
index f23a00f03..795057d04 100644
--- a/src/Storage/Directive/OrderDirective.php
+++ b/src/Storage/Directive/OrderDirective.php
@@ -5,6 +5,7 @@
namespace Bolt\Storage\Directive;
use Bolt\Storage\QueryInterface;
+use Bolt\Utils\ContentHelper;
/**
* Directive to alter query based on 'order' parameter.
@@ -27,36 +28,55 @@ public function __invoke(QueryInterface $query, string $order): void
foreach ($separatedOrders as $order) {
[ $order, $direction ] = $this->createSortBy($order);
- if (in_array($order, $query->getCoreFields(), true)) {
- $query->getQueryBuilder()->addOrderBy('content.' . $order, $direction);
- } elseif ($order === 'author') {
- $query
- ->getQueryBuilder()
- ->leftJoin('content.author', 'user')
- ->addOrderBy('user.username', $direction);
- } else {
- if (! $this->isActualField($query, $order)) {
- dump("A query with ordering on a Field (`${order}`) that's not defined, will yield unexpected results. Update your `{% setcontent %}`-statement");
+ if ($order === 'title' && $this->getTitleFormat($query) !== null) {
+ $order = ContentHelper::getFieldNames($this->getTitleFormat($query));
+ }
+
+ if (is_array($order)) {
+ foreach ($order as $orderitem) {
+ $this->setOrderBy($query, $orderitem, $direction);
}
- $fieldsAlias = 'fields_order_' . $query->getIndex();
- $fieldAlias = 'order_' . $query->getIndex();
- $translationsAlias = 'translations_order_' . $query->getIndex();
-
- // Note the `lower()` in the `addOrderBy()`. It is essential to sorting the
- // results correctly. See also https://github.com/bolt/core/issues/1190
- $query
- ->getQueryBuilder()
- ->leftJoin('content.fields', $fieldsAlias)
- ->leftJoin($fieldsAlias . '.translations', $translationsAlias)
- ->andWhere($fieldsAlias . '.name = :' . $fieldAlias)
- ->addOrderBy('lower(' . $translationsAlias . '.value)', $direction)
- ->setParameter($fieldAlias, $order);
-
- $query->incrementIndex();
+ } else {
+ $this->setOrderBy($query, $order, $direction);
}
}
}
+ /**
+ * Set the query OrderBy directives
+ * given an order (e.g. 'heading', 'id') and direction (ASC|DESC)
+ */
+ private function setOrderBy(QueryInterface $query, string $order, string $direction): void
+ {
+ if (in_array($order, $query->getCoreFields(), true)) {
+ $query->getQueryBuilder()->addOrderBy('content.' . $order, $direction);
+ } elseif ($order === 'author') {
+ $query
+ ->getQueryBuilder()
+ ->leftJoin('content.author', 'user')
+ ->addOrderBy('user.username', $direction);
+ } else {
+ if (! $this->isActualField($query, $order)) {
+ dump("A query with ordering on a Field (`${order}`) that's not defined, will yield unexpected results. Update your `{% setcontent %}`-statement");
+ }
+ $fieldsAlias = 'fields_order_' . $query->getIndex();
+ $fieldAlias = 'order_' . $query->getIndex();
+ $translationsAlias = 'translations_order_' . $query->getIndex();
+
+ // Note the `lower()` in the `addOrderBy()`. It is essential to sorting the
+ // results correctly. See also https://github.com/bolt/core/issues/1190
+ $query
+ ->getQueryBuilder()
+ ->leftJoin('content.fields', $fieldsAlias)
+ ->leftJoin($fieldsAlias . '.translations', $translationsAlias)
+ ->andWhere($fieldsAlias . '.name = :' . $fieldAlias)
+ ->addOrderBy('lower(' . $translationsAlias . '.value)', $direction)
+ ->setParameter($fieldAlias, $order);
+
+ $query->incrementIndex();
+ }
+ }
+
/**
* Cobble together the sorting order, and whether or not it's a column in `content` or `fields`.
*/
@@ -98,4 +118,11 @@ protected function isActualField(QueryInterface $query, string $name): bool
return in_array($name, $contentType->get('fields')->keys()->all(), true);
}
+
+ private function getTitleFormat(QueryInterface $query): ?string
+ {
+ $contentType = $query->getConfig()->get('contenttypes/' . $query->getContentType());
+
+ return $contentType->get('title_format', null);
+ }
}
diff --git a/src/Twig/ContentExtension.php b/src/Twig/ContentExtension.php
index 8648a0991..97b005f53 100644
--- a/src/Twig/ContentExtension.php
+++ b/src/Twig/ContentExtension.php
@@ -17,7 +17,7 @@
use Bolt\Repository\ContentRepository;
use Bolt\Repository\TaxonomyRepository;
use Bolt\Storage\Query;
-use Bolt\Utils\ComposeValueHelper;
+use Bolt\Utils\ContentHelper;
use Bolt\Utils\Excerpt;
use Bolt\Utils\Html;
use Pagerfanta\Pagerfanta;
@@ -166,45 +166,26 @@ public function getAnyTitle(Content $content, int $length = 120): string
public function getTitle(Content $content, string $locale = '', int $length = 120): string
{
- if (ComposeValueHelper::isSuitable($content)) {
- $title = ComposeValueHelper::get($content, $content->getDefinition()->get('title_format'));
- } else {
- $title = $this->getFieldBasedTitle($content);
+ if (empty($locale)) {
+ $locale = $this->request->getLocale();
}
- return Html::trimText($title, $length);
- }
-
- private function getFieldBasedTitle(Content $content): string
- {
- $titleParts = [];
-
- foreach (ComposeValueHelper::guessTitleFields($content) as $fieldName) {
- $field = $content->getField($fieldName);
-
- if (! empty($locale)) {
- $field->setCurrentLocale($locale);
- }
-
- $value = $field->getParsedValue();
-
- if (empty($value)) {
- $value = $field->setLocale($field->getDefaultLocale())->getParsedValue();
- }
-
- $titleParts[] = $value;
+ if (ContentHelper::isSuitable($content)) {
+ $title = ContentHelper::get($content, $content->getDefinition()->get('title_format'), $locale);
+ } else {
+ $title = ContentHelper::getFieldBasedTitle($content, $locale);
}
- return implode(' ', $titleParts);
+ return Html::trimText($title, $length);
}
public function getTitleFields(Content $content): array
{
- if (ComposeValueHelper::isSuitable($content)) {
- return ComposeValueHelper::getFieldNames($content->getDefinition()->get('title_format'));
+ if (ContentHelper::isSuitable($content)) {
+ return ContentHelper::getFieldNames($content->getDefinition()->get('title_format'));
}
- return ComposeValueHelper::guessTitleFields($content);
+ return ContentHelper::guessTitleFields($content);
}
/**
@@ -242,8 +223,8 @@ public function getExcerpt($content, int $length = 280, bool $includeTitle = fal
return Excerpt::getExcerpt((string) $content, $length);
}
- if (ComposeValueHelper::isSuitable($content, 'excerpt_format')) {
- $excerpt = ComposeValueHelper::get($content, $content->getDefinition()->get('excerpt_format'));
+ if (ContentHelper::isSuitable($content, 'excerpt_format')) {
+ $excerpt = ContentHelper::get($content, $content->getDefinition()->get('excerpt_format'));
} else {
$excerpt = $this->getFieldBasedExcerpt($content, $length, $includeTitle);
}
@@ -263,7 +244,7 @@ private function getFieldBasedExcerpt(Content $content, int $length, bool $inclu
}
}
- $skipFields = ComposeValueHelper::guessTitleFields($content);
+ $skipFields = $this->getTitleFields($content);
foreach ($content->getFields() as $field) {
if ($field instanceof Excerptable && in_array($field->getName(), $skipFields, true) === false) {
@@ -574,7 +555,7 @@ private function selectOptionsContentType(SelectField $field): LaravelCollection
foreach ($records as $record) {
$options[] = [
'key' => $record->getId(),
- 'value' => ComposeValueHelper::get($record, $format),
+ 'value' => ContentHelper::get($record, $format),
];
}
diff --git a/src/Twig/RelatedExtension.php b/src/Twig/RelatedExtension.php
index 083213ca3..568c2141c 100644
--- a/src/Twig/RelatedExtension.php
+++ b/src/Twig/RelatedExtension.php
@@ -9,7 +9,7 @@
use Bolt\Entity\Relation;
use Bolt\Repository\RelationRepository;
use Bolt\Storage\Query;
-use Bolt\Utils\ComposeValueHelper;
+use Bolt\Utils\ContentHelper;
use Tightenco\Collect\Support\Collection;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
@@ -151,7 +151,7 @@ public function getRelatedOptions(string $contentTypeSlug, ?string $order = null
foreach ($records as $record) {
$options[] = [
'key' => $record->getId(),
- 'value' => ComposeValueHelper::get($record, $format),
+ 'value' => ContentHelper::get($record, $format),
];
}
diff --git a/src/Utils/ComposeValueHelper.php b/src/Utils/ContentHelper.php
similarity index 63%
rename from src/Utils/ComposeValueHelper.php
rename to src/Utils/ContentHelper.php
index 3586b2342..c81165ef1 100644
--- a/src/Utils/ComposeValueHelper.php
+++ b/src/Utils/ContentHelper.php
@@ -6,15 +6,14 @@
use Bolt\Entity\Content;
use Bolt\Entity\Field\Excerptable;
-use Tightenco\Collect\Support\Collection;
-class ComposeValueHelper
+class ContentHelper
{
public static function isSuitable(Content $record, string $which = 'title_format'): bool
{
$definition = $record->getDefinition();
- if ($definition !== null && $definition->has($which)) {
+ if ($record->getId() && $definition !== null && $definition->has($which)) {
$format = $definition->get($which);
if (is_string($format) && mb_strpos($format, '{') !== false) {
return true;
@@ -45,6 +44,10 @@ function ($match) use ($record, $locale) {
return $record->getStatus();
}
+ if ($match[1] === 'author') {
+ return $record->getAuthor();
+ }
+
if ($record->hasField($match[1])) {
$field = $record->getField($match[1]);
@@ -55,10 +58,6 @@ function ($match) use ($record, $locale) {
return $field;
}
- if (array_key_exists($match[1], $record->getExtras())) {
- return $record->getExtras()[$match[1]];
- }
-
return '(unknown)';
},
$format
@@ -74,34 +73,7 @@ public static function getFieldNames(string $format): array
public static function guessTitleFields(Content $content): array
{
- $definition = $content->getDefinition();
-
- // First, see if we have a "title format" in the Content Type.
- if ($definition !== null && $definition->has('title_format')) {
- if (self::isSuitable($content)) {
- $names = self::getFieldNames($definition->get('title_format'));
- } else {
- $names = $definition->get('title_format');
- }
-
- $namesCollection = Collection::wrap($names)->filter(function (string $name) use ($content): bool {
- if ($content->hasFieldDefined($name) === false) {
- throw new \RuntimeException(sprintf(
- "Content '%s' has field '%s' added to title_format config option, but the field is not present in Content's definition.",
- $content->getContentTypeName(),
- $name
- ));
- }
-
- return $content->hasField($name);
- });
-
- if ($namesCollection->isNotEmpty()) {
- return $namesCollection->values()->toArray();
- }
- }
-
- // Alternatively, see if we have a field named 'title' or somesuch.
+ // Check if we have a field named 'title' or somesuch.
$names = ['title', 'name', 'caption', 'subject']; // English
$names = array_merge($names, ['titel', 'naam', 'kop', 'onderwerp']); // Dutch
$names = array_merge($names, ['nom', 'sujet']); // French
@@ -121,4 +93,27 @@ public static function guessTitleFields(Content $content): array
return [];
}
+
+ public static function getFieldBasedTitle(Content $content, string $locale = ''): string
+ {
+ $titleParts = [];
+
+ foreach (self::guessTitleFields($content) as $fieldName) {
+ $field = $content->getField($fieldName);
+
+ if (! empty($locale)) {
+ $field->setCurrentLocale($locale);
+ }
+
+ $value = $field->getParsedValue();
+
+ if (empty($value)) {
+ $value = $field->setLocale($field->getDefaultLocale())->getParsedValue();
+ }
+
+ $titleParts[] = $value;
+ }
+
+ return implode(' ', $titleParts);
+ }
}
diff --git a/templates/content/listing.html.twig b/templates/content/listing.html.twig
index 4f28f35d4..f6259d587 100644
--- a/templates/content/listing.html.twig
+++ b/templates/content/listing.html.twig
@@ -41,17 +41,8 @@
:csrftoken="{{ csrf_token('batch')|json_encode }}"
>
- {# Set `titleField` and `titleLabel`, but only if we have records #}
- {% if records|first %}
- {% set titleField = records|first|title_fields|first %}
- {% set titleLabel = contentType.fields[titleField].label %}
- {% else %}
- {% set titleField = '-' %}
- {% set titleLabel = '-' %}
- {% endif %}
-
{% set filterOptions = {
- 'id': "Id", (titleField): titleLabel, 'author': 'Author', 'status': 'Status', 'createdAt': 'Created date',
+ 'id': "Id", 'title': 'Title', 'author': 'Author', 'status': 'Status', 'createdAt': 'Created date',
'modifiedAt': 'Modified date', 'publishedAt': 'Published date', 'depublishedAt': 'Depublished date'
} %}
diff --git a/tests/e2e/display_record_test.feature b/tests/e2e/display_record_test.feature
index c40e6fad5..e289bbb90 100644
--- a/tests/e2e/display_record_test.feature
+++ b/tests/e2e/display_record_test.feature
@@ -1,5 +1,12 @@
Feature: Test field output
+ @javascript
+ Scenario: As a user I want to see how the record title is displayed
+ Given I am on "/test/title-of-the-test"
+ Then I wait for ".title"
+
+ And I should see "74: Title of the test" in the ".title" element
+
@javascript
Scenario: As a user I want to see how fields are escaped