Repository: fgtclb/academic-extensions
Pull request #74: [!!!][TASK] Add active state filtering to `acdemic_projects`
Author: login: dot3media
Labels: name: needs-tests
Created: 2025-04-02T09:26:51Z | Merged: 2025-04-09T11:41:55Z

* Add activeState property to project demand
* Add processing for activeState property to project demand factory
* Optimize plugin settings in FlexForm
* Remove now obsolete `hideCompletedProjects` setting
* Add upgrade wizard to migrate the `hideCompletedProjects` to `activeState = active` in plugin settings
* Optimize upgrade wizard to migrate plugins from `list_type`to `CType`
* Update language files for backend and frontend
* Add partial to select the `activeState` in the frontend
* Add proper labelling for sorting select options
* Optimize templates and partials


Diff:
diff --git a/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ActiveState.php b/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ActiveState.php
new file mode 100644
index 00000000..10fe2f94
--- /dev/null
+++ b/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ActiveState.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Domain\Model\Dto;
+
+enum ActiveState: string
+{
+    case ALL = 'all';
+    case ACTIVE = 'active';
+    case COMPLETED = 'completed';
+
+    public static function default(): self
+    {
+        return self::ALL;
+    }
+
+    public static function tryFromDefault(string $value): self
+    {
+        return self::tryFrom($value) ?? self::ALL;
+    }
+
+    /**
+     * @return string[]
+     */
+    public static function values(): array
+    {
+        $values = [];
+        foreach (self::cases() as $case) {
+            $values[] = $case->value;
+        }
+        return $values;
+    }
+}
diff --git a/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php b/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
index 75c81c19..1cf1c74b 100644
--- a/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
+++ b/packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
@@ -11,18 +11,12 @@ class ProjectDemand
 {
     /** @var int[] */
     protected array $pages = [];
-
     protected bool $showSelected = false;
-
-    protected bool $hideCompletedProjects = false;
-
-    protected ?FilterCollection $filterCollection = null;
-
     protected string $sorting = '';
-
     protected string $sortingField = '';
-
     protected string $sortingDirection = '';
+    protected string $activeState = 'all';
+    protected ?FilterCollection $filterCollection = null;
 
     public function __construct()
     {
@@ -55,14 +49,32 @@ public function getShowSelected(): bool
         return $this->showSelected;
     }
 
-    public function setHideCompletedProjects(bool $hideCompletedProjects): void
+    public function setActiveState(string $activeState): void
     {
-        $this->hideCompletedProjects = $hideCompletedProjects;
+        if (ActiveState::tryFrom($activeState) === null) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    'Invalid $activeState value given, possible vales are "%s". Provided: %s',
+                    implode(', ', ActiveState::values()),
+                    $activeState,
+                ),
+                1743627158
+            );
+        }
+        $this->activeState = $activeState;
     }
 
-    public function getHideCompletedProjects(): bool
+    public function getActiveState(): string
+    {
+        return $this->activeState;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getPossibleActiveStates(): array
     {
-        return $this->hideCompletedProjects;
+        return ActiveState::values();
     }
 
     public function setFilterCollection(?FilterCollection $filterCollection): void
@@ -77,9 +89,12 @@ public function getFilterCollection(): ?FilterCollection
 
     public function setSorting(string $sorting): void
     {
-        if (in_array($sorting, SortingOptions::getConstants())) {
+        if (in_array($sorting, SortingOptions::getConstants(), true)) {
             $this->sorting = $sorting;
-            [$this->sortingField, $this->sortingDirection] = explode(' ', $sorting);
+            [
+                $this->sortingField,
+                $this->sortingDirection,
+            ] = array_pad(explode(' ', $sorting, 2), 2, '');
         }
     }
 
@@ -90,8 +105,7 @@ public function getSorting(): string
 
     public function setSortingField(string $sortingField): void
     {
-        $newSorting = $sortingField . ' ' . $this->sortingDirection;
-        $this->setSorting($newSorting);
+        $this->setSorting($sortingField . ' ' . $this->sortingDirection);
     }
 
     public function getSortingField(): string
@@ -101,8 +115,7 @@ public function getSortingField(): string
 
     public function setSortingDirection(string $sortingDirection): void
     {
-        $newSorting = $this->sortingField . ' ' . $sortingDirection;
-        $this->setSorting($newSorting);
+        $this->setSorting($this->sortingField . ' ' . $sortingDirection);
     }
 
     public function getSortingDirection(): string
diff --git a/packages/fgtclb/academic-projects/Classes/Domain/Repository/ProjectRepository.php b/packages/fgtclb/academic-projects/Classes/Domain/Repository/ProjectRepository.php
index 0a4391ee..0a88d6d5 100644
--- a/packages/fgtclb/academic-projects/Classes/Domain/Repository/ProjectRepository.php
+++ b/packages/fgtclb/academic-projects/Classes/Domain/Repository/ProjectRepository.php
@@ -4,6 +4,7 @@
 
 namespace FGTCLB\AcademicProjects\Domain\Repository;
 
+use FGTCLB\AcademicProjects\Domain\Model\Dto\ActiveState;
 use FGTCLB\AcademicProjects\Domain\Model\Dto\ProjectDemand;
 use FGTCLB\AcademicProjects\Domain\Model\Project;
 use FGTCLB\AcademicProjects\Enumeration\PageTypes;
@@ -43,7 +44,8 @@ public function findByDemand(ProjectDemand $demand): QueryResult
             }
         }
 
