add rotation live tutorial
This commit is contained in:
parent
23a24f8189
commit
693abdf7aa
8 changed files with 117 additions and 41 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
transparent={isPreview}
|
||||
tutorialParams={isPreview && store.scheduleStore.rotationFormLiveParams}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue