diff --git a/.github/workflows/docker_image_update_autobuild.yml b/.github/workflows/docker_image_update_autobuild.yml index 0491d96b8c..d2e1c8aff3 100644 --- a/.github/workflows/docker_image_update_autobuild.yml +++ b/.github/workflows/docker_image_update_autobuild.yml @@ -39,11 +39,22 @@ jobs: target: 'otobo-web' docker_tag: 'latest-11_0-autobuild' base_image: 'perl:5.38-bookworm' + + # otobo-nginx-kerberos-webproxy uses different dockerfiles in 10.0 and 10.1 - - target: 'otobo-web' - docker_tag: 'latest-11_1-autobuild' - base_image: 'perl:5.40-slim-bookworm' - # common info for every target; for nginx this includes the base image + target: 'otobo-nginx-kerberos-webproxy' + docker_tag: 'latest-10_0-autobuild' + dockerfile: 'otobo.nginx-kerberos.dockerfile' + - + target: 'otobo-nginx-kerberos-webproxy' + docker_tag: 'latest-10_1-autobuild' + dockerfile: 'otobo.nginx-kerberos.dockerfile' + - + target: 'otobo-nginx-kerberos-webproxy' + docker_tag: 'latest-11_0-autobuild' + dockerfile: 'otobo.nginx.dockerfile' + + # common info for every target; for nginx this includes the base imagie - target: 'otobo-web' dockerfile: 'otobo.web.dockerfile' @@ -57,7 +68,6 @@ jobs: base_image: 'nginx:mainline' - target: 'otobo-nginx-kerberos-webproxy' - dockerfile: 'otobo.nginx.dockerfile' context: 'scripts/nginx' repository: 'rotheross/otobo-nginx-kerberos-webproxy' base_image: 'nginx:mainline' diff --git a/.github/workflows/docker_image_update_checker.yml b/.github/workflows/docker_image_update_checker.yml index 7ca88d9b55..f3a5f4d29c 100644 --- a/.github/workflows/docker_image_update_checker.yml +++ b/.github/workflows/docker_image_update_checker.yml @@ -19,7 +19,6 @@ on: jobs: CheckDockerImageUpdate: - runs-on: 'ubuntu-latest' strategy: # create different images @@ -39,7 +38,7 @@ jobs: dockerfile: 'otobo.elasticsearch.dockerfile' context: 'scripts/elasticsearch' repository: 'rotheross/otobo-elasticsearch' - base_image: 'docker.elastic.co/elasticsearch/elasticsearch:7.17.25' + base_image: 'elasticsearch:7.17.3' - target: 'otobo-nginx-webproxy' dockerfile: 'otobo.nginx.dockerfile' @@ -59,13 +58,13 @@ jobs: repository: 'rotheross/otobo-selenium-chrome' base_image: 'selenium/standalone-chrome-debug:3.141.59-20210422' - + runs-on: ${{ matrix.target }} steps: - name: Setting up the environment file run: | - patch="${{ matrix.patch }}" - docker_tag="rel-${patch}_test" + patch=${{ matrix.patch }} + docker_tag="rel-${patch}" mixed_case_repository="${{ github.repository }}" lowercased_repository="${mixed_case_repository,,}" build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ') @@ -120,8 +119,8 @@ jobs: uses: docker/build-push-action@v6 with: load: true - context: . - file: otobo.web.dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} pull: true build-args: | BUILD_DATE=${{ env.otobo_build_date }} @@ -129,7 +128,7 @@ jobs: GIT_REPO=${{ github.repositoryUrl }} GIT_BRANCH=${{ env.otobo_branch }} GIT_COMMIT=${{ env.otobo_commit }} - target: otobo-web + target: ${{ matrix.target }} tags: ${{ env.otobo_image }} cache-from: type=gha cache-to: type=gha,mode=max` @@ -137,7 +136,7 @@ jobs: - # otobo_first_time hasn't run yet, so /opt/otobo is still empty name: Info - if: steps.check.outputs.needs-updating == 'true' + if: ${{ steps.check.outputs.needs-updating == 'true' && matrix.target == 'otobo-web' }} run: | docker run --rm -w /opt/otobo_install/otobo_next --entrypoint /bin/bash $otobo_image -c "more git-repo.txt git-branch.txt git-commit.txt RELEASE | cat" @@ -158,8 +157,8 @@ jobs: uses: docker/build-push-action@v6 with: push: true - context: . - file: otobo.web.dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} pull: true build-args: | BUILD_DATE=${{ env.otobo_build_date }} @@ -167,7 +166,7 @@ jobs: GIT_REPO=${{ github.repositoryUrl }} GIT_BRANCH=${{ env.otobo_branch }} GIT_COMMIT=${{ env.otobo_commit }} - target: otobo-web + target: ${{ matrix.target }} tags: ${{ env.otobo_image }} cache-from: type=gha - cache-to: type=gha,mode=max` + cache-to: type=gha,mode=max diff --git a/Kernel/Modules/AgentTicketHistory.pm b/Kernel/Modules/AgentTicketHistory.pm index 1616b0619b..866876f751 100644 --- a/Kernel/Modules/AgentTicketHistory.pm +++ b/Kernel/Modules/AgentTicketHistory.pm @@ -154,10 +154,10 @@ sub Run { # values stored previously in older format might not be compatible with new human readable form. # Please see bug#11520 for more information. # - # HistoryType: TicketDynamicFieldUpdate + # HistoryType: ArticleDynamicFieldUpdate & TicketDynamicFieldUpdate # - Old: %%FieldName%%$FieldName%%Value%%$HistoryValue%%OldValue%%$HistoryOldValue # - New: %%$FieldName%%$HistoryOldValue%%$HistoryValue - if ( $Data->{HistoryType} eq 'TicketDynamicFieldUpdate' ) { + if ( $Data->{HistoryType} eq 'ArticleDynamicFieldUpdate' || $Data->{HistoryType} eq 'TicketDynamicFieldUpdate' ) { @Values = ( $Values[1], $Values[5] // '', $Values[3] // '' ); } diff --git a/Kernel/Output/HTML/Layout.pm b/Kernel/Output/HTML/Layout.pm index 61f7b9a4be..76ee84f317 100644 --- a/Kernel/Output/HTML/Layout.pm +++ b/Kernel/Output/HTML/Layout.pm @@ -4019,7 +4019,7 @@ sub BuildDateSelection { if ( $Prefix !~ /^DynamicField_/ || $Suffix ne '_Template' ) { my $DatepickerJS = ' Core.UI.Datepicker.Init({ - Day: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Day"' . ( $Suffix ? ' + Core.App.EscapeSelector("' . $Suffix . '")' : '' ) . '), + Day: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Day"' . ( $Suffix ? ' + Core.App.EscapeSelector("' . $Suffix . '")' : '' ) . '), Month: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Month"' . ( $Suffix ? ' + Core.App.EscapeSelector("' . $Suffix . '")' : '' ) . '), Year: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Year"' . ( $Suffix ? ' + Core.App.EscapeSelector("' . $Suffix . '")' : '' ) . '), Hour: $("#" + Core.App.EscapeSelector("' . $Prefix . '") + "Hour"' . ( $Suffix ? ' + Core.App.EscapeSelector("' . $Suffix . '")' : '' ) . '), @@ -4028,6 +4028,8 @@ sub BuildDateSelection { DateInFuture: ' . ( $ValidateDateInFuture ? 'true' : 'false' ) . ', DateNotInFuture: ' . ( $ValidateDateNotInFuture ? 'true' : 'false' ) . ', WeekDayStart: ' . $WeekDayStart . ' + }, { + Disabled: ' . ( $Param{Disabled} ? 'true' : 'false' ) . ' });'; $Self->AddJSOnDocumentComplete( Code => $DatepickerJS ); @@ -5348,9 +5350,9 @@ sub RichTextDocumentComplete { ); # verify HTML document - my $CustomerInterface = ($Self->{SessionSource} && ($Self->{SessionSource} eq 'CustomerInterface')) ? 1 : 0; - my $HTMLString = $Kernel::OM->Get('Kernel::System::HTMLUtils')->DocumentComplete( - String => $StringRef->$*, + my $CustomerInterface = ( $Self->{SessionSource} && ( $Self->{SessionSource} eq 'CustomerInterface' ) ) ? 1 : 0; + my $HTMLString = $Kernel::OM->Get('Kernel::System::HTMLUtils')->DocumentComplete( + String => $StringRef->$*, CustomerInterface => $CustomerInterface ); diff --git a/Kernel/System/Console/Command/Maint/GenericInterface/TriggerInvoker.pm b/Kernel/System/Console/Command/Maint/GenericInterface/TriggerInvoker.pm new file mode 100644 index 0000000000..61fc689a98 --- /dev/null +++ b/Kernel/System/Console/Command/Maint/GenericInterface/TriggerInvoker.pm @@ -0,0 +1,118 @@ +# -- +# OTOBO is a web-based ticketing system for service organisations. +# -- +# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ +# Copyright (C) 2019-2024 Rother OSS GmbH, https://otobo.io/ +# -- +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# -- + +package Kernel::System::Console::Command::Maint::GenericInterface::TriggerInvoker; + +use strict; +use warnings; + +use parent qw(Kernel::System::Console::BaseCommand); + +use Kernel::System::VariableCheck qw(:all); + +our @ObjectDependencies = ( + 'Kernel::GenericInterface::Requester', + 'Kernel::System::Daemon::SchedulerDB', + 'Kernel::System::GenericInterface::Webservice', +); + +sub Configure { + my ( $Self, %Param ) = @_; + + $Self->Description('Triggers a given Invoker webservice.'); + $Self->AddArgument( + Name => 'webservice', + Description => "Select name of web service to be triggered.", + Required => 1, + HasValue => 1, + ValueRegex => qr/./smx, + ); + $Self->AddArgument( + Name => 'invoker', + Description => "Select Invoker to be triggered.", + Required => 1, + HasValue => 1, + ValueRegex => qr/./, + ); + + return; +} + +sub PreRun { + my ( $Self, %Param ) = @_; + + my $Invoker = $Self->GetArgument('invoker'); + my $WebserviceName = $Self->GetArgument('webservice'); + + # Check if all requirements are met (web service exists and has needed method). + my $Webservice = $Kernel::OM->Get('Kernel::System::GenericInterface::Webservice')->WebserviceGet( + Name => $WebserviceName, + ); + + if ( !IsHashRefWithData($Webservice) ) { + $Self->Print( + "Required web service '$WebserviceName' does not exist!\n" + ); + return $Self->ExitCodeError(); + } + if ( $Webservice->{ValidID} ne '1' ) { + $Self->Print( + "Required web service '$WebserviceName' is invalid!\n" + ); + return $Self->ExitCodeError(); + } + + my $InvokerControllerMapping = $Webservice->{Config}{Requester}{Transport}{Config}{InvokerControllerMapping}; + + if ( !IsHashRefWithData($InvokerControllerMapping) ) { + $Self->Print( + "Web service '$WebserviceName' does not contain required REST controller mapping!\n" + ); + return $Self->ExitCodeError(); + } + if ( !IsHashRefWithData( $InvokerControllerMapping->{$Invoker} ) ) { + $Self->Print( + "Web service '$WebserviceName' does not contain the Invoker '$Invoker'!\n" + ); + return $Self->ExitCodeError(); + } + + # Remember data for task. + $Self->{InvokerTaskData} = { + WebserviceID => $Webservice->{ID}, + Invoker => $Invoker, + Data => { Dummy => 1 }, + }; + + return; +} + +sub Run { + my ( $Self, %Param ) = @_; + + my $Invoker = $Self->GetArgument('invoker'); + + $Self->Print( + "Triggering $Invoker for immediate (asynchronous) execution.\n" + ); + + my $Result = $Kernel::OM->Get('Kernel::GenericInterface::Requester')->Run( $Self->{InvokerTaskData}->%* ); + + $Self->Print("Done.\n"); + return $Self->ExitCodeOk(); +} + +1; diff --git a/Kernel/System/DynamicField/Driver/BaseDateTime.pm b/Kernel/System/DynamicField/Driver/BaseDateTime.pm index 7869b6147a..1f4e23317b 100644 --- a/Kernel/System/DynamicField/Driver/BaseDateTime.pm +++ b/Kernel/System/DynamicField/Driver/BaseDateTime.pm @@ -264,6 +264,11 @@ sub EditFieldRender { $FieldClass .= ' Validate_Required'; } + # set readonly css class + if ( $Param{Readonly} ) { + $FieldClass .= ' Readonly'; + } + # set error css class if ( $Param{ServerError} ) { $FieldClass .= ' ServerError'; diff --git a/Kernel/System/DynamicField/Driver/Checkbox.pm b/Kernel/System/DynamicField/Driver/Checkbox.pm index 5f9b663dd3..2660260216 100644 --- a/Kernel/System/DynamicField/Driver/Checkbox.pm +++ b/Kernel/System/DynamicField/Driver/Checkbox.pm @@ -373,6 +373,11 @@ sub EditFieldRender { $FieldClass .= ' Validate_Required'; } + # set readonly css class + if ( $Param{Readonly} ) { + $FieldClass .= ' Readonly'; + } + # set error css class if ( $Param{ServerError} ) { $FieldClass .= ' ServerError'; diff --git a/Kernel/System/DynamicField/Driver/Date.pm b/Kernel/System/DynamicField/Driver/Date.pm index 2907542141..ec296ed6f7 100644 --- a/Kernel/System/DynamicField/Driver/Date.pm +++ b/Kernel/System/DynamicField/Driver/Date.pm @@ -144,8 +144,8 @@ sub ValueSet { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', - Message => "The value for the field Date is invalid!\n" - . "The date must be valid and the time must be 00:00:00", + Message => "The value $ValueItem for the field Date is invalid!\n" + . "The date must be in format 0000-00-00 and the time must be 00:00:00", ); return; @@ -370,6 +370,11 @@ sub EditFieldRender { $FieldClass .= ' Validate_Required'; } + # set readonly css class + if ( $Param{Readonly} ) { + $FieldClass .= ' Readonly'; + } + # set error css class if ( $Param{ServerError} ) { $FieldClass .= ' ServerError'; diff --git a/Kernel/System/DynamicField/Driver/RichText.pm b/Kernel/System/DynamicField/Driver/RichText.pm index d675c2b024..e3d0d7cb91 100644 --- a/Kernel/System/DynamicField/Driver/RichText.pm +++ b/Kernel/System/DynamicField/Driver/RichText.pm @@ -129,6 +129,11 @@ sub EditFieldRender { $FieldClass .= ' Validate_Required'; } + # set readonly css class + if ( $Param{Readonly} ) { + $FieldClass .= ' Readonly'; + } + # set error css class if ( $Param{ServerError} ) { $FieldClass .= ' ServerError'; diff --git a/Kernel/System/JSON.pm b/Kernel/System/JSON.pm index f9273b979b..ec90c01fde 100644 --- a/Kernel/System/JSON.pm +++ b/Kernel/System/JSON.pm @@ -59,7 +59,7 @@ create a JSON object. Do not use it directly, instead use: sub new { my ($Type) = @_; - # allocate new hash for object + # allocate a new hash for object, even though that hash is never used return bless {}, $Type; } @@ -185,6 +185,15 @@ sub Decode { # grudgingly accept data that is neither a hash- nor an array reference $JSONObject->allow_nonref(1); + # In OTOBO 10.0.x and OTOBO 10.1.x there is a tree walker that + # replaces the boolean values, that is instances of JSON::PP::Boolean, + # with the plain integer values 0 and 1. + # This behavior is reproduced with explicitly declaring + # what should be emitted for JSON booleans 'true' and 'false'. + # Note that when using Cpanel::JSON::XS, the attribute unblessed_bool can be used + # for the same purpose. + $JSONObject->boolean_values( 0, 1 ); + # Deserialize JSON and get a Perl data structure. # Use Try::Tiny as JSON::XS->decode() dies when providing a malformed JSON string. # In that case we want to return an empty list. @@ -264,4 +273,22 @@ sub ToBoolean { return $Scalar ? $Self->True : $Self->False; } +=head2 IsBool() + +Indicates whether the passed in variable is a boolean value. Specifically whether it is an +instance of C. Note that C is an alias for C. + + my $IsBool1 = $JSONObject->IsBool(1); # assigns undef + my $IsBool2 = $JSONObject->IsBool( $JSONObject->False); # assigns 1 + +In this case the returned JSON will be C. For true expressions we get C. + +=cut + +sub IsBool { + my ( $Self, $Scalar ) = @_; + + return Types::Serialiser::is_bool($Scalar); +} + 1; diff --git a/scripts/test/GenericInterface/Mapping/XSLT.t b/scripts/test/GenericInterface/Mapping/XSLT.t index 7f75704e42..f98a12f1dc 100644 --- a/scripts/test/GenericInterface/Mapping/XSLT.t +++ b/scripts/test/GenericInterface/Mapping/XSLT.t @@ -14,6 +14,7 @@ # along with this program. If not, see . # -- +use v5.24; use strict; use warnings; use utf8; @@ -21,14 +22,13 @@ use utf8; # core modules # CPAN modules +use Test2::V0; # OTOBO modules -use Kernel::System::UnitTest::RegisterDriver; # Set up $Kernel::OM and the test driver $Self +use Kernel::System::UnitTest::RegisterOM; # Set up $Kernel::OM use Kernel::GenericInterface::Debugger (); use Kernel::GenericInterface::Mapping (); -our $Self; - my $Home = $Kernel::OM->Get('Kernel::Config')->Get('Home'); my $DebuggerObject = Kernel::GenericInterface::Debugger->new( @@ -40,7 +40,7 @@ my $DebuggerObject = Kernel::GenericInterface::Debugger->new( CommunicationType => 'Provider', ); -my @MappingTests = ( +my @Tests = ( { Name => 'Test invalid xml', Config => { @@ -54,7 +54,7 @@ my @MappingTests = ( ConfigSuccess => 1, }, { - Name => 'Test no xslt', + Name => 'Test no XSLT', Config => { Template => ' ', @@ -67,7 +67,7 @@ my @MappingTests = ( ConfigSuccess => 1, }, { - Name => 'Test invalid xslt', + Name => 'Test invalid XSLT', Config => { Template => ' @@ -324,74 +324,241 @@ my @MappingTests = ( }, ); -TEST: -for my $Test (@MappingTests) { +# add some tests that take the input data from a JSON string +{ + my $JSONObject = $Kernel::OM->Get('Kernel::System::JSON'); - # create a mapping instance - my $MappingObject = Kernel::GenericInterface::Mapping->new( - DebuggerObject => $DebuggerObject, - MappingConfig => { - Type => 'XSLT', - Config => $Test->{Config}, - }, - ); - if ( $Test->{ConfigSuccess} ) { - $Self->Is( - ref $MappingObject, - 'Kernel::GenericInterface::Mapping', - $Test->{Name} . ' MappingObject was correctly instantiated', - ); - next TEST if ref $MappingObject ne 'Kernel::GenericInterface::Mapping'; + # show that the mapping works with parsed JSON + push @Tests, + { + Name => 'ammend simple JSON array', + Config => { + Template => << 'END_TEMPLATE', + + + + + + + + ℘ - U+02118 - WEIERSTRASS ELLIPTIC FUNCTION: + + + + + + +END_TEMPLATE + }, + Data => $JSONObject->Decode( Data => <<'END_JSON' ), +{ + "Structure1": { + "Key2": "is ignored", + "Array1": [ "Element 🅐", "Element 🅑", "Element 🅒" ] } - else { - $Self->IsNot( - ref $MappingObject, - 'Kernel::GenericInterface::Mapping', - $Test->{Name} . ' MappingObject was not correctly instantiated', - ); - next TEST; +} +END_JSON + ResultData => { + WeierstrassArray => [ + '℘ - U+02118 - WEIERSTRASS ELLIPTIC FUNCTION:Element 🅐', + '℘ - U+02118 - WEIERSTRASS ELLIPTIC FUNCTION:Element 🅑', + '℘ - U+02118 - WEIERSTRASS ELLIPTIC FUNCTION:Element 🅒', + ], + }, + ResultSuccess => 1, + ConfigSuccess => 1, + }; + + # now with boolean checks, + # actually string tests as the node values have no type assigned + push @Tests, + { + Name => 'truthiness', + Config => { + Template => << 'END_TEMPLATE', + + + + + + + + + + ⊭ NOT TRUE: empty string: + + + ⊭ NOT TRUE: empty string: + + + ⊭ NOT TRUE: integer 0: + + + ⊨ TRUE: otherwise: + + + + , + + + + + + +END_TEMPLATE + }, + Data => $JSONObject->Decode( Data => <<'END_JSON' ), +{ + "Structure1": { + "Array1": [ "Element 🅐", "Element 🅑", "Element 🅒", 1, "", true, false ] } +} +END_JSON + ResultData => { + 'TrueOrNotTrueArray' => [ + "\x{22a8} TRUE: otherwise:Element \x{1f150},Array1", + "\x{22a8} TRUE: otherwise:Element \x{1f151},Array1", + "\x{22a8} TRUE: otherwise:Element \x{1f152},Array1", + "\x{22a8} TRUE: otherwise:1,Array1", + "\x{22ad} NOT TRUE: empty string:,Array1", + "\x{22a8} TRUE: otherwise:1,Array1", + "\x{22ad} NOT TRUE: integer 0:0,Array1" + ] + }, + ResultSuccess => 1, + ConfigSuccess => 1, + }; - my $MappingResult = $MappingObject->Map( - Data => $Test->{Data}, - DataInclude => $Test->{DataInclude}, - ); + # using a number variable + push @Tests, + { + Name => 'number variable', + Config => { + Template => << 'END_TEMPLATE', + + + + + + + + + + + + ⊨ TRUE: + + + ⊭ NOT TRUE: + + + + , + + + + + + +END_TEMPLATE + }, + Data => $JSONObject->Decode( Data => <<'END_JSON' ), +{ + "Structure1": { + "Array1": [ -1, 0, 1, "-1", "0", "1", false, true ] + } +} +END_JSON + ResultData => { + 'TrueOrNotTrueArray' => [ + "\x{22a8} TRUE:-1,99", + "\x{22ad} NOT TRUE:0,100", + "\x{22a8} TRUE:1,101", + "\x{22a8} TRUE:-1,99", + "\x{22ad} NOT TRUE:0,100", + "\x{22a8} TRUE:1,101", + "\x{22ad} NOT TRUE:0,100", + "\x{22a8} TRUE:1,101", + ] + }, + ResultSuccess => 1, + ConfigSuccess => 1, + }; +} - # check if function return correct status - $Self->Is( - $MappingResult->{Success}, - $Test->{ResultSuccess}, - $Test->{Name} . ' (Success).', - ); +for my $Test (@Tests) { - # check if function return correct data - $Self->IsDeeply( - $MappingResult->{Data}, - $Test->{ResultData}, - $Test->{Name} . ' (Data Structure).', - ); + subtest $Test->{Name} => sub { - if ( !$Test->{ResultSuccess} ) { - $Self->True( - $MappingResult->{ErrorMessage}, - $Test->{Name} . ' error message found', + # create a mapping instance + my $MappingObject = Kernel::GenericInterface::Mapping->new( + DebuggerObject => $DebuggerObject, + MappingConfig => { + Type => 'XSLT', + Config => $Test->{Config}, + }, ); - } + if ( $Test->{ConfigSuccess} ) { + is( + ref $MappingObject, + 'Kernel::GenericInterface::Mapping', + 'MappingObject was correctly instantiated', + ); - # instantiate another object - my $SecondMappingObject = Kernel::GenericInterface::Mapping->new( - DebuggerObject => $DebuggerObject, - MappingConfig => { - Type => 'XSLT', - Config => $Test->{Config}, - }, - ); + return unless ref $MappingObject eq 'Kernel::GenericInterface::Mapping'; + } + else { + isnt( + ref $MappingObject, + 'Kernel::GenericInterface::Mapping', + 'MappingObject was not correctly instantiated', + ); + + return; + } + + my $MappingResult = $MappingObject->Map( + Data => $Test->{Data}, + DataInclude => $Test->{DataInclude}, + ); + + # check if function return correct status + is( + $MappingResult->{Success}, + $Test->{ResultSuccess}, + ( $Test->{ResultSuccess} ? 'Map() was successful' : 'Map() was not successful' ), + ); + + # check if function return correct data + is( + $MappingResult->{Data}, + $Test->{ResultData}, + 'Data Structure', + ); - $Self->Is( - ref $SecondMappingObject, - 'Kernel::GenericInterface::Mapping', - $Test->{Name} . ' SecondMappingObject was correctly instantiated', - ); + if ( !$Test->{ResultSuccess} ) { + diag $MappingResult->{ErrorMessage}; + ok( + $MappingResult->{ErrorMessage}, + 'error message found', + ); + } + + # instantiate another object + my $SecondMappingObject = Kernel::GenericInterface::Mapping->new( + DebuggerObject => $DebuggerObject, + MappingConfig => { + Type => 'XSLT', + Config => $Test->{Config}, + }, + ); + + is( + ref $SecondMappingObject, + 'Kernel::GenericInterface::Mapping', + 'SecondMappingObject was correctly instantiated', + ); + }; } -$Self->DoneTesting(); +done_testing; diff --git a/scripts/test/JSON.t b/scripts/test/JSON.t index 9a49212cad..ce59a9c73a 100644 --- a/scripts/test/JSON.t +++ b/scripts/test/JSON.t @@ -498,16 +498,16 @@ my @DecodeTests = ( Name => 'JSON - complex structure' }, { - Result => 1, - InputDecode => - 'true', - Name => 'JSON - boolean true' + Result => 1, + VerifyScalar => 1, + InputDecode => 'true', + Name => 'JSON - boolean true' }, { - Result => 0, - InputDecode => - 'false', - Name => 'JSON - boolean false' + Result => 0, + VerifyScalar => 1, + InputDecode => 'false', + Name => 'JSON - boolean false' }, { Result => undef, @@ -567,8 +567,34 @@ for my $Test (@DecodeTests) { my $Thingy = $JSONObject->Decode( Data => $Test->{InputDecode}, ); + is( $Thingy, $Test->{Result}, "Decode: $Test->{Name}" ); - is( $Thingy, $Test->{Result}, "decode: $Test->{Name}" ); + # double check because 'is()' does not complain about instances JSON::PP::Boolean + if ( $Test->{VerifyScalar} ) { + is( ref $Thingy, '', "Decode: $Test->{Name}, result is not a reference" ); + } } +# Testing IsBool() +subtest 'IsBool() for non-Booleans' => sub { + is( $JSONObject->IsBool(), undef, 'no argument' ); + is( $JSONObject->IsBool(undef), undef, 'explicit undef' ); + is( $JSONObject->IsBool(''), undef, 'empty string' ); + is( $JSONObject->IsBool(1), undef, 'integer 1' ); + is( $JSONObject->IsBool(2), undef, 'integer 2' ); + + # not sure why these return an empty string instead of undef + is( $JSONObject->IsBool('true'), '', 'string "true"' ); + is( $JSONObject->IsBool('⊨ - U+022A8 - TRUE'), '', 'a string' ); +}; + +subtest 'IsBool() for Booleans' => sub { + is( $JSONObject->IsBool( $JSONObject->True ), 1, 'true' ); + is( $JSONObject->IsBool( $JSONObject->False ), 1, 'false' ); + is( $JSONObject->IsBool( $JSONObject->ToBoolean(undef) ), 1, 'unded boolified' ); + is( $JSONObject->IsBool( $JSONObject->ToBoolean(0) ), 1, '0 boolified' ); + is( $JSONObject->IsBool( $JSONObject->ToBoolean(1) ), 1, '1 boolified' ); + is( $JSONObject->IsBool( $JSONObject->ToBoolean(' ') ), 1, 'single space boolified' ); +}; + done_testing; diff --git a/var/httpd/htdocs/js/Core.UI.Datepicker.js b/var/httpd/htdocs/js/Core.UI.Datepicker.js index edf8a504d4..5e759b389f 100644 --- a/var/httpd/htdocs/js/Core.UI.Datepicker.js +++ b/var/httpd/htdocs/js/Core.UI.Datepicker.js @@ -113,10 +113,12 @@ Core.UI.Datepicker = (function (TargetNS) { * @returns {Boolean} false, if Parameter Element is not of the correct type. * @param {jQueryObject|Object} Element - The jQuery object of a text input field which should get a datepicker. * Or a hash with the Keys 'Year', 'Month' and 'Day' and as values the jQueryObjects of the select drop downs. + * @param {Object} [Attributes] - Optional Attributes to be passed to the datepicker. Possible Attributes: + * - Disabled: Set to true to disable the datepicker. * @description * This function initializes the datepicker on the defined elements. */ - TargetNS.Init = function (Element) { + TargetNS.Init = function (Element, Attributes={}) { var $DatepickerElement, HasDateSelectBoxes = false, @@ -149,6 +151,8 @@ Core.UI.Datepicker = (function (TargetNS) { // Increment number of initialized datepickers on this site DatepickerCount++; + let Disabled = Attributes.Disabled || false; + // Check, if datepicker is used with three input element or with three select boxes if (typeof Element === 'object' && typeof Element.Day !== 'undefined' && @@ -159,7 +163,6 @@ Core.UI.Datepicker = (function (TargetNS) { // Ignore in this case. Element.Day.length ) { - $DatepickerElement = $('').attr('type', 'hidden').attr('id', 'Datepicker' + DatepickerCount); // insert DatepickerElement if ( Core.Config.Get('SessionName') === Core.Config.Get('CustomerPanelSessionName') ) { @@ -288,9 +291,11 @@ Core.UI.Datepicker = (function (TargetNS) { // Check if one additional DOM node is already present. if (!$('#' + Core.App.EscapeSelector(Element.Day.attr('id')) + 'DatepickerIcon').length) { + let disableDatepickerHTML = Disabled ? ' DisabledLink' : ''; + // add datepicker icon and click event if ( Core.Config.Get('SessionName') === Core.Config.Get('CustomerPanelSessionName') ) { - var Icon = $(''); + var Icon = $(''); // auto activate dynamic field on click on Datepicker var DateContainer = $DatepickerElement.parent(); @@ -304,7 +309,7 @@ Core.UI.Datepicker = (function (TargetNS) { $DatepickerElement.after(Icon); } else { - $DatepickerElement.after(''); + $DatepickerElement.after(''); } if (Element.DateInFuture) { @@ -331,10 +336,13 @@ Core.UI.Datepicker = (function (TargetNS) { } } - $('#' + Core.App.EscapeSelector(Element.Day.attr('id')) + 'DatepickerIcon').off('click.Datepicker').on('click.Datepicker', function () { - $DatepickerElement.datepicker('show'); - return false; - }); + + if (!Disabled) { + $('#' + Core.App.EscapeSelector(Element.Day.attr('id')) + 'DatepickerIcon').off('click.Datepicker').on('click.Datepicker', function () { + $DatepickerElement.datepicker('show'); + return false; + }); + }; //adjust z-index of date picker to prevent overlapping with richtexteditors $DatepickerElement.css('position', 'relative'); diff --git a/var/httpd/htdocs/js/Core.UI.RichTextEditor.js b/var/httpd/htdocs/js/Core.UI.RichTextEditor.js index 66d24fbd6f..cdf147ba7d 100644 --- a/var/httpd/htdocs/js/Core.UI.RichTextEditor.js +++ b/var/httpd/htdocs/js/Core.UI.RichTextEditor.js @@ -272,6 +272,11 @@ Core.UI.RichTextEditor = (function (TargetNS) { $domEditableElement = $($EditorArea).closest(".RichTextHolder"); } + //Set to Readonly mode if required + if ($EditorArea.hasClass('Readonly')) { + editor.enableReadOnlyMode('DF_Readonly'); + } + var sourceEditingActive = false; $domEditableElement.resizable(); diff --git a/var/httpd/htdocs/skins/Agent/default/css/Core.Default.css b/var/httpd/htdocs/skins/Agent/default/css/Core.Default.css index 2d7648cbfa..8d0c651524 100644 --- a/var/httpd/htdocs/skins/Agent/default/css/Core.Default.css +++ b/var/httpd/htdocs/skins/Agent/default/css/Core.Default.css @@ -557,6 +557,11 @@ table.AttachmentList.DataTable thead th { position: absolute !important; } +a.DisabledLink { + pointer-events: none; + cursor: default; +} + /** * @subsection Spacings */ @@ -1147,6 +1152,10 @@ span.ImportantArticles i { color: var(--colTextDark); } +.DatepickerIcon.DisabledLink { + color: var(--colTextLight); +} + .RTL .DatepickerIcon { margin: 0 4px 0 0; } diff --git a/var/httpd/htdocs/skins/Customer/default/css/CKEditorCustomStyles.css b/var/httpd/htdocs/skins/Customer/default/css/CKEditorCustomStyles.css index 51b01beb88..cf72c32489 100644 --- a/var/httpd/htdocs/skins/Customer/default/css/CKEditorCustomStyles.css +++ b/var/httpd/htdocs/skins/Customer/default/css/CKEditorCustomStyles.css @@ -82,3 +82,8 @@ along with this program. If not, see . font-size: 16px; color: #ea2400; } + +textarea.Readonly ~ .ck .ck-editor__editable { + box-shadow: inset 0px 0px 8px 2px #bfbfce; + box-shadow: inset 0px 0px 8px 2px var(--colBGDark); +} diff --git a/var/httpd/htdocs/skins/Customer/default/css/Core.Default.css b/var/httpd/htdocs/skins/Customer/default/css/Core.Default.css index 65f956f475..c56069ad5e 100644 --- a/var/httpd/htdocs/skins/Customer/default/css/Core.Default.css +++ b/var/httpd/htdocs/skins/Customer/default/css/Core.Default.css @@ -85,6 +85,10 @@ h3 { color: inherit; } +a.DisabledLink { + pointer-events: none; +} + @media only screen and (max-width: 767px) { h1 { diff --git a/var/httpd/htdocs/skins/Customer/default/css/Core.InputFields.css b/var/httpd/htdocs/skins/Customer/default/css/Core.InputFields.css index 3c7fc156ec..034d6c9b03 100644 --- a/var/httpd/htdocs/skins/Customer/default/css/Core.InputFields.css +++ b/var/httpd/htdocs/skins/Customer/default/css/Core.InputFields.css @@ -607,7 +607,6 @@ input[readonly] { top: 21px; left: -32px; margin-left: -22px; /* +4px for empty spaces */ - z-index: 1001; } .Tooltip > .Content { @@ -622,11 +621,14 @@ input[readonly] { } .oooTooltip i { + position: relative; + z-index: 100; font-size: 18px; } .oooTooltip > .Content { - z-index: 1; + z-index: 1100; + position: relative; display: none; color: #00023c; color: var(--colTextDark); @@ -638,6 +640,7 @@ input[readonly] { left: -260px; top: -8px; width: 280px; + z-index: 1100; } .TooltipContainer { diff --git a/var/httpd/htdocs/skins/Customer/default/css/Core.TicketZoom.css b/var/httpd/htdocs/skins/Customer/default/css/Core.TicketZoom.css index 7bb8c0c5bb..713f78aea1 100644 --- a/var/httpd/htdocs/skins/Customer/default/css/Core.TicketZoom.css +++ b/var/httpd/htdocs/skins/Customer/default/css/Core.TicketZoom.css @@ -392,7 +392,7 @@ button.oooM.ActivitySubmitButton { height: auto; border-radius: 15px; box-shadow: 0 1px 4px 0 rgba(4, 0, 71, 0.16); - box-shadow: 0 12px 16px 0 var(--colShadowDark); + box-shadow: 0 1px 4px 0 var(--colShadowDark); background-color: #ffffff; }