diff --git a/UPGRADE.md b/UPGRADE.md
index f3733d72b..da03c2e9d 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -214,6 +214,8 @@ New reusable templates for building back-office pages:
- RealmsAwareWebResponseInterface
- Removed obsolete `roadiz/fonts-bundle`
- Removed `getFontsFilesPath` and `getFontsFilesBasePath` methods from `RZ\Roadiz\Documents\Models\FileAwareInterface`
+- `NodesSources` now ships a **built-in `unpublishedAt`** date-time field and `unpublishedAt` becomes a **reserved node-type field name**. Projects that already declared an `unpublished_at` (as a custom node-type field, or as a project-level column) must migrate — see [Built-in `unpublishedAt` scheduled expiration field](#built-in-unpublishedat-scheduled-expiration-field).
+- `NodeTypeInterface` gained an `isUnpublishable(): bool` method. Any custom implementation must add it.
## New custom-form webhook system
@@ -223,6 +225,66 @@ New reusable templates for building back-office pages:
- Field mapping and provider settings are configured per form in the admin UI; this controls how form fields map to provider-specific fields.
- The system is idempotent per CustomFormAnswer ID and uses Messenger retry policies on failure
+## Built-in `unpublishedAt` scheduled expiration field
+
+`NodesSources` now provides a built-in, nullable `unpublishedAt` date-time column (`nodes_sources.unpublished_at`),
+symmetrical to `publishedAt`. It lets editors schedule content **expiration**: a node-source is publicly
+visible only when
+
+```
+node.status = PUBLISHED
+AND publishedAt <= now
+AND (unpublishedAt IS NULL OR unpublishedAt > now)
+```
+
+Enable it per node-type with the new `unpublishable: true` option (mirroring `publishable`). `unpublishedAt`
+defaults to `null` (*never expires*), so enabling it is backward-compatible for existing content.
+
+A core bundle migration adds the column and its indexes. It is **guarded**: if `nodes_sources.unpublished_at`
+already exists it is a no-op, so it will not clash with a column you added yourself.
+
+### If your project already has an `unpublished_at`
+
+`unpublishedAt` is now a reserved name and a built-in property, so an existing project-level `unpublished_at`
+**will collide** (a custom node-type field named `unpublished_at` would generate a duplicate `$unpublishedAt`
+property on the generated entity). You must remove the legacy definition and reconcile the schema:
+
+1. **Remove the custom field from your node-type(s).** Delete the `unpublished_at` field from every
+ `config/node_types/*.yaml` (or from the node-type definition in database), then regenerate entities:
+
+ ```bash
+ bin/console app:node-types:regenerate # or your project's node-type sync/update command
+ ```
+
+2. **Add a project migration** (`bin/console make:migration`, then adjust it) to preserve legacy data and
+ drop the legacy schema. Adapt the table name(s) to your node-type(s):
+
+ ```php
+ public function up(Schema $schema): void
+ {
+ // Copy legacy per-node-type values up into the built-in column, then drop the custom column.
+ if ($schema->hasTable('ns_article') && $schema->getTable('ns_article')->hasColumn('unpublished_at')) {
+ $this->addSql('UPDATE nodes_sources ns INNER JOIN ns_article a ON a.id = ns.id SET ns.unpublished_at = a.unpublished_at WHERE a.unpublished_at IS NOT NULL');
+ $this->addSql('ALTER TABLE ns_article DROP unpublished_at');
+ }
+
+ // If you previously added a project-level column/index on nodes_sources, drop the redundant index
+ // so it does not conflict with the built-in one created by the core bundle migration.
+ if ($schema->getTable('nodes_sources')->hasIndex('nsapp_unpublished_at')) {
+ $this->addSql('DROP INDEX nsapp_unpublished_at ON nodes_sources');
+ }
+ }
+ ```
+
+3. **Run the migrations** and verify the schema is in sync:
+
+ ```bash
+ bin/console doctrine:migrations:migrate
+ bin/console doctrine:schema:validate
+ ```
+
+If you implement `NodeTypeInterface` yourself, also add the new `isUnpublishable(): bool` method.
+
## Other changes
- Roadiz can integrate with external translation services to automatically translate Markdown fields.
diff --git a/composer.json b/composer.json
index fd8e492a9..4cccb633f 100644
--- a/composer.json
+++ b/composer.json
@@ -80,7 +80,7 @@
"rezozero/intervention-request-bundle": "^5.1",
"rezozero/liform-bundle": "^0.20.1",
"rezozero/tree-walker": "^1.7.0",
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"scheb/2fa-backup-code": "^7.5",
"scheb/2fa-bundle": "^7.5",
"scheb/2fa-google-authenticator": "^7.5",
diff --git a/config/node_types/article.yaml b/config/node_types/article.yaml
index 92737843e..c46b18568 100644
--- a/config/node_types/article.yaml
+++ b/config/node_types/article.yaml
@@ -4,6 +4,7 @@ displayName: Article
description: Article
visible: true
publishable: true
+unpublishable: true
attributable: true
sortingAttributesByWeight: false
reachable: true
@@ -40,12 +41,6 @@ fields:
- GroupBlock
- AliasBlock
type: children-nodes
- -
- name: unpublished_at
- universal: true
- indexed: true
- label: 'Date de dépublication'
- type: date-time
-
name: only_on_webresponse
serializationGroups:
diff --git a/docs/developer/nodes-system/node_types.md b/docs/developer/nodes-system/node_types.md
index 9a55f53e2..fde025433 100644
--- a/docs/developer/nodes-system/node_types.md
+++ b/docs/developer/nodes-system/node_types.md
@@ -31,8 +31,13 @@ To add a new node type, follow these steps:
visible: true
# 'publishable' is an optional boolean.
+ # When true, node-sources get a "publishedAt" date-time field (scheduled publication).
publishable: false
+ # 'unpublishable' is an optional boolean.
+ # When true, node-sources get an "unpublishedAt" date-time field (scheduled expiration).
+ unpublishable: false
+
# 'attributable' is an optional boolean.
attributable: true
diff --git a/docs/developer/nodes-system/nodes.md b/docs/developer/nodes-system/nodes.md
index 6c9069dcf..28671fef1 100644
--- a/docs/developer/nodes-system/nodes.md
+++ b/docs/developer/nodes-system/nodes.md
@@ -99,6 +99,23 @@ There are two parameters that you must take care of in your themes and your cont
For example, *publication date and time* won’t be necessary in plain text pages and non-timestampable contents. But we decided to add it directly to the `NodesSources` entity to be able to filter and order with this field in the Roadiz back office. This would not be possible if you manually created your own `publishedAt` as a node-type field.
+### Scheduled publication window (`publishedAt` / `unpublishedAt`)
+
+Node-sources expose two optional, symmetric date-time fields that define a **publication window**:
+
+- `publishedAt` — enabled when the node-type is `publishable`. Content becomes visible once `publishedAt <= now`.
+- `unpublishedAt` — enabled when the node-type is `unpublishable`. Content becomes hidden again once `unpublishedAt` is reached.
+
+A node-source is considered publicly visible (outside preview mode) when **all** of the following hold:
+
+```
+node.status = PUBLISHED
+AND publishedAt <= now
+AND (unpublishedAt IS NULL OR unpublishedAt > now)
+```
+
+`unpublishedAt` is nullable and defaults to `null`, meaning *never expires* — so enabling `unpublishable` is fully backward-compatible with existing content. Both fields are applied consistently by the node-source repository, the API Platform query extensions and `NodesSources::isPublished()`. As with `publishedAt`, preview mode bypasses the date-time gate so editors can review expired or not-yet-published content.
+
::: warning
Pay attention that *publication date and time* (`publishedAt`) and visibility (`node.visible`) **do not prevent** your node-source from being viewed if you do not explicitly forbid access to its controller. This field is not deeply set into Roadiz security mechanics.
diff --git a/lib/DocGenerator/composer.json b/lib/DocGenerator/composer.json
index 9cc86fbeb..d8e069f37 100644
--- a/lib/DocGenerator/composer.json
+++ b/lib/DocGenerator/composer.json
@@ -4,7 +4,7 @@
"type": "library",
"require": {
"php": ">=8.3",
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"symfony/translation": "7.4.*",
"symfony/http-foundation": "7.4.*"
},
diff --git a/lib/DtsGenerator/composer.json b/lib/DtsGenerator/composer.json
index d25b38711..a2b594ffa 100644
--- a/lib/DtsGenerator/composer.json
+++ b/lib/DtsGenerator/composer.json
@@ -3,7 +3,7 @@
"description": "Roadiz sub-package which generates Typescript interfaces skeleton based on your schema",
"type": "library",
"require": {
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"symfony/http-foundation": "7.4.*"
},
"require-dev": {
diff --git a/lib/EntityGenerator/composer.json b/lib/EntityGenerator/composer.json
index 2df32fa3a..c029ef2b7 100644
--- a/lib/EntityGenerator/composer.json
+++ b/lib/EntityGenerator/composer.json
@@ -15,7 +15,7 @@
"php": ">=8.3",
"ext-json": "*",
"nette/php-generator": "^4.1",
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"symfony/string": "7.4.*",
"symfony/yaml": "7.4.*",
"symfony/serializer": "7.4.*",
diff --git a/lib/EntityGenerator/src/EntityGenerator.php b/lib/EntityGenerator/src/EntityGenerator.php
index 57d343075..c7859a2ee 100644
--- a/lib/EntityGenerator/src/EntityGenerator.php
+++ b/lib/EntityGenerator/src/EntityGenerator.php
@@ -323,6 +323,14 @@ private function addClassMethods(ClassType $classType): self
->setBody('return '.($this->nodeType->isPublishable() ? 'true' : 'false').';')
;
+ $classType->addMethod('isUnpublishable')
+ ->addComment('$this->nodeType->isUnpublishable() proxy.')
+ ->addComment('@return bool Does this nodeSource is unpublishable with date and time?')
+ ->addAttribute(\Override::class)
+ ->setReturnType('bool')
+ ->setBody('return '.($this->nodeType->isUnpublishable() ? 'true' : 'false').';')
+ ;
+
$classType->addMethod('__toString')
->setReturnType('string')
->addAttribute(\Override::class)
diff --git a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSources/NSMock.php b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSources/NSMock.php
index 43aa2626f..83833ff25 100644
--- a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSources/NSMock.php
+++ b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSources/NSMock.php
@@ -1260,6 +1260,16 @@ public function isPublishable(): bool
return true;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return true;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithDocumentDto/NSMock.php b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithDocumentDto/NSMock.php
index 39befbf4d..7cf05b80d 100644
--- a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithDocumentDto/NSMock.php
+++ b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithDocumentDto/NSMock.php
@@ -121,6 +121,16 @@ public function isPublishable(): bool
return true;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return true;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithRepository/NSMock.php b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithRepository/NSMock.php
index 9e7cb5097..66815627b 100644
--- a/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithRepository/NSMock.php
+++ b/lib/EntityGenerator/tests/Mocks/GeneratedNodesSourcesWithRepository/NSMock.php
@@ -1260,6 +1260,16 @@ public function isPublishable(): bool
return true;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return true;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/lib/EntityGenerator/tests/NodeTypeAwareTestTrait.php b/lib/EntityGenerator/tests/NodeTypeAwareTestTrait.php
index a574024cd..76fb948ae 100644
--- a/lib/EntityGenerator/tests/NodeTypeAwareTestTrait.php
+++ b/lib/EntityGenerator/tests/NodeTypeAwareTestTrait.php
@@ -292,6 +292,9 @@ classname: Themes\MyTheme\Entities\PositionedCity
$mockNodeType
->method('isPublishable')
->willReturn(true);
+ $mockNodeType
+ ->method('isUnpublishable')
+ ->willReturn(true);
return $mockNodeType;
}
@@ -329,6 +332,9 @@ protected function getMockDocumentNodeType(): NodeTypeInterface
$mockNodeType
->method('isPublishable')
->willReturn(true);
+ $mockNodeType
+ ->method('isUnpublishable')
+ ->willReturn(true);
return $mockNodeType;
}
diff --git a/lib/Models/composer.json b/lib/Models/composer.json
index 9b284736e..34614d92e 100644
--- a/lib/Models/composer.json
+++ b/lib/Models/composer.json
@@ -22,7 +22,7 @@
"doctrine/orm": "^3.6",
"api-platform/doctrine-orm": "^4.1.18",
"api-platform/metadata": "^4.1.18",
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"symfony/string": "7.4.*",
"symfony/translation-contracts": "^3.0",
"symfony/http-foundation": "7.4.*",
diff --git a/lib/RoadizCoreBundle/composer.json b/lib/RoadizCoreBundle/composer.json
index d38908264..3726bcfb2 100644
--- a/lib/RoadizCoreBundle/composer.json
+++ b/lib/RoadizCoreBundle/composer.json
@@ -47,7 +47,7 @@
"roadiz/jwt": "2.8.x-dev",
"roadiz/markdown": "2.8.x-dev",
"roadiz/models": "2.8.x-dev",
- "roadiz/nodetype-contracts": "^3.1.1",
+ "roadiz/nodetype-contracts": "^4.0.0",
"roadiz/random": "2.8.x-dev",
"scienta/doctrine-json-functions": "^6.0",
"symfony-cmf/routing-bundle": "^3.1.0",
diff --git a/lib/RoadizCoreBundle/migrations/Version20260713120000.php b/lib/RoadizCoreBundle/migrations/Version20260713120000.php
new file mode 100644
index 000000000..a722268bd
--- /dev/null
+++ b/lib/RoadizCoreBundle/migrations/Version20260713120000.php
@@ -0,0 +1,65 @@
+ now OR unpublished_at IS NULL).
+ *
+ * BC NOTE: this migration is guarded so it is a no-op on projects that already own an
+ * unpublished_at column on nodes_sources (e.g. added by a project-level migration or by a
+ * custom node-type field). Projects that previously declared a custom "unpublished_at"
+ * node-type field must remove that field from their node-type definitions, regenerate their
+ * entities and add a project migration copying the legacy per-type values into
+ * nodes_sources.unpublished_at before dropping the old per-type column.
+ */
+final class Version20260713120000 extends AbstractMigration
+{
+ public function getDescription(): string
+ {
+ return 'Add built-in unpublished_at field and indexes on nodes_sources for scheduled content expiration.';
+ }
+
+ public function up(Schema $schema): void
+ {
+ $table = $schema->getTable('nodes_sources');
+
+ if (!$table->hasColumn('unpublished_at')) {
+ $this->addSql('ALTER TABLE nodes_sources ADD unpublished_at DATETIME DEFAULT NULL');
+ }
+ if ($table->hasIndex('nsapp_unpublished_at')) {
+ $this->addSql('DROP INDEX nsapp_unpublished_at ON nodes_sources');
+ }
+ $this->addSql('CREATE INDEX ns_unpublished_at ON nodes_sources (unpublished_at)');
+
+ if (!$table->hasIndex('ns_node_translation_unpublished')) {
+ $this->addSql('CREATE INDEX ns_node_translation_unpublished ON nodes_sources (node_id, translation_id, unpublished_at)');
+ }
+ if (!$table->hasIndex('ns_node_discr_translation_unpublished')) {
+ $this->addSql('CREATE INDEX ns_node_discr_translation_unpublished ON nodes_sources (node_id, discr, translation_id, unpublished_at)');
+ }
+ if (!$table->hasIndex('ns_discr_translation_unpublished')) {
+ $this->addSql('CREATE INDEX ns_discr_translation_unpublished ON nodes_sources (discr, translation_id, unpublished_at)');
+ }
+ if (!$table->hasIndex('ns_title_translation_unpublished')) {
+ $this->addSql('CREATE INDEX ns_title_translation_unpublished ON nodes_sources (title, translation_id, unpublished_at)');
+ }
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX ns_unpublished_at ON nodes_sources');
+ $this->addSql('DROP INDEX ns_node_translation_unpublished ON nodes_sources');
+ $this->addSql('DROP INDEX ns_node_discr_translation_unpublished ON nodes_sources');
+ $this->addSql('DROP INDEX ns_discr_translation_unpublished ON nodes_sources');
+ $this->addSql('DROP INDEX ns_title_translation_unpublished ON nodes_sources');
+ $this->addSql('ALTER TABLE nodes_sources DROP unpublished_at');
+ }
+}
diff --git a/lib/RoadizCoreBundle/src/Api/Extension/NodeQueryExtension.php b/lib/RoadizCoreBundle/src/Api/Extension/NodeQueryExtension.php
index f6d7d0dba..05bd420cc 100644
--- a/lib/RoadizCoreBundle/src/Api/Extension/NodeQueryExtension.php
+++ b/lib/RoadizCoreBundle/src/Api/Extension/NodeQueryExtension.php
@@ -65,8 +65,13 @@ private function apply(
);
$queryBuilder
->andWhere($queryBuilder->expr()->lte($alias.'.publishedAt', ':lte_published_at'))
+ ->andWhere($queryBuilder->expr()->orX(
+ $queryBuilder->expr()->gt($alias.'.unpublishedAt', ':gt_unpublished_at'),
+ $queryBuilder->expr()->isNull($alias.'.unpublishedAt')
+ ))
->andWhere($queryBuilder->expr()->eq('o.status', ':status'))
->setParameter(':lte_published_at', new \DateTime())
+ ->setParameter(':gt_unpublished_at', new \DateTime())
->setParameter(':status', NodeStatus::PUBLISHED);
}
diff --git a/lib/RoadizCoreBundle/src/Api/Extension/NodesSourcesQueryExtension.php b/lib/RoadizCoreBundle/src/Api/Extension/NodesSourcesQueryExtension.php
index d7bf69287..6e63def55 100644
--- a/lib/RoadizCoreBundle/src/Api/Extension/NodesSourcesQueryExtension.php
+++ b/lib/RoadizCoreBundle/src/Api/Extension/NodesSourcesQueryExtension.php
@@ -88,8 +88,13 @@ private function apply(
$queryBuilder
->andWhere($queryBuilder->expr()->lte('o.publishedAt', ':lte_published_at'))
+ ->andWhere($queryBuilder->expr()->orX(
+ $queryBuilder->expr()->gt('o.unpublishedAt', ':gt_unpublished_at'),
+ $queryBuilder->expr()->isNull('o.unpublishedAt')
+ ))
->andWhere($queryBuilder->expr()->eq($alias.'.status', ':status'))
->setParameter(':lte_published_at', new \DateTime())
+ ->setParameter(':gt_unpublished_at', new \DateTime())
->setParameter(':status', NodeStatus::PUBLISHED);
}
}
diff --git a/lib/RoadizCoreBundle/src/Api/Filter/NodesTagsFilter.php b/lib/RoadizCoreBundle/src/Api/Filter/NodesTagsFilter.php
index 78365e121..c6237464d 100644
--- a/lib/RoadizCoreBundle/src/Api/Filter/NodesTagsFilter.php
+++ b/lib/RoadizCoreBundle/src/Api/Filter/NodesTagsFilter.php
@@ -134,9 +134,14 @@ private function alterQueryBuilder(QueryBuilder $queryBuilder, array $parameters
$ntgQb
->innerJoin('n.nodeSources', 'ns')
->andWhere($ntgQb->expr()->lte('ns.publishedAt', ':lte_published_at'))
+ ->andWhere($ntgQb->expr()->orX(
+ $ntgQb->expr()->gt('ns.unpublishedAt', ':gt_unpublished_at'),
+ $ntgQb->expr()->isNull('ns.unpublishedAt')
+ ))
->andWhere($ntgQb->expr()->eq('n.status', ':status'));
$queryBuilder
->setParameter(':lte_published_at', new \DateTime())
+ ->setParameter(':gt_unpublished_at', new \DateTime())
->setParameter(':status', NodeStatus::PUBLISHED);
}
diff --git a/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php b/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php
index c570197e0..5d1e94815 100644
--- a/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php
+++ b/lib/RoadizCoreBundle/src/Doctrine/DBAL/Types/ArrayType.php
@@ -13,7 +13,6 @@
*/
final class ArrayType extends JsonType
{
- #[\Override]
public function getName(): string
{
return 'array';
diff --git a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/NodesSourcesInheritanceSubscriber.php b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/NodesSourcesInheritanceSubscriber.php
index 6bb96d362..2c2371d97 100644
--- a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/NodesSourcesInheritanceSubscriber.php
+++ b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/NodesSourcesInheritanceSubscriber.php
@@ -74,15 +74,20 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void
['columns' => ['discr']],
['columns' => ['title']],
['columns' => ['published_at']],
+ 'ns_unpublished_at' => ['columns' => ['unpublished_at']],
'ns_no_index' => ['columns' => ['no_index']],
'ns_node_translation_published' => ['columns' => ['node_id', 'translation_id', 'published_at']],
+ 'ns_node_translation_unpublished' => ['columns' => ['node_id', 'translation_id', 'unpublished_at']],
'ns_node_discr_translation' => ['columns' => ['node_id', 'discr', 'translation_id']],
'ns_node_discr_translation_published' => ['columns' => ['node_id', 'discr', 'translation_id', 'published_at']],
+ 'ns_node_discr_translation_unpublished' => ['columns' => ['node_id', 'discr', 'translation_id', 'unpublished_at']],
'ns_translation_published' => ['columns' => ['translation_id', 'published_at']],
'ns_discr_translation' => ['columns' => ['discr', 'translation_id']],
'ns_discr_translation_published' => ['columns' => ['discr', 'translation_id', 'published_at']],
+ 'ns_discr_translation_unpublished' => ['columns' => ['discr', 'translation_id', 'unpublished_at']],
'ns_title_published' => ['columns' => ['title', 'published_at']],
'ns_title_translation_published' => ['columns' => ['title', 'translation_id', 'published_at']],
+ 'ns_title_translation_unpublished' => ['columns' => ['title', 'translation_id', 'unpublished_at']],
],
'uniqueConstraints' => [
['columns' => ['node_id', 'translation_id']],
diff --git a/lib/RoadizCoreBundle/src/Entity/NodeType.php b/lib/RoadizCoreBundle/src/Entity/NodeType.php
index 39e08e1ba..0f79b73e6 100644
--- a/lib/RoadizCoreBundle/src/Entity/NodeType.php
+++ b/lib/RoadizCoreBundle/src/Entity/NodeType.php
@@ -40,6 +40,8 @@ final class NodeType implements NodeTypeInterface, \Stringable
private bool $visible = true;
#[SymfonySerializer\Groups(['node_type', 'node_type:import']),]
private bool $publishable = false;
+ #[SymfonySerializer\Groups(['node_type', 'node_type:import']),]
+ private bool $unpublishable = false;
/**
* @var bool define if this node-type produces nodes that will have attributes
@@ -154,6 +156,19 @@ public function setPublishable(bool $publishable): NodeType
return $this;
}
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return $this->unpublishable;
+ }
+
+ public function setUnpublishable(bool $unpublishable): NodeType
+ {
+ $this->unpublishable = $unpublishable;
+
+ return $this;
+ }
+
public function getReachable(): bool
{
return $this->reachable;
diff --git a/lib/RoadizCoreBundle/src/Entity/NodesSources.php b/lib/RoadizCoreBundle/src/Entity/NodesSources.php
index 317cab1fc..9edae6762 100644
--- a/lib/RoadizCoreBundle/src/Entity/NodesSources.php
+++ b/lib/RoadizCoreBundle/src/Entity/NodesSources.php
@@ -37,15 +37,20 @@
ORM\Index(columns: ['discr']),
ORM\Index(columns: ['title']),
ORM\Index(columns: ['published_at']),
+ ORM\Index(columns: ['unpublished_at'], name: 'ns_unpublished_at'),
ORM\Index(columns: ['no_index'], name: 'ns_no_index'),
ORM\Index(columns: ['node_id', 'translation_id', 'published_at'], name: 'ns_node_translation_published'),
+ ORM\Index(columns: ['node_id', 'translation_id', 'unpublished_at'], name: 'ns_node_translation_unpublished'),
ORM\Index(columns: ['node_id', 'discr', 'translation_id'], name: 'ns_node_discr_translation'),
ORM\Index(columns: ['node_id', 'discr', 'translation_id', 'published_at'], name: 'ns_node_discr_translation_published'),
+ ORM\Index(columns: ['node_id', 'discr', 'translation_id', 'unpublished_at'], name: 'ns_node_discr_translation_unpublished'),
ORM\Index(columns: ['translation_id', 'published_at'], name: 'ns_translation_published'),
ORM\Index(columns: ['discr', 'translation_id'], name: 'ns_discr_translation'),
ORM\Index(columns: ['discr', 'translation_id', 'published_at'], name: 'ns_discr_translation_published'),
+ ORM\Index(columns: ['discr', 'translation_id', 'unpublished_at'], name: 'ns_discr_translation_unpublished'),
ORM\Index(columns: ['title', 'published_at'], name: 'ns_title_published'),
ORM\Index(columns: ['title', 'translation_id', 'published_at'], name: 'ns_title_translation_published'),
+ ORM\Index(columns: ['title', 'translation_id', 'unpublished_at'], name: 'ns_title_translation_unpublished'),
ORM\UniqueConstraint(columns: ['node_id', 'translation_id']),
ORM\InheritanceType('JOINED'),
// Limit discriminator column to 30 characters for indexing optimization
@@ -94,6 +99,16 @@ class NodesSources implements PersistableInterface, Loggable, \Stringable
)]
protected ?\DateTime $publishedAt = null;
+ #[ApiFilter(BaseFilter\DateFilter::class)]
+ #[ApiFilter(BaseFilter\OrderFilter::class)]
+ #[ORM\Column(name: 'unpublished_at', type: 'datetime', unique: false, nullable: true)]
+ #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base'])]
+ #[Gedmo\Versioned]
+ #[ApiProperty(
+ description: 'Content unpublication date and time',
+ )]
+ protected ?\DateTime $unpublishedAt = null;
+
#[ApiFilter(BaseFilter\SearchFilter::class, strategy: 'partial')]
#[ORM\Column(name: 'meta_title', type: 'string', length: 150, unique: false)]
#[SymfonySerializer\Groups(['nodes_sources'])]
@@ -386,6 +401,18 @@ public function setPublishedAt(?\DateTime $publishedAt = null): NodesSources
return $this;
}
+ public function getUnpublishedAt(): ?\DateTime
+ {
+ return $this->unpublishedAt;
+ }
+
+ public function setUnpublishedAt(?\DateTime $unpublishedAt = null): NodesSources
+ {
+ $this->unpublishedAt = $unpublishedAt;
+
+ return $this;
+ }
+
/**
* Final on purpose: like getMetaDescription(), this raw getter is bound to
* the admin SEO form and its setter round-trip, so it must never become a
@@ -559,9 +586,12 @@ public function getNodeTypeColor(): string
#[SymfonySerializer\Groups(['nodes_sources_published'])]
public function isPublished(): bool
{
+ $now = new \DateTime();
+
return $this->getNode()->isPublished()
&& null !== $this->getPublishedAt()
- && $this->getPublishedAt() <= new \DateTime();
+ && $this->getPublishedAt() <= $now
+ && (null === $this->getUnpublishedAt() || $this->getUnpublishedAt() > $now);
}
/**
@@ -572,6 +602,14 @@ public function isPublishable(): bool
throw new \RuntimeException('This method should only be called from children classes');
}
+ /**
+ * Overridden in NS classes.
+ */
+ public function isUnpublishable(): bool
+ {
+ throw new \RuntimeException('This method should only be called from children classes');
+ }
+
/**
* Overridden in NS classes.
*/
diff --git a/lib/RoadizCoreBundle/src/Form/Constraint/NodeSourceReservedName.php b/lib/RoadizCoreBundle/src/Form/Constraint/NodeSourceReservedName.php
index 86e9221b7..ec9dfb516 100644
--- a/lib/RoadizCoreBundle/src/Form/Constraint/NodeSourceReservedName.php
+++ b/lib/RoadizCoreBundle/src/Form/Constraint/NodeSourceReservedName.php
@@ -51,6 +51,9 @@ final class NodeSourceReservedName extends Constraint
'publishable',
'published',
'publishedAt',
+ 'unpublishable',
+ 'unpublished',
+ 'unpublishedAt',
'reachable',
'redirections',
'shareImage',
diff --git a/lib/RoadizCoreBundle/src/Model/NodeTreeDto.php b/lib/RoadizCoreBundle/src/Model/NodeTreeDto.php
index bda4a0f94..1f44f33f4 100644
--- a/lib/RoadizCoreBundle/src/Model/NodeTreeDto.php
+++ b/lib/RoadizCoreBundle/src/Model/NodeTreeDto.php
@@ -32,11 +32,13 @@ public function __construct(
?int $sourceId,
?string $title,
?\DateTime $publishedAt,
+ ?\DateTime $unpublishedAt = null,
) {
$this->nodeSource = new NodesSourcesTreeDto(
$sourceId,
$title,
$publishedAt,
+ $unpublishedAt,
);
}
diff --git a/lib/RoadizCoreBundle/src/Model/NodeTypeTreeDto.php b/lib/RoadizCoreBundle/src/Model/NodeTypeTreeDto.php
index f2765a6b9..b67fb5969 100644
--- a/lib/RoadizCoreBundle/src/Model/NodeTypeTreeDto.php
+++ b/lib/RoadizCoreBundle/src/Model/NodeTypeTreeDto.php
@@ -13,6 +13,7 @@
public function __construct(
private string $name,
private bool $publishable,
+ private bool $unpublishable,
private bool $reachable,
private string $displayName,
private string $color,
@@ -33,6 +34,12 @@ public function isPublishable(): bool
return $this->publishable;
}
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return $this->unpublishable;
+ }
+
public function getDisplayName(): string
{
return $this->displayName;
diff --git a/lib/RoadizCoreBundle/src/Model/NodesSourcesTreeDto.php b/lib/RoadizCoreBundle/src/Model/NodesSourcesTreeDto.php
index 452e928a2..7a089891b 100644
--- a/lib/RoadizCoreBundle/src/Model/NodesSourcesTreeDto.php
+++ b/lib/RoadizCoreBundle/src/Model/NodesSourcesTreeDto.php
@@ -12,6 +12,7 @@ public function __construct(
private ?int $id,
private ?string $title,
private ?\DateTime $publishedAt,
+ private ?\DateTime $unpublishedAt = null,
) {
}
@@ -30,4 +31,9 @@ public function getPublishedAt(): ?\DateTime
{
return $this->publishedAt;
}
+
+ public function getUnpublishedAt(): ?\DateTime
+ {
+ return $this->unpublishedAt;
+ }
}
diff --git a/lib/RoadizCoreBundle/src/NodeType/Configuration/NodeTypeConfiguration.php b/lib/RoadizCoreBundle/src/NodeType/Configuration/NodeTypeConfiguration.php
index abac9e7f9..7cadd37d8 100644
--- a/lib/RoadizCoreBundle/src/NodeType/Configuration/NodeTypeConfiguration.php
+++ b/lib/RoadizCoreBundle/src/NodeType/Configuration/NodeTypeConfiguration.php
@@ -33,6 +33,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('description')->end()
->booleanNode('visible')->defaultTrue()->end()
->booleanNode('publishable')->defaultFalse()->end()
+ ->booleanNode('unpublishable')->defaultFalse()->end()
->booleanNode('attributable')->defaultFalse()->end()
->booleanNode('searchable')->defaultTrue()->end()
->booleanNode('sortingAttributesByWeight')->defaultFalse()->end()
diff --git a/lib/RoadizCoreBundle/src/Repository/NodeRepository.php b/lib/RoadizCoreBundle/src/Repository/NodeRepository.php
index 9bc725e6b..fbab5a2cb 100644
--- a/lib/RoadizCoreBundle/src/Repository/NodeRepository.php
+++ b/lib/RoadizCoreBundle/src/Repository/NodeRepository.php
@@ -387,7 +387,8 @@ protected function alterQueryBuilderAsNodeTreeDto(QueryBuilder $qb, string $alia
%s.nodeTypeName,
%s.id,
%s.title,
- %s.publishedAt
+ %s.publishedAt,
+ %s.unpublishedAt
)
EOT,
NodeTreeDto::class,
@@ -405,6 +406,7 @@ protected function alterQueryBuilderAsNodeTreeDto(QueryBuilder $qb, string $alia
self::NODESSOURCES_ALIAS,
self::NODESSOURCES_ALIAS,
self::NODESSOURCES_ALIAS,
+ self::NODESSOURCES_ALIAS,
));
return $qb;
diff --git a/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php b/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php
index e5f5183aa..191035b17 100644
--- a/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php
+++ b/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php
@@ -226,6 +226,12 @@ public function alterQueryBuilderWithAuthorizationChecker(
* Forbid unpublished node for anonymous and not backend users.
*/
$qb->andWhere($qb->expr()->lte($prefix.'.publishedAt', ':now'));
+ $qb->andWhere(
+ $qb->expr()->orX(
+ $qb->expr()->gt($prefix.'.unpublishedAt', ':now'),
+ $qb->expr()->isNull($prefix.'.unpublishedAt')
+ )
+ );
$qb->andWhere($qb->expr()->eq(static::NODE_ALIAS.'.status', ':node_status'));
$qb->setParameter('node_status', NodeStatus::PUBLISHED);
$qb->setParameter('now', new \DateTime('now'));
diff --git a/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceBaseType.php b/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceBaseType.php
index b939ab4aa..1d5809ad7 100644
--- a/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceBaseType.php
+++ b/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceBaseType.php
@@ -48,6 +48,21 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
],
]);
}
+
+ if (true === $options['unpublishable']) {
+ $builder->add('unpublishedAt', DateTimeType::class, [
+ 'label' => 'unpublishedAt',
+ 'required' => false,
+ 'attr' => [
+ 'data-dev-name' => '{{ nodeSource.'.StringHandler::camelCase('unpublishedAt').' }}',
+ ],
+ 'html5' => true,
+ 'placeholder' => [
+ 'hour' => 'hour',
+ 'minute' => 'minute',
+ ],
+ ]);
+ }
}
#[\Override]
@@ -63,6 +78,7 @@ public function configureOptions(OptionsResolver $resolver): void
'label' => false,
'inherit_data' => true,
'publishable' => false,
+ 'unpublishable' => false,
'attr' => [
'no-field-group' => true,
],
diff --git a/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceType.php b/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceType.php
index 15cac5d05..a4487bbae 100644
--- a/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceType.php
+++ b/lib/RoadizRozierBundle/src/Form/NodeSource/NodeSourceType.php
@@ -62,6 +62,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
if (true === $options['withTitle']) {
$builder->add('base', NodeSourceBaseType::class, [
'publishable' => $options['nodeType']->isPublishable(),
+ 'unpublishable' => $options['nodeType']->isUnpublishable(),
'translation' => $builder->getData()->getTranslation(),
]);
}
diff --git a/lib/RoadizRozierBundle/translations/rozier/messages.en.xlf b/lib/RoadizRozierBundle/translations/rozier/messages.en.xlf
index ed006e42f..1347164b6 100644
--- a/lib/RoadizRozierBundle/translations/rozier/messages.en.xlf
+++ b/lib/RoadizRozierBundle/translations/rozier/messages.en.xlf
@@ -4403,6 +4403,16 @@
Date and time publishable
Field label for node-type publishable status.
+
+ unpublishedAt
+ Unpublication date and time
+ Field label for node-source scheduled expiration date and time.
+
+
+ unpublishable
+ Date and time unpublishable
+ Field label for node-type unpublishable status.
+
this_node_type_will_be_available_for_creating_root_nodes
diff --git a/lib/RoadizRozierBundle/translations/rozier/messages.fr.xlf b/lib/RoadizRozierBundle/translations/rozier/messages.fr.xlf
index 47cef9474..e3ecf86c7 100644
--- a/lib/RoadizRozierBundle/translations/rozier/messages.fr.xlf
+++ b/lib/RoadizRozierBundle/translations/rozier/messages.fr.xlf
@@ -4403,6 +4403,16 @@
Publiable par date et heure
Field label for node-type publishable status.
+
+ unpublishedAt
+ Date et heure de dépublication
+ Field label for node-source scheduled expiration date and time.
+
+
+ unpublishable
+ Dépubliable par date et heure
+ Field label for node-type unpublishable status.
+
this_node_type_will_be_available_for_creating_root_nodes
diff --git a/lib/RoadizSolrBundle/src/EventListener/DefaultNodesSourcesIndexingSubscriber.php b/lib/RoadizSolrBundle/src/EventListener/DefaultNodesSourcesIndexingSubscriber.php
index 45c9f12bb..553262805 100644
--- a/lib/RoadizSolrBundle/src/EventListener/DefaultNodesSourcesIndexingSubscriber.php
+++ b/lib/RoadizSolrBundle/src/EventListener/DefaultNodesSourcesIndexingSubscriber.php
@@ -74,6 +74,10 @@ public function onIndexing(NodesSourcesIndexingEvent $event): void
$assoc['published_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getPublishedAt());
}
+ if (null !== $nodeSource->getUnpublishedAt()) {
+ $assoc['unpublished_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getUnpublishedAt());
+ }
+
if ($this->canIndexTitleInCollection($nodeSource)) {
$collection[] = $title;
}
diff --git a/lib/RoadizSolrBundle/src/NodeSourceSearchHandler.php b/lib/RoadizSolrBundle/src/NodeSourceSearchHandler.php
index 4b3eccaca..d0bbb04df 100644
--- a/lib/RoadizSolrBundle/src/NodeSourceSearchHandler.php
+++ b/lib/RoadizSolrBundle/src/NodeSourceSearchHandler.php
@@ -192,6 +192,45 @@ protected function argFqProcess(array &$args): array
$args['fq'][] = $tmp;
}
+ /*
+ * Handle unpublication (expiration) date-time filtering
+ */
+ $hasExplicitUnpublishedAtFilter = isset($args['unpublishedAt']);
+ if (isset($args['unpublishedAt'])) {
+ $tmp = 'unpublished_at_dt:';
+ if (!is_array($args['unpublishedAt']) && $args['unpublishedAt'] instanceof \DateTimeInterface) {
+ $tmp .= $this->formatDateTimeToUTC($args['unpublishedAt']);
+ } elseif (
+ isset($args['unpublishedAt'][0])
+ && 'BETWEEN' === $args['unpublishedAt'][0]
+ && isset($args['unpublishedAt'][1])
+ && $args['unpublishedAt'][1] instanceof \DateTimeInterface
+ && isset($args['unpublishedAt'][2])
+ && $args['unpublishedAt'][2] instanceof \DateTimeInterface
+ ) {
+ $tmp .= '['.
+ $this->formatDateTimeToUTC($args['unpublishedAt'][1]).
+ ' TO '.
+ $this->formatDateTimeToUTC($args['unpublishedAt'][2]).']';
+ } elseif (
+ isset($args['unpublishedAt'][0])
+ && '<=' === $args['unpublishedAt'][0]
+ && isset($args['unpublishedAt'][1])
+ && $args['unpublishedAt'][1] instanceof \DateTimeInterface
+ ) {
+ $tmp .= '[* TO '.$this->formatDateTimeToUTC($args['unpublishedAt'][1]).']';
+ } elseif (
+ isset($args['unpublishedAt'][0])
+ && '>=' === $args['unpublishedAt'][0]
+ && isset($args['unpublishedAt'][1])
+ && $args['unpublishedAt'][1] instanceof \DateTimeInterface
+ ) {
+ $tmp .= '['.$this->formatDateTimeToUTC($args['unpublishedAt'][1]).' TO *]';
+ }
+ unset($args['unpublishedAt']);
+ $args['fq'][] = $tmp;
+ }
+
$status = $args['status'] ?? $args['node.status'] ?? null;
if (isset($status)) {
$tmp = 'node_status_i:';
@@ -217,6 +256,13 @@ protected function argFqProcess(array &$args): array
if (!$hasExplicitPublishedAtFilter) {
$args['fq'][] = 'published_at_dt:[* TO NOW/MINUTE]';
}
+ if (!$hasExplicitUnpublishedAtFilter) {
+ /*
+ * Exclude expired content: keep documents whose unpublished_at_dt is in the
+ * future or missing (unpublishedAt IS NULL OR unpublishedAt > now).
+ */
+ $args['fq'][] = '(*:* -unpublished_at_dt:[* TO NOW/MINUTE])';
+ }
}
/*
diff --git a/lib/Rozier/app/assets/less/widgets/nestable.less b/lib/Rozier/app/assets/less/widgets/nestable.less
index ff3af38d5..2b535dc27 100644
--- a/lib/Rozier/app/assets/less/widgets/nestable.less
+++ b/lib/Rozier/app/assets/less/widgets/nestable.less
@@ -376,6 +376,44 @@
}
}
}
+
+ &.datetime-unpublishable {
+ .non-nestable-handle:before,
+ .uk-nestable-handle:before {
+ z-index: 1;
+ }
+ .nodetree-unpublished-at {
+ margin-left: 0;
+ left: -20px;
+ top: -1px;
+ position: relative;
+ z-index: 0;
+ border-radius: 15px 0 0 15px;
+ background-color: #b8b8b8;
+ color: #fff;
+ margin-right: -15px;
+ line-height: 1;
+ border: 0 none;
+ padding: 4px 7px 4px 25px;
+ min-width: 50px;
+ text-align: right;
+ text-shadow: none;
+ letter-spacing: 0.02em;
+
+ i {
+ display: none;
+ }
+ }
+
+ &.datetime-unpublishable-past {
+ .nodetree-unpublished-at {
+ background-color: #ca8776;
+ i {
+ display: inline-block;
+ }
+ }
+ }
+ }
}
.root-tree,
diff --git a/src/GeneratedEntity/NSAliasBlock.php b/src/GeneratedEntity/NSAliasBlock.php
index 3cc1a32b3..061e93842 100644
--- a/src/GeneratedEntity/NSAliasBlock.php
+++ b/src/GeneratedEntity/NSAliasBlock.php
@@ -111,6 +111,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSArticle.php b/src/GeneratedEntity/NSArticle.php
index f3b5889db..74b26ca25 100644
--- a/src/GeneratedEntity/NSArticle.php
+++ b/src/GeneratedEntity/NSArticle.php
@@ -30,7 +30,6 @@
#[Gedmo\Loggable(logEntryClass: UserLogEntry::class)]
#[ORM\Entity(repositoryClass: Repository\NSArticleRepository::class)]
#[ORM\Table(name: 'ns_article')]
-#[ORM\Index(columns: ['unpublished_at'])]
#[ApiFilter(PropertyFilter::class)]
class NSArticle extends NodesSources
{
@@ -75,17 +74,6 @@ class NSArticle extends NodesSources
#[ORM\Column(name: 'realm_a_secret', type: 'string', nullable: true, length: 250)]
private ?string $realmASecret = null;
- /** Date de dépublication. */
- #[Serializer\SerializedName(serializedName: 'unpublishedAt')]
- #[Serializer\Groups(['nodes_sources', 'nodes_sources_default'])]
- #[ApiProperty(description: 'Date de dépublication')]
- #[Serializer\MaxDepth(2)]
- #[ApiFilter(Filter\OrderFilter::class)]
- #[ApiFilter(Filter\DateFilter::class)]
- #[Gedmo\Versioned]
- #[ORM\Column(name: 'unpublished_at', type: 'datetime', nullable: true)]
- private ?\DateTime $unpublishedAt = null;
-
/** Only on web response. */
#[Serializer\SerializedName(serializedName: 'onlyOnWebresponse')]
#[Serializer\Groups(['article_get_by_path'])]
@@ -215,23 +203,6 @@ public function setRealmASecret(?string $realmASecret): static
return $this;
}
- /**
- * @return \DateTime|null
- */
- public function getUnpublishedAt(): ?\DateTime
- {
- return $this->unpublishedAt;
- }
-
- /**
- * @return $this
- */
- public function setUnpublishedAt(?\DateTime $unpublishedAt): static
- {
- $this->unpublishedAt = $unpublishedAt;
- return $this;
- }
-
/**
* @return string|null
*/
@@ -318,6 +289,16 @@ public function isPublishable(): bool
return true;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return true;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSArticleContainer.php b/src/GeneratedEntity/NSArticleContainer.php
index 938629e59..5bfa898dd 100644
--- a/src/GeneratedEntity/NSArticleContainer.php
+++ b/src/GeneratedEntity/NSArticleContainer.php
@@ -68,6 +68,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSArticleFeedBlock.php b/src/GeneratedEntity/NSArticleFeedBlock.php
index cdfa52888..6b72eecae 100644
--- a/src/GeneratedEntity/NSArticleFeedBlock.php
+++ b/src/GeneratedEntity/NSArticleFeedBlock.php
@@ -96,6 +96,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSBasicBlock.php b/src/GeneratedEntity/NSBasicBlock.php
index 16995489a..aca1be1de 100644
--- a/src/GeneratedEntity/NSBasicBlock.php
+++ b/src/GeneratedEntity/NSBasicBlock.php
@@ -291,6 +291,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSGroupBlock.php b/src/GeneratedEntity/NSGroupBlock.php
index 0d0b4292e..c2ad5c1e8 100644
--- a/src/GeneratedEntity/NSGroupBlock.php
+++ b/src/GeneratedEntity/NSGroupBlock.php
@@ -68,6 +68,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSMenu.php b/src/GeneratedEntity/NSMenu.php
index 083aff4a6..9dd4e294b 100644
--- a/src/GeneratedEntity/NSMenu.php
+++ b/src/GeneratedEntity/NSMenu.php
@@ -68,6 +68,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSMenuLink.php b/src/GeneratedEntity/NSMenuLink.php
index ab5014e50..056090f42 100644
--- a/src/GeneratedEntity/NSMenuLink.php
+++ b/src/GeneratedEntity/NSMenuLink.php
@@ -202,6 +202,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSNeutral.php b/src/GeneratedEntity/NSNeutral.php
index a40dbb108..ed03c3fa5 100644
--- a/src/GeneratedEntity/NSNeutral.php
+++ b/src/GeneratedEntity/NSNeutral.php
@@ -101,6 +101,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSOffer.php b/src/GeneratedEntity/NSOffer.php
index bb1ffe1b0..e901627f1 100644
--- a/src/GeneratedEntity/NSOffer.php
+++ b/src/GeneratedEntity/NSOffer.php
@@ -216,6 +216,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{
diff --git a/src/GeneratedEntity/NSPage.php b/src/GeneratedEntity/NSPage.php
index c11dd54a2..337956db4 100644
--- a/src/GeneratedEntity/NSPage.php
+++ b/src/GeneratedEntity/NSPage.php
@@ -971,6 +971,16 @@ public function isPublishable(): bool
return false;
}
+ /**
+ * $this->nodeType->isUnpublishable() proxy.
+ * @return bool Does this nodeSource is unpublishable with date and time?
+ */
+ #[\Override]
+ public function isUnpublishable(): bool
+ {
+ return false;
+ }
+
#[\Override]
public function __toString(): string
{