diff --git a/docs/docs/widgets/calendar.md b/docs/docs/widgets/calendar.md new file mode 100644 index 0000000000..717d308ba3 --- /dev/null +++ b/docs/docs/widgets/calendar.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 22 +--- + +# Calendar +Calendar widget comes with the following features: +- Day, month and week level views +- Events +- Resource scheduling + +ToolJet - Widget Reference - Calendar + +### Properties + +#### Date format +Determines the format in which any date passed to the calendar via any of the properties will be parsed. +It also determines the format in which any date made available by the calendar via exposed variables will be displayed. +It uses the date format conventions of [moment.js](https://momentjs.com/). +#### Default date +Determines the date on which the calendar's view will be centered on. +If the calendar is on `month` view, it will show the month on which this date exists. +If the calendar is on `week` view, it will show the week on which this date exists. +This property needs to be formatted using the `Date format` property which is configurable on the inspector. +#### Events +`Events` property should contain an array of objects, each of which describes the events that the calendar needs to display. + +Assuming that you set the date format to `MM-DD-YYYY HH:mm:ss A Z`, setting the `Events` property to the following code snippet will display an event titled `Sample Event` at the first hour of this day, as displayed in the image of calendar at the beginning of this page. + +```javascript +{{[ + { + title: 'Sample event', + start: `${moment().startOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`, + end: `${moment().endOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`, + allDay: false, + tooltip: 'Sample event', + color: 'lightgreen', + } +]}} +``` + +##### Event object properties + +| Name | Description | +|------|-------------| +| title | Title of the event | +| start | The date(and time) on which this event begins. Needs to be formatted in the `Date format` you've supplied | +| end | The date(and time) on which this event ends. Needs to be formatted in the `Date format` you've supplied | +| allDay | Optional. Qualifies the event as an 'All day event', which will pin it to date headers on `day` and `week` level views | +| tooltip | Tooltip which will be display when the user hovers over the event | +| color | Background color of the event, any css supported color name or hex code can be used | +| textOrientation | Optional. If it is set to `vertical`, the title of the event will be oriented vertically. | +| resourceId | Applicable only if you're using resource scheduling. This is the id of the resource to which this event correspond to. | + +You may supply any other additional property to the event(s). These additional properties will available to you when the calendar widget +exposes any of the events via its exposed variables. + +#### Resources + +Specifying resources will make the calendar categorize `week` view and `day` view for each of the resources specified. + + For example, to categorize week/day view into for three rooms, we specify `resources` this way: + +```javascript +{{ + [ + {resourceId: 1, title: 'Room A'}, + {resourceId: 2, title: 'Room B'}, + {resourceId: 3, title: 'Room C'}, + ] +}} +``` + +If we specify the `resourceId` of any of the events as `1`, then that event will be assigned to `Room A`, generating the following calendar, assuming that we've set the view to `day` and are viewing the day on which this event exists. + +ToolJet - Widget Reference - Calendar Resources + +#### Default view + +Determines whether the calendar would display a `day`, a `week` or a `month`. +Setting this property to anything other than these values will make the calendar default to `month` view. + +#### Show toolbar + +Determines whether the calendar toolbar should be displayed or not. + +#### Show view switcher + +Determinues whether the calendar's buttons that allow user to switch between `month`, `week` and `day` level views will be displayed. +### Styles +#### Cell size in views classified by resource + +When `resources` are specified, the calendar could take up quite a lot of horizontal space, making the horizontal scroll bar of calendar having to be relied upon all the time. + +If we set this property to `compact`, the cell sizes will be smaller in `week` and `day` views. + +### Events + +#### On Event selected + +This event is fired when the user clicks on a calendar event. + +Last selected event is exposed as `selectedEvent`. + +#### on Slot selected + +This event is fired when the user either clicks on an calendar slot(empty cell or empty space of a cell with event) or when they click and drag to select multiple slots. + +Last selected slot(s) are exposed as `selectedSlots`. \ No newline at end of file diff --git a/docs/static/img/widgets/calendar/calendar-day.png b/docs/static/img/widgets/calendar/calendar-day.png new file mode 100644 index 0000000000..740f9f09e3 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-day.png differ diff --git a/docs/static/img/widgets/calendar/calendar-resource.png b/docs/static/img/widgets/calendar/calendar-resource.png new file mode 100644 index 0000000000..3bd551187c Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-resource.png differ diff --git a/docs/static/img/widgets/calendar/calendar-week.png b/docs/static/img/widgets/calendar/calendar-week.png new file mode 100644 index 0000000000..81a9743620 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar-week.png differ diff --git a/docs/static/img/widgets/calendar/calendar1.png b/docs/static/img/widgets/calendar/calendar1.png new file mode 100644 index 0000000000..a6d6a090e2 Binary files /dev/null and b/docs/static/img/widgets/calendar/calendar1.png differ diff --git a/frontend/assets/images/icons/widgets/calendar.svg b/frontend/assets/images/icons/widgets/calendar.svg new file mode 100644 index 0000000000..ec39ebc307 --- /dev/null +++ b/frontend/assets/images/icons/widgets/calendar.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 386ffb856a..72b571e3c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,6 +41,7 @@ "plotly.js-basic-dist-min": "^1.58.4", "query-string": "^6.13.6", "react": "^16.14.0", + "react-big-calendar": "^0.38.0", "react-bootstrap": "^1.5.2", "react-color": "^2.19.3", "react-copy-to-clipboard": "^5.0.3", @@ -7594,6 +7595,11 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" + }, "node_modules/date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -14233,6 +14239,11 @@ "node": ">= 0.6" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -17404,6 +17415,47 @@ "pure-color": "^1.2.0" } }, + "node_modules/react-big-calendar": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-0.38.0.tgz", + "integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==", + "dependencies": { + "@babel/runtime": "^7.1.5", + "clsx": "^1.0.4", + "date-arithmetic": "^4.1.0", + "dom-helpers": "^5.1.0", + "invariant": "^2.2.4", + "lodash": "^4.17.11", + "lodash-es": "^4.17.11", + "memoize-one": "^5.1.1", + "prop-types": "^15.7.2", + "react-overlays": "^4.1.1", + "uncontrollable": "^7.0.0" + }, + "peerDependencies": { + "react": "^16.6.1 || ^17", + "react-dom": "^16.6.1 || ^17" + } + }, + "node_modules/react-big-calendar/node_modules/react-overlays": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", + "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@popperjs/core": "^2.5.3", + "@restart/hooks": "^0.3.25", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/react-bootstrap": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz", @@ -30304,6 +30356,11 @@ } } }, + "date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" + }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -35470,6 +35527,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -38058,6 +38120,41 @@ "pure-color": "^1.2.0" } }, + "react-big-calendar": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-0.38.0.tgz", + "integrity": "sha512-eoVkt9gTo+f1HBL09+o7dYLxp6QxHv52fcn50P5PfaWp3S98uGLQqoqsvghT85koMKvGfDVa5V0+J7yHcaF07Q==", + "requires": { + "@babel/runtime": "^7.1.5", + "clsx": "^1.0.4", + "date-arithmetic": "^4.1.0", + "dom-helpers": "^5.1.0", + "invariant": "^2.2.4", + "lodash": "^4.17.11", + "lodash-es": "^4.17.11", + "memoize-one": "^5.1.1", + "prop-types": "^15.7.2", + "react-overlays": "^4.1.1", + "uncontrollable": "^7.0.0" + }, + "dependencies": { + "react-overlays": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", + "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "@popperjs/core": "^2.5.3", + "@restart/hooks": "^0.3.25", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + } + } + } + }, "react-bootstrap": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index deb6477167..d44ad27a84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "plotly.js-basic-dist-min": "^1.58.4", "query-string": "^6.13.6", "react": "^16.14.0", + "react-big-calendar": "^0.38.0", "react-bootstrap": "^1.5.2", "react-color": "^2.19.3", "react-copy-to-clipboard": "^5.0.3", diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index 43b5e4ed75..ec1d1b0aab 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -23,6 +23,7 @@ import { StarRating } from './Components/StarRating'; import { Divider } from './Components/Divider'; import { FilePicker } from './Components/FilePicker'; import { PasswordInput } from './Components/PasswordInput'; +import { Calendar } from './Components/Calendar'; import { renderTooltip } from '../_helpers/appUtils'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import '@/_styles/custom.scss'; @@ -54,6 +55,7 @@ const AllComponents = { Divider, FilePicker, PasswordInput, + Calendar, }; export const Box = function Box({ diff --git a/frontend/src/Editor/Components/Calendar.jsx b/frontend/src/Editor/Components/Calendar.jsx new file mode 100644 index 0000000000..3d1121f351 --- /dev/null +++ b/frontend/src/Editor/Components/Calendar.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar'; +import moment from 'moment'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; + +const localizer = momentLocalizer(moment); + +const prepareEvent = (event, dateFormat) => ({ + ...event, + start: moment(event.start, dateFormat).toDate(), + end: moment(event.end, dateFormat).toDate(), +}); + +const parseDate = (date, dateFormat) => moment(date, dateFormat).toDate(); + +const allowedCalendarViews = ['month', 'week', 'day']; + +export const Calendar = function ({ height, width, properties, styles, fireEvent, darkMode }) { + const style = { height, width }; + const resourcesParam = properties.resources?.length === 0 ? {} : { resources: properties.resources }; + + const events = properties.events ? properties.events.map((event) => prepareEvent(event, properties.dateFormat)) : []; + const defaultDate = parseDate(properties.defaultDate, properties.dateFormat); + + const eventPropGetter = (event) => { + const backgroundColor = event.color; + const textStyle = + event.textOrientation === 'vertical' ? { writingMode: 'vertical-rl', textOrientation: 'mixed' } : {}; + const style = { backgroundColor, ...textStyle, padding: 3, paddingLeft: 5, paddingRight: 5 }; + + return { style }; + }; + + const slotSelectHandler = (calendarSlots) => { + const { slots, start, end, resourceId, action } = calendarSlots; + const formattedSlots = slots.map((slot) => moment(slot).format(properties.dateFormat)); + const formattedStart = moment(start).format(properties.dateFormat); + const formattedEnd = moment(end).format(properties.dateFormat); + + const selectedSlots = { + slots: formattedSlots, + start: formattedStart, + end: formattedEnd, + resourceId, + action, + }; + + fireEvent('onCalendarSlotSelect', { selectedSlots }); + }; + + const defaultView = allowedCalendarViews.includes(properties.defaultView) + ? properties.defaultView + : allowedCalendarViews[0]; + + return ( +
+ fireEvent('onCalendarEventSelect', { calendarEvent })} + selectable={true} + onSelectSlot={slotSelectHandler} + toolbar={properties.displayToolbar} + eventPropGetter={eventPropGetter} + tooltipAccessor="tooltip" + popup={true} + /> +
+ ); +}; diff --git a/frontend/src/Editor/Components/components.js b/frontend/src/Editor/Components/components.js index 4aaf62a09e..ae9f98afeb 100644 --- a/frontend/src/Editor/Components/components.js +++ b/frontend/src/Editor/Components/components.js @@ -1003,7 +1003,7 @@ export const componentTypes = [ canSearch: { value: `{{true}}`, }, - addNewMarkers : {value:`{{true}}`}, + addNewMarkers: { value: `{{true}}` }, }, events: [], styles: { @@ -1189,4 +1189,85 @@ export const componentTypes = [ }, }, }, + { + name: 'Calendar', + displayName: 'Calendar', + description: 'Calendar', + component: 'Calendar', + defaultSize: { + width: 700, + height: 600, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + dateFormat: { type: 'code', displayName: 'Date format' }, + defaultDate: { type: 'code', displayName: 'Default date' }, + events: { type: 'code', displayName: 'Events' }, + resources: { type: 'code', displayName: 'Resources' }, + defaultView: { type: 'code', displayName: 'Default view' }, + displayToolbar: { type: 'toggle', displayName: 'Show toolbar' }, + displayViewSwitcher: { type: 'toggle', displayName: 'Show view switcher' }, + highlightToday: { type: 'toggle', displayName: 'Highlight today' }, + }, + events: { + onCalendarEventSelect: { displayName: 'On Event Select' }, + onCalendarSlotSelect: { displayName: 'On Slot Select' }, + }, + styles: { + visibility: { type: 'code', displayName: 'Visibility' }, + cellSizeInViewsClassifiedByResource: { + type: 'select', + displayName: 'Cell size in views classified by resource', + options: [ + { name: 'Compact', value: 'compact' }, + { name: 'Spacious', value: 'spacious' }, + ], + }, + }, + exposedVariables: { + selectedEvent: {}, + selectedSlots: {}, + }, + definition: { + others: { + showOnDesktop: { value: true }, + showOnMobile: { value: false }, + }, + properties: { + dateFormat: { + value: 'MM-DD-YYYY HH:mm:ss A Z', + }, + defaultDate: { + value: '{{moment().format("MM-DD-YYYY HH:mm:ss A Z")}}', + }, + events: { + value: + "{{[\n\t\t{\n\t\t\t title: 'Sample event',\n\t\t\t start: `${moment().startOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`,\n\t\t\t end: `${moment().endOf('day').format('MM-DD-YYYY HH:mm:ss A Z')}`,\n\t\t\t allDay: false,\n\t\t\t color: '#4D72DA'\n\t\t}\n]}}", + }, + resources: { + value: '{{[]}}', + }, + defaultView: { + value: "{{'month'}}", + }, + displayToolbar: { + value: true, + }, + displayViewSwitcher: { + value: true, + }, + highlightToday: { + value: true, + }, + }, + events: [], + styles: { + visibility: { value: '{{true}}' }, + cellSizeInViewsClassifiedByResource: { value: 'spacious' }, + }, + }, + }, ]; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 2c6ef6f0ac..77d2d5f989 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -224,6 +224,48 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') { ); } + if (eventName === 'onCalendarEventSelect') { + const { component, calendarEvent } = options; + _self.setState( + { + currentState: { + ..._self.state.currentState, + components: { + ..._self.state.currentState.components, + [component.name]: { + ..._self.state.currentState.components[component.name], + selectedEvent: { ...calendarEvent }, + }, + }, + }, + }, + () => { + executeActionsForEventId(_ref, 'onCalendarEventSelect', component, mode); + } + ); + } + + if (eventName === 'onCalendarSlotSelect') { + const { component, selectedSlots } = options; + _self.setState( + { + currentState: { + ..._self.state.currentState, + components: { + ..._self.state.currentState.components, + [component.name]: { + ..._self.state.currentState.components[component.name], + selectedSlots, + }, + }, + }, + }, + () => { + executeActionsForEventId(_ref, 'onCalendarSlotSelect', component, mode); + } + ); + } + if (eventName === 'onTableActionButtonClicked') { const { component, data, action, rowId } = options; _self.setState( diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index a5154a3208..910882385e 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -2391,6 +2391,104 @@ input[type='text'] { overflow-y: scroll; } +.calendar-widget.compact { + .rbc-time-view-resources .rbc-time-header-content { + min-width: auto; + } + + .rbc-time-view-resources .rbc-day-slot { + min-width: 50px; + } + + .rbc-time-view-resources .rbc-header, + .rbc-time-view-resources .rbc-day-bg { + width: 50px; + } +} + +.calendar-widget.dont-highlight-today { + .rbc-today { + background-color: inherit; + } + + .rbc-current-time-indicator { + display: none; + } +} + +.calendar-widget { + padding: 10px; + background-color: white; + + .rbc-day-slot .rbc-event, .rbc-day-slot .rbc-background-event { + border-left: 3px solid #26598533; + } + + .rbc-toolbar { + font-size: 14px; + } + + .rbc-event { + .rbc-event-label { + display: none; + } + } + + .rbc-off-range-bg { + background-color: #f4f6fa; + } + + .rbc-toolbar { + .rbc-btn-group { + button { + box-shadow: none; + border-radius: 0; + border-width: 1px; + } + } + } +} + +.calendar-widget.hide-view-switcher { + .rbc-toolbar { + .rbc-btn-group:nth-of-type(3) { + display: none; + } + } +} + +.calendar-widget.dark-mode { + background-color: #1d2a39; + + .rbc-toolbar { + button { + color: white; + } + + button:hover, button.rbc-active { + color: black; + } + } + + .rbc-off-range-bg { + background-color: #2b394b; + } + + .rbc-selected-cell { + background-color: #22242d; + } + + .rbc-today { + background-color: #5a7ca8; + } +} + +.calendar-widget.dark-mode.dont-highlight-today { + .rbc-today { + background-color: inherit; + } +} + .navbar .navbar-nav { min-height: 2rem; }