-        if ($demand->getHideCompletedProjects() === true) {
+        $activeState = ActiveState::tryFromDefault($demand->getActiveState());
+        if ($activeState === ActiveState::ACTIVE) {
             $constraints[] = $query->logicalOr(
                 ...array_values(
                     [
@@ -53,6 +55,18 @@ public function findByDemand(ProjectDemand $demand): QueryResult
                 )
             );
         }
+
+        if ($activeState === ActiveState::COMPLETED) {
+            $constraints[] = $query->logicalAnd(
+                ...array_values(
+                    [
+                        $query->greaterThan('txAcademicprojectsEndDate', 0),
+                        $query->lessThan('txAcademicprojectsEndDate', new \DateTime()),
+                    ]
+                )
+            );
+        }
+
         $query->matching(
             $query->logicalAnd(...array_values($constraints))
         );
diff --git a/packages/fgtclb/academic-projects/Classes/Enumeration/SortingOptions.php b/packages/fgtclb/academic-projects/Classes/Enumeration/SortingOptions.php
index 75cf5fa9..32c04c9e 100644
--- a/packages/fgtclb/academic-projects/Classes/Enumeration/SortingOptions.php
+++ b/packages/fgtclb/academic-projects/Classes/Enumeration/SortingOptions.php
@@ -6,27 +6,20 @@
 
 use TYPO3\CMS\Core\Type\Enumeration;
 
+/**
+ * @todo Should be migrated into a native PHP Enum, TYPO3 deprecated `Enumeration` in TYPO3 v13.
+ */
 final class SortingOptions extends Enumeration
 {
     public const __default = self::SORT_BY_TITLE_ASC;
-
     public const SORT_BY_TITLE_ASC = 'title asc';
-
     public const SORT_BY_TITLE_DESC = 'title desc';
-
     public const SORT_BY_LASTUPDATED_ASC = 'lastUpdated asc';
-
     public const SORT_BY_LASTUPDATED_DESC = 'lastUpdated desc';
-
     public const SORT_BY_SORTING_ASC = 'sorting asc';
-
     public const SORT_BY_SORTING_DESC = 'sorting desc';
-
     public const SORT_BY_BUDGET_ASC = 'tx_academicprojects_budget asc';
-
     public const SORT_BY_BUDGET_DESC = 'tx_academicprojects_budget desc';
-
     public const SORT_BY_STARTDATE_ASC = 'tx_academicprojects_start_date asc';
-
     public const SORT_BY_STARTDATE_desc = 'tx_academicprojects_start_date desc';
 }
diff --git a/packages/fgtclb/academic-projects/Classes/Factory/DemandFactory.php b/packages/fgtclb/academic-projects/Classes/Factory/DemandFactory.php
index 500c4878..9d5d8d50 100644
--- a/packages/fgtclb/academic-projects/Classes/Factory/DemandFactory.php
+++ b/packages/fgtclb/academic-projects/Classes/Factory/DemandFactory.php
@@ -4,6 +4,7 @@
 
 namespace FGTCLB\AcademicProjects\Factory;
 
+use FGTCLB\AcademicProjects\Domain\Model\Dto\ActiveState;
 use FGTCLB\AcademicProjects\Domain\Model\Dto\ProjectDemand;
 use FGTCLB\CategoryTypes\Collection\FilterCollection;
 use FGTCLB\CategoryTypes\Domain\Repository\CategoryRepository;
@@ -30,6 +31,9 @@ public function createDemandObject(
 
         // Init demand properties with plugin settings, which can be overwritten by the form
         if ($demandFromForm === null) {
+            if (isset($settings['activeState'])) {
+                $demand->setActiveState(ActiveState::tryFromDefault((string)($settings['activeState']))->value);
+            }
             if (isset($settings['sorting'])) {
                 $demand->setSorting($settings['sorting']);
             }
@@ -40,19 +44,20 @@ public function createDemandObject(
             }
         } else {
             // Set demand properties, if form data is available
+            if (isset($demandFromForm['activeState'])) {
+                $demand->setActiveState(ActiveState::tryFromDefault($demandFromForm['activeState'])->value);
+            }
             if (isset($demandFromForm['sorting'])) {
                 $demand->setSorting($demandFromForm['sorting']);
             }
-
             if (isset($demandFromForm['sortingField'])) {
                 $demand->setSortingField($demandFromForm['sortingField']);
             }
-
             if (isset($demandFromForm['sortingDirection'])) {
                 $demand->setSortingDirection($demandFromForm['sortingDirection']);
             }
-
             if (isset($demandFromForm['filterCollection'])) {
+                // @todo Does this really make sense combined with the intExplode() ?
                 $categoryUids = [];
                 foreach ($demandFromForm['filterCollection'] as $uids) {
                     $categoryUids = array_merge($categoryUids, GeneralUtility::intExplode(',', $uids));
@@ -78,9 +83,7 @@ public function createDemandObject(
         $demand->setShowSelected(
             $contentElementData['list_type'] === 'academicprojects_projectlistsingle'
         );
-        if (isset($settings['hide_completed_projects'])) {
-            $demand->setHideCompletedProjects((bool)$settings['hide_completed_projects']);
-        }
+
         return $demand;
     }
 }
diff --git a/packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php b/packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
new file mode 100644
index 00000000..21a0992f
--- /dev/null
+++ b/packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
@@ -0,0 +1,146 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use FGTCLB\AcademicProjects\Domain\Model\Dto\ActiveState;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'settings.hideCompletedProjects' => 'settings.activeState',
+        'settings.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
+        $queryBuilder->getRestrictions()->removeAll();
+        $resultSet = $queryBuilder
+            ->select('*')
+            ->from('tt_content')
+            ->where(
+                $queryBuilder->expr()->in(
+                    'CType',
+                    $queryBuilder->quoteArrayBasedValueListToStringList(self::MIGRATE_CONTENT_TYPES_LIST)
+                ),
+            )
+            ->executeQuery();
+        while ($record = $resultSet->fetchAssociative()) {
+            /** @var array<string, mixed> $record */
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+            if (!is_array($flexFormData)) {
+                continue;
+            }
+
+            foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                    $oldValue = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                    $newValue = match ($newName) {
+                        'settings.activeState' => ($oldValue === '1') ? ActiveState::ACTIVE->value : ActiveState::ALL->value,
+                        default => $oldValue,
+                    };
+                    $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $newValue;
+                    unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                }
+            }
+
+            $updateQueryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
+            $updateQueryBuilder->update('tt_content')
+                ->set('pi_flexform', $this->array2xml($flexFormData))
+                ->where(
+                    $queryBuilder->expr()->in(
+                        'uid',
+                        $queryBuilder->createNamedParameter($record['uid'], Connection::PARAM_INT)
+                    )
+                )
+                ->executeStatement();
+        }
+
+        return true;
+    }
+
+    public function updateNecessary(): bool
+    {
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
+        $queryBuilder->getRestrictions()->removeAll();
+        return $queryBuilder
+            ->select('*')
+            ->from('tt_content')
+            ->where(
+                $queryBuilder->expr()->in(
+                    'CType',
+                    $queryBuilder->quoteArrayBasedValueListToStringList(self::MIGRATE_CONTENT_TYPES_LIST)
+                ),
+            )
+            ->setMaxResults(1)
+            ->executeQuery()
+            ->fetchAllAssociative() !== [];
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getPrerequisites(): array
+    {
+        return [
+            DatabaseUpdatedPrerequisite::class,
+            PluginUpgradeWizard::class,
+        ];
+    }
+
+    /**
+     * @param string[] $input
+     */
+    protected function array2xml(array $input = []): string
+    {
+        $options = [
+            'parentTagMap' => [
+                'data' => 'sheet',
+                'sheet' => 'language',
+                'language' => 'field',
+                'el' => 'field',
+                'field' => 'value',
+                'field:el' => 'el',
+                'el:_IS_NUM' => 'section',
+                'section' => 'itemType',
+            ],
+            'disableTypeAttrib' => 2,
+        ];
+        $spaceInd = 4;
+        $output = GeneralUtility::array2xml($input, '', 0, 'T3FlexForms', $spaceInd, $options);
+        return '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . $output;
+    }
+}
diff --git a/packages/fgtclb/academic-projects/Classes/Upgrades/PluginUpgradeWizard.php b/packages/fgtclb/academic-projects/Classes/Upgrades/PluginUpgradeWizard.php
index 827541f4..7e14bf86 100644
--- a/packages/fgtclb/academic-projects/Classes/Upgrades/PluginUpgradeWizard.php
+++ b/packages/fgtclb/academic-projects/Classes/Upgrades/PluginUpgradeWizard.php
@@ -33,18 +33,15 @@ public function getDescription(): string
 
     public function executeUpdate(): bool
     {
-        $connection = $this->connectionPool->getConnectionForTable('tt_content');
         foreach (self::MIGRATE_CONTENT_TYPES_LIST as $contentType) {
-            $connection->update(
-                'tt_content',
-                [ // set
-                    'CType' => $contentType,
-                ],
-                [ // where
-                    'CType' => 'list',
-                    'list_type' => $contentType,
-                ]
-            );
+            $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
+            $queryBuilder->getRestrictions()->removeAll();
+            $queryBuilder->update('tt_content')
+                ->set('CType', $contentType)
+                ->where(
+                    $queryBuilder->expr()->eq('CType', 'list'),
+                    $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter($contentType))
+                )->executeStatement();
         }
 
         return true;
diff --git a/packages/fgtclb/academic-projects/Configuration/FlexForms/ProjectSettings.xml b/packages/fgtclb/academic-projects/Configuration/FlexForms/ProjectSettings.xml
index eafd3a03..8c9910d1 100644
--- a/packages/fgtclb/academic-projects/Configuration/FlexForms/ProjectSettings.xml
+++ b/packages/fgtclb/academic-projects/Configuration/FlexForms/ProjectSettings.xml
@@ -3,8 +3,8 @@
         <sheetTitle>Filter</sheetTitle>
         <type>array</type>
         <el>
-            <settings.hide_completed_projects>
-                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.filter.options.hide_completed_projects</label>
+            <settings.hideActiveState>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.hideActiveState.label</label>
                 <config>
                     <type>check</type>
                     <renderType>checkboxToggle</renderType>
@@ -17,9 +17,30 @@
                         </numIndex>
                     </items>
                 </config>
-            </settings.hide_completed_projects>
-            <settings.filter.options>
-                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.filter.options.title</label>
+            </settings.hideActiveState>
+            <settings.activeState>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.activeState.label</label>
+                <config>
+                    <type>select</type>
+                    <renderType>selectSingle</renderType>
+                    <items>
+                        <numIndex index="0">
+                            <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.activeState.all</label>
+                            <value>all</value>
+                        </numIndex>
+                        <numIndex index="1">
+                            <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.activeState.active</label>
+                            <value>active</value>
+                        </numIndex>
+                        <numIndex index="2">
+                            <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.activeState.completed</label>
+                            <value>completed</value>
+                        </numIndex>
+                    </items>
+                </config>
+            </settings.activeState>
+            <settings.hideFilter>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.hideFilter.label</label>
                 <config>
                     <type>check</type>
                     <renderType>checkboxToggle</renderType>
@@ -32,9 +53,9 @@
                         </numIndex>
                     </items>
                 </config>
-            </settings.filter.options>
-            <settings.sorting.options>
-                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.sorting.options.title</label>
+            </settings.hideFilter>
+            <settings.hideSorting>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.hideSorting.label</label>
                 <config>
                     <type>check</type>
                     <renderType>checkboxToggle</renderType>
@@ -47,9 +68,9 @@
                         </numIndex>
                     </items>
                 </config>
-            </settings.sorting.options>
+            </settings.hideSorting>
             <settings.sorting>
-                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.sorting</label>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.sorting.label</label>
                 <config>
                     <type>select</type>
                     <renderType>selectSingle</renderType>
@@ -57,8 +78,8 @@
                 </config>
             </settings.sorting>
             <settings.categories>
-                <label>LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.categories</label>
-                <description>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.category.description</description>
+                <label>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.categories.label</label>
+                <description>LLL:EXT:academic_projects/Resources/Private/Language/locallang_be.xlf:flexform.categories.description</description>
                 <config>
                     <type>category</type>
                     <default>0</default>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang.xlf b/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang.xlf
index 92a00dff..04e71a34 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang.xlf
+++ b/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang.xlf
@@ -35,8 +35,31 @@
                 <target>Mittelgebende</target>
             </trans-unit>
 
+            <!-- Active state -->
+
+            <trans-unit id="activeState.label">
+                <source>Active state</source>
+                <target>Aktivitätsstatus</target>
+            </trans-unit>
+            <trans-unit id="activeState.all">
+                <source>all</source>
+                <target>alle</target>
+            </trans-unit>
+            <trans-unit id="activeState.active">
+                <source>active</source>
+                <target>aktiv</target>
+            </trans-unit>
+            <trans-unit id="activeState.completed">
+                <source>completed</source>
+                <target>abgeschlossen</target>
+            </trans-unit>
+
             <!-- Sorting direction options -->
 
+            <trans-unit id="sorting.sortingDirection.label">
+                <source>Sorting field</source>
+                <target>Sortierungsfeld</target>
+            </trans-unit>
             <trans-unit id="sorting.direction.asc">
                 <source>ascending</source>
                 <target>aufsteigend</target>
@@ -48,6 +71,10 @@
 
             <!-- Sorting field options -->
 
+            <trans-unit id="sorting.sortingField.label">
+                <source>Sorting direction</source>
+                <target>Sortierungsrichtung</target>
+            </trans-unit>
             <trans-unit id="sorting.field.lastUpdated">
                 <source>Last updated</source>
                 <target>Zuletzt aktualisiert</target>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang_be.xlf b/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang_be.xlf
index a3840e07..22f51c27 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang_be.xlf
+++ b/packages/fgtclb/academic-projects/Resources/Private/Language/de.locallang_be.xlf
@@ -24,19 +24,43 @@
                 <source>Configuration</source>
                 <target>Konfiguration</target>
             </trans-unit>
-            <trans-unit id="flexform.category.description">
+            <trans-unit id="flexform.hideActiveState.label">
+                <source>Show active state options</source>
+                <target>Aktivstatus-Optionen anzeigen</target>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.label">
+                <source>Active state</source>
+                <target>Aktivstatus</target>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.all">
+                <source>All</source>
+                <target>Alle</target>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.active">
+                <source>Active</source>
+                <target>Aktiv</target>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.completed">
+                <source>Completed</source>
+                <target>Abgeschlossen</target>
+            </trans-unit>
+            <trans-unit id="flexform.categories.label">
+                <source>Categories</source>
+                <target>Kategorien</target>
+            </trans-unit>
+            <trans-unit id="flexform.categories.description">
                 <source>Select categories. Main categories will select all subcategories by OR, multiple categories selects by AND</source>
                 <target>Wählen Sie Kategorien aus. Hauptkategorien wählen alle Unterkategorien durch ODER aus, mehrere Kategorien wählen durch UND aus</target>
             </trans-unit>
-            <trans-unit id="flexform.filter.options.title">
+            <trans-unit id="flexform.hideFilter.label">
                 <source>Show filter options</source>
                 <target>Filteroptionen anzeigen</target>
             </trans-unit>
-            <trans-unit id="flexform.filter.options.hide_completed_projects">
-                <source>Show completed projects</source>
-                <target>Abgeschlossene Projekte anzeigen</target>
+            <trans-unit id="flexform.hideSorting.label">
+                <source>Show sorting options</source>
+                <target>Sortieroptionen anzeigen</target>
             </trans-unit>
-            <trans-unit id="flexform.sorting">
+            <trans-unit id="flexform.sorting.label">
                 <source>Sorting</source>
                 <target>Sortierung</target>
             </trans-unit>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Language/locallang.xlf b/packages/fgtclb/academic-projects/Resources/Private/Language/locallang.xlf
index 97c246c9..835cfd57 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Language/locallang.xlf
+++ b/packages/fgtclb/academic-projects/Resources/Private/Language/locallang.xlf
@@ -28,8 +28,26 @@
                 <source>Funding partner</source>
             </trans-unit>
 
+            <!-- Active state -->
+
+            <trans-unit id="activeState.label">
+                <source>Active state</source>
+            </trans-unit>
+            <trans-unit id="activeState.all">
+                <source>all</source>
+            </trans-unit>
+            <trans-unit id="activeState.active">
+                <source>active</source>
+            </trans-unit>
+            <trans-unit id="activeState.completed">
+                <source>completed</source>
+            </trans-unit>
+
             <!-- Sorting direction options -->
 
+            <trans-unit id="sorting.sortingDirection.label">
+                <source>Sorting field</source>
+            </trans-unit>
             <trans-unit id="sorting.direction.asc">
                 <source>ascending</source>
             </trans-unit>
@@ -39,6 +57,9 @@
 
             <!-- Sorting field options -->
 
+            <trans-unit id="sorting.sortingField.label">
+                <source>Sorting direction</source>
+            </trans-unit>
             <trans-unit id="sorting.field.lastUpdated">
                 <source>Last updated</source>
             </trans-unit>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Language/locallang_be.xlf b/packages/fgtclb/academic-projects/Resources/Private/Language/locallang_be.xlf
index 184e5653..aeb7bf7e 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Language/locallang_be.xlf
+++ b/packages/fgtclb/academic-projects/Resources/Private/Language/locallang_be.xlf
@@ -20,16 +20,34 @@
             <trans-unit id="element.tab.configuration">
                 <source>Configuration</source>
             </trans-unit>
-            <trans-unit id="flexform.category.description">
+            <trans-unit id="flexform.hideActiveState.label">
+                <source>Show active state options</source>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.label">
+                <source>Active state</source>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.all">
+                <source>All</source>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.active">
+                <source>Active</source>
+            </trans-unit>
+            <trans-unit id="flexform.activeState.completed">
+                <source>Completed</source>
+            </trans-unit>
+            <trans-unit id="flexform.categories.label">
+                <source>Categories</source>
+            </trans-unit>
+            <trans-unit id="flexform.categories.description">
                 <source>Select categories. Main categories will select all subcategories by OR, multiple categories selects by AND</source>
             </trans-unit>
-            <trans-unit id="flexform.filter.options.title">
+            <trans-unit id="flexform.hideFilter.label">
                 <source>Show filter options</source>
             </trans-unit>
-            <trans-unit id="flexform.filter.options.hide_completed_projects">
-                <source>Show completed projects</source>
+            <trans-unit id="flexform.hideSorting.label">
+                <source>Show sorting options</source>
             </trans-unit>
-            <trans-unit id="flexform.sorting">
+            <trans-unit id="flexform.sorting.label">
                 <source>Sorting</source>
             </trans-unit>
             <trans-unit id="flexform.sorting.lastUpdated.asc">
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandActiveState.html b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandActiveState.html
new file mode 100644
index 00000000..f58edcf6
--- /dev/null
+++ b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandActiveState.html
@@ -0,0 +1,39 @@
+<html
+    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+    data-namespace-typo3-fluid="true"
+>
+    <f:variable name="activeStateOptions" value="{demand.possibleActiveStates}" />
+    <label for="activeState">
+        {f:translate(
+            key: 'activeState.label',
+            extensionName: 'academic_projects'
+        )}
+    </label>
+    <f:form.select
+        id="activeState"
+        property="activeState"
+        value="{demand.activeState}"
+        additionalAttributes="{'onchange': 'this.form.submit()'}"
+    >
+        <f:for each="{activeStateOptions}" as="option">
+            <f:if condition="{option} == {demand.activeState}">
+                <f:then>
+                    <option value="{option}" selected>
+                        {f:translate(
+                            key: 'activeState.{option}',
+                            extensionName: 'academic_projects'
+                        )}
+                    </option>
+                </f:then>
+                <f:else>
+                    <option value="{option}">
+                        {f:translate(
+                            key: 'activeState.{option}',
+                            extensionName: 'academic_projects'
+                        )}
+                    </option>
+                </f:else>
+            </f:if>
+        </f:for>
+    </f:form.select>
+</html>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandSorting.html b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandSorting.html
index 95ca4ddc..dca671d5 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandSorting.html
+++ b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandSorting.html
@@ -1,24 +1,34 @@
 <html
-    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
-    xmlns:ace="http://typo3.org/ns/FGTCLB/AcademicProjects/ViewHelpers"
+    xmlns:ap="http://typo3.org/ns/FGTCLB/AcademicPrograms/ViewHelpers"
     data-namespace-typo3-fluid="true"
 >
-    <label for="sortingField">Sortierfeld</label>
-    <ace:form.sortingSelect
+    <label for="sortingField">
+        {f:translate(
+            key: 'sorting.sortingField.label',
+            extensionName: 'academic_projects'
+        )}
+    </label>
+    <ap:form.sortingSelect
         id="sortingField"
         property="sortingField"
         value="{demand.sortingField}"
         type="fields"
         additionalAttributes="{'onchange': 'this.form.submit()'}"
-        l10n="filter.sortingField" 
+        l10n="filter.sortingField"
     />
-    <label for="sortingDirection">Sortierreihenfolge</label>
-    <ace:form.sortingSelect
+
+    <label for="sortingDirection">
+        {f:translate(
+            key: 'sorting.sortingDirection.label',
+            extensionName: 'academic_projects'
+        )}
+    </label>
+    <ap:form.sortingSelect
         id="sortingDirection"
         property="sortingDirection"
         value="{demand.sortingDirection}"
         type="directions"
         additionalAttributes="{'onchange': 'this.form.submit()'}"
-        l10n="filter.sortingDirection" 
+        l10n="filter.sortingDirection"
     />
-</html>
\ No newline at end of file
+</html>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/Items.html b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/Items.html
index 71cb2ef0..5a00489f 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/Items.html
+++ b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/Items.html
@@ -5,18 +5,16 @@
 >
     <f:if condition="{projects}">
         <f:then>
-            <div class="project-list">
+            <div class="ace-list">
                 <f:for
                     each="{projects}"
                     as="project"
                     iteration="iterator"
                 >
-                    <div class="project-item">
-                        <f:render
-                            partial="Project/SingleItem"
-                            arguments="{_all}"
-                        />
-                    </div>
+                    <f:render
+                        partial="Project/SingleItem"
+                        arguments="{_all}"
+                    />
                 </f:for>
             </div>
         </f:then>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/SingleItem.html b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/SingleItem.html
index 11cb61f1..88d53b78 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/SingleItem.html
+++ b/packages/fgtclb/academic-projects/Resources/Private/Partials/Project/SingleItem.html
@@ -3,14 +3,14 @@
     xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
     data-namespace-typo3-fluid="true"
 >
-    <div class="project-item">
+    <div class="ace-item">
         <f:if condition="{project.projectTitle}">
-            <h3 class="project-item__title">
+            <h3 class="ace-item__title">
                 {project.projectTitle}
             </h3>
         </f:if>
         <f:if condition="{project.shortDescription}">
-            <div class="project-item__description">
+            <div class="ace-item__description">
                 <f:format.html>{project.shortDescription}</f:format.html>
             </div>
         </f:if>
@@ -49,8 +49,10 @@ <h3 class="project-item__title">
                 </div>
             </f:for>
         </f:if>
-        <f:link.page pageUid="{project.uid}" class="project-item__link">
-            {f:translate(key: 'fe.button.project.details.text')}
+        <f:link.page pageUid="{project.uid}" class="ace-item__button">
+            <button>
+                Details
+            </button>
         </f:link.page>
     </div>
 </html>
diff --git a/packages/fgtclb/academic-projects/Resources/Private/Templates/Project/List.html b/packages/fgtclb/academic-projects/Resources/Private/Templates/Project/List.html
index 4a08f2d6..c9b6a602 100644
--- a/packages/fgtclb/academic-projects/Resources/Private/Templates/Project/List.html
+++ b/packages/fgtclb/academic-projects/Resources/Private/Templates/Project/List.html
@@ -10,7 +10,7 @@ <h1>{data.title}</h1>
         </header>
     </f:section>
     <f:section name="Main">
-        <f:if condition="{settings.sorting.options}==false || {settings.filter.options}==false">
+        <f:if condition="{settings.hideSorting}==false || {settings.hideActiveState}==false || {settings.hideFilter}==false">
             <div class="project-filter">
                 <f:form
                     action="list"
@@ -19,13 +19,19 @@ <h1>{data.title}</h1>
                     object="{demand}"
                 >
                     <div class="row">
-                        <f:if condition="{settings.sorting.options}==false">
+                        <f:if condition="{settings.hideSorting}==false">
                             <f:render
                                 partial="Project/DemandSorting"
                                 arguments="{_all}"
                             />
                         </f:if>
-                        <f:if condition="{settings.filter.options}==false">
+                        <f:if condition="{settings.hideActiveState}==false">
+                            <f:render
+                                partial="Project/DemandActiveState"
+                                arguments="{_all}"
+                            />
+                        </f:if>
+                        <f:if condition="{settings.hideFilter}==false">
                             <f:render
                                 partial="Project/DemandCategories"
                                 arguments="{_all}"
diff --git a/packages/fgtclb/academic-projects/UPGRADE.md b/packages/fgtclb/academic-projects/UPGRADE.md
index 20f6e906..28ad34e1 100644
--- a/packages/fgtclb/academic-projects/UPGRADE.md
+++ b/packages/fgtclb/academic-projects/UPGRADE.md
@@ -1,6 +1,74 @@
 # Upgrade 2.0
 
-## Switch to `EXT:category_types` 2.0
+## BREAKING: Removed methods from `\FGTCLB\AcademicProjects\Domain\Model\Dto\ProjectDemand`
+
+Following class methods has been removed:
+
+* `ProfileDemand::getHideCompletedProjects()`
+* `ProfileDemand::setHideCompletedProjects()`
+
+Custom usage should usually not exit in consuming projects, but in some case it may be possible that
+the `ProjectsRepository` has been used for custom project code implementation or plugins, using the
+demand based method to retrieve data. In these cases, the new methods:
+
+* `ProfileDemand::setActiveState(string $activeState): void`
+* `ProfileDemand::getActiveState(): string`
+
+can be used to adopt for corresponding code places. Take care of changed value types, as the new
+setter expects string of valid PHP Enum `\FGTCLB\AcademicProjects\Domain\Model\Dto\ActiveState`
+cases and the getter returns a string.
+
+## BREAKING: Changed plugin FlexForm option and values
+
+FlexForm options of following plugins has changed:
+
+* `academicprojects_projectlist`
+* `academicprojects_projectlistsingle`
+
+Changed options:
+
+* `settings.hideCompletedProjects` => `settings.activeState`
+* `settings.filter.options` => `settings.hideFilter`
+* `settings.sorting.options` => `settings.hideSorting`
+
+In case of `settings.activeState` that also includes supported values for the field, which also matches possible
+values of newly added PHP Enum `\FGTCLB\AcademicProjects\Domain\Model\Dto\ActiveState`:
+
+| flexform  | Enum                   | old value            |
+|-----------|------------------------|----------------------|
+| all       | ActiveState::ALL       | any value except `1` |
+| active    | ActiveState::ACTIVE    | 1                    |
+| completed | ActiveState::COMPLETED | no old value         |
+
+> [!NOTE]
+> An TYPO3 UpgradeWizard `academicProjects_flexFormUpgradeWizard` is provided to migrate old
+> plugin FlexForm options and values to the new settings and values, in case of `activeState`
+> also transforming the value based on mapping mentioned in the table above.
+
+> [!IMPORTANT]
+> The upgrade wizard should be executed after the plugin `list_type` to `CType` upgrade wizard
+> has been executed and before lowlevel flexform data cleanup (v13) has been executed.
+
+> [!WARNING]
+> Note that renamed option has an impact in the fluid templates, and projects overriding the
+> default shipped templates and partials requires to adopt changes in the overridden templates.
+
+## BREAKING: Plugin changed from `list_type` to `CType`
+
+TYPO3 v13 deprecated the `tt_content` sub-type feature, only used for `CType=list` sub-typing also known
+as `list_type` and mostly used based on old times for extbase based plugins. It has been possible since
+the very beginning to register Extbase Plugins directly as `CType` instead of `CType=list` sub-type, which
+has now done for `2.0.0`.
+
+Technically this is a breaking change, and instances upgrading from `1.x` version of the plugin needs to
+update corresponding `tt_content` records in the database and eventually adopt addition, adjustments or
+overrides requiring to use the correct CType.
+
+> [!NOTE]
+> An TYPO3 UpgradeWizard `academicProjects_pluginUpgradeWizard` is provided to migrate
+> plugins from `CType=list` to dedicated `CTypes` matching the new registration.
+
+## BREAKING: Switch to `EXT:category_types 2.0`
 
 Category type based handling has been streamlined and centralized within the `EXT:category_types` extension
 and the known implementation based on deprecated TYPO3 Enumeration has been replaced with a modern PHP API


Reviews:
user: login: sbuerk
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T19:49:53Z
user: login: sbuerk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T21:02:55Z
user: login: sbuerk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T21:09:19Z
user: login: sbuerk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T21:11:53Z
user: login: sbuerk
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T11:40:56Z

Review comments:
diff_hunk: @@ -55,14 +55,18 @@ public function getShowSelected(): bool
         return $this->showSelected;
     }
 
-    public function setHideCompletedProjects(bool $hideCompletedProjects): void
+    public function setActiveState(string $activeState): void
path: packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
user: login: sbuerk
body: We should use native PHP BackedEnums for ActiveState instead of string values again,
also for all related places.
created_at: 2025-04-02T19:32:35Z
author_association: COLLABORATOR
diff_hunk: @@ -55,14 +55,18 @@ public function getShowSelected(): bool
         return $this->showSelected;
     }
 
