Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions config/bolt/contenttypes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -326,6 +327,7 @@ blocks:
tests:
name: Tests
singular_name: Test
title_format: "{id}: {title}"
fields:
title:
type: text
Expand Down
6 changes: 1 addition & 5 deletions public/theme/skeleton/custom/test.twig
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@

<marquee>This template is for testing only! This template is for testing only! This template is for testing only!</marquee>

{% if record.hasField('title') %}
<h1 class="title">{{ record.title }}</h1>
{% else %}
<h1 class="heading">{{ record|title }}</h1>
{% endif %}
<h1 class="title">{{ record|title }}</h1>

<hr>

Expand Down
3 changes: 2 additions & 1 deletion src/Configuration/Parser/ContentTypesParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
77 changes: 52 additions & 25 deletions src/Storage/Directive/OrderDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Bolt\Storage\Directive;

use Bolt\Storage\QueryInterface;
use Bolt\Utils\ContentHelper;

/**
* Directive to alter query based on 'order' parameter.
Expand All @@ -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`.
*/
Expand Down Expand Up @@ -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);
}
}
49 changes: 15 additions & 34 deletions src/Twig/ContentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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),
];
}

Expand Down
4 changes: 2 additions & 2 deletions src/Twig/RelatedExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
];
}

Expand Down
65 changes: 30 additions & 35 deletions src/Utils/ComposeValueHelper.php → src/Utils/ContentHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
}
}
11 changes: 1 addition & 10 deletions templates/content/listing.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,8 @@
:csrftoken="{{ csrf_token('batch')|json_encode }}"
></listing-select-box>

{# 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'
} %}

Expand Down
7 changes: 7 additions & 0 deletions tests/e2e/display_record_test.feature
Original file line number Diff line number Diff line change
@@ -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

Expand Down