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 @@ This template is for testing only! This template is for testing only! This template is for testing only! - {% 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