N°2847 - Add activity panel to object details (and some variables renaming)

This commit is contained in:
Molkobain
2020-08-13 18:57:07 +02:00
parent f2725c5a5c
commit df20d10afa
33 changed files with 1775 additions and 18 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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{

View File

@@ -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{

View File

@@ -18,4 +18,6 @@
@import "navigation-menu";
@import "top-bar";
@import "content";
@import "content";
@import "activity-panel/activity-panel";
@import "activity-panel/activity-entry";

View File

@@ -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;
}

View File

@@ -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{

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -17,6 +17,7 @@
*/
@import "typography";
@import "depression";
@import "elevation";
@import "misc";
@import "font-icon";

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 */

View File

@@ -16,4 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
*/
$family-sans-serif: "Monorale";
$family-sans-serif: "Monorale";
$body-overflow-x: hidden !default;
$body-overflow-y: auto !default;

View File

@@ -0,0 +1,39 @@
<?php
/**
* 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
*/
// Activity panel
Dict::Add('EN US', 'English', 'English', array(
'UI:Layout:ActivityPanel:SizeToggler:Expand:Tooltip' => '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',
));

View File

@@ -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);
}
});
}
});
});

View File

@@ -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',

View File

@@ -13,9 +13,6 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {

View File

@@ -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',

View File

@@ -0,0 +1,223 @@
<?php
/**
* 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
*/
namespace Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry;
use AttributeDateTime;
use Combodo\iTop\Application\UI\UIBlock;
use DateTime;
use User;
use UserRights;
/**
* Class ActivityEntry
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @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;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* 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
*/
namespace Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry;
use AttributeDateTime;
use DateTime;
use MetaModel;
/**
* Class ActivityEntryFactory
*
* @internal
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @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;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* 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
*/
namespace Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry;
use AttributeDateTime;
use Combodo\iTop\Application\UI\UIBlock;
use DateTime;
use User;
use UserRights;
/**
* Class CaseLogEntry
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @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;
}
}

View File

@@ -0,0 +1,397 @@
<?php
/**
* 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
*/
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;
use MetaModel;
/**
* Class ActivityPanel
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @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();
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* 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
*/
namespace Combodo\iTop\Application\UI\Layout\ActivityPanel;
use Combodo\iTop\Application\UI\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
use DBObject;
use MetaModel;
/**
* Class ActivityPanelFactory
*
* @internal
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @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;
}
}

View File

@@ -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;
}

View File

@@ -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 %}

View File

@@ -0,0 +1,31 @@
<div class="ibo-activity-entry {% if oUIBlock.IsFromCurrentUser() %}ibo-is-current-user{% endif %} {% block iboActivityEntryExtraClasses %}{% endblock %}"
data-role="ibo-activity-entry"
data-entry-type="{% block iboActivityEntryType %}generic{% endblock %}"
data-entry-datetime-raw="{{ oUIBlock.GetRawDateTime() }}"
data-entry-author-login="{{ oUIBlock.GetAuthorLogin() }}"
{% block iboActivityEntryExtraDataAttributes %}{% endblock %}>
<div class="ibo-activity-entry--medallion {% if oUIBlock.GetAuthorPictureAbsUrl() is not empty %}ibo-has-image{% endif %}" data-role="ibo-activity-entry--medallion" data-tooltip-content="{{ oUIBlock.GetAuthorFriendlyname() }}">
{% block iboActivityEntryMedallion %}
{% if oUIBlock.GetAuthorPictureAbsUrl() is not empty %}
<img class="ibo-activity-entry--author-picture" src="{{ oUIBlock.GetAuthorPictureAbsUrl() }}" alt="{{ oUIBlock.GetAuthorInitials() }}" />
{% else %}
<div class="ibo-activity-entry--author-initials">{{ oUIBlock.GetAuthorInitials() }}</div>
{% endif %}
{% endblock %}
</div>
<div class="ibo-activity-entry--information" data-role="ibo-activity-entry--information">
{% block iboActivityEntryInformation %}
<div class="ibo-activity-entry--main-information" data-role="ibo-activity-entry--main-information">
{% block iboActivityEntryMainInformation %}
{# Content is printed as raw because it is stored as HTML in the database and should have been sanitized before storage, so we can assume it is safe #}
{{ oUIBlock.GetContent()|raw }}
{% endblock %}
</div>
<div class="ibo-activity-entry--sub-information" data-role="ibo-activity-entry--sub-information">
{% block iboActivityEntrySubInformation %}
<span class="ibo-activity-entry--datetime" data-role="ibo-activity-entry--datetime" data-tooltip-content="{{ oUIBlock.GetFormattedDateTime() }}">{{ oUIBlock.GetFormattedDateTime() }}</span>
{% endblock %}
</div>
{% endblock %}
</div>
</div>

View File

@@ -0,0 +1,9 @@
{% set oFirstEntry = aEntryGroup.entries|first %}
<div class="ibo-activity-panel--entry-group {% if oFirstEntry.IsFromCurrentUser() %}ibo-is-current-user{% endif %}"
data-role="ibo-activity-panel--entry-group"
data-entry-group-author-login="{{ oFirstEntry.GetAuthorLogin() }}"
data-entry-group-origin="{{ oFirstEntry.GetOrigin() }}">
{% for oEntry in aEntryGroup.entries %}
{{ render_block(oEntry) }}
{% endfor %}
</div>

View File

@@ -0,0 +1,75 @@
<div id="{{ oUIBlock.GetId() }}" class="ibo-activity-panel">
<div class="ibo-activity-panel--header">
<div class="ibo-activity-panel--tabs">
{% for sCaseLogAttCode, aCaseLogData in oUIBlock.GetCaseLogTabs() %}
<div class="ibo-activity-panel--tab ibo-activity-panel--tab-for-caselog" data-role="ibo-activity-panel--tab" data-tab-type="caselog" data-caselog-attribute-code="{{ sCaseLogAttCode }}">
<a href="#" class="ibo-activity-panel--tab-title" data-role="ibo-activity-panel--tab-title">
<span class="ibo-activity-panel--tab-decoration ibo-activity-panel--tab-decoration-for-caselog-{{ loop.index }}"></span>
<span class="ibo-activity-panel--tab-text">{{ aCaseLogData.title }}</span>
</a>
<div class="ibo-activity-panel--tab-toolbar">
<div class="ibo-activity-panel--tab-left-actions">
<a href="#" class="ibo-activity-panel--tab-action ibo-activity-panel--tab-action-open-all" data-role="ibo-activity-panel--caselog-open-all" data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:OpenAll:Tooltip'|dict_s }}">
<span class="fas fa-book-open"></span>
</a>
<a href="#" class="ibo-activity-panel--tab-action ibo-activity-panel--tab-action-close-all" data-role="ibo-activity-panel--caselog-close-all" data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:CloseAll:Tooltip'|dict_s }}">
<span class="fas fa-book"></span>
</a>
</div>
<div class="ibo-activity-panel--tab-right-actions">
<span class="ibo-activity-panel--tab-info ibo-activity-panel--tab-info-authors-count" data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:AuthorsCount:Tooltip'|dict_s }}">
<span class="ibo-activity-panel--tab-info-text">{{ aCaseLogData.authors|length }}</span>
<span class="ibo-activity-panel--tab-info-icon fas fa-users"></span>
</span>
<span class="ibo-activity-panel--tab-info ibo-activity-panel--tab-info-messages-count" data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Caselog:Toolbar:MessagesCount:Tooltip'|dict_s }}">
<span class="ibo-activity-panel--tab-info-text">{{ aCaseLogData.total_messages_count }}</span>
<span class="ibo-activity-panel--tab-info-icon fas fa-comment-alt"></span>
</span>
</div>
</div>
</div>
{% endfor %}
<div class="ibo-activity-panel--tab ibo-activity-panel--tab-for-activity ibo-is-active" data-role="ibo-activity-panel--tab" data-tab-type="activity">
<a href="#" class="ibo-activity-panel--tab-title" data-role="ibo-activity-panel--tab-title">
<span class="ibo-activity-panel--tab-text">{{ 'UI:Layout:ActivityPanel:Tab:Activity:Title'|dict_s }}</span>
</a>
<div class="ibo-activity-panel--tab-toolbar">
<div class="ibo-activity-panel--tab-middle-actions">
<label class="ibo-activity-panel--tab-action"
data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:CaselogsFilter:Tooltip'|dict_s }}">
<input type="checkbox" name="caselogs" data-role="ibo-activity-panel--activity-filter" data-target-entry-types="caselog" checked />
{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:CaselogsFilter:Title'|dict_s }}
</label>
<label class="ibo-activity-panel--tab-action"
data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:TransitionsFilter:Tooltip'|dict_s }}">
<input type="checkbox" name="transitions" data-role="ibo-activity-panel--activity-filter" data-target-entry-types="transition" checked />
{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:TransitionsFilter:Title'|dict_s }}
</label>
<label class="ibo-activity-panel--tab-action"
data-tooltip-content="{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:EditsFilter:Tooltip'|dict_s }}">
<input type="checkbox" name="edits" data-role="ibo-activity-panel--activity-filter" data-target-entry-types="edit" checked />
{{ 'UI:Layout:ActivityPanel:Tab:Activity:Toolbar:EditsFilter:Title'|dict_s }}
</label>
</div>
</div>
</div>
</div>
<div class="ibo-activity-panel--size-toggler" data-role="ibo-activity-panel--size-toggler">
<a href="#" class="ibo-activity-panel--expand-icon"
data-role="ibo-activity-panel--expand-icon"
data-tooltip-content="{{ 'UI:Layout:ActivityPanel:SizeToggler:Expand:Tooltip'|dict_s }}">
<span class="fas fa-fw fa-expand-alt"></span>
</a>
<a href="#" class="ibo-activity-panel--collapse-icon"
data-role="ibo-activity-panel--collapse-icon"
data-tooltip-content="{{ 'UI:Layout:ActivityPanel:SizeToggler:Collapse:Tooltip'|dict_s }}">
<span class="fas fa-fw fa-compress-alt"></span>
</a>
</div>
</div>
<div class="ibo-activity-panel--body">
{% for aEntryGroup in oUIBlock.GetGroupedEntries() %}
{{ include('layouts/activity-panel/entry-group.html.twig', {aEntryGroup: aEntryGroup}) }}
{% endfor %}
</div>
</div>

View File

@@ -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);
});

View File

@@ -2,7 +2,6 @@
{% block iboPageCenterContainer %}
<main id="ibo-main-content">
{% block iboPageMainContent %}
Before GetMainBlocks
{% for oSubBlock in oUIBlock.GetMainBlocks() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% endfor %}

View File

@@ -6,7 +6,6 @@
{{ parent() }}
<aside id="ibo-side-content">
{% block iboPageSideContent %}
Before GetSideBlocks
{% for oSubBlock in oUIBlock.GetSideBlocks() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% endfor %}