add rotation live tutorial

This commit is contained in:
Maxim 2022-11-03 16:05:57 +00:00
parent 23a24f8189
commit 693abdf7aa
8 changed files with 117 additions and 41 deletions

View file

@ -73,21 +73,18 @@
.slots--tutorial {
position: absolute;
background: rgba(61, 113, 217, 0.15);
}
.pointer {
position: absolute;
top: -9px;
transition: left 500ms ease;
}
.tutorial-slot {
width: 175px;
height: 28px;
background: rgba(61, 113, 217, 0.15);
/* opacity: 0.15; */
/* background: var(--background-primary); */
border-radius: 2px;
margin: 0 1px;
padding: 4px;

View file

@ -5,7 +5,7 @@ import cn from 'classnames/bind';
import dayjs from 'dayjs';
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
import { Schedule, Event } from 'models/schedule/schedule.types';
import { Schedule, Event, RotationFormLiveParams } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { getLabel } from './Rotation.helpers';
@ -26,6 +26,7 @@ interface RotationProps {
onClick?: (moment: dayjs.Dayjs) => void;
days?: number;
transparent?: boolean;
tutorialParams?: RotationFormLiveParams;
}
const Rotation: FC<RotationProps> = (props) => {
@ -40,6 +41,7 @@ const Rotation: FC<RotationProps> = (props) => {
onClick,
days = 7,
transparent = false,
tutorialParams,
} = props;
const [animate, _setAnimate] = useState<boolean>(true);
@ -74,7 +76,7 @@ const Rotation: FC<RotationProps> = (props) => {
return (
<div className={cx('root')} onClick={handleClick}>
<div className={cx('timeline')}>
{/*<RotationTutorial />*/}
{tutorialParams && <RotationTutorial startMoment={startMoment} {...tutorialParams} />}
{events ? (
events.length ? (
<div

View file

@ -1,47 +1,46 @@
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useMemo } from 'react';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { Schedule, Event, ScheduleType } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { RotationFormLiveParams } from 'models/schedule/schedule.types';
import styles from './Rotation.module.css';
const cx = cn.bind(styles);
interface RotationProps {
scheduleId: Schedule['id'];
interface RotationProps extends RotationFormLiveParams {
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
days?: number;
}
const RotationTutorial: FC<RotationProps> = (props) => {
const { startMoment, days = 7 /* shiftStart, shiftEnd, rotationStart*/ } = props;
const shiftStart = dayjs(startMoment);
const shiftEnd = dayjs(startMoment).add(1, 'days');
const rotationStart = dayjs(startMoment).add(1, 'days');
const { startMoment, days = 7, shiftStart, shiftEnd, rotationStart, focusElementName } = props;
const duration = shiftEnd.diff(shiftStart, 'seconds');
const events = useMemo(() => {
const events = [];
for (let i = 0; i < days; i++) {
events.push({
start: dayjs(shiftStart).add(i, 'days'),
end: dayjs(shiftStart).add(duration, 'seconds').add(i, 'days'),
});
return [
{
start: dayjs(shiftStart),
end: dayjs(shiftStart).add(duration, 'seconds'),
},
];
}, [shiftStart, duration]);
const base = 60 * 60 * 24 * days;
const pointerX = useMemo(() => {
if (focusElementName === undefined) {
return undefined;
}
return events;
}, []);
const base = 60 * 60 * 24 * 7;
const moment = props[focusElementName];
const firstEvent = events[0];
const diff = dayjs(moment).diff(firstEvent.start, 'seconds');
const diff = dayjs(rotationStart).diff(startMoment, 'seconds');
const currentTimeX = diff / base;
return diff / base;
}, [focusElementName, events, rotationStart]);
const x = useMemo(() => {
if (!events || !events.length) {
@ -57,20 +56,29 @@ const RotationTutorial: FC<RotationProps> = (props) => {
return (
<div className={cx('slots', 'slots--tutorial')} style={{ transform: `translate(${x * 100}%, 0)` }}>
<Pointer className={cx('pointer')} style={{ left: `calc(${currentTimeX * 100}% - 5px)` }} />
<Pointer
className={cx('pointer')}
style={{ left: `calc(${pointerX * 100}% - 5px)`, visibility: pointerX === undefined ? 'hidden' : 'visible' }}
/>
{events.map((event, index) => {
const duration = event.end.diff(event.start, 'seconds');
const width = duration / base;
return <TutorialSlot style={{ width: `${width * 100}%` }} key={index} />;
return (
<TutorialSlot
active={focusElementName === 'shiftStart' || focusElementName === 'shiftEnd'}
style={{ width: `${width * 100}%` }}
key={index}
/>
);
})}
</div>
);
};
const TutorialSlot = (props: { style: React.CSSProperties }) => {
const { style } = props;
const TutorialSlot = (props: { style: React.CSSProperties; active: boolean }) => {
const { style, active } = props;
return <div className={cx('tutorial-slot')} style={style} />;
return <div className={cx('tutorial-slot', { 'tutorial-slot--active': active })} style={style} />;
};
const Pointer = (props: { className: string; style: React.CSSProperties }) => {

View file

@ -12,6 +12,8 @@ interface UserTooltipProps {
onChange: (value: dayjs.Dayjs) => void;
disabled?: boolean;
minMoment?: dayjs.Dayjs;
onFocus?: () => void;
onBlur?: () => void;
}
const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => {
@ -28,7 +30,7 @@ const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => {
};
const DateTimePicker = (props: UserTooltipProps) => {
const { value: propValue, minMoment, timezone, onChange, disabled } = props;
const { value: propValue, minMoment, timezone, onChange, disabled, onFocus, onBlur } = props;
const value = useMemo(() => toDate(propValue, timezone), [propValue, timezone]);
@ -66,8 +68,12 @@ const DateTimePicker = (props: UserTooltipProps) => {
return (
<HorizontalGroup spacing="sm">
<DatePickerWithInput minDate={minDate} disabled={disabled} value={value} onChange={handleDateChange} />
<TimeOfDayPicker disabled={disabled} value={dateTime(value)} onChange={handleTimeChange} />
<div onFocus={onFocus} onBlur={onBlur}>
<DatePickerWithInput minDate={minDate} disabled={disabled} value={value} onChange={handleDateChange} />
</div>
<div onFocus={onFocus} onBlur={onBlur}>
<TimeOfDayPicker disabled={disabled} value={dateTime(value)} onChange={handleTimeChange} />
</div>
</HorizontalGroup>
);
};

View file

@ -233,6 +233,27 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
const isFormValid = useMemo(() => userGroups.some((group) => group.length), [userGroups]);
const [focusElementName, setFocusElementName] = useState<undefined | string>(undefined);
const getFocusHandler = (elementName: string) => {
return () => {
setFocusElementName(elementName);
};
};
const handleBlur = useCallback(() => {
setFocusElementName(undefined);
}, []);
useEffect(() => {
store.scheduleStore.setRotationFormLiveParams({
rotationStart,
shiftStart,
shiftEnd,
focusElementName,
});
}, [params, focusElementName]);
return (
<Modal
isOpen={isOpen}
@ -276,6 +297,8 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
value={rotationStart}
onChange={setRotationStart}
timezone={currentTimezone}
onFocus={getFocusHandler('rotationStart')}
onBlur={handleBlur}
/>
</Field>
<Field
@ -338,7 +361,13 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
</Text>
}
>
<DateTimePicker value={shiftStart} onChange={updateShiftStart} timezone={currentTimezone} />
<DateTimePicker
value={shiftStart}
onChange={updateShiftStart}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftStart')}
onBlur={handleBlur}
/>
</Field>
<Field
className={cx('date-time-picker')}
@ -348,7 +377,13 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
</Text>
}
>
<DateTimePicker value={shiftEnd} onChange={setShiftEnd} timezone={currentTimezone} />
<DateTimePicker
value={shiftEnd}
onChange={setShiftEnd}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftEnd')}
onBlur={handleBlur}
/>
</Field>
</div>
<UserGroups

View file

@ -145,6 +145,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
startMoment={startMoment}
currentTimezone={currentTimezone}
transparent={isPreview}
tutorialParams={isPreview && store.scheduleStore.rotationFormLiveParams}
/>
</CSSTransition>
))}

View file

@ -14,7 +14,17 @@ import {
splitToLayers,
splitToShiftsAndFillGaps,
} from './schedule.helpers';
import { Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer, ShiftEvents } from './schedule.types';
import {
Rotation,
RotationType,
Schedule,
ScheduleEvent,
Shift,
Event,
Layer,
ShiftEvents,
RotationFormLiveParams,
} from './schedule.types';
export class ScheduleStore extends BaseStore {
@observable
@ -57,6 +67,9 @@ export class ScheduleStore extends BaseStore {
@observable
overridePreview?: Array<{ shiftId: Shift['id']; isPreview?: boolean; events: Event[] }>;
@observable
rotationFormLiveParams: RotationFormLiveParams = undefined;
@observable
scheduleToScheduleEvents: {
[id: string]: ScheduleEvent[];
@ -187,6 +200,10 @@ export class ScheduleStore extends BaseStore {
return response;
}
setRotationFormLiveParams(params: RotationFormLiveParams) {
this.rotationFormLiveParams = params;
}
async updateRotationPreview(
scheduleId: Schedule['id'],
shiftId: Shift['id'] | 'new',
@ -227,6 +244,7 @@ export class ScheduleStore extends BaseStore {
this.finalPreview = undefined;
this.rotationPreview = undefined;
this.overridePreview = undefined;
this.rotationFormLiveParams = undefined;
}
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {

View file

@ -1,3 +1,5 @@
import dayjs from 'dayjs';
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { User } from 'models/user/user.types';
@ -9,6 +11,13 @@ export enum ScheduleType {
'API',
}
export interface RotationFormLiveParams {
rotationStart: dayjs.Dayjs;
shiftStart: dayjs.Dayjs;
shiftEnd: dayjs.Dayjs;
focusElementName: string;
}
export interface Schedule {
id: string;
ical_url_primary: string;