Skip to content

Commit a1c7630

Browse files
authored
Help users getting started with schedules (#359)
resolves #280
2 parents beb8a21 + 3d735b0 commit a1c7630

File tree

7 files changed

+179
-26
lines changed

7 files changed

+179
-26
lines changed

application/controllers/ScheduleController.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ public function indexAction(): void
4242
null,
4343
Links::scheduleSettings($id),
4444
'cog'
45-
))->openInModal(),
46-
(new ButtonLink(
47-
$this->translate('Add Rotation'),
48-
Links::rotationAdd($id),
49-
'plus'
5045
))->openInModal()
5146
);
5247

library/Notifications/View/ScheduleRenderer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function assembleCaption($item, HtmlDocument $caption, string $layout): v
4343
{
4444
// Number of days is set to 7, since default mode for schedule is week
4545
// and the start day should be the current day
46-
$timeline = (new Timeline((new DateTime())->setTime(0, 0), 7))
46+
$timeline = (new Timeline($item->id, (new DateTime())->setTime(0, 0), 7))
4747
->minimalLayout()
4848
->setStyle(
4949
(new Style())

library/Notifications/Widget/Detail/ScheduleDetail.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
use ipl\Html\Attributes;
1313
use ipl\Html\BaseHtmlElement;
1414
use ipl\Html\HtmlElement;
15+
use ipl\Html\Text;
16+
use ipl\I18n\Translation;
1517
use ipl\Web\Common\BaseTarget;
1618
use ipl\Web\Style;
19+
use ipl\Web\Widget\Icon;
1720

1821
class ScheduleDetail extends BaseHtmlElement
1922
{
2023
use BaseTarget;
24+
use Translation;
2125

2226
protected $tag = 'div';
2327

@@ -29,6 +33,9 @@ class ScheduleDetail extends BaseHtmlElement
2933
/** @var Controls */
3034
protected $controls;
3135

36+
/** @var bool */
37+
private bool $hasRotation = false;
38+
3239
/**
3340
* Create a new Schedule
3441
*
@@ -50,6 +57,7 @@ protected function assembleTimeline(Timeline $timeline): void
5057
{
5158
foreach ($this->schedule->rotation->with('timeperiod')->orderBy('first_handoff', SORT_DESC) as $rotation) {
5259
$timeline->addRotation(new Rotation($rotation));
60+
$this->hasRotation = true;
5361
}
5462
}
5563

@@ -60,7 +68,11 @@ protected function assembleTimeline(Timeline $timeline): void
6068
*/
6169
protected function createTimeline(): Timeline
6270
{
63-
$timeline = new Timeline($this->controls->getStartDate(), $this->controls->getNumberOfDays());
71+
$timeline = new Timeline(
72+
$this->schedule->id,
73+
$this->controls->getStartDate(),
74+
$this->controls->getNumberOfDays()
75+
);
6476
$timeline->setStyle(
6577
(new Style())
6678
->setNonce(Csp::getStyleNonce())
@@ -75,8 +87,28 @@ protected function createTimeline(): Timeline
7587
protected function assemble()
7688
{
7789
$this->addHtml(
78-
new HtmlElement('div', Attributes::create(['class' => 'schedule-header']), $this->controls),
79-
new HtmlElement('div', Attributes::create(['class' => 'schedule-container']), $this->createTimeline())
90+
new HtmlElement('div', Attributes::create(['class' => 'schedule-header']), $this->controls)
91+
);
92+
93+
$timeline = $this->createTimeline();
94+
if (! $this->hasRotation) {
95+
$this->addHtml(new HtmlElement(
96+
'div',
97+
Attributes::create(['class' => 'from-scratch-hint']),
98+
new Icon('info-circle'),
99+
new HtmlElement(
100+
'div',
101+
null,
102+
Text::create($this->translate(
103+
'With schedules contacts can rotate in recurring shifts. You can add'
104+
. ' multiple rotation layers to a schedule.'
105+
))
106+
)
107+
));
108+
}
109+
110+
$this->addHtml(
111+
new HtmlElement('div', Attributes::create(['class' => 'schedule-container']), $timeline)
80112
);
81113
}
82114
}

library/Notifications/Widget/Timeline.php

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
use Icinga\Module\Notifications\Widget\TimeGrid\Timescale;
1515
use Icinga\Module\Notifications\Widget\TimeGrid\Util;
1616
use Icinga\Module\Notifications\Widget\Timeline\Entry;
17+
use Icinga\Module\Notifications\Widget\Timeline\FakeEntry;
1718
use Icinga\Module\Notifications\Widget\Timeline\FutureEntry;
1819
use Icinga\Module\Notifications\Widget\Timeline\MinimalGrid;
1920
use Icinga\Module\Notifications\Widget\Timeline\Rotation;
2021
use IntlDateFormatter;
2122
use ipl\Html\Attributes;
2223
use ipl\Html\BaseHtmlElement;
2324
use ipl\Html\HtmlElement;
25+
use ipl\Html\TemplateString;
2426
use ipl\Html\Text;
2527
use ipl\I18n\Translation;
2628
use ipl\Web\Style;
@@ -42,6 +44,9 @@ class Timeline extends BaseHtmlElement implements EntryProvider
4244
/** @var array<int, Rotation> */
4345
protected $rotations = [];
4446

47+
/** @var int */
48+
protected int $scheduleId;
49+
4550
/** @var DateTime */
4651
protected $start;
4752

@@ -88,11 +93,13 @@ public function getStyle(): Style
8893
/**
8994
* Create a new Timeline
9095
*
96+
* @param int $scheduleId The schedule ID
9197
* @param DateTime $start The day the grid should start on
9298
* @param int $days Number of days to show on the grid
9399
*/
94-
public function __construct(DateTime $start, int $days)
100+
public function __construct(int $scheduleId, DateTime $start, int $days)
95101
{
102+
$this->scheduleId = $scheduleId;
96103
$this->start = $start;
97104
$this->days = $days;
98105
}
@@ -194,6 +201,14 @@ public function getEntries(): Traversable
194201
}
195202
}
196203

204+
if (! $this->minimalLayout) {
205+
// Always yield a fake entry to reserve the position for the add-rotation button
206+
yield (new FakeEntry())
207+
->setPosition($resultPosition++)
208+
->setStart($this->getGrid()->getGridStart())
209+
->setEnd($this->getGrid()->getGridEnd());
210+
}
211+
197212
$entryToCellsMap = new SplObjectStorage();
198213
foreach ($occupiedCells as $cell => $entry) {
199214
$cells = $entryToCellsMap[$entry] ?? [];
@@ -313,22 +328,18 @@ protected function assembleSidebarEntry(Rotation $rotation): BaseHtmlElement
313328

314329
protected function assemble()
315330
{
316-
if (empty($this->rotations)) {
317-
$emptyNotice = new HtmlElement(
331+
if ($this->minimalLayout && empty($this->rotations)) {
332+
$this->addHtml(new HtmlElement(
318333
'div',
319334
Attributes::create(['class' => 'empty-notice']),
320335
Text::create($this->translate('No rotations configured'))
321-
);
322-
323-
if ($this->minimalLayout) {
324-
$this->getAttributes()->add(['class' => 'minimal-layout']);
325-
$this->addHtml($emptyNotice);
326-
} else {
327-
$this->getGrid()->addToSideBar($emptyNotice);
328-
}
336+
));
329337
}
330338

331339
if (! $this->minimalLayout) {
340+
// We yield a fake overlay entry, so we also have to fake a sidebar entry
341+
$this->getGrid()->addToSideBar(new HtmlElement('div'));
342+
332343
$this->getGrid()->addToSideBar(
333344
new HtmlElement(
334345
'div',
@@ -369,7 +380,35 @@ protected function assemble()
369380
new HtmlElement('div', new Attributes(['class' => 'current-day']), $currentTime)
370381
);
371382

383+
if (empty($this->rotations)) {
384+
$newRotationMsg = $this->translate(
385+
'No rotations configured, yet. {{#button}}Add your first Rotation{{/button}}'
386+
);
387+
} else {
388+
$newRotationMsg = $this->translate(
389+
'{{#button}}Add another Rotation{{/button}} to override rotations above'
390+
);
391+
}
392+
372393
$this->getGrid()
394+
->addHtml(new HtmlElement(
395+
'div',
396+
new Attributes(['class' => 'new-rotation-container']),
397+
new HtmlElement(
398+
'div',
399+
Attributes::create(['class' => 'new-rotation-content']),
400+
TemplateString::create(
401+
$newRotationMsg,
402+
[
403+
'button' => (new Link(
404+
new Icon('circle-plus'),
405+
Links::rotationAdd($this->scheduleId),
406+
['class' => empty($this->rotations) ? 'btn-primary' : null]
407+
))->openInModal()
408+
]
409+
)
410+
)
411+
))
373412
->addHtml(new Timescale($this->days, $this->getStyle()))
374413
->addHtml($clock);
375414
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Widget\Timeline;
6+
7+
use Icinga\Module\Notifications\Widget\TimeGrid\Entry;
8+
use ipl\Html\BaseHtmlElement;
9+
10+
/**
11+
* @internal Reserved for internal use.
12+
*/
13+
final class FakeEntry extends Entry
14+
{
15+
public function __construct()
16+
{
17+
parent::__construct(0);
18+
}
19+
20+
public function getColor(int $transparency): string
21+
{
22+
return '';
23+
}
24+
25+
protected function assembleContainer(BaseHtmlElement $container): void
26+
{
27+
}
28+
29+
public function renderUnwrapped(): string
30+
{
31+
return '';
32+
}
33+
}

public/css/schedule.less

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
h2 {
99
display: inline;
1010
}
11-
12-
> a:last-of-type {
13-
float: right;
14-
}
1511
}
1612

1713
.schedule-detail {
@@ -33,6 +29,21 @@
3329
}
3430
}
3531
}
32+
33+
.from-scratch-hint {
34+
display: flex;
35+
align-items: center;
36+
font-size: 14/12em;
37+
38+
i.icon {
39+
float: left;
40+
font-size: 1.5em;
41+
}
42+
43+
div {
44+
margin: 0 auto;
45+
}
46+
}
3647
}
3748

3849
.schedule-container {
@@ -67,3 +78,10 @@
6778
background-color: @gray-lighter;
6879
border-color: @gray-light;
6980
}
81+
82+
.schedule-detail .from-scratch-hint {
83+
.rounded-corners();
84+
border: 1px solid @gray-light;
85+
padding: .5em;
86+
color: @text-color-light;
87+
}

public/css/timeline.less

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,31 @@
198198
}
199199
}
200200
}
201+
202+
.new-rotation-container {
203+
pointer-events: none;
204+
display: flex;
205+
flex-direction: column;
206+
justify-content: flex-end;
207+
grid-area: ~"3 / 1 / 4 / 3";
208+
padding-bottom: var(--stepRowHeight);
209+
210+
.new-rotation-content {
211+
pointer-events: all;
212+
z-index: 2; // Grid gaps must not bleed through, in day mode the time-hand must be below the button
213+
height: var(--stepRowHeight);
214+
display: flex;
215+
align-items: baseline;
216+
justify-content: center;
217+
align-content: center;
218+
flex-wrap: wrap;
219+
gap: .417em;
220+
}
221+
}
201222
}
202223
}
203224

204-
.timeline.minimal-layout{
225+
.timeline:has(.empty-notice) {
205226
position: relative;
206227

207228
.empty-notice {
@@ -271,9 +292,24 @@
271292
color: red;
272293
.user-select(none);
273294
}
295+
296+
.new-rotation-content {
297+
background: @gray-lighter;
298+
color: @text-color-light;
299+
300+
a {
301+
&:not(.btn-primary) {
302+
.button(@gray-lighter, @text-color-light);
303+
}
304+
305+
&.btn-primary {
306+
.button(@gray-lighter);
307+
}
308+
}
309+
}
274310
}
275311

276-
.timeline.minimal-layout .empty-notice {
312+
.timeline .empty-notice {
277313
font-size: 1.25em;
278314
}
279315

0 commit comments

Comments
 (0)