From d1ad7a34bd97c3b61eac9ab9617ff8634794ae7e Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 29 Jul 2026 16:36:06 +0200 Subject: [PATCH 01/26] [!!!][TASK] Add salary grade structure for jobs Introduce a salary grade catalog (SalaryTable, SalaryGrade, SalaryStep) so editors can attach either a tariff-based civil-service pay grade with seniority steps, or a free min/max salary range, to a job posting. This is required by an upcoming law that forbids displaying job postings without salary information; SalaryGrade uses TYPO3's native starttime/endtime so an expired grade automatically makes the referencing job unresolvable via Extbase relation loading. Job's ctrl[type] switches from is_import to salary_mode so the backend can show distinct icons and field sets for grade-based versus free-entry salary jobs. is_import becomes a plain field that now drives a displayCond on vacancy_id instead of driving the record type. This is a breaking change for any project still relying on the previous is_import-based type icon behaviour. Co-Authored-By: Claude Sonnet 5 --- Classes/Domain/Model/Job.php | 48 +++++ Classes/Domain/Model/SalaryGrade.php | 143 ++++++++++++++ Classes/Domain/Model/SalaryStep.php | 56 ++++++ Classes/Domain/Model/SalaryTable.php | 76 ++++++++ Configuration/Icons.php | 24 +++ .../TCA/tx_jobfair2_domain_model_job.php | 87 ++++++++- .../tx_jobfair2_domain_model_salarygrade.php | 180 ++++++++++++++++++ .../tx_jobfair2_domain_model_salarystep.php | 141 ++++++++++++++ .../tx_jobfair2_domain_model_salarytable.php | 146 ++++++++++++++ .../Private/Language/de.locallang_db.xlf | 143 ++++++++++++++ Resources/Private/Language/locallang_db.xlf | 108 +++++++++++ Resources/Public/Icons/job-freeentry.svg | 1 + Resources/Public/Icons/job-grade.svg | 1 + Resources/Public/Icons/salarygrade-flat.svg | 1 + .../Public/Icons/salarygrade-stepped.svg | 1 + Resources/Public/Icons/salarygrade.svg | 1 + Resources/Public/Icons/salarystep.svg | 1 + Resources/Public/Icons/salarytable.svg | 1 + ext_tables.sql | 22 ++- 19 files changed, 1171 insertions(+), 10 deletions(-) create mode 100644 Classes/Domain/Model/SalaryGrade.php create mode 100644 Classes/Domain/Model/SalaryStep.php create mode 100644 Classes/Domain/Model/SalaryTable.php create mode 100644 Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php create mode 100644 Configuration/TCA/tx_jobfair2_domain_model_salarystep.php create mode 100644 Configuration/TCA/tx_jobfair2_domain_model_salarytable.php create mode 100644 Resources/Public/Icons/job-freeentry.svg create mode 100644 Resources/Public/Icons/job-grade.svg create mode 100644 Resources/Public/Icons/salarygrade-flat.svg create mode 100644 Resources/Public/Icons/salarygrade-stepped.svg create mode 100644 Resources/Public/Icons/salarygrade.svg create mode 100644 Resources/Public/Icons/salarystep.svg create mode 100644 Resources/Public/Icons/salarytable.svg diff --git a/Classes/Domain/Model/Job.php b/Classes/Domain/Model/Job.php index 51a1c6d..39780eb 100644 --- a/Classes/Domain/Model/Job.php +++ b/Classes/Domain/Model/Job.php @@ -55,6 +55,14 @@ class Job extends AbstractEntity protected bool $isInternal = false; + protected int $salaryMode = 0; + + protected ?SalaryGrade $salaryGrade = null; + + protected float $salaryMin = 0.0; + + protected float $salaryMax = 0.0; + public function getTitle(): string { return $this->title; @@ -234,4 +242,44 @@ public function setIsInternal(bool $isInternal): void { $this->isInternal = $isInternal; } + + public function getSalaryMode(): int + { + return $this->salaryMode; + } + + public function setSalaryMode(int $salaryMode): void + { + $this->salaryMode = $salaryMode; + } + + public function getSalaryGrade(): ?SalaryGrade + { + return $this->salaryGrade; + } + + public function setSalaryGrade(SalaryGrade $salaryGrade): void + { + $this->salaryGrade = $salaryGrade; + } + + public function getSalaryMin(): float + { + return $this->salaryMin; + } + + public function setSalaryMin(float $salaryMin): void + { + $this->salaryMin = $salaryMin; + } + + public function getSalaryMax(): float + { + return $this->salaryMax; + } + + public function setSalaryMax(float $salaryMax): void + { + $this->salaryMax = $salaryMax; + } } diff --git a/Classes/Domain/Model/SalaryGrade.php b/Classes/Domain/Model/SalaryGrade.php new file mode 100644 index 0000000..505b3c0 --- /dev/null +++ b/Classes/Domain/Model/SalaryGrade.php @@ -0,0 +1,143 @@ + + */ + protected ObjectStorage $salarySteps; + + public function __construct() + { + $this->salarySteps = new ObjectStorage(); + } + + public function initializeObject(): void + { + $this->salarySteps ??= new ObjectStorage(); + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function hasSteps(): bool + { + return $this->hasSteps; + } + + public function setHasSteps(bool $hasSteps): void + { + $this->hasSteps = $hasSteps; + } + + public function getFlatAmount(): float + { + return $this->flatAmount; + } + + public function setFlatAmount(float $flatAmount): void + { + $this->flatAmount = $flatAmount; + } + + public function getSalaryTable(): ?SalaryTable + { + return $this->salaryTable; + } + + public function setSalaryTable(SalaryTable $salaryTable): void + { + $this->salaryTable = $salaryTable; + } + + /** + * @return ObjectStorage + */ + public function getSalarySteps(): ObjectStorage + { + return $this->salarySteps; + } + + /** + * @param ObjectStorage $salarySteps + */ + public function setSalarySteps(ObjectStorage $salarySteps): void + { + $this->salarySteps = $salarySteps; + } + + /** + * Minimum amount of this grade: the flat amount if it has no steps, otherwise + * the lowest amount among the steps that actually exist (gaps are not positional). + */ + public function getMinAmount(): float + { + if (!$this->hasSteps) { + return $this->flatAmount; + } + + $amounts = $this->getStepAmounts(); + + return $amounts === [] ? 0.0 : min($amounts); + } + + /** + * Maximum amount of this grade: the flat amount if it has no steps, otherwise + * the highest amount among the steps that actually exist (gaps are not positional). + */ + public function getMaxAmount(): float + { + if (!$this->hasSteps) { + return $this->flatAmount; + } + + $amounts = $this->getStepAmounts(); + + return $amounts === [] ? 0.0 : max($amounts); + } + + /** + * @return float[] + */ + private function getStepAmounts(): array + { + $amounts = []; + foreach ($this->salarySteps as $step) { + $amounts[] = $step->getAmount(); + } + + return $amounts; + } +} diff --git a/Classes/Domain/Model/SalaryStep.php b/Classes/Domain/Model/SalaryStep.php new file mode 100644 index 0000000..8a655cd --- /dev/null +++ b/Classes/Domain/Model/SalaryStep.php @@ -0,0 +1,56 @@ +stepLabel; + } + + public function setStepLabel(string $stepLabel): void + { + $this->stepLabel = $stepLabel; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function setAmount(float $amount): void + { + $this->amount = $amount; + } + + public function getSalaryGrade(): ?SalaryGrade + { + return $this->salaryGrade; + } + + public function setSalaryGrade(SalaryGrade $salaryGrade): void + { + $this->salaryGrade = $salaryGrade; + } +} diff --git a/Classes/Domain/Model/SalaryTable.php b/Classes/Domain/Model/SalaryTable.php new file mode 100644 index 0000000..d64ab1f --- /dev/null +++ b/Classes/Domain/Model/SalaryTable.php @@ -0,0 +1,76 @@ + + */ + protected ObjectStorage $salaryGrades; + + public function __construct() + { + $this->salaryGrades = new ObjectStorage(); + } + + public function initializeObject(): void + { + $this->salaryGrades ??= new ObjectStorage(); + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + /** + * @return ObjectStorage + */ + public function getSalaryGrades(): ObjectStorage + { + return $this->salaryGrades; + } + + /** + * @param ObjectStorage $salaryGrades + */ + public function setSalaryGrades(ObjectStorage $salaryGrades): void + { + $this->salaryGrades = $salaryGrades; + } +} diff --git a/Configuration/Icons.php b/Configuration/Icons.php index 553537a..c16e9d6 100644 --- a/Configuration/Icons.php +++ b/Configuration/Icons.php @@ -13,4 +13,28 @@ 'provider' => SvgIconProvider::class, 'source' => 'EXT:jobfair2/Resources/Public/Icons/job-import.svg', ], + 'ext-jobfair2-record-job-grade' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/job-grade.svg', + ], + 'ext-jobfair2-record-job-freeentry' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/job-freeentry.svg', + ], + 'ext-jobfair2-record-salarytable' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/salarytable.svg', + ], + 'ext-jobfair2-record-salarygrade-stepped' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/salarygrade-stepped.svg', + ], + 'ext-jobfair2-record-salarygrade-flat' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/salarygrade-flat.svg', + ], + 'ext-jobfair2-record-salarystep' => [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:jobfair2/Resources/Public/Icons/salarystep.svg', + ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index aae4f6c..affd0cf 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -10,12 +10,12 @@ 'label' => 'title', 'tstamp' => 'tstamp', 'crdate' => 'crdate', - 'type' => 'is_import', - 'typeicon_column' => 'is_import', + 'type' => 'salary_mode', + 'typeicon_column' => 'salary_mode', 'typeicon_classes' => [ - 'default' => 'ext-jobfair2-record-job', - 0 => 'ext-jobfair2-record-job', - 1 => 'ext-jobfair2-record-job-import', + 'default' => 'ext-jobfair2-record-job-grade', + 0 => 'ext-jobfair2-record-job-grade', + 1 => 'ext-jobfair2-record-job-freeentry', ], 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', @@ -29,12 +29,23 @@ ], 'types' => [ '0' => [ - 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - --palette--;Job;titleReference, - --palette--;Import;importVacancy, + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + --palette--;Job;titleReference, + --palette--;Import;importVacancy, description, address, --palette--;;areaType, --palette--;;startEndDate, --div--;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer, employer, email, employer_address, - --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --div--;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary, salary_grade, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', + ], + '1' => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + --palette--;Job;titleReference, + --palette--;Import;importVacancy, + description, address, --palette--;;areaType, --palette--;;startEndDate, + --div--;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer, employer, email, employer_address, + --div--;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary, salary_min, salary_max, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], ], @@ -151,6 +162,7 @@ ], 'vacancy_id' => [ 'exclude' => 1, + 'displayCond' => 'FIELD:is_import:REQ:true', 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.vacancy_id', 'config' => [ 'type' => 'input', @@ -409,5 +421,62 @@ 'default' => 0, ], ], + 'salary_mode' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode.description', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode.grade', 'value' => 0], + ['label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode.freeEntry', 'value' => 1], + ], + 'default' => 0, + ], + ], + 'salary_grade' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade.description', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => '', 'value' => 0], + ], + 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', + 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0) ORDER BY tx_jobfair2_domain_model_salarygrade.title ASC', + 'minitems' => 1, + 'maxitems' => 1, + 'default' => 0, + ], + ], + 'salary_min' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], + 'salary_max' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php new file mode 100644 index 0000000..1475895 --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -0,0 +1,180 @@ + [ + 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade', + 'label' => 'title', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'sortby' => 'sorting', + 'type' => 'has_steps', + 'typeicon_column' => 'has_steps', + 'typeicon_classes' => [ + 'default' => 'ext-jobfair2-record-salarygrade-stepped', + 1 => 'ext-jobfair2-record-salarygrade-stepped', + 0 => 'ext-jobfair2-record-salarygrade-flat', + ], + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarygrade.svg', + ], + 'types' => [ + '1' => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + title, has_steps, salary_steps, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --palette--;;access', + ], + '0' => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + title, has_steps, flat_amount, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --palette--;;access', + ], + ], + 'palettes' => [ + 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], + 'access' => [ + 'showitem' => 'starttime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.starttime,endtime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.endtime', + ], + ], + 'columns' => [ + 'sys_language_uid' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', + 'config' => ['type' => 'language'], + ], + 'l10n_parent' => [ + 'displayCond' => 'FIELD:sys_language_uid:>:0', + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => '', 'value' => 0], + ], + 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', + 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarygrade.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0)', + 'fieldWizard' => [ + 'selectIcons' => [ + 'disabled' => true, + ], + ], + 'default' => 0, + ], + ], + 'l10n_diffsource' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'hidden' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'items' => [ + [ + 'label' => '', + 'invertStateDisplay' => true, + ], + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.starttime.description', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'endtime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.endtime.description', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'salary_table' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'title' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.title', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.title.description', + 'config' => [ + 'type' => 'input', + 'size' => 13, + 'max' => 60, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'has_steps' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.has_steps', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.has_steps.description', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 1, + ], + ], + 'flat_amount' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.flat_amount', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.flat_amount.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], + 'salary_steps' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.salary_steps', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.salary_steps.description', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_jobfair2_domain_model_salarystep', + 'foreign_field' => 'salary_grade', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'useSortable' => true, + 'collapseAll' => true, + 'expandSingle' => true, + 'levelLinksPosition' => 'top', + ], + ], + ], + ], +]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php new file mode 100644 index 0000000..006685b --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -0,0 +1,141 @@ + [ + 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep', + 'label' => 'step_label', + 'label_alt' => 'amount', + 'label_alt_force' => true, + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'sortby' => 'sorting', + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarystep.svg', + ], + 'types' => [ + '0' => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + step_label, amount, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --palette--;;access', + ], + ], + 'palettes' => [ + 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], + 'access' => [ + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + ], + ], + 'columns' => [ + 'sys_language_uid' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', + 'config' => ['type' => 'language'], + ], + 'l10n_parent' => [ + 'displayCond' => 'FIELD:sys_language_uid:>:0', + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => '', 'value' => 0], + ], + 'foreign_table' => 'tx_jobfair2_domain_model_salarystep', + 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarystep.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarystep.sys_language_uid IN (-1,0)', + 'fieldWizard' => [ + 'selectIcons' => [ + 'disabled' => true, + ], + ], + 'default' => 0, + ], + ], + 'l10n_diffsource' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'hidden' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'items' => [ + [ + 'label' => '', + 'invertStateDisplay' => true, + ], + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'endtime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'salary_grade' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'step_label' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.step_label', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.step_label.description', + 'config' => [ + 'type' => 'input', + 'size' => 10, + 'max' => 30, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'amount' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.amount', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.amount.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'range' => [ + 'lower' => 0, + ], + 'required' => true, + 'default' => 0.00, + ], + ], + ], +]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php new file mode 100644 index 0000000..9d758fc --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -0,0 +1,146 @@ + [ + 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable', + 'label' => 'title', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'dividers2tabs' => true, + 'languageField' => 'sys_language_uid', + 'transOrigPointerField' => 'l10n_parent', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'delete' => 'deleted', + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarytable.svg', + ], + 'types' => [ + '0' => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + title, description, salary_grades, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, + --palette--;;access', + ], + ], + 'palettes' => [ + 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], + 'access' => [ + 'showitem' => 'starttime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.starttime,endtime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.endtime', + ], + ], + 'columns' => [ + 'sys_language_uid' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', + 'config' => ['type' => 'language'], + ], + 'l10n_parent' => [ + 'displayCond' => 'FIELD:sys_language_uid:>:0', + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['label' => '', 'value' => 0], + ], + 'foreign_table' => 'tx_jobfair2_domain_model_salarytable', + 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarytable.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarytable.sys_language_uid IN (-1,0)', + 'fieldWizard' => [ + 'selectIcons' => [ + 'disabled' => true, + ], + ], + 'default' => 0, + ], + ], + 'l10n_diffsource' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'hidden' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'items' => [ + [ + 'label' => '', + 'invertStateDisplay' => true, + ], + ], + ], + ], + 'starttime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.starttime.description', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'endtime' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.endtime.description', + 'config' => [ + 'type' => 'datetime', + 'size' => 16, + 'default' => 0, + 'behaviour' => [ + 'allowLanguageSynchronization' => true, + ], + ], + ], + 'title' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.title', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'max' => 250, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'description' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.description', + 'config' => [ + 'type' => 'text', + 'cols' => 40, + 'rows' => 3, + ], + ], + 'salary_grades' => [ + 'exclude' => 1, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.salary_grades', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', + 'foreign_field' => 'salary_table', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'useSortable' => true, + 'collapseAll' => true, + 'expandSingle' => true, + 'levelLinksPosition' => 'top', + ], + ], + ], + ], +]; diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 59f1bbe..8d42477 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -100,6 +100,50 @@ Is internal? Ist Intern? + + Salary + Gehalt + + + Salary mode + Gehaltsmodus + + + Determines whether a tariff-based salary grade or a free salary entry is used. Changing this afterwards reloads the form, since the visible fields differ. + Bestimmt, ob eine tarifliche Besoldungsgruppe oder eine freie Gehaltsangabe genutzt wird. Ein nachträglicher Wechsel lädt das Formular neu, da sich die sichtbaren Felder unterscheiden. + + + Salary grade (tariff-based) + Besoldungsgruppe (tariflich) + + + Free salary entry + Freie Gehaltsangabe + + + Salary grade + Besoldungsgruppe + + + The minimum/maximum salary is calculated automatically in the frontend from the steps stored for this salary grade. + Min./Max.-Gehalt wird im Frontend automatisch aus den hinterlegten Stufen dieser Besoldungsgruppe berechnet. + + + Minimum salary + Mindestgehalt + + + For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + Für ein exaktes Gehalt bitte denselben Betrag in Min. und Max. eintragen. Da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen, müssen beide Felder befüllt sein. Beträge bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17), nicht mit Komma oder Tausendertrennzeichen — der Browser kann den Wert sonst stillschweigend abschneiden. + + + Maximum salary + Höchstgehalt + + + For an exact salary, please enter the same amount as in minimum salary. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + Für ein exaktes Gehalt bitte denselben Betrag wie beim Mindestgehalt eintragen. Beträge bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17), nicht mit Komma oder Tausendertrennzeichen — der Browser kann den Wert sonst stillschweigend abschneiden. + Occupational field @@ -118,6 +162,105 @@ Type of employment Beschäftigungsart + + + Salary table + Besoldungstabelle + + + Title + Titel + + + Description + Beschreibung + + + Salary grades + Besoldungsgruppen + + + Valid from (document) + Gültig ab (Dokument) + + + Valid until (document) + Gültig bis (Dokument) + + + For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). + Dient nur der redaktionellen Dokumentation/Organisation dieses Dokuments. Die rechtlich wirksame Gültigkeit wird auf Ebene der einzelnen Besoldungsgruppe gepflegt (siehe dort). + + + For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). + Dient nur der redaktionellen Dokumentation/Organisation dieses Dokuments. Die rechtlich wirksame Gültigkeit wird auf Ebene der einzelnen Besoldungsgruppe gepflegt (siehe dort). + + + + Salary grade + Besoldungsgruppe + + + Grade + Gruppe + + + e.g. "A9", "R3" or "B3" — depending on the pay scale. + z. B. "A9", "R3" oder "B3" — je nach Besoldungstabelle. + + + Has steps? + Hat Stufen? + + + Disable for salary grades without seniority steps (e.g. R3 and above, B-grades) — a fixed amount is used instead. + Deaktivieren für Besoldungsgruppen ohne Erfahrungsstufen (z. B. R3 aufwärts, B-Besoldung) — es wird dann ein fester Betrag statt einzelner Stufen hinterlegt. + + + Fixed amount + Fester Betrag + + + Only relevant for salary grades without steps. Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + Nur relevant für Besoldungsgruppen ohne Stufen. Betrag bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17) — kein Tausendertrennzeichen und kein Komma verwenden, da der Browser den Wert sonst stillschweigend abschneiden kann. + + + Steps + Stufen + + + Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. + Nicht jede Stufe muss existieren — Lücken sind zulässig. Minimum/Maximum werden im Frontend automatisch aus den tatsächlich vorhandenen Stufen ermittelt. + + + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. + Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. + + + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. + Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. + + + + Salary step + Besoldungsstufe + + + Step + Stufe + + + Free text, e.g. "3", "3a" or "Entry step" — naming varies by pay scale. + Freitext, z. B. "3", "3a" oder "Eingangsstufe" — je nach Tarifwerk unterschiedlich benannt. + + + Amount + Betrag + + + Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + Betrag bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17) — kein Tausendertrennzeichen und kein Komma verwenden, da der Browser den Wert sonst stillschweigend abschneiden kann. + diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index b6263e4..9715561 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -76,6 +76,39 @@ Is internal? + + Salary + + + Salary mode + + + Determines whether a tariff-based salary grade or a free salary entry is used. Changing this afterwards reloads the form, since the visible fields differ. + + + Salary grade (tariff-based) + + + Free salary entry + + + Salary grade + + + The minimum/maximum salary is calculated automatically in the frontend from the steps stored for this salary grade. + + + Minimum salary + + + For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + + + Maximum salary + + + For an exact salary, please enter the same amount as in minimum salary. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + Occupational field @@ -90,6 +123,81 @@ Type of employment + + + Salary table + + + Title + + + Description + + + Salary grades + + + Valid from (document) + + + Valid until (document) + + + For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). + + + For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). + + + + Salary grade + + + Grade + + + e.g. "A9", "R3" or "B3" — depending on the pay scale. + + + Has steps? + + + Disable for salary grades without seniority steps (e.g. R3 and above, B-grades) — a fixed amount is used instead. + + + Fixed amount + + + Only relevant for salary grades without steps. Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + + + Steps + + + Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. + + + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. + + + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. + + + + Salary step + + + Step + + + Free text, e.g. "3", "3a" or "Entry step" — naming varies by pay scale. + + + Amount + + + Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + diff --git a/Resources/Public/Icons/job-freeentry.svg b/Resources/Public/Icons/job-freeentry.svg new file mode 100644 index 0000000..56a7abe --- /dev/null +++ b/Resources/Public/Icons/job-freeentry.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/job-grade.svg b/Resources/Public/Icons/job-grade.svg new file mode 100644 index 0000000..a138506 --- /dev/null +++ b/Resources/Public/Icons/job-grade.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/salarygrade-flat.svg b/Resources/Public/Icons/salarygrade-flat.svg new file mode 100644 index 0000000..6ae238a --- /dev/null +++ b/Resources/Public/Icons/salarygrade-flat.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/salarygrade-stepped.svg b/Resources/Public/Icons/salarygrade-stepped.svg new file mode 100644 index 0000000..0ab2037 --- /dev/null +++ b/Resources/Public/Icons/salarygrade-stepped.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/salarygrade.svg b/Resources/Public/Icons/salarygrade.svg new file mode 100644 index 0000000..0ab2037 --- /dev/null +++ b/Resources/Public/Icons/salarygrade.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/salarystep.svg b/Resources/Public/Icons/salarystep.svg new file mode 100644 index 0000000..34f9018 --- /dev/null +++ b/Resources/Public/Icons/salarystep.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/Icons/salarytable.svg b/Resources/Public/Icons/salarytable.svg new file mode 100644 index 0000000..cc43425 --- /dev/null +++ b/Resources/Public/Icons/salarytable.svg @@ -0,0 +1 @@ + diff --git a/ext_tables.sql b/ext_tables.sql index 62ce0fa..b1e6b9a 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -20,7 +20,27 @@ CREATE TABLE tx_jobfair2_domain_model_job tender_file int(11) DEFAULT '0' NOT NULL, pdf_files int(11) DEFAULT '0' NOT NULL, pdf_tstamp int(10) DEFAULT '0' NOT NULL, - is_internal tinyint(4) UNSIGNED DEFAULT '0' NOT NULL + is_internal tinyint(4) UNSIGNED DEFAULT '0' NOT NULL, + salary_min decimal(10,2) DEFAULT '0.00' NOT NULL, + salary_max decimal(10,2) DEFAULT '0.00' NOT NULL +); + +# +# Table structure for table 'tx_jobfair2_domain_model_salarygrade' +# +CREATE TABLE tx_jobfair2_domain_model_salarygrade +( + salary_table int(11) DEFAULT '0' NOT NULL, + flat_amount decimal(10,2) DEFAULT '0.00' NOT NULL +); + +# +# Table structure for table 'tx_jobfair2_domain_model_salarystep' +# +CREATE TABLE tx_jobfair2_domain_model_salarystep +( + salary_grade int(11) DEFAULT '0' NOT NULL, + amount decimal(10,2) DEFAULT '0.00' NOT NULL ); # From 4eb9c41a9593453447f499b41765a867cf612ef0 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 11:11:31 +0200 Subject: [PATCH 02/26] [TASK] Use group field for Job salary grade selection Switch Job's salary_grade from a plain select dropdown to a group field. selectSingle has no search/filter, and once several SalaryTable catalogs exist in parallel, grade titles like "A7" can no longer be told apart. Group gives editors a searchable picker instead. SalaryGrade's own salary_table pointer changes from passthrough to a group relation so its title can be resolved and appended via label_alt/label_alt_force, e.g. "A7, Grundgehaltssaetze Baden- Wuerttemberg" wherever the record title is used (group field, list module). Add a formattedLabel_userFunc for the inline child header inside SalaryTable's own edit form, where that same parent-table suffix would just repeat information the editor already sees. Co-Authored-By: Claude Sonnet 5 --- .../UserFunc/InlineRecordTitleFormatter.php | 25 +++++++++++++++++++ .../TCA/tx_jobfair2_domain_model_job.php | 15 +++++------ .../tx_jobfair2_domain_model_salarygrade.php | 6 ++++- 3 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 Classes/UserFunc/InlineRecordTitleFormatter.php diff --git a/Classes/UserFunc/InlineRecordTitleFormatter.php b/Classes/UserFunc/InlineRecordTitleFormatter.php new file mode 100644 index 0000000..ad683e5 --- /dev/null +++ b/Classes/UserFunc/InlineRecordTitleFormatter.php @@ -0,0 +1,25 @@ + 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade.description', 'config' => [ - 'type' => 'select', - 'renderType' => 'selectSingle', - 'items' => [ - ['label' => '', 'value' => 0], - ], + 'type' => 'group', + 'allowed' => 'tx_jobfair2_domain_model_salarygrade', 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', - 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0) ORDER BY tx_jobfair2_domain_model_salarygrade.title ASC', 'minitems' => 1, 'maxitems' => 1, - 'default' => 0, + 'size' => 1, + 'suggestOptions' => [ + 'default' => [ + 'addWhere' => 'AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0)', + ], + ], ], ], 'salary_min' => [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 1475895..15ea316 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -8,6 +8,9 @@ 'ctrl' => [ 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade', 'label' => 'title', + 'label_alt' => 'salary_table', + 'label_alt_force' => true, + 'formattedLabel_userFunc' => \JWeiland\Jobfair2\UserFunc\InlineRecordTitleFormatter::class . '->formatSalaryGradeTitle', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'sortby' => 'sorting', @@ -121,7 +124,8 @@ ], 'salary_table' => [ 'config' => [ - 'type' => 'passthrough', + 'type' => 'group', + 'allowed' => 'tx_jobfair2_domain_model_salarytable', ], ], 'title' => [ From 7b80e397cd3a688a3a44b951061c7abb081c0551 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 13:37:56 +0200 Subject: [PATCH 03/26] [TASK] Improve SalaryStep title with step label prefix The label_alt-based title just concatenated the raw step_label and amount ("3, 3421.84"), which reads more like a typo than a step number. Prefix it with the step_label field's own label ("Step 3") and format the amount for the current backend user's locale, joined by a dash for readability. Co-Authored-By: Claude Sonnet 5 --- Classes/UserFunc/SalaryStepTitleFormatter.php | 48 +++++++++++++++++++ .../tx_jobfair2_domain_model_salarystep.php | 1 + 2 files changed, 49 insertions(+) create mode 100644 Classes/UserFunc/SalaryStepTitleFormatter.php diff --git a/Classes/UserFunc/SalaryStepTitleFormatter.php b/Classes/UserFunc/SalaryStepTitleFormatter.php new file mode 100644 index 0000000..a80d3fa --- /dev/null +++ b/Classes/UserFunc/SalaryStepTitleFormatter.php @@ -0,0 +1,48 @@ +getStepLabelPrefix() . ' ' . $row['step_label']); + } + $parts[] = $this->formatAmount((float)($row['amount'] ?? 0.0)); + $parameters['title'] = implode(' - ', $parts); + } + + private function getStepLabelPrefix(): string + { + return $GLOBALS['LANG']?->sL(self::STEP_LABEL_LLL) ?? 'Step'; + } + + private function formatAmount(float $amount): string + { + $locale = $GLOBALS['LANG']?->getLocale()?->getName() ?? 'en'; + $formatter = new \NumberFormatter($locale, \NumberFormatter::DECIMAL); + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 2); + + return (string)$formatter->format($amount); + } +} diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 006685b..84eeacf 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -10,6 +10,7 @@ 'label' => 'step_label', 'label_alt' => 'amount', 'label_alt_force' => true, + 'label_userFunc' => \JWeiland\Jobfair2\UserFunc\SalaryStepTitleFormatter::class . '->formatTitle', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'sortby' => 'sorting', From e3e3516148d8aaa434ea3808223f7fc24df00b7a Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 13:39:59 +0200 Subject: [PATCH 04/26] [TASK] Compact SalaryGrade form and improve step creation Combine title and has_steps into one palette so they sit side by side instead of stacking, saving vertical space in a form that can already hold many inline levels. Also show the "create new" button below the step list in addition to above, and append the foreign table's own title to it, so long lists of steps don't require scrolling back up just to add another one. Co-Authored-By: Claude Sonnet 5 --- .../TCA/tx_jobfair2_domain_model_salarygrade.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 15ea316..31d3b70 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -35,19 +35,20 @@ 'types' => [ '1' => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - title, has_steps, salary_steps, + --palette--;;titleStep, salary_steps, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;;access', ], '0' => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - title, has_steps, flat_amount, + --palette--;;titleStep, flat_amount, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;;access', ], ], 'palettes' => [ 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], + 'titleStep' => ['showitem' => 'title, has_steps'], 'access' => [ 'showitem' => 'starttime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.starttime,endtime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.endtime', ], @@ -176,7 +177,8 @@ 'useSortable' => true, 'collapseAll' => true, 'expandSingle' => true, - 'levelLinksPosition' => 'top', + 'levelLinksPosition' => 'both', + 'newRecordLinkAddTitle' => true, ], ], ], From 37bff2ce13315542416a08126770ee4f4bce5452 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 13:40:41 +0200 Subject: [PATCH 05/26] [TASK] Add bottom create-new button for salary grades Same treatment as the SalaryGrade -> SalaryStep relation: show the "create new" button below the salary grade list in addition to above, with the foreign table's title appended to it. Co-Authored-By: Claude Sonnet 5 --- Configuration/TCA/tx_jobfair2_domain_model_salarytable.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php index 9d758fc..49a4a98 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -138,7 +138,8 @@ 'useSortable' => true, 'collapseAll' => true, 'expandSingle' => true, - 'levelLinksPosition' => 'top', + 'levelLinksPosition' => 'both', + 'newRecordLinkAddTitle' => true, ], ], ], From 98844a7a76a8bcf75f2a06c7ca6f1e79d1cc1494 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 13:41:42 +0200 Subject: [PATCH 06/26] [FEATURE] Accept locale-formatted amounts in salary inputs Native fields strip characters like "," while typing or pasting, so a value copied from a German pay scale table (e.g. "3.421,84") could never be entered correctly - editors had to manually remove the thousands separator first. Add a custom renderType, jobfair2LocalizedDecimal, backed by a JavaScriptModuleInstruction that normalizes the typed value to the machine format on blur, based on where the last "," or "." occurs in the string rather than on the backend user's own UI language - the source data's notation and the editor's language preference are independent of each other. type=>number/format=>decimal is kept unchanged, so the automatic DECIMAL(10,2) column derivation is unaffected. Applied to flat_amount, amount, salary_min and salary_max. Require ext-intl for the NumberFormatter calls this and the SalaryStep title formatter rely on. Co-Authored-By: Claude Sonnet 5 --- .../Element/LocalizedDecimalElement.php | 108 ++++++++++++++++++ Configuration/JavaScriptModules.php | 15 +++ .../TCA/tx_jobfair2_domain_model_job.php | 2 + .../tx_jobfair2_domain_model_salarygrade.php | 1 + .../tx_jobfair2_domain_model_salarystep.php | 1 + .../Private/Language/de.locallang_db.xlf | 16 +-- Resources/Private/Language/locallang_db.xlf | 8 +- .../form-engine-localized-decimal.js | 41 +++++++ composer.json | 1 + ext_localconf.php | 6 + 10 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 Classes/Backend/Element/LocalizedDecimalElement.php create mode 100644 Configuration/JavaScriptModules.php create mode 100644 Resources/Public/JavaScript/form-engine-localized-decimal.js diff --git a/Classes/Backend/Element/LocalizedDecimalElement.php b/Classes/Backend/Element/LocalizedDecimalElement.php new file mode 100644 index 0000000..bdf824c --- /dev/null +++ b/Classes/Backend/Element/LocalizedDecimalElement.php @@ -0,0 +1,108 @@ +. + * Browsers filter out characters like "," while typing or pasting into a + * real number input, which makes entering formatted amounts (e.g. + * "3.421,84") impossible before TYPO3 even gets to see the value. A small + * JavaScript module normalizes the typed value to the machine format + * ("3421.84") on blur - by position, not by backend locale, since editors + * enter values in whatever convention the source data uses, independent of + * their own backend UI language. The field itself always shows the machine + * format; locale-formatted display is a label concern (see label_alt / + * label_userFunc on SalaryStep), not something this input should do. + */ +final class LocalizedDecimalElement extends AbstractFormElement +{ + protected $defaultFieldInformation = [ + 'tcaDescription' => [ + 'renderType' => 'tcaDescription', + ], + ]; + + public function render(): array + { + $parameterArray = $this->data['parameterArray']; + $resultArray = $this->initializeResultArray(); + + $fieldInformationResult = $this->renderFieldInformation(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + $fieldControlResult = $this->renderFieldControl(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false); + $fieldWizardResult = $this->renderFieldWizard(); + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); + + $fieldId = StringUtility::getUniqueId('formengine-input-'); + + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( + '@jweiland/jobfair2/form-engine-localized-decimal.js' + )->invoke('initialize', $fieldId); + + $resultArray['html'] = $this->renderLabel($fieldId) . ' +
+ ' . $fieldInformationResult['html'] . $this->buildFieldHtml( + $fieldId, + (string)$parameterArray['itemFormElValue'], + $fieldControlResult['html'], + $fieldWizardResult['html'] + ) . ' +
'; + + return $resultArray; + } + + private function buildFieldHtml(string $fieldId, string $value, string $fieldControlHtml, string $fieldWizardHtml): string + { + $parameterArray = $this->data['parameterArray']; + $config = $parameterArray['fieldConf']['config']; + $width = $this->formMaxWidth( + MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth) + ); + $attributes = [ + 'value' => $value, + 'id' => $fieldId, + 'name' => (string)$parameterArray['itemFormElName'], + 'inputmode' => 'decimal', + 'class' => 'form-control form-control-clearable t3js-clearable', + 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config), + ]; + + $html = []; + $html[] = '
'; + $html[] = '
'; + $html[] = '
'; + $html[] = ''; + $html[] = '
'; + if ($fieldControlHtml !== '') { + $html[] = '
'; + $html[] = '
' . $fieldControlHtml . '
'; + $html[] = '
'; + } + if ($fieldWizardHtml !== '') { + $html[] = '
' . $fieldWizardHtml . '
'; + } + $html[] = '
'; + $html[] = '
'; + + return implode(LF, $html); + } +} diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..950e8f4 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,15 @@ + [ + 'backend', + ], + 'tags' => [ + 'backend.form', + ], + 'imports' => [ + '@jweiland/jobfair2/' => 'EXT:jobfair2/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index d06ca91..d4e1f34 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -460,6 +460,7 @@ 'config' => [ 'type' => 'number', 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', 'range' => [ 'lower' => 0, ], @@ -473,6 +474,7 @@ 'config' => [ 'type' => 'number', 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', 'range' => [ 'lower' => 0, ], diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 31d3b70..6927b45 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -158,6 +158,7 @@ 'config' => [ 'type' => 'number', 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', 'range' => [ 'lower' => 0, ], diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 84eeacf..90225f0 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -131,6 +131,7 @@ 'config' => [ 'type' => 'number', 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', 'range' => [ 'lower' => 0, ], diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 8d42477..ed429d8 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -133,16 +133,16 @@ Mindestgehalt
- For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. - Für ein exaktes Gehalt bitte denselben Betrag in Min. und Max. eintragen. Da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen, müssen beide Felder befüllt sein. Beträge bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17), nicht mit Komma oder Tausendertrennzeichen — der Browser kann den Wert sonst stillschweigend abschneiden. + For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. + Für ein exaktes Gehalt bitte denselben Betrag in Min. und Max. eintragen. Da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen, müssen beide Felder befüllt sein. Der Betrag kann mit Tausendertrennzeichen und Komma als Dezimaltrennzeichen eingegeben werden (z. B. 3.842,17) — er wird beim Verlassen des Feldes automatisch normalisiert. Maximum salary Höchstgehalt - For an exact salary, please enter the same amount as in minimum salary. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. - Für ein exaktes Gehalt bitte denselben Betrag wie beim Mindestgehalt eintragen. Beträge bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17), nicht mit Komma oder Tausendertrennzeichen — der Browser kann den Wert sonst stillschweigend abschneiden. + For an exact salary, please enter the same amount as in minimum salary. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. + Für ein exaktes Gehalt bitte denselben Betrag wie beim Mindestgehalt eintragen. Der Betrag kann mit Tausendertrennzeichen und Komma als Dezimaltrennzeichen eingegeben werden (z. B. 3.842,17) — er wird beim Verlassen des Feldes automatisch normalisiert. @@ -221,8 +221,8 @@ Fester Betrag - Only relevant for salary grades without steps. Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. - Nur relevant für Besoldungsgruppen ohne Stufen. Betrag bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17) — kein Tausendertrennzeichen und kein Komma verwenden, da der Browser den Wert sonst stillschweigend abschneiden kann. + Only relevant for salary grades without steps. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. + Nur relevant für Besoldungsgruppen ohne Stufen. Der Betrag kann mit Tausendertrennzeichen und Komma als Dezimaltrennzeichen eingegeben werden (z. B. 3.842,17) — er wird beim Verlassen des Feldes automatisch normalisiert. Steps @@ -258,8 +258,8 @@ Betrag - Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. - Betrag bitte mit Punkt als Dezimaltrennzeichen eingeben (z. B. 3842.17) — kein Tausendertrennzeichen und kein Komma verwenden, da der Browser den Wert sonst stillschweigend abschneiden kann. + You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. + Der Betrag kann mit Tausendertrennzeichen und Komma als Dezimaltrennzeichen eingegeben werden (z. B. 3.842,17) — er wird beim Verlassen des Feldes automatisch normalisiert. diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 9715561..a2b21a5 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -101,13 +101,13 @@ Minimum salary - For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + For an exact salary, please enter the same amount in both minimum and maximum salary. Since jobs without salary information may no longer be displayed by law, both fields must be filled in. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. Maximum salary - For an exact salary, please enter the same amount as in minimum salary. Enter amounts using a decimal point (e.g. 3842.17), not a comma or thousands separator — the browser may otherwise silently truncate the value. + For an exact salary, please enter the same amount as in minimum salary. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. @@ -168,7 +168,7 @@ Fixed amount - Only relevant for salary grades without steps. Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + Only relevant for salary grades without steps. You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. Steps @@ -196,7 +196,7 @@ Amount - Enter the amount using a decimal point (e.g. 3842.17) — do not use a thousands separator or comma, as the browser may otherwise silently truncate the value. + You may enter the amount with a thousands separator and decimal comma (e.g. 3.842,17) — it is normalized automatically once you leave the field. diff --git a/Resources/Public/JavaScript/form-engine-localized-decimal.js b/Resources/Public/JavaScript/form-engine-localized-decimal.js new file mode 100644 index 0000000..1d8fae0 --- /dev/null +++ b/Resources/Public/JavaScript/form-engine-localized-decimal.js @@ -0,0 +1,41 @@ +class FormEngineLocalizedDecimal { + static initialize(fieldId) { + const field = document.getElementById(fieldId); + if (!field) { + return; + } + field.addEventListener('change', () => { + field.value = FormEngineLocalizedDecimal.normalize(field.value); + }); + } + + static normalize(rawValue) { + let value = String(rawValue).trim(); + if (value === '') { + return value; + } + + const negative = value.startsWith('-'); + if (negative) { + value = value.slice(1); + } + + const decimalPos = Math.max(value.lastIndexOf(','), value.lastIndexOf('.')); + let integerPart = value; + let fractionPart = '0'; + if (decimalPos !== -1) { + integerPart = value.slice(0, decimalPos); + fractionPart = value.slice(decimalPos + 1) || '0'; + } + integerPart = integerPart.split(',').join('').split('.').join(''); + + const number = parseFloat(integerPart + '.' + fractionPart); + if (Number.isNaN(number)) { + return ''; + } + + return (negative ? -number : number).toFixed(2); + } +} + +export default FormEngineLocalizedDecimal; diff --git a/composer.json b/composer.json index 01d2fab..19f612c 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ }, "require": { "typo3/cms-core": "^13.4", + "ext-intl": "*", "jweiland/maps2": "*", "friendsoftypo3/tt-address": "*", "bithost-gmbh/pdfviewhelpers": "*" diff --git a/ext_localconf.php b/ext_localconf.php index 04b7c1a..285a022 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -16,6 +16,12 @@ \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, ); +$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][] = [ + 'nodeName' => 'jobfair2LocalizedDecimal', + 'priority' => 40, + 'class' => \JWeiland\Jobfair2\Backend\Element\LocalizedDecimalElement::class, +]; + if (!isset($GLOBALS['TYPO3_CONF_VARS']['LOG']['JWeiland']['Jobfair2']['writerConfiguration'])) { $GLOBALS['TYPO3_CONF_VARS']['LOG']['JWeiland']['Jobfair2']['writerConfiguration'] = [ \Psr\Log\LogLevel::INFO => [ From f7291972d59d0f3e25da0e77ea1ed053100c55c9 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 13:48:22 +0200 Subject: [PATCH 07/26] [TASK] Use imports instead of FQCN in config files Configuration/TCA/*, Configuration/TCA/Overrides/*, RequestMiddlewares.php, Extbase/Persistence/Classes.php and ext_localconf.php referenced classes by their fully qualified name in the code body. Add use imports and reference the short class names instead, per project convention. Co-Authored-By: Claude Sonnet 5 --- Configuration/Extbase/Persistence/Classes.php | 4 +++- Configuration/RequestMiddlewares.php | 4 +++- Configuration/TCA/Overrides/tt_address.php | 6 +++-- Configuration/TCA/Overrides/tt_content.php | 9 +++++--- .../tx_jobfair2_domain_model_job.php | 7 ++++-- .../TCA/tx_jobfair2_domain_model_job.php | 22 ++++++++++--------- .../tx_jobfair2_domain_model_salarygrade.php | 4 +++- .../tx_jobfair2_domain_model_salarystep.php | 4 +++- ext_localconf.php | 20 +++++++++++------ 9 files changed, 52 insertions(+), 28 deletions(-) diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php index 1e6cfb0..82ab811 100644 --- a/Configuration/Extbase/Persistence/Classes.php +++ b/Configuration/Extbase/Persistence/Classes.php @@ -1,7 +1,9 @@ [ + Address::class => [ 'tableName' => 'tt_address', ], ]; diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index 31ed001..90deb74 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,9 +1,11 @@ [ 'jweiland/jobfair2-address-search' => [ - 'target' => \JWeiland\Jobfair2\Middleware\AddressSearchMiddleware::class, + 'target' => AddressSearchMiddleware::class, 'after' => [ // Must be loaded after "frontend.user" aspect (Context API) was initialized. // Needed for FrontendUserRestrictionContainer of QueryBuilder. diff --git a/Configuration/TCA/Overrides/tt_address.php b/Configuration/TCA/Overrides/tt_address.php index 9529c9e..30d7a5d 100644 --- a/Configuration/TCA/Overrides/tt_address.php +++ b/Configuration/TCA/Overrides/tt_address.php @@ -1,10 +1,12 @@ [ @@ -17,7 +19,7 @@ ], ], ); -\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( +ExtensionManagementUtility::addToAllTCAtypes( 'tt_address', 'import_key', '', diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index 29eec9f..0a7427f 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -1,18 +1,21 @@ add( +if (ExtensionManagementUtility::isLoaded('tt_address')) { + Maps2Registry::getInstance()->add( 'tt_address', 'tt_address', [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index d4e1f34..25bc877 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -1,5 +1,7 @@ value => [ + FileType::TEXT->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [ + FileType::IMAGE->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::AUDIO->value => [ + FileType::AUDIO->value => [ 'showitem' => ' --palette--;;audioOverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::VIDEO->value => [ + FileType::VIDEO->value => [ 'showitem' => ' --palette--;;videoOverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::APPLICATION->value => [ + FileType::APPLICATION->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', @@ -362,27 +364,27 @@ --palette--;;imageoverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::TEXT->value => [ + FileType::TEXT->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::IMAGE->value => [ + FileType::IMAGE->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::AUDIO->value => [ + FileType::AUDIO->value => [ 'showitem' => ' --palette--;;audioOverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::VIDEO->value => [ + FileType::VIDEO->value => [ 'showitem' => ' --palette--;;videoOverlayPalette, --palette--;;filePalette', ], - \TYPO3\CMS\Core\Resource\FileType::APPLICATION->value => [ + FileType::APPLICATION->value => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 6927b45..b5e031f 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -1,5 +1,7 @@ 'title', 'label_alt' => 'salary_table', 'label_alt_force' => true, - 'formattedLabel_userFunc' => \JWeiland\Jobfair2\UserFunc\InlineRecordTitleFormatter::class . '->formatSalaryGradeTitle', + 'formattedLabel_userFunc' => InlineRecordTitleFormatter::class . '->formatSalaryGradeTitle', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'sortby' => 'sorting', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 90225f0..c5429ce 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -1,5 +1,7 @@ 'step_label', 'label_alt' => 'amount', 'label_alt_force' => true, - 'label_userFunc' => \JWeiland\Jobfair2\UserFunc\SalaryStepTitleFormatter::class . '->formatTitle', + 'label_userFunc' => SalaryStepTitleFormatter::class . '->formatTitle', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'sortby' => 'sorting', diff --git a/ext_localconf.php b/ext_localconf.php index 285a022..fd1355d 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,31 +1,37 @@ 'list, search, detail', + JobfairController::class => 'list, search, detail', ], [ - \JWeiland\Jobfair2\Controller\JobfairController::class => 'search', + JobfairController::class => 'search', ], - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, + ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT, ); $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][] = [ 'nodeName' => 'jobfair2LocalizedDecimal', 'priority' => 40, - 'class' => \JWeiland\Jobfair2\Backend\Element\LocalizedDecimalElement::class, + 'class' => LocalizedDecimalElement::class, ]; if (!isset($GLOBALS['TYPO3_CONF_VARS']['LOG']['JWeiland']['Jobfair2']['writerConfiguration'])) { $GLOBALS['TYPO3_CONF_VARS']['LOG']['JWeiland']['Jobfair2']['writerConfiguration'] = [ - \Psr\Log\LogLevel::INFO => [ - \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [ + LogLevel::INFO => [ + FileWriter::class => [ 'logFileInfix' => 'jobfair2', ], ], From 6b245f7bc3afea0edabe95b04ebe3f2a2901670e Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:01:53 +0200 Subject: [PATCH 08/26] [TASK] Remove native TCA fields already added by core sys_language_uid, l10n_parent, l10n_diffsource, hidden, starttime and endtime were defined manually in all four tables' 'columns' arrays, duplicating exactly what TYPO3 core's TcaEnrichment already generates from the corresponding 'ctrl' settings (languageField, transOrigPointerField, transOrigDiffSourceField, enablecolumns). Move the only real customizations - the legal-notice description on SalaryTable/SalaryGrade's starttime/endtime and the allowLanguageSynchronization behaviour on all four tables - into small Configuration/TCA/Overrides/*.php files, which core applies after enrichment. While at it, fix Job's l10n_source column: the ctrl entry and the actual database column are both named l10n_diffsource, so that 'l10n_source' definition was dead configuration that never matched anything - and add the two missing label translations for SalaryGrade's starttime/endtime, which resolved to blank because their LLL keys never existed. Co-Authored-By: Claude Sonnet 5 --- .../tx_jobfair2_domain_model_job.php | 3 + .../tx_jobfair2_domain_model_salarygrade.php | 13 ++++ .../tx_jobfair2_domain_model_salarystep.php | 8 +++ .../tx_jobfair2_domain_model_salarytable.php | 13 ++++ .../TCA/tx_jobfair2_domain_model_job.php | 67 ------------------ .../tx_jobfair2_domain_model_salarygrade.php | 69 ------------------- .../tx_jobfair2_domain_model_salarystep.php | 67 ------------------ .../tx_jobfair2_domain_model_salarytable.php | 69 ------------------- .../Private/Language/de.locallang_db.xlf | 8 +++ Resources/Private/Language/locallang_db.xlf | 6 ++ 10 files changed, 51 insertions(+), 272 deletions(-) create mode 100644 Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarygrade.php create mode 100644 Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarystep.php create mode 100644 Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarytable.php diff --git a/Configuration/TCA/Overrides/tx_jobfair2_domain_model_job.php b/Configuration/TCA/Overrides/tx_jobfair2_domain_model_job.php index 72fc422..aed1333 100644 --- a/Configuration/TCA/Overrides/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/Overrides/tx_jobfair2_domain_model_job.php @@ -23,3 +23,6 @@ ], ); } + +$GLOBALS['TCA']['tx_jobfair2_domain_model_job']['columns']['starttime']['config']['behaviour']['allowLanguageSynchronization'] = true; +$GLOBALS['TCA']['tx_jobfair2_domain_model_job']['columns']['endtime']['config']['behaviour']['allowLanguageSynchronization'] = true; diff --git a/Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarygrade.php new file mode 100644 index 0000000..01d0f60 --- /dev/null +++ b/Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarygrade.php @@ -0,0 +1,13 @@ + [ - 'sys_language_uid' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', - 'config' => ['type' => 'language'], - ], - 'l10n_parent' => [ - 'displayCond' => 'FIELD:sys_language_uid:>:0', - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectSingle', - 'items' => [ - ['label' => '', 'value' => 0], - ], - 'foreign_table' => 'tx_jobfair2_domain_model_job', - 'foreign_table_where' => 'AND tx_jobfair2_domain_model_job.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_job.sys_language_uid IN (-1,0)', - 'fieldWizard' => [ - 'selectIcons' => [ - 'disabled' => true, - ], - ], - 'default' => 0, - ], - ], - 'l10n_source' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - 'hidden' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 'label' => '', - 'invertStateDisplay' => true, - ], - ], - ], - ], - 'starttime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], - 'endtime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], 'title' => [ 'exclude' => 1, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.title', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index b5e031f..cdeaf41 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -56,75 +56,6 @@ ], ], 'columns' => [ - 'sys_language_uid' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', - 'config' => ['type' => 'language'], - ], - 'l10n_parent' => [ - 'displayCond' => 'FIELD:sys_language_uid:>:0', - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectSingle', - 'items' => [ - ['label' => '', 'value' => 0], - ], - 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', - 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarygrade.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0)', - 'fieldWizard' => [ - 'selectIcons' => [ - 'disabled' => true, - ], - ], - 'default' => 0, - ], - ], - 'l10n_diffsource' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - 'hidden' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 'label' => '', - 'invertStateDisplay' => true, - ], - ], - ], - ], - 'starttime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.starttime.description', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], - 'endtime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.endtime.description', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], 'salary_table' => [ 'config' => [ 'type' => 'group', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index c5429ce..65a5021 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -42,73 +42,6 @@ ], ], 'columns' => [ - 'sys_language_uid' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', - 'config' => ['type' => 'language'], - ], - 'l10n_parent' => [ - 'displayCond' => 'FIELD:sys_language_uid:>:0', - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectSingle', - 'items' => [ - ['label' => '', 'value' => 0], - ], - 'foreign_table' => 'tx_jobfair2_domain_model_salarystep', - 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarystep.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarystep.sys_language_uid IN (-1,0)', - 'fieldWizard' => [ - 'selectIcons' => [ - 'disabled' => true, - ], - ], - 'default' => 0, - ], - ], - 'l10n_diffsource' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - 'hidden' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 'label' => '', - 'invertStateDisplay' => true, - ], - ], - ], - ], - 'starttime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], - 'endtime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], 'salary_grade' => [ 'config' => [ 'type' => 'passthrough', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php index 49a4a98..6cccc1e 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -37,75 +37,6 @@ ], ], 'columns' => [ - 'sys_language_uid' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', - 'config' => ['type' => 'language'], - ], - 'l10n_parent' => [ - 'displayCond' => 'FIELD:sys_language_uid:>:0', - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', - 'config' => [ - 'type' => 'select', - 'renderType' => 'selectSingle', - 'items' => [ - ['label' => '', 'value' => 0], - ], - 'foreign_table' => 'tx_jobfair2_domain_model_salarytable', - 'foreign_table_where' => 'AND tx_jobfair2_domain_model_salarytable.pid=###CURRENT_PID### AND tx_jobfair2_domain_model_salarytable.sys_language_uid IN (-1,0)', - 'fieldWizard' => [ - 'selectIcons' => [ - 'disabled' => true, - ], - ], - 'default' => 0, - ], - ], - 'l10n_diffsource' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], - 'hidden' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible', - 'config' => [ - 'type' => 'check', - 'renderType' => 'checkboxToggle', - 'items' => [ - [ - 'label' => '', - 'invertStateDisplay' => true, - ], - ], - ], - ], - 'starttime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.starttime.description', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], - 'endtime' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.endtime.description', - 'config' => [ - 'type' => 'datetime', - 'size' => 16, - 'default' => 0, - 'behaviour' => [ - 'allowLanguageSynchronization' => true, - ], - ], - ], 'title' => [ 'exclude' => 1, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.title', diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index ed429d8..d5b6bc7 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -232,10 +232,18 @@ Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. Nicht jede Stufe muss existieren — Lücken sind zulässig. Minimum/Maximum werden im Frontend automatisch aus den tatsächlich vorhandenen Stufen ermittelt. + + Valid from + Gültig ab + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. + + Valid until + Gültig bis + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index a2b21a5..7c5f4d7 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -176,9 +176,15 @@ Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. + + Valid from + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. + + Valid until + Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. From 931b9db9bce39fb87ef670c0bbb70b1a9994b015 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:05:12 +0200 Subject: [PATCH 09/26] [TASK] Use boolean for TCA exclude flag 'exclude' is documented as a boolean; the integer 1 worked by coincidence (PHP truthiness) but did not match the type TCA actually expects. Co-Authored-By: Claude Sonnet 5 --- .../TCA/tx_jobfair2_domain_model_job.php | 40 +++++++++---------- .../TCA/tx_jobfair2_domain_model_jobarea.php | 2 +- .../TCA/tx_jobfair2_domain_model_jobtype.php | 2 +- .../tx_jobfair2_domain_model_salarygrade.php | 8 ++-- .../tx_jobfair2_domain_model_salarystep.php | 4 +- .../tx_jobfair2_domain_model_salarytable.php | 6 +-- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index 6026894..ff28df0 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -63,7 +63,7 @@ ], 'columns' => [ 'title' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.title', 'config' => [ 'type' => 'input', @@ -74,7 +74,7 @@ ], ], 'reference_number' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.reference_number', 'config' => [ 'type' => 'input', @@ -85,7 +85,7 @@ ], ], 'is_import' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.is_import', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.is_import.description', 'config' => [ @@ -96,7 +96,7 @@ ], ], 'vacancy_id' => [ - 'exclude' => 1, + 'exclude' => true, 'displayCond' => 'FIELD:is_import:REQ:true', 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.vacancy_id', 'config' => [ @@ -106,7 +106,7 @@ ], ], 'description' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.description', 'config' => [ 'type' => 'text', @@ -114,7 +114,7 @@ ], ], 'address' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.address', 'config' => [ 'type' => 'group', @@ -139,7 +139,7 @@ ], ], 'job_area' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.job_area', 'config' => [ 'type' => 'select', @@ -155,7 +155,7 @@ ], ], 'job_type' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.job_type', 'config' => [ 'type' => 'select', @@ -171,7 +171,7 @@ ], ], 'start_date' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.start_date', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.start_date.description', 'config' => [ @@ -182,7 +182,7 @@ ], ], 'ending_date' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.ending_date', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.ending_date.description', 'config' => [ @@ -193,7 +193,7 @@ ], ], 'employer' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer', 'config' => [ 'type' => 'input', @@ -204,7 +204,7 @@ ], ], 'employer_address' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer_address', 'config' => [ 'type' => 'group', @@ -222,7 +222,7 @@ ], ], 'email' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.email', 'config' => [ 'type' => 'email', @@ -230,7 +230,7 @@ ], ], 'tender_file' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.tender_file', 'config' => [ //## !!! Watch out for fieldName different from columnName @@ -280,7 +280,7 @@ ], ], 'pdf_files' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.pdf_files', 'config' => [ //## !!! Watch out for fieldName different from columnName @@ -342,7 +342,7 @@ ], ], 'is_internal' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.is_internal', 'config' => [ 'type' => 'select', @@ -357,7 +357,7 @@ ], ], 'salary_mode' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_mode.description', 'config' => [ @@ -371,7 +371,7 @@ ], ], 'salary_grade' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade.description', 'config' => [ @@ -389,7 +389,7 @@ ], ], 'salary_min' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min.description', 'config' => [ @@ -403,7 +403,7 @@ ], ], 'salary_max' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max.description', 'config' => [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php index 409b3dc..f4e3dae 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php @@ -97,7 +97,7 @@ ], ], 'title' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_jobarea.title', 'config' => [ 'type' => 'input', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php b/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php index 65aee49..c0dbc0e 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php @@ -97,7 +97,7 @@ ], ], 'title' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_jobtype.title', 'config' => [ 'type' => 'input', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index cdeaf41..73e48c3 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -63,7 +63,7 @@ ], ], 'title' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.title', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.title.description', 'config' => [ @@ -75,7 +75,7 @@ ], ], 'has_steps' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.has_steps', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.has_steps.description', 'config' => [ @@ -85,7 +85,7 @@ ], ], 'flat_amount' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.flat_amount', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.flat_amount.description', 'config' => [ @@ -99,7 +99,7 @@ ], ], 'salary_steps' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.salary_steps', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.salary_steps.description', 'config' => [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 65a5021..2045526 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -48,7 +48,7 @@ ], ], 'step_label' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.step_label', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.step_label.description', 'config' => [ @@ -60,7 +60,7 @@ ], ], 'amount' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.amount', 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.amount.description', 'config' => [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php index 6cccc1e..834ccdb 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -38,7 +38,7 @@ ], 'columns' => [ 'title' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.title', 'config' => [ 'type' => 'input', @@ -49,7 +49,7 @@ ], ], 'description' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.description', 'config' => [ 'type' => 'text', @@ -58,7 +58,7 @@ ], ], 'salary_grades' => [ - 'exclude' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.salary_grades', 'config' => [ 'type' => 'inline', From 3269826791e17bd8b7908a729c4aedeffce5265f Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:13:51 +0200 Subject: [PATCH 10/26] [TASK] Type and sort TCA types/typeicon_classes keys 'types' array keys must match the actual type of the value the ctrl['type'] pointer field holds. has_steps and salary_mode are int-valued (checkbox / select with integer item values), and tables without a ctrl['type'] resolve to int 0 internally (BackendUtility::getTCAtypeValue()) - so all of them get int keys instead of numeric strings. Also reorder them ascending (SalaryGrade had '1' before '0') and apply the same key typing/order to typeicon_classes. Co-Authored-By: Claude Sonnet 5 --- Configuration/TCA/tx_jobfair2_domain_model_job.php | 8 ++++---- Configuration/TCA/tx_jobfair2_domain_model_jobarea.php | 2 +- Configuration/TCA/tx_jobfair2_domain_model_jobtype.php | 2 +- .../TCA/tx_jobfair2_domain_model_salarygrade.php | 10 +++++----- .../TCA/tx_jobfair2_domain_model_salarystep.php | 2 +- .../TCA/tx_jobfair2_domain_model_salarytable.php | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index ff28df0..7b40f4d 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -30,7 +30,7 @@ ], ], 'types' => [ - '0' => [ + 0 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, --palette--;Job;titleReference, --palette--;Import;importVacancy, @@ -40,7 +40,7 @@ --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], - '1' => [ + 1 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, --palette--;Job;titleReference, --palette--;Import;importVacancy, @@ -242,7 +242,7 @@ // Use the imageoverlayPalette instead of the basicoverlayPalette 'overrideChildTca' => [ 'types' => [ - '0' => [ + 0 => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', @@ -292,7 +292,7 @@ // Use the imageoverlayPalette instead of the basicoverlayPalette 'overrideChildTca' => [ 'types' => [ - '0' => [ + 0 => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette', diff --git a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php index f4e3dae..1cf34da 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php @@ -23,7 +23,7 @@ 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/jobarea.svg', ], 'types' => [ - '0' => ['showitem' => 'title'], + 0 => ['showitem' => 'title'], ], 'palettes' => [ '1' => ['showitem' => ''], diff --git a/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php b/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php index c0dbc0e..b63a92b 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php @@ -23,7 +23,7 @@ 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/jobtype.svg', ], 'types' => [ - '0' => ['showitem' => 'hidden, title'], + 0 => ['showitem' => 'hidden, title'], ], 'palettes' => [ '1' => ['showitem' => ''], diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 73e48c3..0e0ee0d 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -20,8 +20,8 @@ 'typeicon_column' => 'has_steps', 'typeicon_classes' => [ 'default' => 'ext-jobfair2-record-salarygrade-stepped', - 1 => 'ext-jobfair2-record-salarygrade-stepped', 0 => 'ext-jobfair2-record-salarygrade-flat', + 1 => 'ext-jobfair2-record-salarygrade-stepped', ], 'languageField' => 'sys_language_uid', 'transOrigPointerField' => 'l10n_parent', @@ -35,15 +35,15 @@ 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarygrade.svg', ], 'types' => [ - '1' => [ + 0 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - --palette--;;titleStep, salary_steps, + --palette--;;titleStep, flat_amount, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;;access', ], - '0' => [ + 1 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - --palette--;;titleStep, flat_amount, + --palette--;;titleStep, salary_steps, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, --palette--;;access', ], diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 2045526..76ee67c 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -28,7 +28,7 @@ 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarystep.svg', ], 'types' => [ - '0' => [ + 0 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, step_label, amount, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php index 834ccdb..ff87540 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -23,7 +23,7 @@ 'iconfile' => 'EXT:jobfair2/Resources/Public/Icons/salarytable.svg', ], 'types' => [ - '0' => [ + 0 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, title, description, salary_grades, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, From d3154583d097f465b76acc98c52c851e3bb4a3eb Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:20:09 +0200 Subject: [PATCH 11/26] [TASK] Order TCA columns to match showitem Reorder each table's 'columns' array to follow the same sequence fields actually appear in via showitem/palettes, so the config file reads top-to-bottom the same way the form does. Fields with no showitem placement at all (SalaryGrade's salary_table, SalaryStep's salary_grade - both exist only to resolve the parent title via label_alt, plus Job's non-form fields like link/tender_file/ pdf_files/is_internal/salary_mode) are grouped at the end. Co-Authored-By: Claude Sonnet 5 --- .../TCA/tx_jobfair2_domain_model_job.php | 112 +++++++++--------- .../tx_jobfair2_domain_model_salarygrade.php | 12 +- .../tx_jobfair2_domain_model_salarystep.php | 10 +- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_job.php b/Configuration/TCA/tx_jobfair2_domain_model_job.php index 7b40f4d..137ed73 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_job.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_job.php @@ -132,12 +132,6 @@ 'required' => true, ], ], - 'link' => [ - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.link', - 'config' => [ - 'type' => 'passthrough', - ], - ], 'job_area' => [ 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.job_area', @@ -203,6 +197,14 @@ 'required' => true, ], ], + 'email' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.email', + 'config' => [ + 'type' => 'email', + 'required' => true, + ], + ], 'employer_address' => [ 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer_address', @@ -221,12 +223,56 @@ ], ], ], - 'email' => [ + 'salary_grade' => [ 'exclude' => true, - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.email', + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade.description', 'config' => [ - 'type' => 'email', - 'required' => true, + 'type' => 'group', + 'allowed' => 'tx_jobfair2_domain_model_salarygrade', + 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', + 'minitems' => 1, + 'maxitems' => 1, + 'size' => 1, + 'suggestOptions' => [ + 'default' => [ + 'addWhere' => 'AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0)', + ], + ], + ], + ], + 'salary_min' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], + 'salary_max' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max', + 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max.description', + 'config' => [ + 'type' => 'number', + 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], + 'link' => [ + 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.link', + 'config' => [ + 'type' => 'passthrough', ], ], 'tender_file' => [ @@ -370,51 +416,5 @@ 'default' => 0, ], ], - 'salary_grade' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_grade.description', - 'config' => [ - 'type' => 'group', - 'allowed' => 'tx_jobfair2_domain_model_salarygrade', - 'foreign_table' => 'tx_jobfair2_domain_model_salarygrade', - 'minitems' => 1, - 'maxitems' => 1, - 'size' => 1, - 'suggestOptions' => [ - 'default' => [ - 'addWhere' => 'AND tx_jobfair2_domain_model_salarygrade.sys_language_uid IN (-1,0)', - ], - ], - ], - ], - 'salary_min' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_min.description', - 'config' => [ - 'type' => 'number', - 'format' => 'decimal', - 'renderType' => 'jobfair2LocalizedDecimal', - 'range' => [ - 'lower' => 0, - ], - 'default' => 0.00, - ], - ], - 'salary_max' => [ - 'exclude' => true, - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max', - 'description' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.salary_max.description', - 'config' => [ - 'type' => 'number', - 'format' => 'decimal', - 'renderType' => 'jobfair2LocalizedDecimal', - 'range' => [ - 'lower' => 0, - ], - 'default' => 0.00, - ], - ], ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 0e0ee0d..0634494 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -56,12 +56,6 @@ ], ], 'columns' => [ - 'salary_table' => [ - 'config' => [ - 'type' => 'group', - 'allowed' => 'tx_jobfair2_domain_model_salarytable', - ], - ], 'title' => [ 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.title', @@ -116,5 +110,11 @@ ], ], ], + 'salary_table' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'tx_jobfair2_domain_model_salarytable', + ], + ], ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index 76ee67c..feb03ff 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -42,11 +42,6 @@ ], ], 'columns' => [ - 'salary_grade' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], 'step_label' => [ 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarystep.step_label', @@ -74,5 +69,10 @@ 'default' => 0.00, ], ], + 'salary_grade' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], ], ]; From 03460946d1e101c33512fab955faa3a48d528d0e Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:27:22 +0200 Subject: [PATCH 12/26] [TASK] Remove redundant foreign_field pointer column SalaryStep's salary_grade column just declared type=>passthrough, duplicating exactly what DefaultTcaSchema already generates for a foreign_field pointer column (int(11) NOT NULL DEFAULT 0) when the child table leaves it undefined or passthrough. Drop it from both the TCA columns array and ext_tables.sql. SalaryGrade's own salary_table pointer (the analogous column for SalaryTable's salary_grades relation) is intentionally left as-is: it was changed from passthrough to type=>group earlier so its title can be resolved for label_alt, and DefaultTcaSchema's passthrough fallback does not apply to a group-typed column - its generic default would be a nullable TEXT column instead of the NOT NULL int the relation actually needs, so the manual ext_tables.sql declaration for that one stays. Co-Authored-By: Claude Sonnet 5 --- Configuration/TCA/tx_jobfair2_domain_model_salarystep.php | 5 ----- ext_tables.sql | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index feb03ff..aa6618b 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -69,10 +69,5 @@ 'default' => 0.00, ], ], - 'salary_grade' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], ], ]; diff --git a/ext_tables.sql b/ext_tables.sql index b1e6b9a..83a9e71 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -39,8 +39,7 @@ CREATE TABLE tx_jobfair2_domain_model_salarygrade # CREATE TABLE tx_jobfair2_domain_model_salarystep ( - salary_grade int(11) DEFAULT '0' NOT NULL, - amount decimal(10,2) DEFAULT '0.00' NOT NULL + amount decimal(10,2) DEFAULT '0.00' NOT NULL ); # From 5fa1cf82f51048c5981b9277041693be765319a6 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 14:38:47 +0200 Subject: [PATCH 13/26] [TASK] Resolve SalaryGrade's parent title without a group field type=>group on salary_table let editors jump straight from a SalaryGrade child record into editing its parent SalaryTable via the group field's built-in edit link - not something an inline relation pointer column should ever offer. Revert it to passthrough (its foreign_field default) and resolve the parent's title through a full label_userFunc instead, using BackendUtility::getRecordWSOL() + getRecordTitle() - both already handle workspace overlay internally, so no manual QueryBuilder code is needed for this. Rename InlineRecordTitleFormatter to SalaryGradeTitleFormatter and add formatTitle() (ctrl-level label_userFunc, includes the parent title) alongside the existing formatInlineChildTitle() (IRRE-only, title alone - FormEngine's TcaRecordTitle prioritizes formattedLabel_userFunc over label_userFunc for inline children, so the two never conflict). label_alt/label_alt_force on SalaryGrade are removed, since label_userFunc bypasses that mechanism entirely. The salary_table column itself drops out of ext_tables.sql again - passthrough is DefaultTcaSchema's foreign_field fallback case, so the int(11) column is auto-generated same as SalaryStep's salary_grade. Co-Authored-By: Claude Sonnet 5 --- .../UserFunc/InlineRecordTitleFormatter.php | 25 -------- .../UserFunc/SalaryGradeTitleFormatter.php | 59 +++++++++++++++++++ .../tx_jobfair2_domain_model_salarygrade.php | 10 ++-- ext_tables.sql | 3 +- 4 files changed, 64 insertions(+), 33 deletions(-) delete mode 100644 Classes/UserFunc/InlineRecordTitleFormatter.php create mode 100644 Classes/UserFunc/SalaryGradeTitleFormatter.php diff --git a/Classes/UserFunc/InlineRecordTitleFormatter.php b/Classes/UserFunc/InlineRecordTitleFormatter.php deleted file mode 100644 index ad683e5..0000000 --- a/Classes/UserFunc/InlineRecordTitleFormatter.php +++ /dev/null @@ -1,25 +0,0 @@ -buildTitle($parameters['row'], true); + } + + public function formatInlineChildTitle(array &$parameters): void + { + $parameters['title'] = $this->buildTitle($parameters['row'], false); + } + + private function buildTitle(array $row, bool $includeSalaryTable): string + { + $title = (string)($row['title'] ?? ''); + $salaryTableTitle = $includeSalaryTable + ? $this->resolveSalaryTableTitle((int)($row['salary_table'] ?? 0)) + : ''; + + return $salaryTableTitle === '' ? $title : $title . ', ' . $salaryTableTitle; + } + + private function resolveSalaryTableTitle(int $uid): string + { + if ($uid <= 0) { + return ''; + } + + $salaryTableRow = BackendUtility::getRecordWSOL('tx_jobfair2_domain_model_salarytable', $uid); + if ($salaryTableRow === null) { + return ''; + } + + return BackendUtility::getRecordTitle('tx_jobfair2_domain_model_salarytable', $salaryTableRow); + } +} diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index 0634494..ad306a4 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -1,6 +1,6 @@ [ 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade', 'label' => 'title', - 'label_alt' => 'salary_table', - 'label_alt_force' => true, - 'formattedLabel_userFunc' => InlineRecordTitleFormatter::class . '->formatSalaryGradeTitle', + 'label_userFunc' => SalaryGradeTitleFormatter::class . '->formatTitle', + 'formattedLabel_userFunc' => SalaryGradeTitleFormatter::class . '->formatInlineChildTitle', 'tstamp' => 'tstamp', 'crdate' => 'crdate', 'sortby' => 'sorting', @@ -112,8 +111,7 @@ ], 'salary_table' => [ 'config' => [ - 'type' => 'group', - 'allowed' => 'tx_jobfair2_domain_model_salarytable', + 'type' => 'passthrough', ], ], ], diff --git a/ext_tables.sql b/ext_tables.sql index 83a9e71..a35b899 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -30,8 +30,7 @@ CREATE TABLE tx_jobfair2_domain_model_job # CREATE TABLE tx_jobfair2_domain_model_salarygrade ( - salary_table int(11) DEFAULT '0' NOT NULL, - flat_amount decimal(10,2) DEFAULT '0.00' NOT NULL + flat_amount decimal(10,2) DEFAULT '0.00' NOT NULL ); # From 3979255055c0e13c0859529e2238ca9a0bbe163e Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:00:27 +0200 Subject: [PATCH 14/26] [TASK] Use TYPO3 native labels for the access palette TcaPreparation::addSystemFieldsToShowitemTypes() only auto-adds the access tab for tt_content, not for custom extension tables, so the access palette/div still has to be declared explicitly in showitem. However, starttime/endtime already have official core labels (LLL:EXT:frontend...:starttime_formlabel / :endtime_formlabel) and the access palette itself has an official title (LLL:EXT:frontend...:pages.palettes.access), exactly as used by EXT:maps2's poicollection TCA and already used in job.php here. Adopt that native pattern for salarygrade, salarystep and salarytable instead of maintaining redundant custom labels, and drop the now-unused tx_jobfair2_domain_model_salarygrade/salarytable .starttime/.endtime label translations (EN+DE) - the .description keys with the domain-specific legal notice stay untouched. --- .../TCA/tx_jobfair2_domain_model_salarygrade.php | 11 +++-------- .../TCA/tx_jobfair2_domain_model_salarystep.php | 2 +- .../TCA/tx_jobfair2_domain_model_salarytable.php | 4 ++-- Resources/Private/Language/de.locallang_db.xlf | 16 ---------------- Resources/Private/Language/locallang_db.xlf | 12 ------------ 5 files changed, 6 insertions(+), 39 deletions(-) diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php index ad306a4..481ee23 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -38,20 +38,20 @@ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, --palette--;;titleStep, flat_amount, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, - --palette--;;access', + --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], 1 => [ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, --palette--;;titleStep, salary_steps, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, - --palette--;;access', + --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], ], 'palettes' => [ 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], 'titleStep' => ['showitem' => 'title, has_steps'], 'access' => [ - 'showitem' => 'starttime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.starttime,endtime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade.endtime', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', ], ], 'columns' => [ @@ -109,10 +109,5 @@ ], ], ], - 'salary_table' => [ - 'config' => [ - 'type' => 'passthrough', - ], - ], ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php index aa6618b..7979cde 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -32,7 +32,7 @@ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, step_label, amount, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, - --palette--;;access', + --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], ], 'palettes' => [ diff --git a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php index ff87540..a7e3995 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -27,13 +27,13 @@ 'showitem' => '--palette--;;languageHidden, l10n_diffsource, title, description, salary_grades, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tabs.access, - --palette--;;access', + --palette--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.access;access', ], ], 'palettes' => [ 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], 'access' => [ - 'showitem' => 'starttime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.starttime,endtime;LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarytable.endtime', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', ], ], 'columns' => [ diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index d5b6bc7..d09982c 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -179,14 +179,6 @@ Salary grades Besoldungsgruppen - - Valid from (document) - Gültig ab (Dokument) - - - Valid until (document) - Gültig bis (Dokument) - For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). Dient nur der redaktionellen Dokumentation/Organisation dieses Dokuments. Die rechtlich wirksame Gültigkeit wird auf Ebene der einzelnen Besoldungsgruppe gepflegt (siehe dort). @@ -232,18 +224,10 @@ Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. Nicht jede Stufe muss existieren — Lücken sind zulässig. Minimum/Maximum werden im Frontend automatisch aus den tatsächlich vorhandenen Stufen ermittelt. - - Valid from - Gültig ab - Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. - - Valid until - Gültig bis - Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. Wichtig: Läuft dieser Zeitraum ab bzw. hat er noch nicht begonnen, wird diese Besoldungsgruppe ausgeblendet — und damit auch jeder Job, der auf sie verweist, da Jobs ohne Gehaltsangabe laut Gesetz nicht mehr angezeigt werden dürfen. Bitte neue Werte rechtzeitig vor Ablauf pflegen. diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 7c5f4d7..4269f85 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -136,12 +136,6 @@ Salary grades - - Valid from (document) - - - Valid until (document) - For editorial documentation/organization of this document only. The legally binding validity is maintained at the individual salary grade level (see there). @@ -176,15 +170,9 @@ Not every step has to exist — gaps are allowed. Minimum/maximum are calculated in the frontend from the steps actually present. - - Valid from - Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. - - Valid until - Important: once this period expires (or has not started yet), this salary grade is hidden — and so is every job referencing it, since jobs without salary information may no longer be displayed by law. Please maintain new values in time. From 90c297cdb433b98483a2d373367f5ec4f60a8cc8 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:04:05 +0200 Subject: [PATCH 15/26] [TASK] Add .editorconfig This extension had no .editorconfig yet, unlike other jweiland/* extensions. Add the standard jweiland.net configuration (4-space indent for PHP, tabs for JSON/XLF/SQL, LF line endings, trimmed trailing whitespace) so editors format new files consistently with the rest of the ecosystem. --- .editorconfig | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..72bdfe1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,64 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +# TS/JS-Files +[*.{ts,js,mjs}] +indent_size = 2 + +# JSON-Files +[*.json] +indent_style = tab + +# ReST-Files +[*.{rst,rst.txt}] +indent_size = 4 +max_line_length = 80 + +# Markdown-Files +[*.md] +max_line_length = 80 + +# YAML-Files +[*.{yaml,yml}] +indent_size = 2 + +# NEON-Files +[*.neon] +indent_size = 2 +indent_style = tab + +# stylelint +[.stylelintrc] +indent_size = 2 + +# package.json +[package.json] +indent_size = 2 + +# TypoScript +[*.{typoscript,tsconfig}] +indent_size = 2 + +# XLF-Files +[*.xlf] +indent_style = tab + +# SQL-Files +[*.sql] +indent_style = tab +indent_size = 2 + +# .htaccess +[{_.htaccess,.htaccess}] +indent_style = tab From eded01509ffcb8911d2c64adc4efb8c957d31894 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:04:14 +0200 Subject: [PATCH 16/26] [TASK] Clean up composer.json metadata - Use the SPDX-preferred "GPL-2.0-or-later" identifier instead of the deprecated "GPL-2.0+" suffix notation. - Prefix the description with "Job fair 2 -" to match ext_emconf.php's title and disambiguate from the original (non-2) jobfair extension. - Alphabetize the require block (ext-intl before typo3/cms-core). --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 19f612c..77163a7 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "jweiland/jobfair2", "type": "typo3-cms-extension", - "description": "Job fair implementation using Maps2 and tt_address to display jobs", - "license": "GPL-2.0+", + "description": "Job fair 2 - Job fair implementation using Maps2 and tt_address to display jobs", + "license": "GPL-2.0-or-later", "keywords": [ "typo3", "TYPO3 CMS", @@ -23,8 +23,8 @@ "source": "https://github.com/jweiland-net/jobfair2" }, "require": { - "typo3/cms-core": "^13.4", "ext-intl": "*", + "typo3/cms-core": "^13.4", "jweiland/maps2": "*", "friendsoftypo3/tt-address": "*", "bithost-gmbh/pdfviewhelpers": "*" From e5fb265a05f902bfb93bb1d4bd639223fad5effc Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:04:23 +0200 Subject: [PATCH 17/26] [TASK] Update extension author to jweiland.net jobfair2 was extracted from Markus Kugler's original project into its own jweiland.net-maintained package (see CLAUDE.md history section). Update author/author_mail to reflect the new maintainer. --- ext_emconf.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext_emconf.php b/ext_emconf.php index 549d960..416e62e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,8 +4,8 @@ 'title' => 'Job fair 2', 'description' => 'Job fair implementation using Maps2 and tt_address to display jobs', 'category' => 'plugin', - 'author' => 'Markus Kugler', - 'author_mail' => 'projects@ma-ku.eu', + 'author' => 'Stefan Froemken', + 'author_mail' => 'projects@jweiland.net', 'state' => 'alpha', 'version' => '0.0.1', 'constraints' => [ From 81d14cb60308fe380797b39d748c1c6460cd731a Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:04:37 +0200 Subject: [PATCH 18/26] [TASK] Remove ext_tables.sql columns auto-generated from TCA Continues the cleanup already applied to salarygrade/salarystep: DefaultTcaSchema derives a matching DB column for every plain input, text, check, datetime, file, select and single-table group field from its TCA config alone, so declaring them again here is redundant boilerplate that has to be kept in sync by hand. Removed: all tx_jobfair2_domain_model_job columns except the two decimal fields, the whole tx_jobfair2_domain_model_jobarea/-jobtype blocks (each only had the auto-generated "title" column), and the tt_address "import_key" column (defined via TCA in Configuration/TCA/Overrides/tt_address.php). Kept: salary_min/salary_max (job), flat_amount (salarygrade) and amount (salarystep) - decimal precision still requires an explicit declaration, TCA alone cannot express it (see Documentation-internal note in the TCA decimal fields). Also reformats the kept decimal(10,2) declarations to decimal(10, 2) for consistent spacing. --- ext_tables.sql | 50 ++++---------------------------------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/ext_tables.sql b/ext_tables.sql index a35b899..86f2220 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -3,26 +3,8 @@ # CREATE TABLE tx_jobfair2_domain_model_job ( - title varchar(250) DEFAULT '' NOT NULL, - reference_number varchar(120) DEFAULT '' NOT NULL, - is_import tinyint(1) DEFAULT '0' NOT NULL, - vacancy_id varchar(30) DEFAULT '0' NOT NULL, - description text, - address int(11) DEFAULT '0' NOT NULL, - link varchar(255) DEFAULT '' NOT NULL, - job_area int(11) DEFAULT '0' NOT NULL, - job_type int(11) DEFAULT '0' NOT NULL, - start_date int(11) DEFAULT '0' NOT NULL, - ending_date int(11) DEFAULT '0' NOT NULL, - employer varchar(120) DEFAULT '' NOT NULL, - email varchar(120) DEFAULT '' NOT NULL, - employer_address int(11) DEFAULT '0' NOT NULL, - tender_file int(11) DEFAULT '0' NOT NULL, - pdf_files int(11) DEFAULT '0' NOT NULL, - pdf_tstamp int(10) DEFAULT '0' NOT NULL, - is_internal tinyint(4) UNSIGNED DEFAULT '0' NOT NULL, - salary_min decimal(10,2) DEFAULT '0.00' NOT NULL, - salary_max decimal(10,2) DEFAULT '0.00' NOT NULL + salary_min decimal(10, 2) DEFAULT '0.00' NOT NULL, + salary_max decimal(10, 2) DEFAULT '0.00' NOT NULL ); # @@ -30,7 +12,7 @@ CREATE TABLE tx_jobfair2_domain_model_job # CREATE TABLE tx_jobfair2_domain_model_salarygrade ( - flat_amount decimal(10,2) DEFAULT '0.00' NOT NULL + flat_amount decimal(10, 2) DEFAULT '0.00' NOT NULL ); # @@ -38,29 +20,5 @@ CREATE TABLE tx_jobfair2_domain_model_salarygrade # CREATE TABLE tx_jobfair2_domain_model_salarystep ( - amount decimal(10,2) DEFAULT '0.00' NOT NULL -); - -# -# Table structure for table 'tx_jobfair2_domain_model_jobarea' -# -CREATE TABLE tx_jobfair2_domain_model_jobarea -( - title varchar(60) DEFAULT '' NOT NULL -); - -# -# Table structure for table 'tx_jobfair2_domain_model_jobtype' -# -CREATE TABLE tx_jobfair2_domain_model_jobtype -( - title varchar(60) DEFAULT '' NOT NULL -); - -# -# Table structure for table 'tt_address' -# -CREATE TABLE tt_address -( - import_key varchar(60) DEFAULT '' NOT NULL + amount decimal(10, 2) DEFAULT '0.00' NOT NULL ); From 960b95914d6c5534447643e25bc7077cc8c2809d Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:05:03 +0200 Subject: [PATCH 19/26] [TASK] Rename LICENSE to LICENSE.txt Matches the composer.json convention of other jweiland/* extensions, which ship their license text as LICENSE.txt. --- LICENSE => LICENSE.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.txt (100%) diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt From 69c157a18e9fbdbf2307da5c914ab36afe98e1d3 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:19:06 +0200 Subject: [PATCH 20/26] [TASK] Add test/build tooling to composer.json Replace the stale require-dev (roave/security-advisories dev-latest, phpunit/phpunit ~4.8.0 - far behind the PHPUnit version shipped by typo3/testing-framework ^13.4) with the actual tooling used by Build/Scripts/runTests.sh: typo3/testing-framework, typo3/coding- standards and ergebnis/composer-normalize. Add autoload-dev for Tests/, and the composer/TYPO3 "config"/"extra" block (.Build vendor- dir, bin-dir, web-dir) the test runner and CI workflow expect. --- composer.json | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 77163a7..fef556f 100644 --- a/composer.json +++ b/composer.json @@ -30,17 +30,35 @@ "bithost-gmbh/pdfviewhelpers": "*" }, "require-dev": { - "roave/security-advisories": "dev-latest", - "phpunit/phpunit": "~4.8.0" + "ergebnis/composer-normalize": "^2.44", + "typo3/coding-standards": "^0.8", + "typo3/testing-framework": "^9.1.2" }, "autoload": { "psr-4": { "JWeiland\\Jobfair2\\": "Classes" } }, + "autoload-dev": { + "psr-4": { + "JWeiland\\Jobfair2\\Tests\\": "Tests" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true + }, + "bin-dir": ".Build/bin", + "sort-packages": true, + "vendor-dir": ".Build/vendor" + }, "extra": { "typo3/cms": { - "extension-key": "jobfair2" + "app-dir": ".Build", + "extension-key": "jobfair2", + "web-dir": ".Build/web" } } } From 0884a31e4d13f2d8cbb80d92d924bd74efcc8113 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:19:17 +0200 Subject: [PATCH 21/26] [TASK] Add .gitignore and .gitattributes Standard jweiland.net repo hygiene files: .gitignore excludes the .Build/ test-tooling vendor dir, editor/OS cruft and composer.lock; .gitattributes export-ignores CI/Build/Tests/dotfiles from composer/TER package archives so they don't ship to end users. --- .gitattributes | 7 +++++++ .gitignore | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a697bca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +/.github/ export-ignore +/Build/ export-ignore +/Tests/ export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.editorconfig export-ignore +/.phpstorm.meta.php export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0b4ee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +######################## +# jobfair2 +# global ignore file +######################## + +# Ignore files generated by docs rendering +*GENERATED* +docker-compose.yaml +docker-compose.yml + +# Ignore environment files +.env + +# Ignore temporary files (left by editors and OS) +*~ +*.bak +*.swp +.DS_Store + +# Ignore by common IDEs used directories/files +nbproject +*.idea +*.project +.buildpath +.settings +.TemporaryItems +.webprj +.fleet + +# Temporary files and folders +/.cache +.php_cs.cache +.php-cs-fixer.cache +.sass-cache +.session +*.log + +# Ignore composer stuff +bin/* +vendor/* +.build +.php_cs.cache +composer.lock + +# Ignore testing stuff +/.Build +/composer.json.orig +/composer.json.testing From 96a3663d396e2a34dc56fab31a9c142a8dc8e85b Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:19:26 +0200 Subject: [PATCH 22/26] [TASK] Add PhpStorm meta file for TYPO3 core APIs Standard TYPO3 core .phpstorm.meta.php: teaches PhpStorm's code completion the concrete return/argument types behind Context::getAspect(), ServerRequestInterface::getAttribute() and similar string-keyed APIs. --- .phpstorm.meta.php | 149 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .phpstorm.meta.php diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..9777bbf --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,149 @@ + \TYPO3\CMS\Core\Context\DateTimeAspect::class, + 'visibility' => \TYPO3\CMS\Core\Context\VisibilityAspect::class, + 'backend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, + 'frontend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, + 'workspace' => \TYPO3\CMS\Core\Context\WorkspaceAspect::class, + 'language' => \TYPO3\CMS\Core\Context\LanguageAspect::class, + 'frontend.preview' => \TYPO3\CMS\Frontend\Aspect\PreviewAspect::class, + ])); + expectedArguments( + \TYPO3\CMS\Core\Context\DateTimeAspect::get(), + 0, + 'timestamp', + 'iso', + 'timezone', + 'full', + 'accessTime' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\VisibilityAspect::get(), + 0, + 'includeHiddenPages', + 'includeHiddenContent', + 'includeDeletedRecords' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\UserAspect::get(), + 0, + 'id', + 'username', + 'isLoggedIn', + 'isAdmin', + 'groupIds', + 'groupNames' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\WorkspaceAspect::get(), + 0, + 'id', + 'isLive', + 'isOffline' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\LanguageAspect::get(), + 0, + 'id', + 'contentId', + 'fallbackChain', + 'overlayType', + 'legacyLanguageMode', + 'legacyOverlayType' + ); + expectedArguments( + \TYPO3\CMS\Frontend\Aspect\PreviewAspect::get(), + 0, + 'isPreview' + ); + + expectedArguments( + \Psr\Http\Message\ServerRequestInterface::getAttribute(), + 0, + 'frontend.user', + 'normalizedParams', + 'site', + 'language', + 'routing', + 'module', + 'moduleData', + 'frontend.controller', + 'frontend.typoscript', + 'frontend.cache.collector', + 'frontend.cache.instruction', + 'frontend.page.information', + ); + override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([ + 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, + 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, + 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, + 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, + 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', + 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, + 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, + 'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, + 'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class, + 'frontend.cache.collector' => \TYPO3\CMS\Core\Cache\CacheDataCollector::class, + 'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class, + 'frontend.page.information' => \TYPO3\CMS\Frontend\Page\PageInformation::class, + ])); + + expectedArguments( + \TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), + 0, + 'frontend.user', + 'normalizedParams', + 'site', + 'language', + 'routing', + 'module', + 'moduleData' + ); + override(\TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), map([ + 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, + 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, + 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, + 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, + 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', + 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, + 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, + ])); + + override(\TYPO3\CMS\Core\Routing\SiteMatcher::matchRequest(), type( + \TYPO3\CMS\Core\Routing\SiteRouteResult::class, + \TYPO3\CMS\Core\Routing\RouteResultInterface::class, + ) + ); + + override(\TYPO3\CMS\Core\Routing\PageRouter::matchRequest(), type( + \TYPO3\CMS\Core\Routing\PageArguments::class, + \TYPO3\CMS\Core\Routing\RouteResultInterface::class, + )); + + override(\Psr\Container\ContainerInterface::get(0), map([ + '' => '@', + ])); + + override(\Psr\EventDispatcher\EventDispatcherInterface::dispatch(0), map([ + '' => '@', + ])); +} From abf0574619a84d0f35b815cc86dc2035cc2434b2 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:19:39 +0200 Subject: [PATCH 23/26] [TASK] Add test/build tooling scripts - Build/Scripts/runTests.sh: docker/podman-based test runner (cgl, lint, composer validate/normalize, unit/functional tests, phpstan), copied from TYPO3's testing-framework template - matches the suites invoked by the new CI workflow. - Build/cgl/config.php: php-cs-fixer ruleset per PER-CS1.0/PSR-12. - Build/phpunit/{Unit,Functional}Tests.xml + bootstrap files: minimal PHPUnit setup for Tests/Unit and Tests/Functional (both still to be created). Fixed a leftover "jweiland/events2" package reference (copy-paste from another jweiland extension) in the header comments of UnitTests.xml, UnitTestsBootstrap.php and FunctionalTestsBootstrap.php to correctly say "jweiland/jobfair2". --- Build/Scripts/runTests.sh | 606 +++++++++++++++++++++ Build/cgl/config.php | 102 ++++ Build/phpunit/FunctionalTests.xml | 26 + Build/phpunit/FunctionalTestsBootstrap.php | 17 + Build/phpunit/UnitTests.xml | 31 ++ Build/phpunit/UnitTestsBootstrap.php | 90 +++ 6 files changed, 872 insertions(+) create mode 100755 Build/Scripts/runTests.sh create mode 100644 Build/cgl/config.php create mode 100644 Build/phpunit/FunctionalTests.xml create mode 100644 Build/phpunit/FunctionalTestsBootstrap.php create mode 100644 Build/phpunit/UnitTests.xml create mode 100644 Build/phpunit/UnitTestsBootstrap.php diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..4549152 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,606 @@ +#!/usr/bin/env bash + +# +# EXT:examples test runner based on docker/podman. +# + +trap 'cleanUp;exit 2' SIGINT + +waitFor() { + local HOST=${1} + local PORT=${2} + local TESTCOMMAND=" + COUNT=0; + while ! nc -z ${HOST} ${PORT}; do + if [ \"\${COUNT}\" -gt 10 ]; then + echo \"Can not connect to ${HOST} port ${PORT}. Aborting.\"; + exit 1; + fi; + sleep 1; + COUNT=\$((COUNT + 1)); + done; + " + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name wait-for-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_ALPINE} /bin/sh -c "${TESTCOMMAND}" + if [[ $? -gt 0 ]]; then + kill -SIGINT -$$ + fi +} + +cleanUp() { + ATTACHED_CONTAINERS=$(${CONTAINER_BIN} ps --filter network=${NETWORK} --format='{{.Names}}') + for ATTACHED_CONTAINER in ${ATTACHED_CONTAINERS}; do + ${CONTAINER_BIN} rm -f ${ATTACHED_CONTAINER} >/dev/null + done + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null +} + +cleanCacheFiles() { + echo -n "Clean caches ... " + rm -rf \ + .Build/.cache \ + .php-cs-fixer.cache + echo "done" +} + +cleanRenderedDocumentationFiles() { + echo -n "Clean rendered documentation files ... " + rm -rf \ + Documentation-GENERATED-temp + echo "done" +} + +handleDbmsOptions() { + # -a, -d, -i depend on each other. Validate input combinations and set defaults. + case ${DBMS} in + mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.11" + if ! [[ ${DBMS_VERSION} =~ ^(10.11|11.0|11.1|11.2|11.3|11.4|11.5|11.6)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + mysql) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="8.0" + if ! [[ ${DBMS_VERSION} =~ ^(8.0|8.1|8.2|8.3)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + postgres) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10" + if ! [[ ${DBMS_VERSION} =~ ^(10|11|12|13|14|15|16)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + if [ -n "${DBMS_VERSION}" ]; then + echo "Invalid combination -d ${DBMS} -i ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + *) + echo "Invalid option -d ${DBMS}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + ;; + esac +} + +loadHelp() { + # Load help text into $HELP + read -r -d '' HELP < + Specifies which test suite to run + - cgl: cgl test and fix all php files + - clean: Clean temporary files + - cleanCache: Clean cache folds for files. + - cleanRenderedDocumentation: Clean existing rendered documentation output. + - composer: "composer" with all remaining arguments dispatched. + - composerNormalize: "composer normalize" + - composerUpdate: "composer update", handy if host has no PHP + - composerUpdateRector: "composer update", for rector subdirectory + - composerValidate: "composer validate" + - functional: PHP functional tests + - lint: PHP linting + - phpstan: PHPStan static analysis + - phpstanBaseline: Generate PHPStan baseline + - unit: PHP unit tests + - rector: Apply Rector rules + - renderDocumentation + - testRenderDocumentation + + -b + Container environment: + - docker + - podman + + If not specified, podman will be used if available. Otherwise, docker is used. + + -a + Only with -s functional|functionalDeprecated + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -d + Only with -s functional|functionalDeprecated|acceptance|acceptanceComposer|acceptanceInstall + Specifies on which DBMS tests are performed + - sqlite: (default): use sqlite + - mariadb: use mariadb + - mysql: use MySQL + - postgres: use postgres + + -i version + Specify a specific database version + With "-d mariadb": + - 10.4 short-term, maintained until 2024-06-18 (default) + - 10.5 short-term, maintained until 2025-06-24 + - 10.6 long-term, maintained until 2026-06 + - 10.7 short-term, no longer maintained + - 10.8 short-term, maintained until 2023-05 + - 10.9 short-term, maintained until 2023-08 + - 10.10 short-term, maintained until 2023-11 + - 10.11 long-term, maintained until 2028-02 + - 11.0 development series + - 11.1 short-term development series + With "-d mysql": + - 8.0 maintained until 2026-04 (default) LTS + - 8.1 unmaintained since 2023-10 + - 8.2 unmaintained since 2024-01 + - 8.3 maintained until 2024-04 + With "-d postgres": + - 10 unmaintained since 2022-11-10 (default) + - 11 unmaintained since 2023-11-09 + - 12 maintained until 2024-11-14 + - 13 maintained until 2025-11-13 + - 14 maintained until 2026-11-12 + - 15 maintained until 2027-11-11 + - 16 maintained until 2028-11-09 + + -p <8.2|8.3|8.4> + Specifies the PHP minor version to be used + - 8.2: use PHP 8.2 + - 8.3: use PHP 8.3 + - 8.4: use PHP 8.4 + + -x + Only with -s functional|unit + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -n + Only with -s cgl, composerNormalize, rector + Activate dry-run in CGL check and composer normalize that does not actively change files and only prints broken ones. + + -u + Update existing typo3/core-testing-*:latest container images and remove dangling local volumes. + New images are published once in a while and only the latest ones are supported by core testing. + Use this if weird test errors occur. Also removes obsolete image versions of typo3/core-testing-*. + + -h + Show this help. + +Examples: + # Run unit tests using PHP 8.3 + ./Build/Scripts/runTests.sh -p 8.3 -s unit + + # Run functional tests using PHP 8.4 and MariaDB 10.6 using pdo_mysql + ./Build/Scripts/runTests.sh -p 8.4 -s functional -d mariadb -i 10.6 -a pdo_mysql + + # Run functional tests on postgres with xdebug, php 8.4 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.4 -s functional -d postgres -- Tests/Functional/DummyTest.php +EOF +} + +# Test if docker exists, else exit out with error +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install" >&2 + exit 1 +fi + +# Option defaults +# @todo Consider to switch from cgl to help as default +TEST_SUITE="cgl" +DATABASE_DRIVER="" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.2" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +CGLCHECK_DRY_RUN=0 +CI_PARAMS="${CI_PARAMS:-}" +DOCS_PARAMS="${DOCS_PARAMS:=--pull always}" +CONTAINER_BIN="" +CONTAINER_HOST="host.docker.internal" + +# Option parsing updates above default vars +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=() +# Simple option parsing based on getopts (! not getopt) +while getopts "a:b:d:i:s:p:xy:nhu" OPT; do + case ${OPT} in + a) + DATABASE_DRIVER=${OPTARG} + ;; + s) + TEST_SUITE=${OPTARG} + ;; + b) + if ! [[ ${OPTARG} =~ ^(docker|podman)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + CONTAINER_BIN=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + DBMS_VERSION=${OPTARG} + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(8.2|8.3|8.4)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + n) + CGLCHECK_DRY_RUN=1 + ;; + h) + loadHelp + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + \?) + INVALID_OPTIONS+=("${OPTARG}") + ;; + :) + INVALID_OPTIONS+=("${OPTARG}") + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-"${I} >&2 + done + echo >&2 + echo "call \".Build/Scripts/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +handleDbmsOptions + +COMPOSER_ROOT_VERSION="13.0.x-dev" +HOST_UID=$(id -u) +USERSET="" +if [ $(uname) != "Darwin" ]; then + USERSET="--user $HOST_UID" +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called, then go up two dirs. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +ROOT_DIR="${PWD}" + +# Create .cache dir: composer need this. +mkdir -p .Build/.cache +mkdir -p .Build/web/typo3temp/var/tests + +IMAGE_PREFIX="docker.io/" +# Non-CI fetches TYPO3 images (php and nodejs) from ghcr.io +TYPO3_IMAGE_PREFIX="ghcr.io/typo3/" +CONTAINER_INTERACTIVE="-it --init" + +IS_CORE_CI=0 +# ENV var "CI" is set by gitlab-ci. We use it here to distinct 'local' and 'CI' environment. +if [ "${CI}" == "true" ]; then + IS_CORE_CI=1 + IMAGE_PREFIX="" + CONTAINER_INTERACTIVE="" +fi + +# determine default container binary to use: 1. podman 2. docker +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "podman" >/dev/null 2>&1; then + CONTAINER_BIN="podman" + elif type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + fi +fi + +IMAGE_PHP="${TYPO3_IMAGE_PREFIX}core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_ALPINE="${IMAGE_PREFIX}alpine:3.8" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" +IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:latest" + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) + +SUFFIX=$(echo $RANDOM) +NETWORK="t3docsexamples-${SUFFIX}" +${CONTAINER_BIN} network create ${NETWORK} >/dev/null + +if [ ${CONTAINER_BIN} = "docker" ]; then + # docker needs the add-host for xdebug remote debugging. podman has host.container.internal built in + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:/project" +else + # podman + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:/project" +fi + +if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=debug -e XDEBUG_TRIGGER=foo" + XDEBUG_CONFIG="client_port=${PHP_XDEBUG_PORT} client_host=${CONTAINER_HOST}" +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --dry-run --diff --config=Build/cgl/config.php --using-cache=no ." + else + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --config=Build/cgl/config.php --using-cache=no ." + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + clean) + cleanCacheFiles + cleanRenderedDocumentationFiles + ;; + cleanCache) + cleanCacheFiles + ;; + cleanRenderedDocumentation) + cleanRenderedDocumentationFiles + ;; + composer) + COMMAND=(composer "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerNormalize) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND=(composer normalize -n) + else + COMMAND=(composer normalize) + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerUpdate) + rm -rf .Build/bin/ .Build/typo3 .Build/vendor .Build/Web ./composer.lock + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.orig + if [ -f "${ROOT_DIR}/composer.json.testing" ]; then + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.orig + fi + COMMAND=(composer require --no-ansi --no-interaction --no-progress) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.testing + mv ${ROOT_DIR}/composer.json.orig ${ROOT_DIR}/composer.json + ;; + composerUpdateRector) + rm -rf Build/rector/.Build/bin/ Build/rector/.Build/vendor Build/rector/composer.lock + cp ${ROOT_DIR}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.orig + if [ -f "${ROOT_DIR}/Build/rector/composer.json.testing" ]; then + cp ${ROOT_DIR}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.orig + fi + COMMAND=(composer require --working-dir=${ROOT_DIR}/Build/rector --no-ansi --no-interaction --no-progress) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + cp ${ROOT_DIR}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.testing + mv ${ROOT_DIR}/Build/rector/composer.json.orig ${ROOT_DIR}/Build/rector/composer.json + ;; + composerValidate) + COMMAND=(composer validate "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + functional) + CONTAINER_PARAMS="" + COMMAND=(.Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml --exclude-group not-${DBMS} ${EXTRA_TEST_OPTIONS} "$@") + case ${DBMS} in + mariadb) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mariadb-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null + waitFor mariadb-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mariadb-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + mysql) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mysql-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null + waitFor mysql-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mysql-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + postgres) + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name postgres-func-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null + waitFor postgres-func-${SUFFIX} 5432 + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=bamboo -e typo3DatabaseUsername=funcu -e typo3DatabaseHost=postgres-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + sqlite) + # create sqlite tmpfs mount typo3temp/var/tests/functional-sqlite-dbs/ to avoid permission issues + mkdir -p "${ROOT_DIR}/.Build/web/typo3temp/var/tests/functional-sqlite-dbs/" + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_sqlite --tmpfs ${ROOT_DIR}/.Build/web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + esac + ;; + lint) + COMMAND="find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstan) + COMMAND="php -dxdebug.mode=off .Build/bin/phpstan --configuration=Build/phpstan/phpstan.neon" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstanBaseline) + COMMAND="php -dxdebug.mode=off .Build/bin/phpstan --configuration=Build/phpstan/phpstan.neon --generate-baseline=Build/phpstan/phpstan-baseline.neon -v" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + rector) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND=(php -dxdebug.mode=off Build/rector/.Build/bin/rector -n --config=Build/rector/rector.php --clear-cache "$@") + else + COMMAND=(php -dxdebug.mode=off Build/rector/.Build/bin/rector --config=Build/rector/rector.php --clear-cache "$@") + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name rector-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + renderDocumentation) + COMMAND=(--config=Documentation "$@") + mkdir -p Documentation-GENERATED-temp + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} ${CONTAINER_DOCS_PARAMS} --name render-documentation-${SUFFIX} ${IMAGE_DOCS} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + testRenderDocumentation) + COMMAND=(--config=Documentation --no-progress --fail-on-log "$@") + mkdir -p Documentation-GENERATED-temp + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} ${CONTAINER_DOCS_PARAMS} --name render-documentation-test-${SUFFIX} ${IMAGE_DOCS} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + unit) + COMMAND=(.Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + update) + # pull typo3/core-testing-* versions of those ones that exist locally + echo "> pull ${TYPO3_IMAGE_PREFIX}core-testing-* versions of those ones that exist locally" + ${CONTAINER_BIN} images "${TYPO3_IMAGE_PREFIX}core-testing-*" --format "{{.Repository}}:{{.Tag}}" | xargs -I {} ${CONTAINER_BIN} pull {} + echo "" + # remove "dangling" typo3/core-testing-* images (those tagged as ) + echo "> remove \"dangling\" ${TYPO3_IMAGE_PREFIX}/core-testing-* images (those tagged as )" + ${CONTAINER_BIN} images --filter "reference=${TYPO3_IMAGE_PREFIX}/core-testing-*" --filter "dangling=true" --format "{{.ID}}" | xargs -I {} ${CONTAINER_BIN} rmi -f {} + echo "" + ;; + *) + loadHelp + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 + ;; +esac + +cleanUp + +# Print summary +echo "" >&2 +echo "###########################################################################" >&2 +echo "Result of ${TEST_SUITE}" >&2 +echo "Container runtime: ${CONTAINER_BIN}" >&2 +if [[ ${IS_CORE_CI} -eq 1 ]]; then + echo "Environment: CI" >&2 +else + echo "Environment: local" >&2 +fi +echo "PHP: ${PHP_VERSION}" >&2 +echo "TYPO3: ${CORE_VERSION}" >&2 +if [[ ${TEST_SUITE} =~ ^functional$ ]]; then + case "${DBMS}" in + mariadb|mysql) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver ${DATABASE_DRIVER}" >&2 + ;; + postgres) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver pdo_pgsql" >&2 + ;; + sqlite) + echo "DBMS: ${DBMS} driver pdo_sqlite" >&2 + ;; + esac +fi +if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then + echo "SUCCESS" >&2 +else + echo "FAILURE" >&2 +fi +echo "###########################################################################" >&2 +echo "" >&2 + +# Exit with code of test suite - This script return non-zero if the executed test failed. +exit $SUITE_EXIT_CODE diff --git a/Build/cgl/config.php b/Build/cgl/config.php new file mode 100644 index 0000000..9ecffc1 --- /dev/null +++ b/Build/cgl/config.php @@ -0,0 +1,102 @@ +setFinder( + (new Finder()) + ->in(__DIR__ . '/../../') + ->exclude(__DIR__ . '/../../.Build') + ->exclude(__DIR__ . '/../../var') + ) + ->setRiskyAllowed(true) + ->setRules([ + '@DoctrineAnnotation' => true, + 'header_comment' => [ + 'header' => $headerComment, + ], + // @todo: Switch to @PER-CS2.0 once php-cs-fixer's todo list is done: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7247 + '@PER-CS1.0' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'cast_spaces' => ['space' => 'none'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'declare_parentheses' => true, + 'dir_constant' => true, + // @todo: Can be dropped once we enable @PER-CS2.0 + 'function_declaration' => [ + 'closure_fn_spacing' => 'none', + ], + 'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']], + 'type_declaration_spaces' => true, + 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], + 'list_syntax' => ['syntax' => 'short'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'method_argument_space' => true, + 'modernize_strpos' => true, + 'modernize_types_casting' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_leading_namespace_whitespace' => true, + 'no_null_property_initialization' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_useless_nullsafe_operator' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], + 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], + 'php_unit_mock_short_will_return' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], + 'phpdoc_no_access' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + 'return_type_declaration' => ['space_before' => 'none'], + 'single_quote' => true, + 'single_space_around_construct' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'single_line_empty_body' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arguments', 'arrays', 'match', 'parameters']], + 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + + // We need this for documentation! + 'no_useless_else' => false, // We want to preserve else with comments only + + // Add this rule to convert FQCN to use statements + 'full_opening_tag' => true, + ]); diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..7c503be --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,26 @@ + + + + + + + ../../Tests/Functional/ + + + + + + + diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..0e2e893 --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,17 @@ +defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +})(); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..d93f709 --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,31 @@ + + + + + + + ../../Tests/Unit/ + + + + + + + diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 0000000..4e267d9 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,90 @@ +getWebRoot(), '/')); + } + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + // We can use the "typo3/cms-composer-installers" constant "TYPO3_COMPOSER_MODE" to determine composer mode. + // This should be always true except for TYPO3 mono repository. + $composerMode = defined('TYPO3_COMPOSER_MODE') && TYPO3_COMPOSER_MODE === true; + $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; + SystemEnvironmentBuilder::run(0, $requestType, $composerMode); + + $testbase->createDirectory(Environment::getPublicPath() . '/typo3conf/ext'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/assets'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/var/tests'); + $testbase->createDirectory(Environment::getPublicPath() . '/typo3temp/var/transient'); + + // Retrieve an instance of class loader and inject to core bootstrap + $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; + Bootstrap::initializeClassLoader($classLoader); + + // Initialize default TYPO3_CONF_VARS + $configurationManager = new ConfigurationManager(); + $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); + + $cache = new PhpFrontend( + 'core', + new NullBackend('production', []), + ); + $packageManager = Bootstrap::createPackageManager( + UnitTestPackageManager::class, + Bootstrap::createPackageCache($cache), + ); + + GeneralUtility::setSingletonInstance(PackageManager::class, $packageManager); + ExtensionManagementUtility::setPackageManager($packageManager); + + $testbase->dumpClassLoadingInformation(); + + GeneralUtility::purgeInstances(); +})(); From b69e54eeb3121b9e77f8589d0c44ab7e71ffa6d1 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:19:49 +0200 Subject: [PATCH 24/26] [TASK] Add GitHub Actions workflows - ci.yml: runs Build/Scripts/runTests.sh (lint, composer validate/normalize, cgl) on PHP 8.2/8.3 for every pull request. - backport.yml: opens backport PRs on merge, driven by a "Backport " label. - ter-release.yml: publishes to the TYPO3 Extension Repository via typo3/tailor whenever a GitHub release is published. --- .github/workflows/backport.yml | 22 ++++++++++++ .github/workflows/ci.yml | 36 ++++++++++++++++++++ .github/workflows/ter-release.yml | 56 +++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 .github/workflows/backport.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/ter-release.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000..00b8f2b --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,22 @@ +name: Backport merged pull request + +on: + pull_request_target: + types: [closed] + +permissions: + contents: write + pull-requests: write + +jobs: + backport: + name: 'Backport pull request' + runs-on: 'ubuntu-latest' + if: github.event.pull_request.merged == true + steps: + - uses: actions/checkout@v4 + - name: Create backport pull requests + uses: korthout/backport-action@v3 + with: + label_pattern: '^Backport ([^ ]+)$' + github_token: ${{ secrets.BACKPORT_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dbd0e9a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Tests + +on: [pull_request] + +jobs: + testing: + name: Testing + + runs-on: ubuntu-latest + + strategy: + fail-fast: true + + matrix: + php: + - '8.2' + - '8.3' + + steps: + - name: 'Checkout' + uses: actions/checkout@v6 + + - name: 'Lint PHP' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s lint + + - name: 'Install testing system' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerUpdate + + - name: 'Composer validate' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate + + - name: 'Composer normalize' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerNormalize -n + + - name: 'CGL' + run: Build/Scripts/runTests.sh -n -p ${{ matrix.php }} -s cgl diff --git a/.github/workflows/ter-release.yml b/.github/workflows/ter-release.yml new file mode 100644 index 0000000..475d00d --- /dev/null +++ b/.github/workflows/ter-release.yml @@ -0,0 +1,56 @@ +name: ter-release.yml + +permissions: + contents: read + +on: + release: + types: [published] + +jobs: + + publish: + name: Publish Extension to TYPO3 Extension Repository (TER) + runs-on: ubuntu-latest + + env: + TYPO3_EXTENSION_KEY: ${{ secrets.TYPO3_EXTENSION_KEY }} + TYPO3_REPOSITORY_URL: ${{ secrets.TYPO3_REPOSITORY_URL }} + TYPO3_API_TOKEN: ${{ secrets.TYPO3_API_TOKEN }} + TYPO3_API_USERNAME: ${{ secrets.TYPO3_API_USERNAME }} + TYPO3_API_PASSWORD: ${{ secrets.TYPO3_API_PASSWORD }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get version and description + id: prep + run: | + # 1. Clean the version tag (removes 'v' prefix if present, e.g., v1.0.0 -> 1.0.0) + RAW_VERSION="${{ github.event.release.tag_name }}" + VERSION=${RAW_VERSION#v} + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + + # 2. Safely capture the multi-line release body as an ENV variable + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "${{ github.event.release.body }}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: intl, mbstring, json, libxml, xml, zip, curl + tools: composer:v2 + + - name: Install TYPO3 Tailor Extension + run: composer global require typo3/tailor --prefer-dist --no-progress --no-suggest + + - name: Release to TER + run: | + # Use the VERSION from steps and RELEASE_NOTES from env + # We use double quotes around env.RELEASE_NOTES to handle the multi-line content + php ~/.composer/vendor/bin/tailor ter:publish ${{ steps.prep.outputs.VERSION }} \ + --artefact=${{ env.TYPO3_REPOSITORY_URL }}/archive/${{ github.event.release.tag_name }}.zip \ + --comment="${{ env.RELEASE_NOTES }}" From 06d4ce7827a98fe119a48cf159a6cbc4c80a4b8e Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Thu, 30 Jul 2026 15:30:28 +0200 Subject: [PATCH 25/26] [TASK] Fix CGL violations flagged by the new CI workflow The CI workflow added in b69e54e runs Build/Scripts/runTests.sh -s cgl in dry-run mode, which failed on PR #1 (25 of 54 files) because the existing codebase predates the CGL ruleset that ships with this same branch (Build/cgl/config.php). Applied `php-cs-fixer fix` with that exact config: missing package header comments, trailing commas in multiline argument/parameter lists, brace position and single-line empty body style. No behavioral changes. --- Build/cgl/config.php | 9 ++++++++- Classes/ApiModel/ApiMapping.php | 2 +- Classes/ApiModel/JobModel.php | 2 +- .../Backend/Element/LocalizedDecimalElement.php | 14 +++++++------- Classes/Controller/JobfairController.php | 2 +- Classes/Service/JobService.php | 2 +- Classes/Updates/JWeilandJobfair2CTypeMigration.php | 7 +++++++ Configuration/Extbase/Persistence/Classes.php | 7 +++++++ Configuration/Icons.php | 7 +++++++ Configuration/JavaScriptModules.php | 7 +++++++ Configuration/RequestMiddlewares.php | 7 +++++++ Configuration/TCA/Overrides/tt_address.php | 7 +++++++ Configuration/TCA/Overrides/tt_content.php | 7 +++++++ .../TCA/Overrides/tx_jobfair2_domain_model_job.php | 7 +++++++ .../tx_jobfair2_domain_model_salarygrade.php | 7 +++++++ .../tx_jobfair2_domain_model_salarystep.php | 7 +++++++ .../tx_jobfair2_domain_model_salarytable.php | 7 +++++++ Configuration/TCA/tx_jobfair2_domain_model_job.php | 7 +++++++ .../TCA/tx_jobfair2_domain_model_jobarea.php | 7 +++++++ .../TCA/tx_jobfair2_domain_model_jobtype.php | 7 +++++++ .../TCA/tx_jobfair2_domain_model_salarygrade.php | 7 +++++++ .../TCA/tx_jobfair2_domain_model_salarystep.php | 7 +++++++ .../TCA/tx_jobfair2_domain_model_salarytable.php | 7 +++++++ ext_emconf.php | 7 +++++++ ext_localconf.php | 7 +++++++ 25 files changed, 152 insertions(+), 12 deletions(-) diff --git a/Build/cgl/config.php b/Build/cgl/config.php index 9ecffc1..1f99067 100644 --- a/Build/cgl/config.php +++ b/Build/cgl/config.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * This file is part of the package jweiland/jobfair2. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ + use PhpCsFixer\Config; use PhpCsFixer\Finder; @@ -28,7 +35,7 @@ (new Finder()) ->in(__DIR__ . '/../../') ->exclude(__DIR__ . '/../../.Build') - ->exclude(__DIR__ . '/../../var') + ->exclude(__DIR__ . '/../../var'), ) ->setRiskyAllowed(true) ->setRules([ diff --git a/Classes/ApiModel/ApiMapping.php b/Classes/ApiModel/ApiMapping.php index be816a9..b1d4ba7 100644 --- a/Classes/ApiModel/ApiMapping.php +++ b/Classes/ApiModel/ApiMapping.php @@ -17,7 +17,7 @@ public function __construct( private string $apiPath, private bool $isDate = false, private int|string $default = '', - private string $prefix = '' + private string $prefix = '', ) {} public function getApiPath(): string diff --git a/Classes/ApiModel/JobModel.php b/Classes/ApiModel/JobModel.php index df636d5..cc9b701 100644 --- a/Classes/ApiModel/JobModel.php +++ b/Classes/ApiModel/JobModel.php @@ -41,7 +41,7 @@ public function getPrimaryLocation(): LocationModel 'No primary location found for vacancy ID: %s', $this->getValueByPath('vacancy_id', 'int', 0), ), - 1751531883 + 1751531883, ); } } diff --git a/Classes/Backend/Element/LocalizedDecimalElement.php b/Classes/Backend/Element/LocalizedDecimalElement.php index bdf824c..a459cae 100644 --- a/Classes/Backend/Element/LocalizedDecimalElement.php +++ b/Classes/Backend/Element/LocalizedDecimalElement.php @@ -54,17 +54,17 @@ public function render(): array $fieldId = StringUtility::getUniqueId('formengine-input-'); $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( - '@jweiland/jobfair2/form-engine-localized-decimal.js' + '@jweiland/jobfair2/form-engine-localized-decimal.js', )->invoke('initialize', $fieldId); $resultArray['html'] = $this->renderLabel($fieldId) . '
' . $fieldInformationResult['html'] . $this->buildFieldHtml( - $fieldId, - (string)$parameterArray['itemFormElValue'], - $fieldControlResult['html'], - $fieldWizardResult['html'] - ) . ' + $fieldId, + (string)$parameterArray['itemFormElValue'], + $fieldControlResult['html'], + $fieldWizardResult['html'], + ) . '
'; return $resultArray; @@ -75,7 +75,7 @@ private function buildFieldHtml(string $fieldId, string $value, string $fieldCon $parameterArray = $this->data['parameterArray']; $config = $parameterArray['fieldConf']['config']; $width = $this->formMaxWidth( - MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth) + MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth), ); $attributes = [ 'value' => $value, diff --git a/Classes/Controller/JobfairController.php b/Classes/Controller/JobfairController.php index d1a5535..00f9a29 100644 --- a/Classes/Controller/JobfairController.php +++ b/Classes/Controller/JobfairController.php @@ -51,7 +51,7 @@ public function listAction(): ResponseInterface public function searchAction( ?JobArea $jobArea = null, ?JobType $jobType = null, - string $address = '' + string $address = '', ): ResponseInterface { $searchCriteria = []; diff --git a/Classes/Service/JobService.php b/Classes/Service/JobService.php index 7568559..b7890d6 100644 --- a/Classes/Service/JobService.php +++ b/Classes/Service/JobService.php @@ -30,7 +30,7 @@ public function __construct( private JobAreaService $jobAreaService, - private JobTypeService $jobTypeService + private JobTypeService $jobTypeService, ) {} /** diff --git a/Classes/Updates/JWeilandJobfair2CTypeMigration.php b/Classes/Updates/JWeilandJobfair2CTypeMigration.php index 6f74fee..f1f4a39 100644 --- a/Classes/Updates/JWeilandJobfair2CTypeMigration.php +++ b/Classes/Updates/JWeilandJobfair2CTypeMigration.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * This file is part of the package jweiland/jobfair2. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ + namespace JWeiland\Jobfair2\Updates; use TYPO3\CMS\Install\Attribute\UpgradeWizard; diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php index 82ab811..26db475 100644 --- a/Configuration/Extbase/Persistence/Classes.php +++ b/Configuration/Extbase/Persistence/Classes.php @@ -1,5 +1,12 @@ 'Job fair 2', 'description' => 'Job fair implementation using Maps2 and tt_address to display jobs', diff --git a/ext_localconf.php b/ext_localconf.php index fd1355d..db230ac 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,5 +1,12 @@ Date: Thu, 30 Jul 2026 15:33:59 +0200 Subject: [PATCH 26/26] [TASK] Force docker as container runtime in CI runTests.sh auto-detects podman before docker when both are installed. GitHub's ubuntu-latest runners ship both, but their current podman/crun combination fails on every container pull with "OCI runtime error: crun: unknown version specified", so every step (lint, composerUpdate, composerValidate, composerNormalize, cgl) failed regardless of PHP version - reproduced twice, not a one-off flake. Pass -b docker explicitly to bypass the broken podman path. --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbd0e9a..0e897bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,16 @@ jobs: uses: actions/checkout@v6 - name: 'Lint PHP' - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s lint + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s lint - name: 'Install testing system' - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerUpdate + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerUpdate - name: 'Composer validate' - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerValidate - name: 'Composer normalize' - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerNormalize -n + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerNormalize -n - name: 'CGL' - run: Build/Scripts/runTests.sh -n -p ${{ matrix.php }} -s cgl + run: Build/Scripts/runTests.sh -b docker -n -p ${{ matrix.php }} -s cgl