From 2ce1c2efeca0ad0c0461114f4491314b916d5ae7 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Mon, 17 Aug 2020 11:53:39 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02847=20-=20Work=20on=20the=20ActivityPan?= =?UTF-8?q?el=20and=20PopoverMenu=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deprecate cmdbAbstractObject::DisplayBareHistory() as history will be replace by ActivityPanel - Rename illustrations to original filenames to find source more easily - Remove unused "max_history_case_log_entry_length" config. parameter - Activity panel: Introduce iCMDBChangeOp and iCMDBChangeOpSetAttribute interface for better dependency injection - Activity panel: Add placeholder when no entry - Activity panel: Fix tab toolbar icons color - Activity panel: Add history entries (entries after the first 50 are not loaded yet) - Popover menu: Fix no border-radius on first/last entries hover --- application/cmdbabstract.class.inc.php | 2 + core/cmdbchangeop.class.inc.php | 29 ++- core/config.class.inc.php | 9 - .../popover-menu/_popover-menu.scss | 1 + css/backoffice/layout/_all.scss | 4 +- .../activity-panel/_activity-entry.scss | 38 +++- .../activity-panel/_activity-panel.scss | 26 ++- .../layout/activity-panel/_caselog-entry.scss | 9 + .../layout/activity-panel/_edits-entry.scss | 48 ++++ .../activity-panel/_transition-entry.scss | 27 +++ css/backoffice/utils/variables/_color.scss | 2 + dictionaries/en.dictionary.itop.core.php | 4 + .../en.dictionary.itop.activity-panel.php | 3 + ...empty-history.svg => undraw_duplicate.svg} | 0 images/illustrations/undraw_reading_time.svg | 1 + ...mpty-history.svg => undraw_web_search.svg} | 0 js/layouts/activity-panel.js | 16 +- lib/composer/autoload_classmap.php | 11 + lib/composer/autoload_static.php | 11 + .../ActivityEntry/ActivityEntry.php | 38 +++- .../ActivityEntry/ActivityEntryFactory.php | 71 +++++- .../CMDBChangeOpAttachmentAddedFactory.php | 32 +++ .../CMDBChangeOpAttachmentRemovedFactory.php | 34 +++ .../CMDBChangeOpCreateFactory.php | 44 ++++ .../CMDBChangeOpDeleteFactory.php | 44 ++++ .../CMDBChangeOp/CMDBChangeOpFactory.php | 60 +++++ .../CMDBChangeOpSetAttributeFactory.php | 58 +++++ .../CMDBChangeOpSetAttributeScalarFactory.php | 65 ++++++ .../ActivityEntry/CaseLogEntry.php | 10 +- .../ActivityEntry/EditsEntry.php | 205 ++++++++++++++++++ .../ActivityEntry/TransitionEntry.php | 145 +++++++++++++ .../UI/Layout/ActivityPanel/ActivityPanel.php | 7 +- .../ActivityPanel/ActivityPanelFactory.php | 51 ++++- .../components/global-search/layout.html.twig | 2 +- .../components/quick-create/layout.html.twig | 4 +- .../activity-entry/caselog-entry.html.twig | 6 +- .../activity-entry/edits-entry.html.twig | 24 ++ .../activity-entry/layout.html.twig | 17 +- .../activity-entry/transition-entry.html.twig | 15 ++ .../layouts/activity-panel/layout.html.twig | 17 +- 40 files changed, 1145 insertions(+), 45 deletions(-) create mode 100644 css/backoffice/layout/activity-panel/_edits-entry.scss create mode 100644 css/backoffice/layout/activity-panel/_transition-entry.scss rename images/illustrations/{quick-create-empty-history.svg => undraw_duplicate.svg} (100%) create mode 100644 images/illustrations/undraw_reading_time.svg rename images/illustrations/{global-search-empty-history.svg => undraw_web_search.svg} (100%) create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php create mode 100644 templates/layouts/activity-panel/activity-entry/edits-entry.html.twig create mode 100644 templates/layouts/activity-panel/activity-entry/transition-entry.html.twig diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 921ab0014..6d5366260 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -408,6 +408,8 @@ EOF * @param int $iLimitStart * * @throws \CoreException + * + * @deprecated */ public function DisplayBareHistory(WebPage $oPage, $bEditMode = false, $iLimitCount = 0, $iLimitStart = 0) { diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index b1b015516..e86f94e0b 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -31,7 +31,22 @@ * @package iTopORM */ -class CMDBChangeOp extends DBObject +/** + * Interface iCMDBChangeOp + * + * @since 2.8.0 + */ +interface iCMDBChangeOp +{ + /** + * Describe (as a text string) the modifications corresponding to this change + * + * @return string + */ + public function GetDescription(); +} + +class CMDBChangeOp extends DBObject implements iCMDBChangeOp { public static function Init() { @@ -156,13 +171,22 @@ class CMDBChangeOpDelete extends CMDBChangeOp } } +/** + * Interface iCMDBChangeOpSetAttribute + * + * @since 2.8.0 + */ +interface iCMDBChangeOpSetAttribute +{ + +} /** * Record the modification of an attribute (abstract) * * @package iTopORM */ -class CMDBChangeOpSetAttribute extends CMDBChangeOp +class CMDBChangeOpSetAttribute extends CMDBChangeOp implements iCMDBChangeOpSetAttribute { public static function Init() { @@ -799,7 +823,6 @@ class CMDBChangeOpSetAttributeCaseLog extends CMDBChangeOpSetAttribute } $oObj = $oMonoObjectSet->Fetch(); $oCaseLog = $oObj->Get($this->Get('attcode')); - $iMaxVisibleLength = MetaModel::getConfig()->Get('max_history_case_log_entry_length', 0); $sTextEntry = '
'.$oCaseLog->GetEntryAt($this->Get('lastentry')).'
'; $sResult = Dict::Format('Change:AttName_EntryAdded', $sAttName, $sTextEntry); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 10c7e087c..dcf189b4b 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -967,15 +967,6 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ), - 'max_history_case_log_entry_length' => array( - 'type' => 'integer', - 'description' => 'The length (in number of characters) at which to truncate the (expandable) display (in the history) of a case log entry. If zero, the display in the history is not truncated.', - // examples... not used - 'default' => 60, - 'value' => 60, - 'source_of_value' => '', - 'show_in_conf_sample' => false, - ), 'full_text_chunk_duration' => array( 'type' => 'integer', 'description' => 'Delay after which the results are displayed.', diff --git a/css/backoffice/components/popover-menu/_popover-menu.scss b/css/backoffice/components/popover-menu/_popover-menu.scss index 43d0e5ec5..568e95fc2 100644 --- a/css/backoffice/components/popover-menu/_popover-menu.scss +++ b/css/backoffice/components/popover-menu/_popover-menu.scss @@ -46,6 +46,7 @@ $ibo-popover-menu--section-border-radius: $ibo-popover-menu--border-radius !defa margin: 0px 0px; width: 100%; white-space: nowrap; + overflow: hidden; /* To avoid first/last entries of the menu to have no border-radius on hover */ &:first-child{ border-radius: $ibo-popover-menu--section-border-radius $ibo-popover-menu--section-border-radius 0 0; diff --git a/css/backoffice/layout/_all.scss b/css/backoffice/layout/_all.scss index 879a86cc3..20ae44aaa 100644 --- a/css/backoffice/layout/_all.scss +++ b/css/backoffice/layout/_all.scss @@ -21,4 +21,6 @@ @import "content"; @import "activity-panel/activity-panel"; @import "activity-panel/activity-entry"; -@import "activity-panel/caselog-entry"; \ No newline at end of file +@import "activity-panel/caselog-entry"; +@import "activity-panel/edits-entry"; +@import "activity-panel/transition-entry"; \ No newline at end of file diff --git a/css/backoffice/layout/activity-panel/_activity-entry.scss b/css/backoffice/layout/activity-panel/_activity-entry.scss index 3497edb40..f267a8e5b 100644 --- a/css/backoffice/layout/activity-panel/_activity-entry.scss +++ b/css/backoffice/layout/activity-panel/_activity-entry.scss @@ -33,17 +33,25 @@ $ibo-activity-entry--medallion--has-no-image--border: 1px solid $ibo-color-grey- $ibo-activity-entry--information--margin-to-other-side: $ibo-activity-entry--medallion--diameter + $ibo-activity-entry--medallion--margin-with-information !default; -$ibo-activity-entry--main-information--padding-x: 12px !default; -$ibo-activity-entry--main-information--padding-y: $ibo-activity-entry--main-information--padding-x !default; +$ibo-activity-entry--main-information--padding-x: 16px !default; +$ibo-activity-entry--main-information--padding-y: 12px !default; +$ibo-activity-entry--main-information--text-color: $ibo-color-grey-800 !default; $ibo-activity-entry--main-information--background-color: $ibo-color-grey-200 !default; $ibo-activity-entry--main-information--border-radius: $ibo-border-radius-500 !default; $ibo-activity-entry--main-information--border-radius--for-tip: 0 !default; +$ibo-activity-entry--main-information--elements-spacing: $ibo-activity-entry--main-information--padding-x !default; $ibo-activity-entry--main-information-accent-strip--width: 2px !default; +$ibo-activity-entry--main-information-hyperlink--text-color: $ibo-color-blue-700 !default; +$ibo-activity-entry--main-information-hyperlink--on-hover--text-color: $ibo-color-blue-900 !default; +$ibo-activity-entry--main-information-hyperlink--on-active--text-color: $ibo-activity-entry--main-information-hyperlink--on-hover--text-color !default; $ibo-activity-entry--main-information--is-current-user--background-color: $ibo-color-blue-100 !default; $ibo-activity-entry--main-information--is-closed--max-height: 48px !default; $ibo-activity-entry--main-information--is-closed--placeholder-top: 30px !default; $ibo-activity-entry--main-information--is-closed--placeholder-padding-left: $ibo-activity-entry--main-information--padding-x !default; +$ibo-activity-entry--main-information-icon--text-color: $ibo-color-grey-700 !default; +$ibo-activity-entry--main-information-icon--font-size: 16px !default; + $ibo-activity-entry--sub-information--margin-top: 4px !default; $ibo-activity-entry--sub-information--margin-bottom: $ibo-activity-entry--sub-information--margin-top !default; $ibo-activity-entry--sub-information--text-color: $ibo-color-grey-700 !default; @@ -162,7 +170,12 @@ $ibo-activity-entry--sub-information--text-color: $ibo-color-grey-700 !default; } .ibo-activity-entry--main-information{ position: relative; + display: flex; + flex-direction: row; + align-items: baseline; + padding: $ibo-activity-entry--main-information--padding-y $ibo-activity-entry--main-information--padding-x; + color: $ibo-activity-entry--main-information--text-color; background-color: $ibo-activity-entry--main-information--background-color; border-radius: $ibo-activity-entry--main-information--border-radius; @@ -170,6 +183,27 @@ $ibo-activity-entry--sub-information--text-color: $ibo-color-grey-700 !default; pre{ white-space: pre-wrap; } + + /* Specific hyperlink color */ + a{ + color: $ibo-activity-entry--main-information-hyperlink--text-color; + + &:hover{ + color: $ibo-activity-entry--main-information-hyperlink--on-hover--text-color; + } + &:active, + &:focus{ + color: $ibo-activity-entry--main-information-hyperlink--on-active--text-color; + } + } +} +.ibo-activity-entry--main-information-icon{ + margin-right: $ibo-activity-entry--main-information--elements-spacing; + color: $ibo-activity-entry--main-information-icon--text-color; + font-size: $ibo-activity-entry--main-information-icon--font-size; +} +.ibo-activity-entry--main-information-content{ + } .ibo-activity-entry--sub-information{ margin-top: $ibo-activity-entry--sub-information--margin-top; diff --git a/css/backoffice/layout/activity-panel/_activity-panel.scss b/css/backoffice/layout/activity-panel/_activity-panel.scss index 0be1585ed..70bdb745e 100644 --- a/css/backoffice/layout/activity-panel/_activity-panel.scss +++ b/css/backoffice/layout/activity-panel/_activity-panel.scss @@ -50,6 +50,7 @@ $ibo-activity-panel--tab-text--max-width: 100px !default; /* - Tab toolbar */ $ibo-activity-panel--tab-toolbar--padding-x: $ibo-activity-panel--padding-x !default; $ibo-activity-panel--tab-toolbar--height: 32px !default; +$ibo-activity-panel--tab-toolbar--text-color: $ibo-color-grey-800 !default; $ibo-activity-panel--tab-toolbar--background-color: $ibo-activity-panel--tab--is-active--background-color !default; $ibo-activity-panel--tab-for-caselog--elements-spacing: 16px !default; @@ -64,6 +65,11 @@ $ibo-activity-panel--tab-for-activity---checkbox-margin-right: 8px !default; $ibo-activity-panel--body--padding-top: $ibo-activity-panel--tab-toolbar--height + 16px !default; $ibo-activity-panel--body--padding-x: $ibo-activity-panel--padding-x !default; +$ibo-activity-panel--body--placeholder--margin-top: 16px !default; +$ibo-activity-panel--body--placeholder-image--width: 250px !default; +$ibo-activity-panel--body--placeholder-hint--margin-top: 16px !default; +$ibo-activity-panel--body--placeholder-hint--color: $ibo-color-grey-800 !default; + /* Whole layout */ .ibo-activity-panel{ width: $ibo-activity-panel--width; @@ -85,7 +91,7 @@ $ibo-activity-panel--body--padding-x: $ibo-activity-panel--padding-x !default; /* Remove hyperlinks default color */ a{ - color: inherit; + color: $ibo-activity-panel--tab-toolbar--text-color; } } .ibo-activity-panel--tabs{ @@ -220,3 +226,21 @@ $ibo-activity-panel--body--padding-x: $ibo-activity-panel--padding-x !default; padding-right: $ibo-activity-panel--body--padding-x; } +.ibo-activity-panel--body--placeholder{ + margin-top: $ibo-activity-panel--body--placeholder--margin-top; +} +.ibo-activity-panel--body--placeholder-image{ + @extend %ibo-fully-centered-content; + + > svg { + width: $ibo-activity-panel--body--placeholder-image--width; + height: inherit; + } +} +.ibo-activity-panel--body--placeholder-hint{ + margin-top: $ibo-activity-panel--body--placeholder-hint--margin-top; + color: $ibo-activity-panel--body--placeholder-hint--color; + + @extend %ibo-font-ral-ita-100; + @extend %ibo-fully-centered-content; +} diff --git a/css/backoffice/layout/activity-panel/_caselog-entry.scss b/css/backoffice/layout/activity-panel/_caselog-entry.scss index c31eaca57..4705f0922 100644 --- a/css/backoffice/layout/activity-panel/_caselog-entry.scss +++ b/css/backoffice/layout/activity-panel/_caselog-entry.scss @@ -18,10 +18,19 @@ /* SCSS variables */ $ibo-caselog-entry--highlight-colors: $ibo-caselog-highlight-colors !default; +$ibo-caselog-entry--main-information--padding-y: 12px !default; $ibo-caselog-entry--main-information--decoration--width: 3px !default; /* Main information */ .ibo-caselog-entry{ + .ibo-activity-entry--main-information{ + padding-top: $ibo-caselog-entry--main-information--padding-y; + padding-bottom: $ibo-caselog-entry--main-information--padding-y; + } + .ibo-activity-entry--main-information-icon{ + display: none; + } + /* Highlight color */ .ibo-activity-entry--main-information::before{ content: ""; diff --git a/css/backoffice/layout/activity-panel/_edits-entry.scss b/css/backoffice/layout/activity-panel/_edits-entry.scss new file mode 100644 index 000000000..f28cefb07 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_edits-entry.scss @@ -0,0 +1,48 @@ +/*! + * Copyright (C) 2013-2020 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ + +/* SCSS variables */ +$ibo-edits-entry--short-description--text-color: inherit !default; +$ibo-edits-entry--long-description-toggler-icon--margin-left: 12px !default; +$ibo-edits-entry--long-description--margin-top: 8px !default; + +/* CSS rules */ +/* - Long description */ +a.ibo-edits-entry--short-description { + color: $ibo-edits-entry--short-description--text-color; +} +.ibo-edits-entry--long-description-toggler-icon{ + margin-left: $ibo-edits-entry--long-description-toggler-icon--margin-left; + transition: all 0.2s ease-in-out; +} +.ibo-edits-entry--long-description{ + display: none; + margin-top: $ibo-edits-entry--long-description--margin-top; + list-style: inside; +} +/* - Long desc. opened */ +.ibo-edits-entry{ + &.ibo-is-opened{ + .ibo-edits-entry--long-description-toggler-icon{ + transform: rotateX(180deg); + } + .ibo-edits-entry--long-description{ + display: block; + } + } +} diff --git a/css/backoffice/layout/activity-panel/_transition-entry.scss b/css/backoffice/layout/activity-panel/_transition-entry.scss new file mode 100644 index 000000000..5efae69e6 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_transition-entry.scss @@ -0,0 +1,27 @@ +/*! + * Copyright (C) 2013-2020 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ + +/* SCSS variables */ +$ibo-transition-entry--original-state-label--text-color: $ibo-color-grey-800 !default; +$ibo-transition-entry--original-state-label--text-decoration: line-through !default; + +/* Main information */ +.ibo-transition-entry--original-state-label{ + color: $ibo-transition-entry--original-state-label--text-color; + text-decoration: $ibo-transition-entry--original-state-label--text-decoration; +} diff --git a/css/backoffice/utils/variables/_color.scss b/css/backoffice/utils/variables/_color.scss index 466d522ab..8c148a286 100644 --- a/css/backoffice/utils/variables/_color.scss +++ b/css/backoffice/utils/variables/_color.scss @@ -62,6 +62,7 @@ $ibo-color-green-700: hsla(92, 47.9%, 42.2%, 1) !default; $ibo-color-green-800: hsla(95, 49.5%, 36.5%, 1) !default; $ibo-color-green-900: hsla(103, 55.6%, 26.5%, 1) !default; +$ibo-color-blue-grey-50: hsla(210, 36%, 96%, 1) !default; $ibo-color-blue-grey-100: hsla(198, 15.7%, 83.7%, 1) !default; $ibo-color-blue-grey-200: hsla(200, 15.3%, 73.1%, 1) !default; $ibo-color-blue-grey-300: hsla(200, 15.6%, 62.4%, 1) !default; @@ -148,6 +149,7 @@ $ibo-color-pink-900: hsla(318, 51%, 29%, 1) !default; --ibo-color-green-800: #{$ibo-color-green-800}; --ibo-color-green-900: #{$ibo-color-green-900}; + --ibo-color-blue-grey-50: #{$ibo-color-blue-grey-50}; --ibo-color-blue-grey-100: #{$ibo-color-blue-grey-100}; --ibo-color-blue-grey-200: #{$ibo-color-blue-grey-200}; --ibo-color-blue-grey-300: #{$ibo-color-blue-grey-300}; diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index efda67374..b782157ec 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -298,12 +298,16 @@ Dict::Add('EN US', 'English', 'English', array( 'Change:ObjectCreated' => 'Object created', 'Change:ObjectDeleted' => 'Object deleted', 'Change:ObjectModified' => 'Object modified', + 'Change:TwoAttributesChanged' => 'Edited %1$s and %2$s', + 'Change:ThreeAttributesChanged' => 'Edited %1$s, %2$s and 1 other', + 'Change:FourOrMoreAttributesChanged' => 'Edited %1$s, %2$s and %3$s others', 'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s set to %2$s (previous value: %3$s)', 'Change:AttName_SetTo' => '%1$s set to %2$s', 'Change:Text_AppendedTo_AttName' => '%1$s appended to %2$s', 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', 'Change:AttName_Changed' => '%1$s modified', 'Change:AttName_EntryAdded' => '%1$s modified, new entry added: %2$s', + 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s', 'Change:LinkSet:Added' => 'added %1$s', 'Change:LinkSet:Removed' => 'removed %1$s', 'Change:LinkSet:Modified' => 'modified %1$s', diff --git a/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php index 526623e96..36e9f2a22 100644 --- a/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php +++ b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php @@ -36,4 +36,7 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:CloseAll:Tooltip' => 'Close all messages', 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:AuthorsCount:Tooltip' => 'Number of persons interacting in this log', 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:MessagesCount:Tooltip' => 'Number of messages in this log', + + // Placeholder + 'UI:Layout:ActivityPanel:NoEntry:Placeholder:Hint' => 'It\'s calm up here, no activity yet', )); \ No newline at end of file diff --git a/images/illustrations/quick-create-empty-history.svg b/images/illustrations/undraw_duplicate.svg similarity index 100% rename from images/illustrations/quick-create-empty-history.svg rename to images/illustrations/undraw_duplicate.svg diff --git a/images/illustrations/undraw_reading_time.svg b/images/illustrations/undraw_reading_time.svg new file mode 100644 index 000000000..3548ae7c7 --- /dev/null +++ b/images/illustrations/undraw_reading_time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/illustrations/global-search-empty-history.svg b/images/illustrations/undraw_web_search.svg similarity index 100% rename from images/illustrations/global-search-empty-history.svg rename to images/illustrations/undraw_web_search.svg diff --git a/js/layouts/activity-panel.js b/js/layouts/activity-panel.js index 98fc089ea..8f8212293 100644 --- a/js/layouts/activity-panel.js +++ b/js/layouts/activity-panel.js @@ -30,6 +30,7 @@ $(function() css_classes: { is_expanded: 'ibo-is-expanded', + is_opened: 'ibo-is-opened', is_closed: 'ibo-is-closed', is_active: 'ibo-is-active', is_hidden: 'ibo-is-hidden', @@ -45,7 +46,9 @@ $(function() entry_group: '[data-role="ibo-activity-panel--entry-group"]', entry: '[data-role="ibo-activity-entry"]', entry_main_information: '[data-role="ibo-activity-entry--main-information"]', - entry_datetime: '[data-role="ibo-activity-entry--datetime"]' + entry_datetime: '[data-role="ibo-activity-entry--datetime"]', + edits_entry_long_description: '[data-role="ibo-edits-entry--long-description"]', + edits_entry_long_description_toggler: '[data-role="ibo-edits-entry--long-description-toggler"]', }, // the constructor @@ -90,6 +93,10 @@ $(function() this.element.find(this.js_selectors.entry_group).on('click', '.'+this.css_classes.is_closed + ' ' + this.js_selectors.entry_main_information, function(oEvent){ me._onCaseLogClosedMessageClick($(this).closest(me.js_selectors.entry)); }); + // Click on an edits entry long description toggler + this.element.find(this.js_selectors.edits_entry_long_description_toggler).on('click', function(oEvent){ + me._onEditsTogglerClick(oEvent, $(this).closest(me.js_selectors.entry)); + }); // Mostly for outside clicks that should close elements oBodyElem.on('click', function(oEvent){ me._onBodyClick(oEvent); @@ -146,6 +153,13 @@ $(function() { this._OpenMessage(oEntryElem); }, + _onEditsTogglerClick: function(oEvent, oEntryElem) + { + // Avoid anchor glitch + oEvent.preventDefault(); + + oEntryElem.toggleClass(this.css_classes.is_opened); + }, _onBodyClick: function(oEvent) { diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 4cd4357e5..ee7313e68 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -169,7 +169,16 @@ return array( 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreateHelper' => $baseDir . '/sources/application/UI/Component/QuickCreate/QuickCreateHelper.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\ActivityEntry' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\ActivityEntryFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpAttachmentAddedFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpAttachmentRemovedFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpCreateFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpDeleteFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeScalarFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CaseLogEntry' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\EditsEntry' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityPanel' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityPanelFactory' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\NavigationMenu\\NavigationMenu' => $baseDir . '/sources/application/UI/Layout/NavigationMenu/NavigationMenu.php', @@ -2208,6 +2217,8 @@ return array( 'iApplicationObjectExtension' => $baseDir . '/application/applicationextension.inc.php', 'iApplicationUIExtension' => $baseDir . '/application/applicationextension.inc.php', 'iBackgroundProcess' => $baseDir . '/core/backgroundprocess.inc.php', + 'iCMDBChangeOp' => $baseDir . '/core/cmdbchangeop.class.inc.php', + 'iCMDBChangeOpSetAttribute' => $baseDir . '/core/cmdbchangeop.class.inc.php', 'iDBObjectSetIterator' => $baseDir . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php', 'iDisplay' => $baseDir . '/core/dbobject.class.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 51d25f159..31f345bfd 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -399,7 +399,16 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreateHelper' => __DIR__ . '/../..' . '/sources/application/UI/Component/QuickCreate/QuickCreateHelper.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\ActivityEntry' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\ActivityEntryFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpAttachmentAddedFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpAttachmentRemovedFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpCreateFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpDeleteFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeScalarFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\CaseLogEntry' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\EditsEntry' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php', + 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityPanel' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php', 'Combodo\\iTop\\Application\\UI\\Layout\\ActivityPanel\\ActivityPanelFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\NavigationMenu\\NavigationMenu' => __DIR__ . '/../..' . '/sources/application/UI/Layout/NavigationMenu/NavigationMenu.php', @@ -2438,6 +2447,8 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'iApplicationObjectExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iApplicationUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'iBackgroundProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php', + 'iCMDBChangeOp' => __DIR__ . '/../..' . '/core/cmdbchangeop.class.inc.php', + 'iCMDBChangeOpSetAttribute' => __DIR__ . '/../..' . '/core/cmdbchangeop.class.inc.php', 'iDBObjectSetIterator' => __DIR__ . '/../..' . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php', 'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php', diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php index c495fae8b..f73604d6f 100644 --- a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php @@ -41,8 +41,13 @@ class ActivityEntry extends UIBlock const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/activity-entry/layout'; // Specific constants + /** @var string DEFAULT_ORIGIN */ const DEFAULT_ORIGIN = 'unknown'; + /** @var string DEFAULT_DECORATION_CLASSES */ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-mortar-pestle'; + /** @var string $sDecorationClasses CSS classes to use to decorate the entry */ + protected $sDecorationClasses; /** @var string $sContent Raw content of the entry itself (should not have been processed / escaped) */ protected $sContent; /** @var \DateTime $oDateTime Date / time the entry occurred */ @@ -63,23 +68,48 @@ class ActivityEntry extends UIBlock /** * ActivityEntry constructor. * - * @param string $sContent * @param \DateTime $oDateTime * @param \User $sAuthorLogin - * @param string $sId + * @param string $sContent + * @param string $sIdCode * * @throws \OQLException */ - public function __construct($sContent, DateTime $oDateTime, $sAuthorLogin, $sId = null) + public function __construct(DateTime $oDateTime, $sAuthorLogin, $sContent = null, $sIdCode = null) { - parent::__construct($sId); + parent::__construct($sIdCode); + $this->SetDecorationClasses(static::DEFAULT_DECORATION_CLASSES); $this->SetContent($sContent); $this->SetDateTime($oDateTime); $this->SetAuthor($sAuthorLogin); $this->SetOrigin(static::DEFAULT_ORIGIN); } + /** + * Set the CSS decoration classes + * + * @param string $sDecorationClasses Must be a space-separated list of CSS classes + * + * @return $this + */ + public function SetDecorationClasses($sDecorationClasses) + { + $this->sDecorationClasses = $sDecorationClasses; + + return $this; + } + + /** + * Return a string of the space separated CSS decoration classes + * + * @return string + */ + public function GetDecorationClasses() + { + return $this->sDecorationClasses; + } + /** * Set the content without any filtering / escaping * diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php index 81efed4cc..3fb3aa156 100644 --- a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php @@ -21,8 +21,11 @@ namespace Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry; use AttributeDateTime; +use CMDBChangeOp; use DateTime; +use Exception; use MetaModel; +use ReflectionClass; /** * Class ActivityEntryFactory @@ -34,6 +37,31 @@ use MetaModel; */ class ActivityEntryFactory { + /** + * Make an ActivityEntry entry (for ActivityPanel) based on the $oChangeOp. + * + * @param \CMDBChangeOp $oChangeOp + * + * @return \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntry + * @throws \Exception + */ + public static function MakeFromCmdbChangeOp(CMDBChangeOp $oChangeOp) + { + $sFactoryFqcn = static::GetCmdbChangeOpFactoryClass($oChangeOp); + + // If no factory found, throw an exception as the developer most likely forgot to create it + if(empty($sFactoryFqcn)) + { + throw new Exception('No factory found for '.get_class($oChangeOp).', did you forgot to create one?'); + } + + /** @var \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntry $oEntry */ + /** @noinspection PhpUndefinedMethodInspection Call static method from the $sFactoryFqcn class */ + $oEntry = $sFactoryFqcn::MakeFromCmdbChangeOp($oChangeOp); + + return $oEntry; + } + /** * Make a CaseLogEntry entry (for ActivityPanel) from an ormCaseLog array entry. * @@ -51,12 +79,51 @@ class ActivityEntryFactory $sUserLogin = ($oUser === null) ? '' : $oUser->Get('login'); $oEntry = new CaseLogEntry( - $aOrmEntry['message_html'], DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $aOrmEntry['date']), $sUserLogin, - $sAttCode + $sAttCode, + $aOrmEntry['message_html'] ); return $oEntry; } + + /** + * Return the FQCN of the best fitted factory for the $oChangeOp. If none found, null will be returned. + * + * @param \CMDBChangeOp $oChangeOp + * + * @return string|null + * @throws \ReflectionException + */ + protected static function GetCmdbChangeOpFactoryClass(CMDBChangeOp $oChangeOp) + { + // Classes to search a factory for + $aClassesTree = [get_class($oChangeOp)]; + + // Add parent classes to tree if not a root class + $aParentClasses = class_parents($oChangeOp); + if(is_array($aParentClasses)) + { + $aClassesTree = array_merge($aClassesTree, array_values($aParentClasses)); + } + + $sFactoryFqcn = null; + foreach($aClassesTree as $sClass) + { + // Warning: This will replace all occurrences of 'CMDBChangeOp' which can be an issue on classes using this + // We used the case sensitive search to limit this issue. + $sSimplifiedClass = (new ReflectionClass($sClass))->getShortName(); + $sFactoryFqcnToTry = __NAMESPACE__ . '\\CMDBChangeOp\\' . $sSimplifiedClass . 'Factory'; + + // Stop at the first factory found + if(class_exists($sFactoryFqcnToTry)) + { + $sFactoryFqcn = $sFactoryFqcnToTry; + break; + } + } + + return $sFactoryFqcn; + } } \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php new file mode 100644 index 000000000..85bff0f40 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentAddedFactory.php @@ -0,0 +1,32 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpAttachmentAddedFactory extends CMDBChangeOpFactory +{ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-paperclip'; +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php new file mode 100644 index 000000000..adbc3cb73 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpAttachmentRemovedFactory.php @@ -0,0 +1,34 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpAttachmentRemovedFactory extends CMDBChangeOpFactory +{ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-unlink'; +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php new file mode 100644 index 000000000..c9c885c23 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpCreateFactory.php @@ -0,0 +1,44 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpCreateFactory extends CMDBChangeOpFactory +{ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-unlink'; + /** + * @inheritDoc + */ + public static function MakeFromCmdbChangeOp(iCMDBChangeOp $oChangeOp) + { + $oEntry = parent::MakeFromCmdbChangeOp($oChangeOp); + $oEntry->SetDecorationClasses('fas fa-fw fa-seedling'); + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php new file mode 100644 index 000000000..49594f661 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpDeleteFactory.php @@ -0,0 +1,44 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpDeleteFactory extends CMDBChangeOpFactory +{ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-unlink'; + /** + * @inheritDoc + */ + public static function MakeFromCmdbChangeOp(iCMDBChangeOp $oChangeOp) + { + $oEntry = parent::MakeFromCmdbChangeOp($oChangeOp); + $oEntry->SetDecorationClasses('fas fa-fw fa-trash'); + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php new file mode 100644 index 000000000..0703b7626 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpFactory.php @@ -0,0 +1,60 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpFactory +{ + /** @var string DEFAULT_DECORATION_CLASSES Use to overload the decoration classes from the ActivityEntry */ + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-mortar-pestle'; + + /** + * Make an ActivityEntry from the iCMDBChangeOp $oChangeOp + * + * @param \iCMDBChangeOp $oChangeOp + * + * @return \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntry + * @throws \OQLException + */ + public static function MakeFromCmdbChangeOp(iCMDBChangeOp $oChangeOp) + { + $oDateTime = DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $oChangeOp->Get('date')); + $sAuthorFriendlyname = $oChangeOp->Get('userinfo'); + $sContent = $oChangeOp->GetDescription(); + + $oEntry = new ActivityEntry($oDateTime, $sAuthorFriendlyname, $sContent); + $oEntry->SetDecorationClasses(static::DEFAULT_DECORATION_CLASSES); + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php new file mode 100644 index 000000000..48cefeac4 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeFactory.php @@ -0,0 +1,58 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp + */ +class CMDBChangeOpSetAttributeFactory +{ + /** + * Make an EditsEntry from the iCMDBChangeOpSetAttribute $oChangeOp + * + * @param \iCMDBChangeOpSetAttribute $oChangeOp + * + * @return \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\EditsEntry + * @throws \OQLException + */ + public static function MakeFromCmdbChangeOp(iCMDBChangeOpSetAttribute $oChangeOp) + { + $sHostObjectClass = $oChangeOp->Get('objclass'); + $sAttCode = $oChangeOp->Get('attcode'); + $oDateTime = DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $oChangeOp->Get('date')); + $sAuthorFriendlyname = $oChangeOp->Get('userinfo'); + + $oEntry = new EditsEntry($oDateTime, $sAuthorFriendlyname, $sHostObjectClass); + $oEntry->AddAttribute($sAttCode, $oChangeOp->GetDescription()); + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php new file mode 100644 index 000000000..e50a32b0c --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php @@ -0,0 +1,65 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CMDBChangeOp\Factory + * @since 2.8.0 + */ +class CMDBChangeOpSetAttributeScalarFactory extends CMDBChangeOpSetAttributeFactory +{ + /** + * @inheritDoc + * @throws \CoreException + */ + public static function MakeFromCmdbChangeOp(iCMDBChangeOpSetAttribute $oChangeOp) + { + $sHostObjectClass = $oChangeOp->Get('objclass'); + $sAttCode = $oChangeOp->Get('attcode'); + + // Specific ActivityEntry for transition, otherwise just a regular EditsEntry + if($sAttCode === MetaModel::GetStateAttributeCode($sHostObjectClass)) + { + $oDateTime = DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $oChangeOp->Get('date')); + $sAuthorFriendlyname = $oChangeOp->Get('userinfo'); + + $sOriginStateLabel = MetaModel::GetStateLabel($sHostObjectClass, $oChangeOp->Get('oldvalue')); + $sTargetStateLabel = MetaModel::GetStateLabel($sHostObjectClass, $oChangeOp->Get('newvalue')); + + $oEntry = new TransitionEntry($oDateTime, $sAuthorFriendlyname, $sHostObjectClass, $sOriginStateLabel, $sTargetStateLabel); + } + else + { + $oEntry = parent::MakeFromCmdbChangeOp($oChangeOp); + } + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php index a1c3825af..2d89ed22f 100644 --- a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php @@ -20,11 +20,7 @@ namespace Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry; -use AttributeDateTime; -use Combodo\iTop\Application\UI\UIBlock; use DateTime; -use User; -use UserRights; /** * Class CaseLogEntry @@ -51,17 +47,17 @@ class CaseLogEntry extends ActivityEntry /** * CaseLogEntry constructor. * - * @param string $sContent * @param \DateTime $oDateTime * @param \User $sAuthorLogin * @param string $sAttCode + * @param string $sContentCode * @param string $sId * * @throws \OQLException */ - public function __construct($sContent, DateTime $oDateTime, $sAuthorLogin, $sAttCode, $sId = null) + public function __construct(DateTime $oDateTime, $sAuthorLogin, $sAttCode, $sContentCode, $sId = null) { - parent::__construct($sContent, $oDateTime, $sAuthorLogin, $sId); + parent::__construct($oDateTime, $sAuthorLogin, $sContentCode, $sId); $this->sAttCode = $sAttCode; $this->SetCaseLogRank(static::DEFAULT_CASELOG_RANK); diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php new file mode 100644 index 000000000..1e5e4a6ca --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/EditsEntry.php @@ -0,0 +1,205 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry + * @internal + * @since 2.8.0 + */ +class EditsEntry extends ActivityEntry +{ + // Overloaded constants + const BLOCK_CODE = 'ibo-edits-entry'; + const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/activity-entry/edits-entry'; + + // Specific constants + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-pen'; + + /** @var string $sObjectClass */ + protected $sObjectClass; + /** @var array $aAttributes Array of edited attributes with their code, label and description */ + protected $aAttributes; + + /** + * EditsEntry constructor. + * + * @param \DateTime $oDateTime + * @param \User $sAuthorLogin + * @param string $sObjectClass Class of the object concerned by the edits + * @param string $sId + * + * @throws \OQLException + */ + public function __construct(DateTime $oDateTime, $sAuthorLogin, $sObjectClass, $sId = null) + { + parent::__construct($oDateTime, $sAuthorLogin, null, $sId); + + $this->sObjectClass = $sObjectClass; + $this->SetAttributes([]); + } + + /** + * Return the class of the object concerned by the edits + * + * @return string + */ + public function GetObjectClass() + { + return $this->sObjectClass; + } + + /** + * Set all attributes at once, replacing all existing. + * + * @param array $aAttributes + * + * @return $this + */ + public function SetAttributes($aAttributes) + { + $this->aAttributes = $aAttributes; + + return $this; + } + + /** + * Return an array of edited attributes with their code, label and description + * + * @return array + */ + public function GetAttributes() + { + return $this->aAttributes; + } + + /** + * Add the attribute identified by $sAttCode to the edited attribute. + * Note that if an attribute with the same $sAttCode already exists, it will be replaced. + * + * @param string $sAttCode + * @param string $sEditDescriptionAsHtml The description of the edit already in HTML, it MUSt have been sanitized first (Already in HTML because most of the time it comes from CMDBChangeOp::GetDescription()) + * + * @throws \Exception + */ + public function AddAttribute($sAttCode, $sEditDescriptionAsHtml) + { + $this->aAttributes[$sAttCode] = [ + 'code' => $sAttCode, + 'label' => MetaModel::GetLabel($this->sObjectClass, $sAttCode), + 'description' => $sEditDescriptionAsHtml, + ]; + } + + /** + * Remove the attribute of code $sAttCode from the edited attributes. + * Note that if there is no attribute with this code, it will proceed silently. + * + * @param string $sAttCode + * + * @return array + */ + public function RemoveAttribute($sAttCode) + { + if(array_key_exists($sAttCode, $this->aAttributes)) + { + unset($this->aAttributes[$sAttCode]); + } + + return $this->aAttributes; + } + + /** + * Merge $oEntry into the current one ($this). + * Note that edits on any existing attribute codes will be replaced. + * + * @param \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\EditsEntry $oEntry + * + * @return $this + * @throws \Exception + */ + public function Merge(EditsEntry $oEntry) + { + if($oEntry->GetObjectClass() !== $this->GetObjectClass()) + { + throw new Exception("Cannot merge an entry from {$oEntry->GetObjectClass()} into {$this->GetObjectClass()}, they must be for the same class"); + } + + // Merging attributes + foreach($oEntry->GetAttributes() as $sAttCode => $aAttData) + { + $this->aAttributes[$sAttCode] = $aAttData; + } + + return $this; + } + + /** + * Return the short description of the edits entry in HTML + * + * @return string + */ + public function GetShortDescriptionAsHtml() + { + // We need the array to be indexed by numbers instead of being associative + $aAttributesData = array_values($this->GetAttributes()); + $iAttributesCount = count($aAttributesData); + switch($iAttributesCount) + { + case 0: + $sDescriptionAsHtml = ''; + break; + + case 1: + $sDescriptionAsHtml = $aAttributesData[0]['description']; + break; + + default: + $sFirstAttLabelAsHtml = ''.$aAttributesData[0]['label'].''; + $sSecondAttLabelAsHtml = ''.$aAttributesData[1]['label'].''; + + switch($iAttributesCount) + { + case 2: + $sDescriptionAsHtml = Dict::Format('Change:TwoAttributesChanged', $sFirstAttLabelAsHtml, $sSecondAttLabelAsHtml); + break; + + case 3: + $sDescriptionAsHtml = Dict::Format('Change:ThreeAttributesChanged', $sFirstAttLabelAsHtml, $sSecondAttLabelAsHtml); + break; + + default: + $sDescriptionAsHtml = Dict::Format('Change:FourOrMoreAttributesChanged', $sFirstAttLabelAsHtml, $sSecondAttLabelAsHtml, count($aAttributesData) - 2); + break; + } + } + + return $sDescriptionAsHtml; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php new file mode 100644 index 000000000..1c41e3ce4 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php @@ -0,0 +1,145 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry + * @internal + * @since 2.8.0 + */ +class TransitionEntry extends ActivityEntry +{ + // Overloaded constants + const BLOCK_CODE = 'ibo-transition-entry'; + const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/activity-entry/transition-entry'; + + // Specific constants + const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-map-signs'; + + /** @var string $sOriginStateCode Code of the state before the transition */ + protected $sOriginStateCode; + /** @var string $sOriginStateLabel Label of the $sOriginStateCode state */ + protected $sOriginStateLabel; + /** @var string $sTargetStateCode Code of the state after the transition */ + protected $sTargetStateCode; + /** @var string $sTargetStateLabel Label of the $sTargetStateCode state */ + protected $sTargetStateLabel; + + /** + * TransitionEntry constructor. + * + * @param \DateTime $oDateTime + * @param \User $sAuthorLogin + * @param string $sObjectClass Class of the object which made the transition + * @param string $sOriginStateCode + * @param string $sTargetStateCode + * @param string $sId + * + * @throws \CoreException + * @throws \OQLException + */ + public function __construct(DateTime $oDateTime, $sAuthorLogin, $sObjectClass, $sOriginStateCode, $sTargetStateCode, $sId = null) + { + parent::__construct($oDateTime, $sAuthorLogin, null, $sId); + + $this->SetOriginalState($sObjectClass, $sOriginStateCode); + $this->SetTargetState($sObjectClass, $sTargetStateCode); + } + + /** + * Set the code / label of the state before the transition + * + * @param string $sObjectClass Class of the object the state is from + * @param string $sStateCode + * + * @return $this + * @throws \CoreException + */ + public function SetOriginalState($sObjectClass, $sStateCode) + { + $this->sOriginStateCode = $sStateCode; + $this->sOriginStateLabel = \MetaModel::GetStateLabel($sObjectClass, $sStateCode); + + return $this; + } + + /** + * Return the code of the state before the transition + * + * @return string + */ + public function GetOriginalStateCode() + { + return $this->sOriginStateCode; + } + + /** + * Return the label of the state before the transition + * + * @return string + */ + public function GetOriginalStateLabel() + { + return $this->sOriginStateLabel; + } + + /** + * Set the code / label of the state after the transition + * + * @param string $sObjectClas + * @param string $sStateCode + * + * @return $this + * @throws \CoreException + */ + public function SetTargetState($sObjectClas, $sStateCode) + { + $this->sTargetStateCode = $sStateCode; + $this->sTargetStateLabel = \MetaModel::GetStateLabel($sObjectClas, $sStateCode); + + return $this; + } + + /** + * Return the code of the state after the transition + * + * @return string + */ + public function GetTargetStateCode() + { + return $this->sTargetStateCode; + } + + /** + * Return the label of the state after the transition + * + * @return string + */ + public function GetTargetStateLabel() + { + return $this->sTargetStateLabel; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php b/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php index e293f70c7..5a6e5455a 100644 --- a/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php +++ b/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php @@ -22,7 +22,6 @@ namespace Combodo\iTop\Application\UI\Layout\ActivityPanel; use AttributeDateTime; use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntry; -use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory; use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CaseLogEntry; use Combodo\iTop\Application\UI\UIBlock; use DBObject; @@ -184,8 +183,12 @@ class ActivityPanel extends UIBlock $aCurrentGroup['entries'][] = $oEntry; $aPreviousEntryData = ['author_login' => $sAuthorLogin, 'origin' => $sOrigin]; } + // Flush last group - $aGroupedEntries[] = $aCurrentGroup; + if(empty($aCurrentGroup['entries']) === false) + { + $aGroupedEntries[] = $aCurrentGroup; + } return $aGroupedEntries; } diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php index b2660c01b..775f23930 100644 --- a/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php +++ b/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php @@ -20,9 +20,14 @@ namespace Combodo\iTop\Application\UI\Layout\ActivityPanel; +use CMDBChangeOpSetAttributeCaseLog; use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory; +use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\EditsEntry; use DBObject; +use DBObjectSearch; +use DBObjectSet; use MetaModel; +use UserRights; /** * Class ActivityPanelFactory @@ -45,6 +50,9 @@ class ActivityPanelFactory */ public static function MakeForObjectDetails(DBObject $oObject) { + $sObjClass = get_class($oObject); + $iObjId = $oObject->GetKey(); + $oActivityPanel = new ActivityPanel($oObject); // Retrieve case logs entries @@ -60,7 +68,48 @@ class ActivityPanelFactory } } - // Retrieve history changes + // Retrieve history changes (including case logs entries) + // - Prepare query to retrieve changes + $oChangesSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOp WHERE objclass = :obj_class AND objkey = :obj_key'); + $oChangesSet = new DBObjectSet($oChangesSearch, ['date' => false], ['obj_class' => $sObjClass, 'obj_key' => $iObjId]); + // Note: This limit will include case log changes which will be skipped, but still we count them as they are displayed anyway by the case log attributes themselves + $oChangesSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length')); + + // Prepare previous values to group edits within a same CMDBChange + $iPreviousChangeId = 0; + $oPreviousEditsEntry = null; + + /** @var \CMDBChangeOp $oChangeOp */ + while($oChangeOp = $oChangesSet->Fetch()) + { + // Skip case log changes as they are handled directly from the attributes themselves + if($oChangeOp instanceof CMDBChangeOpSetAttributeCaseLog) + { + continue; + } + + // Make entry from CMDBChangeOp + $iChangeId = $oChangeOp->Get('change'); + $oEntry = ActivityEntryFactory::MakeFromCmdbChangeOp($oChangeOp); + + // If same CMDBChange and mergeable edits entry, we merge them + if( ($iChangeId == $iPreviousChangeId) && ($oPreviousEditsEntry instanceof EditsEntry) && ($oEntry instanceof EditsEntry)) + { + $oPreviousEditsEntry->Merge($oEntry); + } + else + { + $oActivityPanel->AddEntry($oEntry); + + // Set previous edits entry + if($oEntry instanceof EditsEntry) + { + $oPreviousEditsEntry = $oEntry; + } + } + + $iPreviousChangeId = $iChangeId; + } return $oActivityPanel; } diff --git a/templates/components/global-search/layout.html.twig b/templates/components/global-search/layout.html.twig index 410bce7bf..4f14929c0 100644 --- a/templates/components/global-search/layout.html.twig +++ b/templates/components/global-search/layout.html.twig @@ -21,7 +21,7 @@ {% else %}
- {{ source("illustrations/global-search-empty-history.svg") }} + {{ source("illustrations/undraw_web_search.svg") }}
{{ 'UI:Component:GlobalSearch:LastQueries:NoQuery:Placeholder'|dict_s }}
diff --git a/templates/components/quick-create/layout.html.twig b/templates/components/quick-create/layout.html.twig index beedd2a89..8b89966ad 100644 --- a/templates/components/quick-create/layout.html.twig +++ b/templates/components/quick-create/layout.html.twig @@ -15,7 +15,7 @@ {{ 'UI:Component:QuickCreate:Recents:Title'|dict_s }}
- {% if oUIBlock.GetLastClasses()|length > 0 %} + {% if oUIBlock.GetLastClasses()|length > 10 %} {% for aClass in oUIBlock.GetLastClasses() %} {% if aClass.icon_url is defined %} @@ -27,7 +27,7 @@ {% else %}
- {{ source("illustrations/quick-create-empty-history.svg") }} + {{ source("illustrations/undraw_duplicate.svg") }}
{{ 'UI:Component:QuickCreate:LastClasses:NoClass:Placeholder'|dict_s }}
diff --git a/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig b/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig index 8d4bbe885..cd5aea905 100644 --- a/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig +++ b/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig @@ -2,4 +2,8 @@ {% block iboActivityEntryExtraClasses %}ibo-caselog-entry ibo-caselog-entry--entry-for-caselog-{{ oUIBlock.GetCaseLogRank() }}{% endblock %} {% block iboActivityEntryType %}caselog{% endblock %} -{% block iboActivityEntryExtraDataAttributes %}data-entry-caselog-attribute-code="{{ oUIBlock.GetAttCode() }}"{% endblock %} \ No newline at end of file +{% block iboActivityEntryExtraDataAttributes %}data-entry-caselog-attribute-code="{{ oUIBlock.GetAttCode() }}"{% endblock %} + +{% block iboActivityEntryMainInformationIcon %} + +{% endblock %} \ No newline at end of file diff --git a/templates/layouts/activity-panel/activity-entry/edits-entry.html.twig b/templates/layouts/activity-panel/activity-entry/edits-entry.html.twig new file mode 100644 index 000000000..aa057be17 --- /dev/null +++ b/templates/layouts/activity-panel/activity-entry/edits-entry.html.twig @@ -0,0 +1,24 @@ +{% extends 'layouts/activity-panel/activity-entry/layout.html.twig' %} + +{% block iboActivityEntryExtraClasses %}ibo-edits-entry{% endblock %} +{% block iboActivityEntryType %}edits{% endblock %} + +{% block iboActivityEntryMainInformationIcon %} + +{% endblock %} + +{% block iboActivityEntryMainInformationContent %} + {% if oUIBlock.GetAttributes()|length == 1 %} + {{ oUIBlock.GetShortDescriptionAsHtml()|raw }} + {% else %} +
+ {{ oUIBlock.GetShortDescriptionAsHtml()|raw }} + + +
    + {% for sAttCode, aAttData in oUIBlock.GetAttributes() %} +
  • {{ aAttData.description|raw }}
  • + {% endfor %} +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/layouts/activity-panel/activity-entry/layout.html.twig b/templates/layouts/activity-panel/activity-entry/layout.html.twig index 322893b40..1834a692a 100644 --- a/templates/layouts/activity-panel/activity-entry/layout.html.twig +++ b/templates/layouts/activity-panel/activity-entry/layout.html.twig @@ -16,10 +16,19 @@
- {% for aEntryGroup in oUIBlock.GetGroupedEntries() %} - {{ include('layouts/activity-panel/entry-group.html.twig', {aEntryGroup: aEntryGroup}) }} - {% endfor %} + {% if oUIBlock.GetGroupedEntries()|length > 0 %} + {% for aEntryGroup in oUIBlock.GetGroupedEntries() %} + {{ include('layouts/activity-panel/entry-group.html.twig', {aEntryGroup: aEntryGroup}) }} + {% endfor %} + {% else %} +
+
+ {{ source("illustrations/undraw_reading_time.svg") }} +
+
{{ 'UI:Layout:ActivityPanel:NoEntry:Placeholder:Hint'|dict_s }}
+
+ {% endif %}
\ No newline at end of file