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 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/.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..0e897bf --- /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 -b docker -p ${{ matrix.php }} -s lint + + - name: 'Install testing system' + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerUpdate + + - name: 'Composer validate' + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerValidate + + - name: 'Composer normalize' + run: Build/Scripts/runTests.sh -b docker -p ${{ matrix.php }} -s composerNormalize -n + + - name: 'CGL' + run: Build/Scripts/runTests.sh -b docker -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 }}" 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 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([ + '' => '@', + ])); +} 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..1f99067 --- /dev/null +++ b/Build/cgl/config.php @@ -0,0 +1,109 @@ +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(); +})(); 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 new file mode 100644 index 0000000..a459cae --- /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/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/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/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/Classes/UserFunc/SalaryGradeTitleFormatter.php b/Classes/UserFunc/SalaryGradeTitleFormatter.php new file mode 100644 index 0000000..d38b173 --- /dev/null +++ b/Classes/UserFunc/SalaryGradeTitleFormatter.php @@ -0,0 +1,59 @@ +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/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/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php index 1e6cfb0..26db475 100644 --- a/Configuration/Extbase/Persistence/Classes.php +++ b/Configuration/Extbase/Persistence/Classes.php @@ -1,7 +1,16 @@ [ + Address::class => [ 'tableName' => 'tt_address', ], ]; diff --git a/Configuration/Icons.php b/Configuration/Icons.php index 553537a..82a9e30 100644 --- a/Configuration/Icons.php +++ b/Configuration/Icons.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 TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider; return [ @@ -13,4 +20,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/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..2d29727 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,22 @@ + [ + 'backend', + ], + 'tags' => [ + 'backend.form', + ], + 'imports' => [ + '@jweiland/jobfair2/' => 'EXT:jobfair2/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index 31ed001..a4ce451 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,9 +1,18 @@ [ '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..1b2e564 100644 --- a/Configuration/TCA/Overrides/tt_address.php +++ b/Configuration/TCA/Overrides/tt_address.php @@ -1,10 +1,19 @@ [ @@ -17,7 +26,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..30e2dad 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -1,18 +1,28 @@ add( +if (ExtensionManagementUtility::isLoaded('tt_address')) { + Maps2Registry::getInstance()->add( 'tt_address', 'tt_address', [ @@ -20,3 +30,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..0a615d5 --- /dev/null +++ b/Configuration/TCA/Overrides/tx_jobfair2_domain_model_salarygrade.php @@ -0,0 +1,20 @@ + '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', @@ -28,13 +37,24 @@ ], ], 'types' => [ - '0' => [ - 'showitem' => '--palette--;;languageHidden, l10n_diffsource, - --palette--;Job;titleReference, - --palette--;Import;importVacancy, + 0 => [ + '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_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: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_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', ], ], @@ -49,75 +69,8 @@ ], ], '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_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, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.title', 'config' => [ 'type' => 'input', @@ -128,7 +81,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', @@ -139,7 +92,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' => [ @@ -150,7 +103,8 @@ ], ], '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' => [ 'type' => 'input', @@ -159,7 +113,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', @@ -167,7 +121,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', @@ -185,14 +139,8 @@ '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' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.job_area', 'config' => [ 'type' => 'select', @@ -208,7 +156,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', @@ -224,7 +172,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' => [ @@ -235,7 +183,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' => [ @@ -246,7 +194,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', @@ -256,8 +204,16 @@ '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' => 1, + 'exclude' => true, 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.employer_address', 'config' => [ 'type' => 'group', @@ -274,16 +230,60 @@ ], ], ], - 'email' => [ - 'exclude' => 1, - 'label' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_job.email', + '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' => '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' => [ - '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 @@ -295,32 +295,32 @@ // Use the imageoverlayPalette instead of the basicoverlayPalette 'overrideChildTca' => [ 'types' => [ - '0' => [ + 0 => [ 'showitem' => ' --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', @@ -333,7 +333,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 @@ -345,32 +345,32 @@ // Use the imageoverlayPalette instead of the basicoverlayPalette 'overrideChildTca' => [ 'types' => [ - '0' => [ + 0 => [ 'showitem' => ' --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', @@ -395,7 +395,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', @@ -409,5 +409,19 @@ 'default' => 0, ], ], + 'salary_mode' => [ + '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' => [ + '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, + ], + ], ], ]; diff --git a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php index 409b3dc..9388e4f 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobarea.php @@ -1,5 +1,12 @@ 'EXT:jobfair2/Resources/Public/Icons/jobarea.svg', ], 'types' => [ - '0' => ['showitem' => 'title'], + 0 => ['showitem' => 'title'], ], 'palettes' => [ '1' => ['showitem' => ''], @@ -97,7 +104,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..c66b47e 100644 --- a/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php +++ b/Configuration/TCA/tx_jobfair2_domain_model_jobtype.php @@ -1,5 +1,12 @@ 'EXT:jobfair2/Resources/Public/Icons/jobtype.svg', ], 'types' => [ - '0' => ['showitem' => 'hidden, title'], + 0 => ['showitem' => 'hidden, title'], ], 'palettes' => [ '1' => ['showitem' => ''], @@ -97,7 +104,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 new file mode 100644 index 0000000..ab3ee04 --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarygrade.php @@ -0,0 +1,120 @@ + [ + 'title' => 'LLL:EXT:jobfair2/Resources/Private/Language/locallang_db.xlf:tx_jobfair2_domain_model_salarygrade', + 'label' => 'title', + 'label_userFunc' => SalaryGradeTitleFormatter::class . '->formatTitle', + 'formattedLabel_userFunc' => SalaryGradeTitleFormatter::class . '->formatInlineChildTitle', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'sortby' => 'sorting', + 'type' => 'has_steps', + 'typeicon_column' => 'has_steps', + 'typeicon_classes' => [ + 'default' => 'ext-jobfair2-record-salarygrade-stepped', + 0 => 'ext-jobfair2-record-salarygrade-flat', + 1 => 'ext-jobfair2-record-salarygrade-stepped', + ], + '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' => [ + 0 => [ + 'showitem' => '--palette--;;languageHidden, l10n_diffsource, + --palette--;;titleStep, flat_amount, + --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--;;titleStep, salary_steps, + --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', + ], + ], + 'palettes' => [ + 'languageHidden' => ['showitem' => 'sys_language_uid, l10n_parent, hidden'], + 'titleStep' => ['showitem' => 'title, has_steps'], + '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' => [ + 'title' => [ + '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' => [ + 'type' => 'input', + 'size' => 13, + 'max' => 60, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'has_steps' => [ + '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' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 1, + ], + ], + 'flat_amount' => [ + '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' => [ + 'type' => 'number', + 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', + 'range' => [ + 'lower' => 0, + ], + 'default' => 0.00, + ], + ], + 'salary_steps' => [ + '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' => [ + 'type' => 'inline', + 'foreign_table' => 'tx_jobfair2_domain_model_salarystep', + 'foreign_field' => 'salary_grade', + 'foreign_sortby' => 'sorting', + 'appearance' => [ + 'useSortable' => true, + 'collapseAll' => true, + 'expandSingle' => true, + 'levelLinksPosition' => 'both', + 'newRecordLinkAddTitle' => true, + ], + ], + ], + ], +]; 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..55cfb05 --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarystep.php @@ -0,0 +1,80 @@ + [ + '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, + 'label_userFunc' => SalaryStepTitleFormatter::class . '->formatTitle', + '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--;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:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + ], + ], + 'columns' => [ + 'step_label' => [ + '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' => [ + 'type' => 'input', + 'size' => 10, + 'max' => 30, + 'eval' => 'trim', + 'required' => true, + ], + ], + 'amount' => [ + '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' => [ + 'type' => 'number', + 'format' => 'decimal', + 'renderType' => 'jobfair2LocalizedDecimal', + '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..cd08062 --- /dev/null +++ b/Configuration/TCA/tx_jobfair2_domain_model_salarytable.php @@ -0,0 +1,85 @@ + [ + '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--;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:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel', + ], + ], + 'columns' => [ + 'title' => [ + 'exclude' => true, + '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' => true, + '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' => true, + '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' => 'both', + 'newRecordLinkAddTitle' => true, + ], + ], + ], + ], +]; diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 59f1bbe..d09982c 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. 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. 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. + Occupational field @@ -118,6 +162,97 @@ Type of employment Beschäftigungsart + + + Salary table + Besoldungstabelle + + + Title + Titel + + + Description + Beschreibung + + + Salary grades + Besoldungsgruppen + + + 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. 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 + 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 + + + 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 b6263e4..4269f85 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. 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. 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. + Occupational field @@ -90,6 +123,75 @@ Type of employment + + + Salary table + + + Title + + + Description + + + Salary grades + + + 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. 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 + + + 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 + + + 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/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/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..fef556f 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,23 +23,42 @@ "source": "https://github.com/jweiland-net/jobfair2" }, "require": { + "ext-intl": "*", "typo3/cms-core": "^13.4", "jweiland/maps2": "*", "friendsoftypo3/tt-address": "*", "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" } } } diff --git a/ext_emconf.php b/ext_emconf.php index 549d960..fdbf34c 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,11 +1,18 @@ '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' => [ diff --git a/ext_localconf.php b/ext_localconf.php index 04b7c1a..db230ac 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,25 +1,44 @@ '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' => 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', ], ], diff --git a/ext_tables.sql b/ext_tables.sql index 62ce0fa..86f2220 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -3,46 +3,22 @@ # 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 ); # -# Table structure for table 'tx_jobfair2_domain_model_jobarea' +# Table structure for table 'tx_jobfair2_domain_model_salarygrade' # -CREATE TABLE tx_jobfair2_domain_model_jobarea +CREATE TABLE tx_jobfair2_domain_model_salarygrade ( - title varchar(60) DEFAULT '' NOT NULL + flat_amount decimal(10, 2) DEFAULT '0.00' NOT NULL ); # -# Table structure for table 'tx_jobfair2_domain_model_jobtype' +# Table structure for table 'tx_jobfair2_domain_model_salarystep' # -CREATE TABLE tx_jobfair2_domain_model_jobtype +CREATE TABLE tx_jobfair2_domain_model_salarystep ( - 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 );