From df20d10afa40eb700360075e2bfa33ced30afde5 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Thu, 13 Aug 2020 18:57:07 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02847=20-=20Add=20activity=20panel=20to?= =?UTF-8?q?=20object=20details=20(and=20some=20variables=20renaming)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/backoffice/base/_base.scss | 7 + css/backoffice/components/_breadcrumbs.scss | 2 +- css/backoffice/components/_global-search.scss | 4 +- css/backoffice/components/_quick-create.scss | 4 +- css/backoffice/layout/_all.scss | 4 +- css/backoffice/layout/_content.scss | 8 +- css/backoffice/layout/_top-bar.scss | 4 +- .../activity-panel/_activity-entry.scss | 176 ++++++++ .../activity-panel/_activity-panel.scss | 227 ++++++++++ css/backoffice/pages/_base.scss | 1 + css/backoffice/utils/helpers/_all.scss | 1 + css/backoffice/utils/helpers/_depression.scss | 27 ++ css/backoffice/utils/helpers/_misc.scss | 15 +- css/backoffice/utils/variables/_color.scss | 20 + .../vendors/_bulma-variables-overload.scss | 5 +- .../en.dictionary.itop.activity-panel.php | 39 ++ js/layouts/activity-panel.js | 278 ++++++++++++ lib/composer/autoload_classmap.php | 5 + lib/composer/autoload_real.php | 3 - lib/composer/autoload_static.php | 5 + .../ActivityEntry/ActivityEntry.php | 223 ++++++++++ .../ActivityEntry/ActivityEntryFactory.php | 62 +++ .../ActivityEntry/CaseLogEntry.php | 74 ++++ .../UI/Layout/ActivityPanel/ActivityPanel.php | 397 ++++++++++++++++++ .../ActivityPanel/ActivityPanelFactory.php | 67 +++ .../Layout/PageContent/PageContentFactory.php | 5 + .../activity-entry/caselog-entry.html.twig | 5 + .../activity-entry/layout.html.twig | 31 ++ .../activity-panel/entry-group.html.twig | 9 + .../layouts/activity-panel/layout.html.twig | 75 ++++ .../layouts/activity-panel/layout.js.twig | 8 + .../layouts/page-content/layout.html.twig | 1 - .../page-content/with-side-content.html.twig | 1 - 33 files changed, 1775 insertions(+), 18 deletions(-) create mode 100644 css/backoffice/layout/activity-panel/_activity-entry.scss create mode 100644 css/backoffice/layout/activity-panel/_activity-panel.scss create mode 100644 css/backoffice/utils/helpers/_depression.scss create mode 100644 dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php create mode 100644 js/layouts/activity-panel.js create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityPanel.php create mode 100644 sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php create mode 100644 templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig create mode 100644 templates/layouts/activity-panel/activity-entry/layout.html.twig create mode 100644 templates/layouts/activity-panel/entry-group.html.twig create mode 100644 templates/layouts/activity-panel/layout.html.twig create mode 100644 templates/layouts/activity-panel/layout.js.twig diff --git a/css/backoffice/base/_base.scss b/css/backoffice/base/_base.scss index 41e4de6f5..503525e58 100644 --- a/css/backoffice/base/_base.scss +++ b/css/backoffice/base/_base.scss @@ -22,22 +22,29 @@ $ibo-hyperlink-color--on-active: $ibo-color-primary-700 !default; $ibo-svg-illustration--fill: $ibo-color-primary-500 !default; +$ibo-content-block--background-color: $ibo-color-white-100 !default; +$ibo-content-block--border: 1px solid $ibo-color-grey-400 !default; + +/* CSS variables */ :root{ --ibo-hyperlink-color: #{$ibo-hyperlink-color}; --ibo-hyperlink-color--on-hover: #{$ibo-hyperlink-color--on-hover}; --ibo-hyperlink-color--on-active: #{$ibo-hyperlink-color--on-active}; } +/* Box sizing reset */ *, *::before, *::after{ box-sizing: border-box; } +/* Base font size (used by all typographies) */ html{ font-size: 12px; } +/* Hyperlinks reset, ensure that they are of the right color and without decoration everywhere (of course this can be overloaded in some components) */ a{ color: var(--ibo-hyperlink-color); text-decoration: none; diff --git a/css/backoffice/components/_breadcrumbs.scss b/css/backoffice/components/_breadcrumbs.scss index 6b0b5fef6..cfc75edb2 100644 --- a/css/backoffice/components/_breadcrumbs.scss +++ b/css/backoffice/components/_breadcrumbs.scss @@ -28,7 +28,7 @@ $ibo-breadcrumbs--item-separator--margin-x: 12px !default; $ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default; .ibo-breadcrumbs{ - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; * { display: flex; diff --git a/css/backoffice/components/_global-search.scss b/css/backoffice/components/_global-search.scss index 90ce3a1ff..a254bfa4f 100644 --- a/css/backoffice/components/_global-search.scss +++ b/css/backoffice/components/_global-search.scss @@ -73,7 +73,7 @@ $ibo-global-search--compartment--placeholder-hint--text-color: $ibo-color-grey-7 /* SCSS rules */ .ibo-global-search{ position: relative; - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; &.ibo-is-opened{ .ibo-global-search--input{ @@ -90,7 +90,7 @@ $ibo-global-search--compartment--placeholder-hint--text-color: $ibo-color-grey-7 } } .ibo-global-search--head{ - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; background-color: $ibo-global-search--head--background-color; } .ibo-global-search--icon{ diff --git a/css/backoffice/components/_quick-create.scss b/css/backoffice/components/_quick-create.scss index 830579dcf..5f0090f30 100644 --- a/css/backoffice/components/_quick-create.scss +++ b/css/backoffice/components/_quick-create.scss @@ -77,7 +77,7 @@ $ibo-quick-create--compartment--placeholder-hint--text-color: $ibo-color-grey-70 /* SCSS rules */ .ibo-quick-create{ position: relative; - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; &.ibo-is-opened{ .ibo-quick-create--input{ @@ -93,7 +93,7 @@ $ibo-quick-create--compartment--placeholder-hint--text-color: $ibo-color-grey-70 } } .ibo-quick-create--head{ - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; background-color: $ibo-quick-create--head--background-color; } .ibo-quick-create--icon{ diff --git a/css/backoffice/layout/_all.scss b/css/backoffice/layout/_all.scss index d025e61bf..eb4069c8f 100644 --- a/css/backoffice/layout/_all.scss +++ b/css/backoffice/layout/_all.scss @@ -18,4 +18,6 @@ @import "navigation-menu"; @import "top-bar"; -@import "content"; \ No newline at end of file +@import "content"; +@import "activity-panel/activity-panel"; +@import "activity-panel/activity-entry"; \ No newline at end of file diff --git a/css/backoffice/layout/_content.scss b/css/backoffice/layout/_content.scss index 60acf5974..a1e93a84f 100644 --- a/css/backoffice/layout/_content.scss +++ b/css/backoffice/layout/_content.scss @@ -16,9 +16,6 @@ * You should have received a copy of the GNU Affero General Public License */ -.ibo-center-container{ - -} .ibo-center-container--with-side-content{ display: flex; align-items: stretch; @@ -26,4 +23,9 @@ #ibo-main-content{ flex-grow: 1; /* To occupy maximum width, side content will handle its width */ } +} + +#ibo-side-content{ + background-color: $ibo-content-block--background-color; + border-left: $ibo-content-block--border; } \ No newline at end of file diff --git a/css/backoffice/layout/_top-bar.scss b/css/backoffice/layout/_top-bar.scss index be9a3a457..63e6756e5 100644 --- a/css/backoffice/layout/_top-bar.scss +++ b/css/backoffice/layout/_top-bar.scss @@ -38,7 +38,7 @@ $ibo-top-bar--quick-actions--margin-right: $ibo-top-bar--elements-spacing !defau --ibo-top-bar--quick-actions--margin-right: #{$ibo-top-bar--quick-actions--margin-right}; } .ibo-top-bar{ - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; height: var(--ibo-top-bar--height); padding: var(--ibo-top-bar--padding-y) var(--ibo-top-bar--padding-right) var(--ibo-top-bar--padding-y) var(--ibo-top-bar--padding-left); background-color: var(--ibo-top-bar--background-color); @@ -50,7 +50,7 @@ $ibo-top-bar--quick-actions--margin-right: $ibo-top-bar--elements-spacing !defau } } .ibo-top-bar--quick-actions{ - @extend %ibo-vertically-centered-content; + @extend %ibo-full-height-content; margin-right: var(--ibo-top-bar--quick-actions--margin-right); .ibo-global-search{ diff --git a/css/backoffice/layout/activity-panel/_activity-entry.scss b/css/backoffice/layout/activity-panel/_activity-entry.scss new file mode 100644 index 000000000..579a19fa9 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_activity-entry.scss @@ -0,0 +1,176 @@ +/*! + * 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 */ + +/* - Entry group */ +$ibo-activity-panel--entry-group--margin-bottom: 24px !default; + +/* - Entry */ +$ibo-activity-entry--medallion--margin-with-information: 8px !default; +$ibo-activity-entry--medallion--margin-bottom: 18px !default; +$ibo-activity-entry--medallion--diameter: 32px !default; +$ibo-activity-entry--medallion--border-radius: $ibo-border-radius-full !default; +$ibo-activity-entry--medallion--has-image--background-color: $ibo-color-blue-100 !default; +$ibo-activity-entry--medallion--has-image--box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.25) !default; +$ibo-activity-entry--medallion--has-no-image--background-color: $ibo-color-blue-grey-600 !default; +$ibo-activity-entry--medallion--has-no-image--text-color: $ibo-color-white-100 !default; +$ibo-activity-entry--medallion--has-no-image--border: 1px solid $ibo-color-grey-200 !default; + +$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--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-accent-strip--width: 2px !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--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; + +/* Entry group */ +.ibo-activity-panel--entry-group{ + &:not(:last-child){ + margin-bottom: $ibo-activity-panel--entry-group--margin-bottom; + } +} + +/* Entry */ +.ibo-activity-entry{ + display: flex; + flex-direction: row; + align-items: flex-end; + + /* Last entry */ + &:not(:last-child){ + .ibo-activity-entry--medallion{ + visibility: hidden; /* Show only medallion on the last entry */ + } + .ibo-activity-entry--sub-information{ + margin-bottom: $ibo-activity-entry--sub-information--margin-bottom; + } + } + + /* Current or not user specificities */ + &.ibo-is-current-user{ + flex-direction: row-reverse; + + .ibo-activity-entry--medallion{ + margin-right: initial; + margin-left: $ibo-activity-entry--medallion--margin-with-information; + } + .ibo-activity-entry--information{ + margin-right: 0; + margin-left: $ibo-activity-entry--information--margin-to-other-side; + } + .ibo-activity-entry--main-information{ + background-color: $ibo-activity-entry--main-information--is-current-user--background-color; + } + .ibo-activity-entry--sub-information{ + text-align: right; + } + + /* Bubble tip on the right for last entry of the group */ + &:last-child{ + .ibo-activity-entry--main-information{ + border-bottom-right-radius: $ibo-activity-entry--main-information--border-radius--for-tip; + border-bottom-left-radius: $ibo-activity-entry--main-information--border-radius; + } + } + } + &:not(.ibo-is-current-user){ + .ibo-activity-entry--information{ + margin-right: $ibo-activity-entry--information--margin-to-other-side; + margin-left: 0; + } + /* Bubble tip on the left for last entry of the group */ + &:last-child{ + .ibo-activity-entry--main-information{ + border-bottom-right-radius: $ibo-activity-entry--main-information--border-radius; + border-bottom-left-radius: $ibo-activity-entry--main-information--border-radius--for-tip; + } + } + } + + &.ibo-is-closed{ + .ibo-activity-entry--main-information{ + max-height: $ibo-activity-entry--main-information--is-closed--max-height; + overflow: hidden; + cursor: pointer; + + &::after{ + content: "..."; + position: absolute; + top: $ibo-activity-entry--main-information--is-closed--placeholder-top; + left: 0; + padding-left: $ibo-activity-entry--main-information--is-closed--placeholder-padding-left; + width: 100%; + height: 100%; + background-color: inherit; + } + } + } +} +.ibo-activity-entry--medallion{ + margin-right: $ibo-activity-entry--medallion--margin-with-information; + margin-bottom: $ibo-activity-entry--medallion--margin-bottom; + min-width: $ibo-activity-entry--medallion--diameter; /* We have to set a min-width, otherwise the medallion will be compressed when sibling element is too large */ + width: $ibo-activity-entry--medallion--diameter; + min-height: $ibo-activity-entry--medallion--diameter; + height: $ibo-activity-entry--medallion--diameter; + overflow: hidden; + + @extend %ibo-fully-centered-content; + + border-radius: $ibo-activity-entry--medallion--border-radius; + + @extend %ibo-font-ral-nor-150; + + &.ibo-has-image{ + background-color: $ibo-activity-entry--medallion--has-image--background-color; + box-shadow: $ibo-activity-entry--medallion--has-image--box-shadow; + } + &:not(.ibo-has-image){ + background-color: $ibo-activity-entry--medallion--has-no-image--background-color; + color: $ibo-activity-entry--medallion--has-no-image--text-color; + border: $ibo-activity-entry--medallion--has-no-image--border; + } + + .ibo-activity-entry--author-picture{ + max-height: 100%; + } +} +.ibo-activity-entry--main-information{ + position: relative; + padding: $ibo-activity-entry--main-information--padding-y $ibo-activity-entry--main-information--padding-x; + background-color: $ibo-activity-entry--main-information--background-color; + border-radius: $ibo-activity-entry--main-information--border-radius; +} +.ibo-activity-entry--sub-information{ + margin-top: $ibo-activity-entry--sub-information--margin-top; + text-align: left; + color: $ibo-activity-entry--sub-information--text-color; + + @extend %ibo-font-ral-nor-50; +} \ No newline at end of file diff --git a/css/backoffice/layout/activity-panel/_activity-panel.scss b/css/backoffice/layout/activity-panel/_activity-panel.scss new file mode 100644 index 000000000..1a4b4b0a3 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_activity-panel.scss @@ -0,0 +1,227 @@ +/*! + * 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 + */ + +/* Color palette for caselog visual identification */ +$ibo-activity-panel--caselog-main-color-1: $ibo-color-green-700 !default; +$ibo-activity-panel--caselog-main-color-2: $ibo-color-pink-700 !default; +$ibo-activity-panel--caselog-main-color-3: $ibo-color-blue-600 !default; +$ibo-activity-panel--caselog-main-color-4: $ibo-color-orange-400 !default; +$ibo-activity-panel--caselog-main-color-5: $ibo-color-cyan-200 !default; +$ibo-activity-panel--caselog-main-colors: $ibo-activity-panel--caselog-main-color-1, $ibo-activity-panel--caselog-main-color-2, $ibo-activity-panel--caselog-main-color-3, $ibo-activity-panel--caselog-main-color-4, $ibo-activity-panel--caselog-main-color-5 !default; + +/* SCSS variables */ +$ibo-activity-panel--width: 460px !default; +/* TODO: This should be changed when responsive breakpoints are defined and used */ +$ibo-activity-panel--is-expanded--width: 1200px !default; +$ibo-activity-panel--padding-x: 16px !default; +$ibo-activity-panel--padding-y: 0 !default; + +@for $iIdx from 1 through 5 { + .ibo-activity-panel--tab-decoration-for-caselog-#{$iIdx} { + background-color: nth($ibo-activity-panel--caselog-main-colors, $iIdx); + } +} + +/* - Header */ +$ibo-activity-panel--header--padding-x: $ibo-activity-panel--padding-x * 3 !default; /* We need to increase this so the size toggler which will be set in abs. pos. can overlap it nicely */ +$ibo-activity-panel--header--background-color: $ibo-color-grey-100 !default; + +$ibo-activity-panel--size-toggler--color: $ibo-color-grey-600 !default; +$ibo-activity-panel--size-toggler--on-hover--color: $ibo-color-grey-800 !default; + +/* - Tab */ +$ibo-activity-panel--tab--is-active--background-color: $ibo-color-grey-200 !default; + +/* - Tab title */ +$ibo-activity-panel--tab-title--padding-x: 16px !default; +$ibo-activity-panel--tab-title--padding-y: 8px !default; +$ibo-activity-panel--tab-title--on-hover--background-color: $ibo-activity-panel--tab--is-active--background-color !default; +$ibo-activity-panel--tab-title--is-active--background-color: $ibo-activity-panel--tab--is-active--background-color !default; + +$ibo-activity-panel--tab-decoration--width: 12px !default; +$ibo-activity-panel--tab-decoration--height: $ibo-activity-panel--tab-decoration--width !default; +$ibo-activity-panel--tab-decoration--margin-right: 8px !default; +$ibo-activity-panel--tab-decoration--border-radius: $ibo-border-radius-300 !default; + +$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--background-color: $ibo-activity-panel--tab--is-active--background-color !default; + +$ibo-activity-panel--tab-for-caselog--elements-spacing: 16px !default; +$ibo-activity-panel--tab-for-caselog--icon-margin-left: 8px !default; +$ibo-activity-panel--tab-for-caselog--icons-separator-content: "-" !default; +$ibo-activity-panel--tab-for-caselog--icons-separator-margin-x: 8px !default; + +$ibo-activity-panel--tab-for-activity--elements-spacing: 36px !default; +$ibo-activity-panel--tab-for-activity---checkbox-margin-right: 8px !default; + +/* - Body */ +$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; + +/* Whole layout */ +.ibo-activity-panel{ + width: $ibo-activity-panel--width; + transition: width 0.2s ease-in-out; + + &.ibo-is-expanded{ + width: $ibo-activity-panel--is-expanded--width; + } +} + +/* Header */ +.ibo-activity-panel--header{ + position: relative; + padding-left: $ibo-activity-panel--header--padding-x; + padding-right: $ibo-activity-panel--header--padding-x; + @extend %ibo-fully-centered-content; + + background-color: $ibo-activity-panel--header--background-color; + + /* Remove hyperlinks default color */ + a{ + color: inherit; + } +} +.ibo-activity-panel--tabs{ + @extend %ibo-fully-centered-content; +} + +/* Tab */ +.ibo-activity-panel--tab{ + &.ibo-is-active{ + .ibo-activity-panel--tab-title{ + background-color: $ibo-activity-panel--tab-title--is-active--background-color; + } + .ibo-activity-panel--tab-toolbar{ + display: flex; + } + } +} + +/* Tab title */ +.ibo-activity-panel--tab-title{ + padding: $ibo-activity-panel--tab-title--padding-y $ibo-activity-panel--tab-title--padding-x; + @extend %ibo-fully-centered-content; + + &:hover{ + background-color: $ibo-activity-panel--tab-title--on-hover--background-color; + } +} +.ibo-activity-panel--tab-decoration{ + display: inline-flex; + margin-right: $ibo-activity-panel--tab-decoration--margin-right; + width: $ibo-activity-panel--tab-decoration--width; + height: $ibo-activity-panel--tab-decoration--height; + border-radius: $ibo-activity-panel--tab-decoration--border-radius; + @extend %ibo-depression-100; +} +.ibo-activity-panel--tab-text{ + max-width: $ibo-activity-panel--tab-text--max-width; + @extend %ibo-text-truncated-with-ellipsis; +} + +/* Tab toolbar */ +.ibo-activity-panel--tab-toolbar{ + display: none; + align-items: center; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: $ibo-activity-panel--tab-toolbar--height; + padding-left: $ibo-activity-panel--tab-toolbar--padding-x; + padding-right: $ibo-activity-panel--tab-toolbar--padding-x; + background-color: $ibo-activity-panel--tab-toolbar--background-color; +} +.ibo-activity-panel--tab-left-actions, +.ibo-activity-panel--tab-right-actions{ + @extend %ibo-vertically-centered-content; +} +.ibo-activity-panel--tab-middle-actions{ + @extend %ibo-fully-centered-content; +} +.ibo-activity-panel--tab-action{ + @extend %ibo-baseline-centered-content; +} +.ibo-activity-panel--tab-for-caselog{ + .ibo-activity-panel--tab-toolbar{ + justify-content: space-between; + + .ibo-activity-panel--tab-action{ + &:not(:first-child){ + &::before{ + content: $ibo-activity-panel--tab-for-caselog--icons-separator-content; + margin: 0 $ibo-activity-panel--tab-for-caselog--icons-separator-margin-x; + } + } + } + .ibo-activity-panel--tab-info{ + > .ibo-activity-panel--tab-info-icon{ + margin-left: $ibo-activity-panel--tab-for-caselog--icon-margin-left; + } + + &:not(:first-child){ + margin-left: $ibo-activity-panel--tab-for-caselog--elements-spacing; + } + } + } +} +.ibo-activity-panel--tab-for-activity{ + .ibo-activity-panel--tab-toolbar{ + justify-content: center; + + .ibo-activity-panel--tab-action{ + > input{ + margin-right: $ibo-activity-panel--tab-for-activity---checkbox-margin-right; + } + + &:not(:first-child){ + margin-left: $ibo-activity-panel--tab-for-activity--elements-spacing; + } + } + } +} + +/* Size toggler */ +.ibo-activity-panel--size-toggler{ + position: absolute; + right: $ibo-activity-panel--padding-x; + top: 0; + bottom: 0; + @extend %ibo-fully-centered-content; + color: $ibo-activity-panel--size-toggler--color; + + &:hover{ + color: $ibo-activity-panel--size-toggler--on-hover--color; + } +} +.ibo-activity-panel--collapse-icon{ + display: none; +} + +/* Body */ +.ibo-activity-panel--body{ + padding-top: $ibo-activity-panel--body--padding-top; + padding-left: $ibo-activity-panel--body--padding-x; + padding-right: $ibo-activity-panel--body--padding-x; +} + diff --git a/css/backoffice/pages/_base.scss b/css/backoffice/pages/_base.scss index 790d957ed..ece932404 100644 --- a/css/backoffice/pages/_base.scss +++ b/css/backoffice/pages/_base.scss @@ -21,6 +21,7 @@ $ibo-body-text-color: $ibo-color-grey-900 !default; $ibo-body-background-color: $ibo-color-white-200 !default; $ibo-page-container--elements-padding-x: 48px !default; + $ibo-main-content--padding-top: 24px !default; $ibo-main-content--padding-bottom: 24px !default; diff --git a/css/backoffice/utils/helpers/_all.scss b/css/backoffice/utils/helpers/_all.scss index 9978e3884..e116b5f7e 100644 --- a/css/backoffice/utils/helpers/_all.scss +++ b/css/backoffice/utils/helpers/_all.scss @@ -17,6 +17,7 @@ */ @import "typography"; +@import "depression"; @import "elevation"; @import "misc"; @import "font-icon"; \ No newline at end of file diff --git a/css/backoffice/utils/helpers/_depression.scss b/css/backoffice/utils/helpers/_depression.scss new file mode 100644 index 000000000..6fdcffba6 --- /dev/null +++ b/css/backoffice/utils/helpers/_depression.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 + */ + +$ibo-depression-100: inset 0 1px 1px 0 rgba(0, 0, 0, 0.15) !default; + +:root{ + --ibo-elevation-100: #{$ibo-depression-100}; +} + +%ibo-depression-100{ + box-shadow: $ibo-depression-100; +} \ No newline at end of file diff --git a/css/backoffice/utils/helpers/_misc.scss b/css/backoffice/utils/helpers/_misc.scss index 7a6e43e90..9fb060628 100644 --- a/css/backoffice/utils/helpers/_misc.scss +++ b/css/backoffice/utils/helpers/_misc.scss @@ -16,13 +16,26 @@ * You should have received a copy of the GNU Affero General Public License */ +.ibo-is-hidden{ + display: none !important; /* Note: !important is necessary as it needs to overload any standard rules */ +} + %ibo-fully-centered-content{ display: flex; justify-content: center; align-items: center; } -/* Note: This might not be named correctly. The intention is to make an element occupy the full height of its parent and to be centered in it */ %ibo-vertically-centered-content{ + display: flex; + align-items: center; +} +/* Typically to align icons and text */ +%ibo-baseline-centered-content{ + display: flex; + align-items: center; +} +/* Note: This might not be named correctly. The intention is to make an element occupy the full height of its parent and to be centered in it */ +%ibo-full-height-content{ display: flex; align-items: stretch; } diff --git a/css/backoffice/utils/variables/_color.scss b/css/backoffice/utils/variables/_color.scss index 4ea44ee7c..003b08f29 100644 --- a/css/backoffice/utils/variables/_color.scss +++ b/css/backoffice/utils/variables/_color.scss @@ -92,6 +92,16 @@ $ibo-color-cyan-700: hsla(186, 100%, 32.7%, 1) !default; $ibo-color-cyan-800: hsla(185, 100%, 28%, 1) !default; $ibo-color-cyan-900: hsla(182, 100%, 19.6%, 1) !default; +$ibo-color-pink-100: hsla(348, 100%, 98%, 1) !default; +$ibo-color-pink-200: hsla(343, 95%, 92%, 1) !default; +$ibo-color-pink-300: hsla(339, 90%, 85%, 1) !default; +$ibo-color-pink-400: hsla(336, 86%, 75%, 1) !default; +$ibo-color-pink-500: hsla(331, 79%, 66%, 1) !default; +$ibo-color-pink-600: hsla(329, 64%, 54%, 1) !default; +$ibo-color-pink-700: hsla(325, 57%, 46%, 1) !default; +$ibo-color-pink-800: hsla(322, 60%, 37%, 1) !default; +$ibo-color-pink-900: hsla(318, 51%, 29%, 1) !default; + :root{ --ibo-color-white-100: #{$ibo-color-white-100}; --ibo-color-white-200: #{$ibo-color-white-200}; @@ -167,6 +177,16 @@ $ibo-color-cyan-900: hsla(182, 100%, 19.6%, 1) !default; --ibo-color-cyan-700: #{$ibo-color-cyan-700}; --ibo-color-cyan-800: #{$ibo-color-cyan-800}; --ibo-color-cyan-900: #{$ibo-color-cyan-900}; + + --ibo-color-pink-100: #{$ibo-color-pink-100}; + --ibo-color-pink-200: #{$ibo-color-pink-200}; + --ibo-color-pink-300: #{$ibo-color-pink-300}; + --ibo-color-pink-400: #{$ibo-color-pink-400}; + --ibo-color-pink-500: #{$ibo-color-pink-500}; + --ibo-color-pink-600: #{$ibo-color-pink-600}; + --ibo-color-pink-700: #{$ibo-color-pink-700}; + --ibo-color-pink-800: #{$ibo-color-pink-800}; + --ibo-color-pink-900: #{$ibo-color-pink-900}; } /* Semantic palettes */ diff --git a/css/backoffice/vendors/_bulma-variables-overload.scss b/css/backoffice/vendors/_bulma-variables-overload.scss index b74356e7e..3ad58a159 100644 --- a/css/backoffice/vendors/_bulma-variables-overload.scss +++ b/css/backoffice/vendors/_bulma-variables-overload.scss @@ -16,4 +16,7 @@ * You should have received a copy of the GNU Affero General Public License */ -$family-sans-serif: "Monorale"; \ No newline at end of file +$family-sans-serif: "Monorale"; + +$body-overflow-x: hidden !default; +$body-overflow-y: auto !default; \ No newline at end of file diff --git a/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php new file mode 100644 index 000000000..526623e96 --- /dev/null +++ b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php @@ -0,0 +1,39 @@ + 'Expand', + 'UI:Layout:ActivityPanel:SizeToggler:Collapse:Tooltip' => 'Reduce', + + // Activity tab + 'UI:Layout:ActivityPanel:Tab:Activity:Title' => 'Activity', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:CaselogsFilter:Title' => 'Case logs', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:CaselogsFilter:Tooltip' => 'Show / hide case log entries', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:TransitionsFilter:Title' => 'State changes', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:TransitionsFilter:Tooltip' => 'Show / hide state changes', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:EditsFilter:Title' => 'Edits', + 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:EditsFilter:Tooltip' => 'Show / hide fields edits', + + // Case log tab + 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:OpenAll:Tooltip' => 'Open all messages', + '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', +)); \ No newline at end of file diff --git a/js/layouts/activity-panel.js b/js/layouts/activity-panel.js new file mode 100644 index 000000000..98fc089ea --- /dev/null +++ b/js/layouts/activity-panel.js @@ -0,0 +1,278 @@ +/* + * 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 + */ + +; +$(function() +{ + $.widget( 'itop.activity_panel', + { + // default options + options: + { + datetime_format: null, + datetimes_reformat_limit: 14, // In days + }, + css_classes: + { + is_expanded: 'ibo-is-expanded', + is_closed: 'ibo-is-closed', + is_active: 'ibo-is-active', + is_hidden: 'ibo-is-hidden', + }, + js_selectors: + { + panel_size_toggler: '[data-role="ibo-activity-panel--size-toggler"]', + tab: '[data-role="ibo-activity-panel--tab"]', + tab_title: '[data-role="ibo-activity-panel--tab-title"]', + activity_tab_filter: '[data-role="ibo-activity-panel--activity-filter"]', + caselog_tab_open_all: '[data-role="ibo-activity-panel--caselog-open-all"]', + caselog_tab_close_all: '[data-role="ibo-activity-panel--caselog-close-all"]', + 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"]' + }, + + // the constructor + _create: function() + { + this.element.addClass('ibo-activity-panel'); + this._bindEvents(); + this._ReformatDateTimes(); + }, + // events bound via _bind are removed automatically + // revert other modifications here + _destroy: function() + { + this.element.removeClass('ibo-activity-panel'); + }, + _bindEvents: function() + { + const me = this; + const oBodyElem = $('body'); + + // Click on collapse/expand toggler + this.element.find(this.js_selectors.panel_size_toggler).on('click', function(oEvent){ + me._onTogglerClick(oEvent); + }); + // Click on tab title + this.element.find(this.js_selectors.tab_title).on('click', function(oEvent){ + me._onTabTitleClick(oEvent, $(this)); + }); + // Change on activity filters + this.element.find(this.js_selectors.activity_tab_filter).on('change', function(){ + me._onActivityFilterChange($(this)); + }); + // Click on open all case log messages + this.element.find(this.js_selectors.caselog_tab_open_all).on('click', function(){ + me._onCaseLogOpenAllClick($(this)); + }); + // Click on close all case log messages + this.element.find(this.js_selectors.caselog_tab_close_all).on('click', function(){ + me._onCaseLogCloseAllClick($(this)); + }); + // Click on a closed case log message + 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)); + }); + // Mostly for outside clicks that should close elements + oBodyElem.on('click', function(oEvent){ + me._onBodyClick(oEvent); + }); + // Mostly for hotkeys + oBodyElem.on('keyup', function(oEvent){ + me._onBodyKeyUp(oEvent); + }); + }, + + // Events callbacks + _onTogglerClick: function(oEvent) + { + // Avoid anchor glitch + oEvent.preventDefault(); + + // Toggle menu + this.element.toggleClass(this.css_classes.is_expanded); + }, + _onTabTitleClick: function(oEvent, oTabTitleElem) + { + // Avoid anchor glitch + oEvent.preventDefault(); + + const oTabElem = oTabTitleElem.closest(this.js_selectors.tab); + + this.element.find(this.js_selectors.tab).removeClass(this.css_classes.is_active); + oTabElem.addClass(this.css_classes.is_active); + + if(oTabElem.attr('data-tab-type') === 'caselog') + { + this._ShowCaseLogTab(oTabElem.attr('data-caselog-attribute-code')) + } + else + { + this._ShowActivityTab(); + } + }, + _onActivityFilterChange: function(oInputElem) + { + this._ApplyEntryFilters(); + }, + _onCaseLogOpenAllClick: function(oIconElem) + { + const sCaseLogAttCode = oIconElem.closest(this.js_selectors.tab).attr('data-caselog-attribute-code'); + this._OpenAllMessages(sCaseLogAttCode); + }, + _onCaseLogCloseAllClick: function(oIconElem) + { + const sCaseLogAttCode = oIconElem.closest(this.js_selectors.tab).attr('data-caselog-attribute-code'); + this._CloseAllMessages(sCaseLogAttCode); + }, + _onCaseLogClosedMessageClick: function(oEntryElem) + { + this._OpenMessage(oEntryElem); + }, + _onBodyClick: function(oEvent) + { + + }, + _onBodyKeyUp: function(oEvent) + { + + }, + + // Methods + // - Helpers on dates + /** + * Reformat date times to be relative (only if they are not too far in the past) + * @private + */ + _ReformatDateTimes: function() + { + const me = this; + + this.element.find(this.js_selectors.entry_datetime).each(function(){ + const oEntryDateTime = moment($(this).text(), me.options.datetime_format); + const oNowDateTime = moment(); + + // Reformat date time only if it is not too far in the past (eg. "2 years ago" is not easy to interpret) + const fDays = moment.duration(oNowDateTime.diff(oEntryDateTime)).asDays(); + if(fDays < me.options.datetimes_reformat_limit) + { + $(this).text( moment($(this).text(), me.options.datetime_format).fromNow() ); + } + }); + }, + // - Helpers on tabs + _ShowCaseLogTab: function(sCaseLogAttCode) + { + // Show only entries from this case log + this._HideAllEntries(); + this.element.find(this.js_selectors.entry+'[data-entry-caselog-attribute-code="'+sCaseLogAttCode+'"]').removeClass(this.css_classes.is_hidden); + this._UpdateEntryGroupsVisibility(); + }, + _ShowActivityTab: function() + { + // Show all entries but regarding the current filters + this._OpenAllMessages(); + this._ShowAllEntries(); + this._ApplyEntryFilters(); + }, + // - Helpers on messages + _OpenMessage: function(oEntryElem) + { + oEntryElem.removeClass(this.css_classes.is_closed); + }, + _OpenAllMessages: function(sCaseLogAttCode = null) + { + this._SwitchAllMessages('open', sCaseLogAttCode); + }, + _CloseAllMessages: function(sCaseLogAttCode = null) + { + this._SwitchAllMessages('close', sCaseLogAttCode); + }, + _SwitchAllMessages: function(sMode, sCaseLogAttCode = null) + { + const sExtraSelector = (sCaseLogAttCode === null) ? '' : '[data-entry-caselog-attribute-code="' + sCaseLogAttCode+'"]'; + const sCallback = (sMode === 'open') ? 'removeClass' : 'addClass'; + + this.element.find(this.js_selectors.entry + sExtraSelector)[sCallback](this.css_classes.is_closed); + }, + // - Helpers on entries + _ApplyEntryFilters: function() + { + const me = this; + + this.element.find(this.js_selectors.activity_tab_filter).each(function(){ + const aTargetEntryTypes = $(this).attr('data-target-entry-types').split(' '); + const sCallbackMethod = ($(this).prop('checked')) ? '_ShowEntries' : '_HideEntries'; + + for(let iIdx in aTargetEntryTypes) + { + me[sCallbackMethod](aTargetEntryTypes[iIdx]); + } + }); + }, + _ShowAllEntries: function() + { + this.element.find(this.js_selectors.entry).removeClass(this.css_classes.is_hidden); + this._UpdateEntryGroupsVisibility(); + }, + _HideAllEntries: function() + { + this.element.find(this.js_selectors.entry).addClass(this.css_classes.is_hidden); + this._UpdateEntryGroupsVisibility(); + }, + /** + * Show entries of type sEntryType but do not hide the others + * + * @param sEntryType string + * @private + */ + _ShowEntries: function(sEntryType) + { + this.element.find(this.js_selectors.entry+'[data-entry-type="'+sEntryType+'"]').removeClass(this.css_classes.is_hidden); + this._UpdateEntryGroupsVisibility(); + }, + /** + * Hide entries of type sEntryType but do not hide the others + * + * @param sEntryType string + * @private + */ + _HideEntries: function(sEntryType) + { + this.element.find(this.js_selectors.entry+'[data-entry-type="'+sEntryType+'"]').addClass(this.css_classes.is_hidden); + this._UpdateEntryGroupsVisibility(); + }, + _UpdateEntryGroupsVisibility: function() + { + const me = this; + + this.element.find(this.js_selectors.entry_group).each(function(){ + if($(this).find(me.js_selectors.entry + ':not(.' + me.css_classes.is_hidden + ')').length === 0) + { + $(this).addClass(me.css_classes.is_hidden); + } + else + { + $(this).removeClass(me.css_classes.is_hidden); + } + }); + } + }); +}); diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 7c608dfbd..c6b9a9556 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -164,6 +164,11 @@ return array( 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreate' => $baseDir . '/sources/application/UI/Component/QuickCreate/QuickCreate.php', 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreateFactory' => $baseDir . '/sources/application/UI/Component/QuickCreate/QuickCreateFactory.php', '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\\CaseLogEntry' => $baseDir . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.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', 'Combodo\\iTop\\Application\\UI\\Layout\\NavigationMenu\\NavigationMenuFactory' => $baseDir . '/sources/application/UI/Layout/NavigationMenu/NavigationMenuFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\PageContent\\PageContent' => $baseDir . '/sources/application/UI/Layout/PageContent/PageContent.php', diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php index ac16a9508..e8c595bf1 100644 --- a/lib/composer/autoload_real.php +++ b/lib/composer/autoload_real.php @@ -13,9 +13,6 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b } } - /** - * @return \Composer\Autoload\ClassLoader - */ public static function getLoader() { if (null !== self::$loader) { diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 69256d760..1c02b3ee0 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -394,6 +394,11 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreate' => __DIR__ . '/../..' . '/sources/application/UI/Component/QuickCreate/QuickCreate.php', 'Combodo\\iTop\\Application\\UI\\Component\\QuickCreate\\QuickCreateFactory' => __DIR__ . '/../..' . '/sources/application/UI/Component/QuickCreate/QuickCreateFactory.php', '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\\CaseLogEntry' => __DIR__ . '/../..' . '/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.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', 'Combodo\\iTop\\Application\\UI\\Layout\\NavigationMenu\\NavigationMenuFactory' => __DIR__ . '/../..' . '/sources/application/UI/Layout/NavigationMenu/NavigationMenuFactory.php', 'Combodo\\iTop\\Application\\UI\\Layout\\PageContent\\PageContent' => __DIR__ . '/../..' . '/sources/application/UI/Layout/PageContent/PageContent.php', diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php new file mode 100644 index 000000000..c495fae8b --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php @@ -0,0 +1,223 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry + * @internal + * @since 2.8.0 + */ +class ActivityEntry extends UIBlock +{ + // Overloaded constants + const BLOCK_CODE = 'ibo-activity-entry'; + const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/activity-entry/layout'; + + // Specific constants + const DEFAULT_ORIGIN = 'unknown'; + + /** @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 */ + protected $oDateTime; + /** @var string $sAuthorLogin Login of the author (user, cron, extension, ...) who made the activity of the entry */ + protected $sAuthorLogin; + /** @var string $sAuthorFriendlyname */ + protected $sAuthorFriendlyname; + /** @var string $sAuthorInitials */ + protected $sAuthorInitials; + /** @var string $sAuthorPictureAbsUrl */ + protected $sAuthorPictureAbsUrl; + /** @var bool $bIsFromCurrentUser Flag to know if the user who made the activity was the current user */ + protected $bIsFromCurrentUser; + /** @var string $sOrigin Origin of the entry (case log, cron, lifecycle, user edit, ...) */ + protected $sOrigin; + + /** + * ActivityEntry constructor. + * + * @param string $sContent + * @param \DateTime $oDateTime + * @param \User $sAuthorLogin + * @param string $sId + * + * @throws \OQLException + */ + public function __construct($sContent, DateTime $oDateTime, $sAuthorLogin, $sId = null) + { + parent::__construct($sId); + + $this->SetContent($sContent); + $this->SetDateTime($oDateTime); + $this->SetAuthor($sAuthorLogin); + $this->SetOrigin(static::DEFAULT_ORIGIN); + } + + /** + * Set the content without any filtering / escaping + * + * @param string $sContent + * + * @return $this + */ + public function SetContent($sContent) + { + $this->sContent = $sContent; + return $this; + } + + /** + * Return the raw content without any filtering / escaping + * + * @return string + */ + public function GetContent() + { + return $this->sContent; + } + + /** + * @param \DateTime $oDateTime + * + * @return $this + */ + public function SetDateTime(DateTime $oDateTime) + { + $this->oDateTime = $oDateTime; + return $this; + } + + /** + * Return the date time without formatting, as per the mysql format + * @return string + */ + public function GetRawDateTime() + { + return $this->oDateTime->format(AttributeDateTime::GetInternalFormat()); + } + + /** + * Return the date time formatted as per the iTop config. + * + * @return string + * @throws \Exception + */ + public function GetFormattedDateTime() + { + $oDateTimeFormat = AttributeDateTime::GetFormat(); + return $oDateTimeFormat->Format($this->oDateTime); + } + + /** + * Set the author and its information based on the $sAuthorLogin + * + * @param string $sAuthorLogin + * + * @return $this + * @throws \OQLException + * @throws \Exception + */ + public function SetAuthor($sAuthorLogin) + { + $this->sAuthorLogin = $sAuthorLogin; + // TODO: Check that this does not return '' when author is the CRON or an extension. + $this->sAuthorFriendlyname = UserRights::GetUserFriendlyName($this->sAuthorLogin); + $this->sAuthorInitials = UserRights::GetUserInitials($this->sAuthorLogin); + $this->sAuthorPictureAbsUrl = UserRights::GetContactPictureAbsUrl($this->sAuthorLogin, false); + $this->bIsFromCurrentUser = UserRights::GetUserId($this->sAuthorLogin) === UserRights::GetUserId(); + + return $this; + } + + /** + * @return string + */ + public function GetAuthorLogin() + { + return $this->sAuthorLogin; + } + + /** + * @return string + */ + public function GetAuthorFriendlyname() + { + return $this->sAuthorFriendlyname; + } + + /** + * @return string + */ + public function GetAuthorInitials() + { + return $this->sAuthorInitials; + } + + /** + * @return string + */ + public function GetAuthorPictureAbsUrl() + { + return $this->sAuthorPictureAbsUrl; + } + + /** + * Return true if the current user is the author of the activity entry + * + * @return bool + */ + public function IsFromCurrentUser() + { + return $this->bIsFromCurrentUser; + } + + /** + * Set the origin of the activity entry + * + * @param string $sOrigin + * + * @return $this + */ + protected function SetOrigin($sOrigin) + { + $this->sOrigin = $sOrigin; + return $this; + } + + /** + * Return the origin of the activity entry + * + * @return string + */ + public function GetOrigin() + { + return $this->sOrigin; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php new file mode 100644 index 000000000..81efed4cc --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php @@ -0,0 +1,62 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry + * @since 2.8.0 + */ +class ActivityEntryFactory +{ + /** + * Make a CaseLogEntry entry (for ActivityPanel) from an ormCaseLog array entry. + * + * @param string $sAttCode Code of the case log attribute + * @param array $aOrmEntry + * + * @return \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\CaseLogEntry + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \OQLException + */ + public static function MakeFromCaseLogEntryArray($sAttCode, $aOrmEntry) + { + $oUser = MetaModel::GetObject('User', $aOrmEntry['user_id'], false, true); + $sUserLogin = ($oUser === null) ? '' : $oUser->Get('login'); + + $oEntry = new CaseLogEntry( + $aOrmEntry['message_html'], + DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $aOrmEntry['date']), + $sUserLogin, + $sAttCode + ); + + 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 new file mode 100644 index 000000000..727a81698 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php @@ -0,0 +1,74 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry + * @internal + * @since 2.8.0 + */ +class CaseLogEntry extends ActivityEntry +{ + // Overloaded constants + const BLOCK_CODE = 'ibo-caselog-entry'; + const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/activity-entry/caselog-entry'; + + /** @var string $sAttCode Code of the corresponding case log attribute */ + protected $sAttCode; + + /** + * CaseLogEntry constructor. + * + * @param string $sContent + * @param \DateTime $oDateTime + * @param \User $sAuthorLogin + * @param string $sAttCode + * @param string $sId + * + * @throws \OQLException + */ + public function __construct($sContent, DateTime $oDateTime, $sAuthorLogin, $sAttCode, $sId = null) + { + parent::__construct($sContent, $oDateTime, $sAuthorLogin, $sId); + + $this->sAttCode = $sAttCode; + $this->SetOrigin('caselog:'.$this->sAttCode); + } + + /** + * Return the code of the corresponding case log attribute + * + * @return string + */ + public function GetAttCode() + { + return $this->sAttCode; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php b/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php new file mode 100644 index 000000000..a59592f70 --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityPanel.php @@ -0,0 +1,397 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel + * @internal + * @since 2.8.0 + */ +class ActivityPanel extends UIBlock +{ + // Overloaded constants + const BLOCK_CODE = 'ibo-activity-panel'; + const HTML_TEMPLATE_REL_PATH = 'layouts/activity-panel/layout'; + const JS_TEMPLATE_REL_PATH = 'layouts/activity-panel/layout'; + const JS_FILES_REL_PATH = [ + 'js/layouts/activity-panel.js', + ]; + + /** @var \DBObject $oObject The object for which the activity panel is for */ + protected $oObject; + /** @var array $aCaseLogs Metadata of the case logs (att. code, color, ...), will be use to make the tabs and identify them easily */ + protected $aCaseLogs; + /** @var ActivityEntry[] $aEntries */ + protected $aEntries; + /** @var bool $bAreEntriedSorted True if the entries have been sorted by date */ + protected $bAreEntriedSorted; + + /** + * ActivityPanel constructor. + * + * @param \DBObject $oObject + * @param ActivityEntry[] $aEntries + * @param string|null $sId + * + * @throws \CoreException + * @throws \Exception + */ + public function __construct(DBObject $oObject, $aEntries = [], $sId = null) + { + parent::__construct($sId); + + $this->InitializeCaseLogTabs(); + $this->SetObject($oObject); + $this->SetEntries($aEntries); + $this->bAreEntriedSorted = false; + } + + /** + * Set the object the panel is for, and initialize the corresponding case log tabs. + * + * @param \DBObject $oObject + * + * @return $this + * @throws \CoreException + * @throws \Exception + */ + protected function SetObject(DBObject $oObject) + { + $this->oObject = $oObject; + $sObjectClass = get_class($this->oObject); + + // Initialize the case log tabs + $this->InitializeCaseLogTabs(); + $aCaseLogAttCodes = MetaModel::GetAttributesList($sObjectClass, ['AttributeCaseLog']); + foreach($aCaseLogAttCodes as $sCaseLogAttCode) + { + $this->AddCaseLogTab($sCaseLogAttCode); + } + + return $this; + } + + /** + * Return the object for which the activity panel is for + * + * @return \DBObject + */ + public function GetObject() + { + return $this->oObject; + } + + /** + * Set all entries at once. + * + * @param ActivityEntry[] $aEntries + * + * @return $this + * @throws \Exception + */ + public function SetEntries($aEntries) + { + // Reset entries + $this->aEntries = []; + + foreach($aEntries as $oEntry) + { + $this->AddEntry($oEntry); + } + return $this; + } + + /** + * Return all the entries + * + * @return ActivityEntry[] + */ + public function GetEntries() + { + if($this->bAreEntriedSorted === false) + { + $this->SortEntries(); + } + + return $this->aEntries; + } + + /** + * Return all the entries grouped by author / origin (case log). + * This is useful for the template as it avoid to make the processing there. + * + * @return array + */ + public function GetGroupedEntries() + { + $aGroupedEntries = []; + + $aCurrentGroup = ['author_login' => null, 'origin' => null, 'entries' => []]; + $aPreviousEntryData = ['author_login' => null, 'origin' => null]; + foreach($this->GetEntries() as $sId => $oEntry) + { + // New entry data + $sAuthorLogin = $oEntry->GetAuthorLogin(); + $sOrigin = $oEntry->GetOrigin(); + + // Check if it's time to change of group + if(($sAuthorLogin !== $aPreviousEntryData['author_login']) || ($sOrigin !== $aPreviousEntryData['origin'])) + { + // Flush current group if necessary + if(empty($aCurrentGroup['entries']) === false) + { + $aGroupedEntries[] = $aCurrentGroup; + } + + // Init (first iteration) or reset (other iterations) current group + $aCurrentGroup = ['author_login' => $sAuthorLogin, 'origin' => $sOrigin, 'entries' => []]; + } + + $aCurrentGroup['entries'][] = $oEntry; + $aPreviousEntryData = ['author_login' => $sAuthorLogin, 'origin' => $sOrigin]; + } + // Flush last group + $aGroupedEntries[] = $aCurrentGroup; + + return $aGroupedEntries; + } + + /** + * Sort all entries based on the their date, descending. + * + * @return $this + */ + protected function SortEntries() + { + if(count($this->aEntries) > 1) + { + uasort($this->aEntries, function($oEntryA, $oEntryB){ + /** @var ActivityEntry $oEntryA */ + /** @var ActivityEntry $oEntryB */ + $sDateTimeA = $oEntryA->GetRawDateTime(); + $sDateTimeB = $oEntryB->GetRawDateTime(); + + if($sDateTimeA === $sDateTimeB) + { + return 0; + } + + return ($sDateTimeA > $sDateTimeB) ? -1 : 1; + }); + } + $this->bAreEntriedSorted = true; + + return $this; + } + + /** + * Add an $oEntry after all others, excepted if there is already an entry with the same ID in which case it replaces it. + * + * @param \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntry $oEntry + * + * @return $this + * @throws \Exception + */ + public function AddEntry(ActivityEntry $oEntry) + { + $this->aEntries[$oEntry->GetId()] = $oEntry; + $this->bAreEntriedSorted = false; + + // Add case log to the panel and update metadata when necessary + if($oEntry instanceof CaseLogEntry) + { + $sCaseLogAttCode = $oEntry->GetAttCode(); + $sAuthorLogin = $oEntry->GetAuthorLogin(); + + // Initialize case log metadata + if($this->HasCaseLogTab($sCaseLogAttCode) === false) + { + $this->AddCaseLogTab($sCaseLogAttCode); + } + + // Update metadata + // - Message count + $this->aCaseLogs[$sCaseLogAttCode]['total_messages_count']++; + // - Authors + if(array_key_exists($sAuthorLogin, $this->aCaseLogs[$sCaseLogAttCode]['authors']) === false) + { + $this->aCaseLogs[$sCaseLogAttCode]['authors'][$sAuthorLogin] = [ + 'messages_count' => 0, + ]; + } + $this->aCaseLogs[$sCaseLogAttCode]['authors'][$sAuthorLogin]['messages_count']++; + } + + return $this; + } + + /** + * Remove entry of ID $sEntryId. + * Note that if there is no entry with that ID, it proceeds silently. + * + * @param string $sEntryId + * + * @return $this + */ + public function RemoveEntry($sEntryId) + { + if(array_key_exists($sEntryId, $this->aEntries)) + { + // Recompute case logs metadata only if necessary + $oEntry = $this->aEntries[$sEntryId]; + if($oEntry instanceof CaseLogEntry) + { + $sCaseLogAttCode = $oEntry->GetAttCode(); + $sAuthorLogin = $oEntry->GetAuthorLogin(); + + // Update metadata + // - Message count + $this->aCaseLogs[$sCaseLogAttCode]['total_messages_count']--; + // - Authors + $this->aCaseLogs[$sCaseLogAttCode]['authors'][$sAuthorLogin]['messages_count']--; + if($this->aCaseLogs[$sCaseLogAttCode]['authors'][$sAuthorLogin]['messages_count'] === 0) + { + unset($this->aCaseLogs[$sCaseLogAttCode]['authors'][$sAuthorLogin]); + } + } + + unset($this->aEntries[$sEntryId]); + } + + return $this; + } + + /** + * Return true if there is at least one entry + * + * @return bool + */ + public function HasEntries() + { + return !empty($this->aEntries); + } + + /** + * Return all the case log tabs metadata, not their entries + * + * @return array + */ + public function GetCaseLogTabs() + { + return $this->aCaseLogs; + } + + /** + * @return $this + */ + protected function InitializeCaseLogTabs() + { + $this->aCaseLogs = []; + return $this; + } + + /** + * Add the case log tab to the panel + * Note: Case log entries are added separately, see static::AddEntry() + * + * @param string $sAttCode + * + * @return $this + * @throws \Exception + */ + protected function AddCaseLogTab($sAttCode) + { + // Add case log only if not already existing + if(!array_key_exists($sAttCode, $this->aCaseLogs)) + { + $this->aCaseLogs[$sAttCode] = [ + 'title' => MetaModel::GetLabel(get_class($this->oObject), $sAttCode), + 'total_messages_count' => 0, + 'authors' => [], + ]; + } + + return $this; + } + + /** + * Remove the case log tab from the panel. + * Note: Case log entries will not be removed. + * + * @param string $sAttCode + * + * @return $this + */ + protected function RemoveCaseLogTab($sAttCode) + { + if(array_key_exists($sAttCode, $this->aCaseLogs)) + { + unset($this->aCaseLogs[$sAttCode]); + } + + return $this; + } + + /** + * Return true if the case log of $sIs code has been initialized. + * + * @param string $sAttCode + * + * @return bool + */ + public function HasCaseLogTab($sAttCode) + { + return isset($this->aCaseLogs[$sAttCode]); + } + + /** + * Return true if there is at least one case log declared. + * + * @return bool + */ + public function HasCaseLogTabs() + { + return !empty($this->aCaseLogs); + } + + /** + * Return the formatted (user-friendly) date time format for the JS widget. + * Will be used by moment.js for instance. + * + * @return string + */ + public function GetDateTimeFormatForJSWidget() + { + $oDateTimeFormat = AttributeDateTime::GetFormat(); + return $oDateTimeFormat->ToMomentJS(); + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php b/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php new file mode 100644 index 000000000..b2660c01b --- /dev/null +++ b/sources/application/UI/Layout/ActivityPanel/ActivityPanelFactory.php @@ -0,0 +1,67 @@ + + * @package Combodo\iTop\Application\UI\Layout\ActivityPanel + * @since 2.8.0 + */ +class ActivityPanelFactory +{ + /** + * Make an activity panel for an object details layout, meaning that it should contain the caselogs and the activity. + * + * @param \DBObject $oObject + * + * @return \Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityPanel + * @throws \CoreException + * @throws \Exception + */ + public static function MakeForObjectDetails(DBObject $oObject) + { + $oActivityPanel = new ActivityPanel($oObject); + + // Retrieve case logs entries + $aCaseLogAttCodes = array_keys($oActivityPanel->GetCaseLogTabs()); + foreach($aCaseLogAttCodes as $sCaseLogAttCode) + { + /** @var \ormCaseLog $oCaseLog */ + $oCaseLog = $oObject->Get($sCaseLogAttCode); + foreach($oCaseLog->GetAsArray() as $aOrmEntry) + { + $oCaseLogEntry = ActivityEntryFactory::MakeFromCaseLogEntryArray($sCaseLogAttCode, $aOrmEntry); + $oActivityPanel->AddEntry($oCaseLogEntry); + } + } + + // Retrieve history changes + + return $oActivityPanel; + } +} \ No newline at end of file diff --git a/sources/application/UI/Layout/PageContent/PageContentFactory.php b/sources/application/UI/Layout/PageContent/PageContentFactory.php index c7c12b252..beb1da69c 100644 --- a/sources/application/UI/Layout/PageContent/PageContentFactory.php +++ b/sources/application/UI/Layout/PageContent/PageContentFactory.php @@ -20,6 +20,8 @@ namespace Combodo\iTop\Application\UI\Layout\PageContent; +use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityPanel; +use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityPanelFactory; use DBObject; /** @@ -48,6 +50,7 @@ class PageContentFactory * @param \DBObject $oObject * * @return \Combodo\iTop\Application\UI\Layout\PageContent\PageContentWithSideContent + * @throws \CoreException */ public static function MakeForObjectDetails(DBObject $oObject) { @@ -55,6 +58,8 @@ class PageContentFactory // Add object details layout // Add object activity layout + $oActivityPanel = ActivityPanelFactory::MakeForObjectDetails($oObject); + $oLayout->AddSideBlock($oActivityPanel); return $oLayout; } diff --git a/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig b/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig new file mode 100644 index 000000000..d25f7f4de --- /dev/null +++ b/templates/layouts/activity-panel/activity-entry/caselog-entry.html.twig @@ -0,0 +1,5 @@ +{% extends 'layouts/activity-panel/activity-entry/layout.html.twig' %} + +{% block iboActivityEntryExtraClasses %}ibo-activity-entry--caselog{% endblock %} +{% block iboActivityEntryType %}caselog{% endblock %} +{% block iboActivityEntryExtraDataAttributes %}data-entry-caselog-attribute-code="{{ oUIBlock.GetAttCode() }}"{% 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 new file mode 100644 index 000000000..322893b40 --- /dev/null +++ b/templates/layouts/activity-panel/activity-entry/layout.html.twig @@ -0,0 +1,31 @@ +
+
+ {% block iboActivityEntryMedallion %} + {% if oUIBlock.GetAuthorPictureAbsUrl() is not empty %} + + {% else %} + + {% endif %} + {% endblock %} +
+ +
\ No newline at end of file diff --git a/templates/layouts/activity-panel/entry-group.html.twig b/templates/layouts/activity-panel/entry-group.html.twig new file mode 100644 index 000000000..fafcb4226 --- /dev/null +++ b/templates/layouts/activity-panel/entry-group.html.twig @@ -0,0 +1,9 @@ +{% set oFirstEntry = aEntryGroup.entries|first %} +
+ {% for oEntry in aEntryGroup.entries %} + {{ render_block(oEntry) }} + {% endfor %} +
\ No newline at end of file diff --git a/templates/layouts/activity-panel/layout.html.twig b/templates/layouts/activity-panel/layout.html.twig new file mode 100644 index 000000000..f25395dad --- /dev/null +++ b/templates/layouts/activity-panel/layout.html.twig @@ -0,0 +1,75 @@ +
+
+
+ {% for sCaseLogAttCode, aCaseLogData in oUIBlock.GetCaseLogTabs() %} +
+ + + {{ aCaseLogData.title }} + +
+ +
+ + {{ aCaseLogData.authors|length }} + + + + {{ aCaseLogData.total_messages_count }} + + +
+
+
+ {% endfor %} +
+ + {{ 'UI:Layout:ActivityPanel:Tab:Activity:Title'|dict_s }} + +
+
+ + + +
+
+
+
+ +
+
+ {% for aEntryGroup in oUIBlock.GetGroupedEntries() %} + {{ include('layouts/activity-panel/entry-group.html.twig', {aEntryGroup: aEntryGroup}) }} + {% endfor %} +
+
\ No newline at end of file diff --git a/templates/layouts/activity-panel/layout.js.twig b/templates/layouts/activity-panel/layout.js.twig new file mode 100644 index 000000000..6bdb58042 --- /dev/null +++ b/templates/layouts/activity-panel/layout.js.twig @@ -0,0 +1,8 @@ +// TODO: We need to find a clean way to launch this script only once the JS scripts are loaded +document.addEventListener("DOMContentLoaded", function(){ + setTimeout(function(){ + $('#{{ oUIBlock.GetId() }}').activity_panel({ + datetime_format: {{ oUIBlock.GetDateTimeFormatForJSWidget()|json_encode|raw }} + }); + }, 500); +}); \ No newline at end of file diff --git a/templates/layouts/page-content/layout.html.twig b/templates/layouts/page-content/layout.html.twig index ec2da4818..86841c4cf 100644 --- a/templates/layouts/page-content/layout.html.twig +++ b/templates/layouts/page-content/layout.html.twig @@ -2,7 +2,6 @@ {% block iboPageCenterContainer %}
{% block iboPageMainContent %} - Before GetMainBlocks {% for oSubBlock in oUIBlock.GetMainBlocks() %} {{ render_block(oSubBlock, {aPage: aPage}) }} {% endfor %} diff --git a/templates/layouts/page-content/with-side-content.html.twig b/templates/layouts/page-content/with-side-content.html.twig index 44ed83b7e..8ad8c4616 100644 --- a/templates/layouts/page-content/with-side-content.html.twig +++ b/templates/layouts/page-content/with-side-content.html.twig @@ -6,7 +6,6 @@ {{ parent() }}