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
11 changes: 11 additions & 0 deletions app/modules/blog/src/Sync/ArticleSynchronizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ private function updateMetadata(Article $article, ArticleNode $entity): void {
$entity->setChangedTime($updated->getTimestamp());

$this->updateTags($article, $entity);
$this->updateCompatibility($article, $entity);
}

private function updateTags(Article $article, ArticleNode $entity): void {
Expand All @@ -149,4 +150,14 @@ private function updateTags(Article $article, ArticleNode $entity): void {
}
}

private function updateCompatibility(Article $article, ArticleNode $entity): void {
$entity->set('field_compatibility', NULL);
foreach ($article->compatibility as $item) {
$entity->get('field_compatibility')->appendItem([
'name' => $item->name,
'constraint' => $item->constraint,
]);
}
}

}
2 changes: 2 additions & 0 deletions app/modules/blog/src/Sync/Domain/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ final class Article implements \IteratorAggregate, \Countable {

/**
* @param array{}|list<string> $tags
* @param list<\Drupal\app_blog\Sync\Domain\SoftwareCompatibility> $compatibility
*/
public function __construct(
public string $id,
public string $created,
public string $updated,
public array $tags,
public string $directory,
public array $compatibility = [],
) {}

public function addTranslation(ArticleTranslation $translation): void {
Expand Down
14 changes: 14 additions & 0 deletions app/modules/blog/src/Sync/Domain/SoftwareCompatibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Drupal\app_blog\Sync\Domain;

final readonly class SoftwareCompatibility {

public function __construct(
public string $name,
public ?string $constraint = NULL,
) {}

}
24 changes: 24 additions & 0 deletions app/modules/blog/src/Sync/Parser/ArticleXmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Drupal\app_blog\Sync\Domain\Article;
use Drupal\app_blog\Sync\Domain\ArticleTranslation;
use Drupal\app_blog\Sync\Domain\SoftwareCompatibility;
use Drupal\app_blog\Sync\Exception\ArticleParseException;
use Drupal\app_blog\Sync\Exception\XmlLoadException;
use Drupal\app_blog\Sync\Exception\XmlValidationException;
Expand Down Expand Up @@ -35,12 +36,14 @@ public function parse(string $file_path): Article {

$article_node = $this->getArticleNode($xpath);
$tags = $this->parseTags($xpath);
$compatibility = $this->parseCompatibility($xpath);
$article = new Article(
id: $article_node->getAttribute('id'),
created: $article_node->getAttribute('created'),
updated: $article_node->getAttribute('updated'),
tags: $tags,
directory: \dirname($file_path),
compatibility: $compatibility,
);

foreach ($this->parseTranslations($xpath, $article) as $translation) {
Expand Down Expand Up @@ -79,6 +82,27 @@ private function parseTags(\DOMXPath $xpath): array {
return $tags;
}

/**
* @return list<\Drupal\app_blog\Sync\Domain\SoftwareCompatibility>
*/
private function parseCompatibility(\DOMXPath $xpath): array {
$software_list = $xpath->query('/article/compatibility/software');
\assert($software_list instanceof \DOMNodeList);

$compatibility = [];
foreach ($software_list as $software_node) {
\assert($software_node instanceof \DOMElement);
$compatibility[] = new SoftwareCompatibility(
name: $software_node->getAttribute('name'),
constraint: $software_node->hasAttribute('constraint')
? $software_node->getAttribute('constraint')
: NULL,
);
}

return $compatibility;
}

private function parseTranslations(\DOMXPath $xpath, Article $article): array {
$translation_list = $xpath->query('/article/translations/translation');
\assert($translation_list instanceof \DOMNodeList);
Expand Down
21 changes: 21 additions & 0 deletions app/modules/blog/tests/fixtures/article.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="softwareNameType">
<xs:restriction base="xs:string">
<xs:pattern value="[a-z0-9-]+"/>
</xs:restriction>
</xs:simpleType>

<!-- Root element -->
<xs:element name="article">
<xs:complexType>
<xs:sequence>
<xs:element ref="translations"/>
<xs:element ref="tags" minOccurs="0"/>
<xs:element ref="compatibility" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="articleIdType" use="required"/>
<xs:attribute name="created" type="xs:dateTime" use="required"/>
Expand Down Expand Up @@ -67,6 +74,20 @@
</xs:complexType>
</xs:element>

<!-- Compatibility block -->
<xs:element name="compatibility">
<xs:complexType>
<xs:sequence>
<xs:element name="software" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="name" type="softwareNameType" use="required"/>
<xs:attribute name="constraint" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>

<!-- Attachments block -->
<xs:element name="attachments">
<xs:complexType>
Expand Down
25 changes: 25 additions & 0 deletions app/modules/blog/tests/src/Kernel/Sync/ArticleSynchronizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ protected function setUp(): void {
$this->createField('field_tags', 'entity_reference', ['target_type' => 'taxonomy_term'], -1);
$this->createField('field_media_image', 'entity_reference', ['target_type' => 'media']);
$this->createField('field_media_attachments', 'entity_reference', ['target_type' => 'media'], -1);
$this->createField('field_compatibility', 'software_compatibility', [], -1);
}

#[DataProvider('sourcePathPrefixProvider')]
Expand Down Expand Up @@ -144,6 +145,25 @@ public function testUnchangedArticleSkipped(): void {
self::assertSame('Existing Title', $node->getTitle());
}

public function testCompatibilitySynced(): void {
$synchronizer = $this->buildSynchronizer();
$context = $this->createSyncContext();

$synchronizer->sync($context);

$nodes = $this->loadBlogNodes();
self::assertCount(1, $nodes);
$node = \reset($nodes);
$field = $node->get('field_compatibility');
self::assertCount(3, $field);
self::assertSame('drupal', $field->get(0)->get('name')->getValue());
self::assertSame('^10.3 || ^11', $field->get(0)->get('constraint')->getValue());
self::assertSame('php', $field->get(1)->get('name')->getValue());
self::assertSame('^8.3', $field->get(1)->get('constraint')->getValue());
self::assertSame('docker', $field->get(2)->get('name')->getValue());
self::assertNull($field->get(2)->get('constraint')->getValue());
}

public function testForcedSyncUpdatesEvenUnchanged(): void {
$existing = Node::create([
'type' => 'blog_entry',
Expand Down Expand Up @@ -271,6 +291,11 @@ private function buildArticleXml(string $source_prefix = './'): string {
<poster src="{$source_prefix}poster.png"/>
</translation>
</translations>
<compatibility>
<software name="drupal" constraint="^10.3 || ^11"/>
<software name="php" constraint="^8.3"/>
<software name="docker"/>
</compatibility>
</article>
XML;
}
Expand Down
28 changes: 28 additions & 0 deletions app/modules/blog/tests/src/Unit/Sync/Domain/ArticleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Drupal\app_blog\Sync\Domain\Article;
use Drupal\app_blog\Sync\Domain\ArticleTranslation;
use Drupal\app_blog\Sync\Domain\SoftwareCompatibility;
use Drupal\app_blog\Sync\Exception\PrimaryTranslationNotFoundException;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\Attributes\CoversClass;
Expand Down Expand Up @@ -68,6 +69,33 @@ public function testMultipleNonPrimaryTranslations(): void {
self::assertFalse($article->hasPrimaryTranslation());
}

public function testCompatibilityDefaultsToEmpty(): void {
$article = $this->createArticle();

self::assertSame([], $article->compatibility);
}

public function testCompatibilityPopulated(): void {
$compatibility = [
new SoftwareCompatibility(name: 'drupal', constraint: '^10.3 || ^11'),
new SoftwareCompatibility(name: 'php', constraint: '^8.3'),
new SoftwareCompatibility(name: 'docker'),
];
$article = new Article(
id: 'test-article',
created: '2026-01-01T00:00:00',
updated: '2026-03-01T00:00:00',
tags: [],
directory: '/content/blog/test',
compatibility: $compatibility,
);

self::assertCount(3, $article->compatibility);
self::assertSame('drupal', $article->compatibility[0]->name);
self::assertSame('^10.3 || ^11', $article->compatibility[0]->constraint);
self::assertNull($article->compatibility[2]->constraint);
}

public function testIterableOverTranslations(): void {
$article = $this->createArticle();
$ru = $this->createTranslation(language: 'ru');
Expand Down
17 changes: 17 additions & 0 deletions app/modules/platform/app_platform.icons.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
software:
label: Software
description: Icons for software and technology platforms.
extractor: svg
config:
sources:
- assets/icons/software/*.svg
template: >
<svg
{{ attributes
.setAttribute('viewBox', attributes.viewBox|default('0 0 24 24'))
.setAttribute('fill', 'currentColor')
.setAttribute('aria-hidden', 'true')
}}
>
{{ content }}
</svg>
1 change: 1 addition & 0 deletions app/modules/platform/assets/icons/software/docker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/modules/platform/assets/icons/software/drupal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/modules/platform/assets/icons/software/php.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Drupal\app_platform\Plugin\Field\FieldType;

use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;

#[FieldType(
id: self::ID,
label: new TranslatableMarkup('Software compatibility'),
description: new TranslatableMarkup('Stores software name and optional version constraint.'),
no_ui: TRUE,
)]
final class SoftwareCompatibilityItem extends FieldItemBase {

public const string ID = 'software_compatibility';

#[\Override]
public function isEmpty(): bool {
$name = $this->get('name')->getValue();

return $name === NULL || $name === '';
}

#[\Override]
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
$properties['name'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Software name'))
->setRequired(TRUE);

$properties['constraint'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Version constraint'));

return $properties;
}

#[\Override]
public static function schema(FieldStorageDefinitionInterface $field_definition): array {
return [
'columns' => [
'name' => [
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
],
'constraint' => [
'type' => 'varchar',
'length' => 255,
],
],
];
}

#[\Override]
public static function mainPropertyName(): string {
return 'name';
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ props:
tags:
title: Tags
type: [ 'null', array ]
compatibility:
title: Compatibility
type: array
default: []
next_link:
title: Next link
type: [ 'null', array ]
Expand Down
10 changes: 9 additions & 1 deletion app/themes/laszlo/components/content/article/article.twig
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@
{% block summary %}{% endblock %}
</div>

{% if tags %}
{% if tags or compatibility is not empty %}
<div class="article__tags">
{{ tags }}
{% for item in compatibility %}
{% include 'laszlo:chip' with {
label: item.label,
icon: item.name,
icon_pack: 'software',
suffix: item.constraint,
} only %}
{% endfor %}
</div>
{% endif %}

Expand Down
16 changes: 12 additions & 4 deletions app/themes/laszlo/components/ui/chip/chip.component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ group: ui
props:
type: object
required:
- href
- label
properties:
attributes:
type: Drupal\Core\Template\Attribute
title: Attributes
description: Button attributes.
href:
title: URL
type: string
type: ['null', string]
label:
title: Label
type: string
icon:
title: Icon name
type: ['null', string]
icon_pack:
title: Icon pack (defaults to laszlo)
type: string
default: laszlo
suffix:
title: Suffix text
type: ['null', string]
variant:
title: Variant
type: string
Expand All @@ -35,4 +43,4 @@ props:
type: string
enum:
- medium
default: medium
default: medium
Loading
Loading