-    public function setHideCompletedProjects(bool $hideCompletedProjects): void
path: packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
user: login: sbuerk
body: Removing public facing methonds, special on DTO/Models is technically a breaking change.

That means, the commit and pull-request needs to get:

* Breaking prefix `[!!!]`
* Migration path / Breaking statement in extension `UPGRADE.md` file.
created_at: 2025-04-02T19:33:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+
+            if (isset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'])) {
+                // Migrate checkbox toggle to select option
+                if ($flexFormData['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'] === '1') {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'active';
+                } else {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'all';
+                }
+                unset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']);
+
+                // Rename settings
+                foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                    if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                        $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                        unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                    }
+                }
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: This loop over a list of settings is in the wrong level. It should be next to level startet in L53, and not within it.

In the current place, the other settings are only migrated if the hideCompletedProjects exits. Not a good thing.
created_at: 2025-04-02T19:36:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+
+            if (isset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'])) {
+                // Migrate checkbox toggle to select option
+                if ($flexFormData['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'] === '1') {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'active';
+                } else {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'all';
+                }
+                unset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']);
+
+                // Rename settings
+                foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                    if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                        $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                        unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                    }
+                }
+
+                $this->updateContentElement(
+                    $record['uid'],
+                    $this->array2xml($flexFormData)
+                );
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: Moving additional foreach loop one level up means, that persiting flexform needs to be also moved one level up. Make not much sense in this level.
created_at: 2025-04-02T19:37:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+
+            if (isset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'])) {
+                // Migrate checkbox toggle to select option
+                if ($flexFormData['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'] === '1') {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'active';
+                } else {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'all';
+                }
+                unset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']);
+
+                // Rename settings
+                foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                    if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                        $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                        unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                    }
+                }
+
+                $this->updateContentElement(
+                    $record['uid'],
+                    $this->array2xml($flexFormData)
+                );
+            }
+        }
+
+        return false;
+    }
+
+    public function updateNecessary(): bool
+    {
+        return count($this->getMigrationRecords()) > 0;
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: Why should we retrieve a whole result set to do a counting on the PHP side to determine if UpgradeWizard needs to be run ? That can grow to a huge performace issue.

Building query should be inlined here directly, and optimized. That means either a `COUNT()` query or using a select with limit of one record (which may be the fastest at all).
created_at: 2025-04-02T19:39:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: Building the query should be inlined here. If argumented against it, at least the `ResultSet` should be returned and not a fully retrieved array list. 

It is better to use cursor based result navigation when ever possible instead of passing huge array results arround, which is considerable best practive, recommended
and lived by the TYPO3 Core (team).
created_at: 2025-04-02T19:41:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: Instead of iterating array a `while($row = $resultSet->fetchAssociative())` should be used to make use of memory saving cursor result iteration.
created_at: 2025-04-02T19:42:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+
+            if (isset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'])) {
+                // Migrate checkbox toggle to select option
+                if ($flexFormData['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'] === '1') {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'active';
+                } else {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'all';
+                }
+                unset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']);
+
+                // Rename settings
+                foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                    if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                        $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                        unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                    }
+                }
+
+                $this->updateContentElement(
+                    $record['uid'],
+                    $this->array2xml($flexFormData)
+                );
+            }
+        }
+
+        return false;
+    }
+
+    public function updateNecessary(): bool
+    {
+        return count($this->getMigrationRecords()) > 0;
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected function getMigrationRecords(): array
+    {
+        // @todo Check if the order of upgrade wizards can be defined to avoid check for unmigrated list_type plugins
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: Why is this todo hidden here and 

* not stated on class level ?
* mentioned in the commit message / pull request as open todo ? 

One possible solution could be to make the wizard repeatable, which may be a goot thing in any case.

However, never tested in by myself but taking the UpradeWizardInterface::getPrerequisites()` PHP Docblock into account, written like:

```php
/**
 * Returns an array of class names of Prerequisite classes
 *
 * This way a wizard can define dependencies like "database up-to-date" or
 * "reference index updated"
 *
 * @return string[]
 */
public function getPrerequisites(): array;
```

this should be simple as definining:

```php
/**
 * @return string[]
 */
public function getPrerequisites(): array
{
  return [
    DatabaseUpdatedPrerequisite::class,
    PluginUpgradeWizard::class,
  ];
}
```
created_at: 2025-04-02T19:43:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
+        'settings.sorting.options' => 'settings.hideSorting',
+    ];
+
+    public function __construct(
+        private readonly ConnectionPool $connectionPool,
+    ) {}
+
+    public function getTitle(): string
+    {
+        return 'Migrate academic_projects FlexForm setting from "hideCompletedProjects" to "hideActiveState" selection.';
+    }
+
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function executeUpdate(): bool
+    {
+        $records = $this->getMigrationRecords();
+
+        foreach ($records as $record) {
+            // Nothing to upgrade
+            if ($record['pi_flexform'] === null || $record['pi_flexform'] === '') {
+                continue;
+            }
+
+            $flexFormData = GeneralUtility::xml2array($record['pi_flexform']);
+
+            if (isset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'])) {
+                // Migrate checkbox toggle to select option
+                if ($flexFormData['data']['sDEF']['lDEF']['settings.hideCompletedProjects']['vDEF'] === '1') {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'active';
+                } else {
+                    $flexFormData['data']['sDEF']['lDEF']['settings.activeState']['vDEF'] = 'all';
+                }
+                unset($flexFormData ['data']['sDEF']['lDEF']['settings.hideCompletedProjects']);
+
+                // Rename settings
+                foreach (self::MIGRATE_PLUGIN_SETTINGS as $oldName => $newName) {
+                    if (isset($flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'])) {
+                        $flexFormData['data']['sDEF']['lDEF'][$newName]['vDEF'] = $flexFormData['data']['sDEF']['lDEF'][$oldName]['vDEF'];
+                        unset($flexFormData['data']['sDEF']['lDEF'][$oldName]);
+                    }
+                }
+
+                $this->updateContentElement(
+                    $record['uid'],
+                    $this->array2xml($flexFormData)
+                );
+            }
+        }
+
+        return false;
+    }
+
+    public function updateNecessary(): bool
+    {
+        return count($this->getMigrationRecords()) > 0;
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected function getMigrationRecords(): array
+    {
+        // @todo Check if the order of upgrade wizards can be defined to avoid check for unmigrated list_type plugins
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
+        $queryBuilder->getRestrictions()->removeAll();
+        return $queryBuilder
+            ->select('*')
+            ->from('tt_content')
+            ->orWhere(
+                $queryBuilder->expr()->in(
+                    'CType',
+                    $queryBuilder->quoteArrayBasedValueListToStringList(self::MIGRATE_CONTENT_TYPES_LIST)
+                ),
+                $queryBuilder->expr()->in(
+                    'list_type',
+                    $queryBuilder->quoteArrayBasedValueListToStringList(self::MIGRATE_CONTENT_TYPES_LIST)
+                ),
+            )
+            ->executeQuery()
+            ->fetchAllAssociative();
+    }
+
+    /**
+     * @return string[]
+     */
+    public function getPrerequisites(): array
+    {
+        return [
+            DatabaseUpdatedPrerequisite::class,
+        ];
+    }
+
+    protected function updateContentElement(int $uid, string $flexform): void
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: `ConnectionPool` is injected in the construcor, why is `GeneralUtility` used here instead of using the property instance ?
created_at: 2025-04-02T19:44:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace FGTCLB\AcademicProjects\Upgrades;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+#[UpgradeWizard('academicProjects_flexFormUpgradeWizard')]
+final class FlexFormUpgradeWizard implements UpgradeWizardInterface
+{
+    private const MIGRATE_CONTENT_TYPES_LIST = [
+        'academicprojects_projectlist',
+        'academicprojects_projectlistsingle',
+    ];
+
+    private const MIGRATE_PLUGIN_SETTINGS = [
+        'setting.filter.options' => 'settings.hideFilter',
path: packages/fgtclb/academic-projects/Classes/Upgrades/FlexFormUpgradeWizard.php
user: login: sbuerk
body: ```suggestion
        'settings.filter.options' => 'settings.hideFilter',
```

Typo in the `old option name`. Should be `settings` instead of `setting` - mind the `s`.
created_at: 2025-04-02T19:49:17Z
author_association: COLLABORATOR
diff_hunk: @@ -55,14 +55,18 @@ public function getShowSelected(): bool
         return $this->showSelected;
     }
 
-    public function setHideCompletedProjects(bool $hideCompletedProjects): void
+    public function setActiveState(string $activeState): void
path: packages/fgtclb/academic-projects/Classes/Domain/Model/Dto/ProjectDemand.php
user: login: sbuerk
body: Thinking about that, or better to say during refactoring towards it it becomes clear that this is not reasonable.

`fluid` currently does not support native PHP Enums, baked or not baked, and cannot be used simply in fluid templates. The same issue has been introduced first as a possible regression recently in the TYPO3 core for FAL related stuff, but returned immediatly.

That means, we should introduce a PHP Enum only for internal use in the meantime and using good old strings in the DemandObject. With that, we can still use the new Enum as sanatized way on PHP handling code but keep compat and usage in fluid templates workable.
created_at: 2025-04-02T21:02:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+<html
+    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+    xmlns:ace="http://typo3.org/ns/FGTCLB/AcademicProjects/ViewHelpers"
+    data-namespace-typo3-fluid="true"
+>
+    <f:variable name="activeStateOptions" value="{0:'all', 1:'active', 2:'completed'}" />
+    <label for="activeState">
+        {f:translate(
+            key: 'activeState.label',
+            extensionName: 'academic_projects'
+        )}
+    </label>
+    <f:form.select
+        id="activeState"
+        property="activeState"
+        value="{demand.activeState}"
+        additionalAttributes="{'onchange': 'this.form.submit()'}"
path: packages/fgtclb/academic-projects/Resources/Private/Partials/Project/DemandActiveState.html
user: login: sbuerk
body: inline js is evil. I get the background that a "workable" state should be shipped,
but we need to make that configurable in some way in the future.

Should also be evaluated if this clashes with content security policies in some way,
not a experienced person here. Needs to be evaluated and tested ! (Frontend CSP).
created_at: 2025-04-02T21:09:19Z
author_association: COLLABORATOR
diff_hunk: @@ -5,18 +5,16 @@
 >
     <f:if condition="{projects}">
         <f:then>
-            <div class="project-list">
+            <div class="ace-list">
path: packages/fgtclb/academic-projects/Resources/Private/Partials/Project/Items.html
user: login: sbuerk
body: Do we need to mention this or take this as breaking, due to having this styled in projects without overriding the partial ?

Same for other markup css class renamings ....
created_at: 2025-04-02T21:11:52Z
author_association: COLLABORATOR

PR comments:
user: login: sbuerk
created_at: 2025-04-02T21:19:41Z
body: Tackled (all) aforeentioned things during the review, directly implementing the changes.

I also added entries to the `UPGRADE.md` of `EXT:academic_projects` for the breaking stuff.

Changes needs to be tested and based on the findings in the review related to the flexform upgrade wizard we really should add tests for the wizard and the plugin along with the options or at least extbase repository tests based on the demand object.

Not feeling well merging or approving this without tests (I do not mean "I have tested that by hand").
author_association: COLLABORATOR
user: login: sbuerk
created_at: 2025-04-09T12:32:59Z
body: Tests will be added later (upgade wizard). On todo lists.
author_association: COLLABORATOR