Commit e9ce4fb1 authored by Richard Haeser's avatar Richard Haeser
Browse files

[FEATURE] Dashboard for TYPO3

A dashboard is introduced into TYPO3 to show the most important
information to the current logged in user.

Every user with access to this backend module can now have one
or more personal dashboards. Each dashboard can contain several
widgets. Which widgets and in which order the widgets are shown
is up to the users themselves.

Resolves: #90333
Releases: master
Change-Id: I964a52846ec9a1c901baa89508b9df0caecdf81f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63139


Tested-by: default avatarKoen Wouters <koen.wouters@maxserv.com>
Tested-by: default avatarRiny van Tiggelen <info@online-gamer.nl>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benjamin Kott's avatarBenjamin Kott <benjamin.kott@outlook.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Björn Jacob's avatarBjörn Jacob <bjoern.jacob@tritum.de>
Reviewed-by: default avatarKoen Wouters <koen.wouters@maxserv.com>
Reviewed-by: default avatarRiny van Tiggelen <info@online-gamer.nl>
Reviewed-by: Benjamin Kott's avatarBenjamin Kott <benjamin.kott@outlook.com>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Björn Jacob's avatarBjörn Jacob <bjoern.jacob@tritum.de>
parent 5be422b7
......@@ -54,6 +54,7 @@ module.exports = function (grunt) {
typescript: '<%= paths.sources %>/TypeScript/',
sysext: '<%= paths.root %>typo3/sysext/',
form: '<%= paths.sysext %>form/Resources/',
dashboard: '<%= paths.sysext %>dashboard/Resources/',
frontend: '<%= paths.sysext %>frontend/Resources/',
adminpanel: '<%= paths.sysext %>adminpanel/Resources/',
install: '<%= paths.sysext %>install/Resources/',
......@@ -109,6 +110,16 @@ module.exports = function (grunt) {
"<%= paths.form %>Public/Css/form.css": "<%= paths.sass %>form.scss"
}
},
dashboard: {
files: {
"<%= paths.dashboard %>Public/Css/dashboard.css": "<%= paths.sass %>dashboard.scss"
}
},
dashboard_modal: {
files: {
"<%= paths.dashboard %>Public/Css/Modal/style.css": "<%= paths.sass %>dashboard_modal.scss"
}
},
adminpanel: {
files: {
"<%= paths.adminpanel %>Public/Css/adminpanel.css": "<%= paths.sass %>adminpanel.scss"
......@@ -168,6 +179,12 @@ module.exports = function (grunt) {
core: {
src: '<%= paths.core %>Public/Css/*.css'
},
dashboard: {
src: '<%= paths.dashboard %>Public/Css/*.css'
},
dashboard_modal: {
src: '<%= paths.dashboard %>Public/Css/Modal/*.css'
},
form: {
src: '<%= paths.form %>Public/Css/*.css'
},
......@@ -430,6 +447,16 @@ module.exports = function (grunt) {
'wordcount/css/': 'ckeditor-wordcount-plugin/wordcount/css/',
}
},
dashboard: {
options: {
destPrefix: "<%= paths.dashboard %>Public/JavaScript/Contrib"
},
files: {
'muuri.js': 'muuri/dist/muuri.min.js',
'web-animations.min.js': 'web-animations-js/web-animations.min.js',
'web-animations.min.js.map': 'web-animations-js/web-animations.min.js.map'
}
},
all: {
options: {
destPrefix: "<%= paths.core %>Public/JavaScript/Contrib"
......@@ -627,7 +654,7 @@ module.exports = function (grunt) {
* - sass
* - postcss
*/
grunt.registerTask('css', ['newer:formatsass', 'newer:sass', 'newer:postcss']);
grunt.registerTask('css', ['formatsass', 'newer:sass', 'newer:postcss']);
/**
* grunt update task
......
@import "variables/main";
@import "variables/dashboard";
// BASE
@import "dashboard/module";
@import "dashboard/header";
@import "dashboard/empty";
@import "dashboard/grid";
@import "dashboard/button";
// WIDGETS
@import "dashboard/widget";
@import "dashboard/widget_table";
@import "dashboard/widget_cta";
@import "dashboard/widget_doughnut_chart";
@import "dashboard/widget_number";
.dashboard-button {
display: inline-flex;
align-items: center;
border-radius: $dashboard-button-border-radius;
background: $dashboard-button-background;
color: $dashboard-button-color;
padding: $dashboard-button-padding;
text-decoration: none;
&:hover,
&:focus {
text-decoration: none;
background: $dashboard-button-hover-background;
color: $dashboard-button-hover-color;
}
.dashboard-button-icon .icon {
display: block;
}
.dashboard-button-icon + .dashboard-button-text {
margin-left: 0.25em;
margin-right: 0.25em;
}
}
.dashboard-button-add {
position: fixed;
padding: $dashboard-button-padding * 2;
right: 24px;
bottom: 24px;
z-index: 2;
}
// Empty
.dashboard-empty {
position: relative;
}
.dashboard-empty-content {
background-color: rgba(0, 0, 0, 0.05);
border: 2px dashed rgba(0, 0, 0, 0.15);
padding: 2.5em;
text-align: center;
h3 {
font-size: 1.5em;
margin-bottom: 0.5em;
}
p {
font-size: 1.25em;
margin-bottom: 1em;
}
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
}
// Grid
.dashboard-grid {
position: relative;
margin-right: -($dashboard-column-gutter / 2);
margin-left: -($dashboard-column-gutter / 2);
}
// Items
.dashboard-item {
position: absolute;
z-index: 1;
padding: ($dashboard-column-gutter / 2);
width: 100%;
height: auto;
@media screen and (min-width: 750px) {
width: 50%;
height: $widget-height;
}
@media screen and (min-width: 1285px) {
width: 25%;
}
&.muuri-item-positioning {
z-index: 2;
.widget-remove {
display: none;
}
}
&.muuri-item-placeholder {
z-index: 2;
margin: 0;
opacity: 0.5;
.widget {
border: 1px dashed $gray;
}
.widget-remove {
display: none;
}
}
&.muuri-item-dragging,
&.muuri-item-releasing {
z-index: 9999;
}
&.muuri-item-releasing {
.widget-remove {
display: none;
}
}
&.muuri-item-dragging {
cursor: move;
}
&.muuri-item-hidden {
z-index: 0;
}
&.widget-waiting {
line-height: $widget-height;
}
}
.dashboard-item--enableSelect {
user-select: auto !important;
}
.dashboard-item--h4 {
@media screen and (min-width: 750px) {
height: $widget-height * 2;
}
}
.dashboard-item--h6 {
@media screen and (min-width: 750px) {
height: $widget-height * 3;
}
}
.dashboard-item--w4 {
width: 100%;
@media screen and (min-width: 1285px) {
width: 50%;
}
}
.dashboard-item-content {
position: relative;
width: 100%;
height: 100%;
}
// Header
.dashboard-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin: (- $module-body-padding-vertical) (- $module-body-padding-horizontal) $module-body-padding-vertical;
padding: $module-body-padding-vertical $module-body-padding-horizontal 0;
background: $dashboard-header-background;
border-bottom: 1px solid darken($dashboard-header-background, 5%);
}
.dashboard-tabs {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.dashboard-tab {
border-radius: $dashboard-header-tab-border-radius $dashboard-header-tab-border-radius 0 0;
display: inline-block;
padding: $dashboard-header-tab-padding;
margin-right: 2px;
background: $dashboard-header-tab-background;
color: $dashboard-header-tab-color;
&:hover,
&:focus {
text-decoration: none;
background: darken($dashboard-header-tab-background, 5%);
color: darken($dashboard-header-tab-color, 5%);
}
}
.dashboard-tab--active {
background: $dashboard-header-tab-active-background;
color: $dashboard-header-tab-active-color;
&:hover,
&:focus {
text-decoration: none;
background: darken($dashboard-header-tab-active-background, 5%);
color: darken($dashboard-header-tab-active-color, 5%);
}
}
.dashboard-button-tab-add {
margin: 5px;
}
.dashboard-configuration {
padding: 10px 0;
}
.dashboard-configuration-button {
margin-left: 10px;
color: $gray;
text-decoration: none;
&:focus,
&:hover {
color: #ff8700;
text-decoration: none;
}
&:active {
color: $text-color;
text-decoration: none;
}
}
// Module
.module.module {
background-color: $dashboard-background;
h1 {
line-height: calc(48 / 32);
margin-bottom: 20px;
font-weight: 900;
font-size: 32px;
}
}
// Main
.widget {
height: 100%;
border-radius: $widget-border-radius;
overflow: hidden;
background-color: $widget-background;
box-shadow: $widget-shadow;
color: $widget-color;
&:hover {
.widget-actions {
opacity: 1;
}
}
}
// Content
.widget-content {
display: flex;
flex-direction: column;
height: 100%;
}
.widget-content-title {
padding: ($widget-padding / 2) $widget-padding;
padding-right: $widget-action-size * 2 + $widget-padding;
border-bottom: 1px solid $gray-light;
font-family: $widget-title-font-family;
font-size: $widget-title-size;
font-weight: $widget-title-weight;
line-height: $widget-title-line-height;
span {
overflow: hidden;
display: block;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.widget-content-main {
flex-grow: 1;
overflow-y: auto;
padding: $widget-padding;
}
.widget-content-footer {
padding: $widget-padding;
padding-top: 0;
}
// Actions
.widget-actions {
position: absolute;
display: flex;
top: calc(((#{$widget-title-size} * #{$widget-title-line-height}) / 2) + (#{$widget-padding} / 2));
right: ($widget-padding / 2);
transform: translate(0, -50%);
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.widget-action {
width: $widget-action-size;
height: $widget-action-size;
position: relative;
color: $gray;
text-align: center;
&:hover,
&:focus {
color: #ff8700;
}
.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.widget-action-move {
cursor: grab;
}
// Misc
.widget-waiting {
position: absolute;
top: 50%;
left: 50%;
line-height: 300px;
margin-right: -50%;
transform: translate(-50%, -50%);
}
.widget-error {
padding: $widget-padding;
position: absolute;
top: 50%;
text-align: center;
transform: translateY(-50%);
color: $state-danger-text;
}
.widget-chart {
width: 100%;
height: 100%;
}
.widget-edit {
width: 45px;
text-align: center;
}
.widget-editIcon {
color: $text-color;
&:hover,
&:focus {
color: #ff8700;
}
}
.widget-cta {
display: flex;
justify-content: center;
align-items: center;
background-color: $widget-cta-background;
color: $widget-cta-color;
border-radius: $widget-cta-border-radius;
padding: $widget-cta-padding;
&:hover,
&:focus {
text-decoration: none;
background: $widget-cta-hover-background;
color: $widget-cta-hover-color;
}
}
.widget-cta-icon {
display: flex;
justify-content: center;
align-items: center;
width: 18px;
height: 18px;
margin-right: 12px;
color: $widget-cta-color;
}
.widget-doughnut--value {
line-height: 1.3;
font-weight: 900;
font-size: 36px;
text-align: center;
}
.widget-doughnut--meta {
margin-top: 10px;
font-style: italic;
color: $gray;
text-align: center;
}
.dashboard-widget-number--icon {
display: flex;
justify-content: center;
align-items: center;
width: 42px;
margin-right: 20px;
color: $text-color;
}
.dashboard-widget-number--content {
display: flex;
flex-direction: column;
justify-content: center;
}
.dashboard-widget-number--title {
line-height: 1.3;
margin-bottom: 5px;
font-size: 16px;
color: $text-color;
}
.dashboard-widget-number--number {
line-height: 1.3;
font-weight: 900;
font-size: 24px;
}
// Table
.widget-table {
width: 100%;
color: $widget-table-color;
thead tr {
background-color: $widget-table-head-background;
}
tr {
&:nth-child(odd) {
background-color: $widget-table-odd-background;
}
&:nth-child(even) {
background-color: $widget-table-even-background;
}
}
tbody {
th,
td {
border-top: 1px solid $widget-table-border-color;
}
}
tbody:first-child {
tr:first-child {
th,
td {
border-top: none;
}
}
}
th,
td {
padding: $widget-table-padding;
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
}
th {
font-weight: bold;
}
}
.widget-content-main {
.widget-table-wrapper {
margin-top: -($widget-padding - $widget-table-padding);
margin-left: -($widget-padding);
margin-right: -($widget-padding);
th,
td {
&:first-child {
padding-left: $widget-padding;
}
&:last-child {
padding-right: $widget-padding;
}
}
}
}
@import "variables/main";
@import "variables/dashboard";
@import "dashboard_modal/modal";
.dashboard-modal-tabpanel {
.nav {
margin: -15px -15px 0;
padding: 15px 15px 0;
}
}
.dashboard-modal-items {
display: flex;
flex-flow: row wrap;
margin-left: -($dashboard-column-gutter / 2);
margin-right: -($dashboard-column-gutter