search envelope-o feed check
Home Unanswered Active Tags New Question
user comment-o

OnEventMove waiting on modal response -- cancel response breaks scheduler

Asked by Katharina
1 month ago.

Dear DayPilot team,

I am trying to add a custom modal into the behavior of my scheduler in the following way: On drop of an event (i. e., in the onEventMove handle), I want to show a modal and wait for the user's response to it (using async/await) before permitting the event move. If the user clicks "Submit" in the modal, the event move should be executed normally; if they click "Cancel", the event move should be aborted and the event should go back to it's previous location.

For some context: Ultimately, I want to use this only on specific events to elicit additional information from the user before moving. In case of these events, the moving of one event will cause the creation of additional events in the scheduler, for which I need to request the dates and resources from the user. I hope that I'll be able to create these events on successful submit of the modal, passing on the information about them which I gathered there.

The successful submit case seems to be working for me so far, but I have trouble cancelling the modal and aborting the current event move. The behavior is the following: On cancel, my modal's Promise seems to send a reject as expected, which I catch as an error in the "OnEventMove" handler of the scheduler, and handle it with a "preventDefault()". I would expect this to abort the current event move and move the event back to it's previous location. However, what happens is that the shadow bar gets stuck at the spot that I dragged the event to, while the "actual" event bar goes back to it's original location but stays in a semi-transparent style like during dragging. If I then drag and drop the "actual" event bar again afterwards, or any other event, the shadow bar disappears and the scheduler freezes completely in the schedule it had before the second drag.

I assume the problem is somewhere in the interaction between my modal and the scheduler, but I can't figure out exactly what I'm doing wrong. I don't see any helpful error messages on the console (or any at all, for that matter). I followed this sandbox code with the communication with my modal (https://codesandbox.io/s/mp544ppo8?file=/src/Modal.js) and this example in the DayPilot demo with the preventDefault() action (https://api.daypilot.org/daypilot-scheduler-oneventmove/).

I'll post a minimal working example below (unfortunately, I still haven't figured out how to format code in this editor, so sorry in advance for the poor readability...). Can you maybe point me the right direction with my cancel action, or is there another/better way to achieve this behavior? I'm using DayPilot Scheduler Pro with Javascript/React.

Scheduler:

import React, { Component } from "react";
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
import HoldModal from "./holdModal";

class Scheduler extends Component {
  constructor(props) {
    super(props);
    this.holdModalRef = React.createRef();
    this.state = {
      schedulerConfig: {
        cellWidth: 30,
        heightSpec: "Auto",
        timeHeaders: [
          { groupBy: "Month" },
          { groupBy: "Week" },
          { groupBy: "Day", format: "d" },
        ],
        weekStarts: 1, // week starts on monday, also sets the calendar week counting to ISO8601 format
        scale: "Day",
        startDate: DayPilot.Date.today().addDays(-60),
        scrollTo: DayPilot.Date.today(),
        days: 120,
        infiniteScrollingEnabled: true,
        infiniteScrollingStepDays: 30,
        eventMoveSkipNonBusiness: true,
        eventEndSpec: "Date",
        timeRangeSelectedHandling: "Disabled",
        eventMoveHandling: "Update",
        onEventMove: (args) => {
          args.async = true;
          const modal = this.holdModalRef;
          const x = setTimeout(async () => {
            try {
              // Wait for user to confirm!
              const result = await modal.current.show({
                defaultStartOfHold: args.newStart,
                defaultEndOfHold: new DayPilot.Date(args.newStart.addDays(1)),
                resourceBeforeHold: args.e.data.resource,
                defaultResourceAfterHold: args.newResource,
                resources: [...this.state.schedulerConfig.resources],
                systemData: args.e.data,
              });
              args.loaded();
              console.log("success", result);
              // this.handleSystemMoving(args, result);
            } catch (err) {
              console.log("some error occurred", err);
              args.preventDefault();
            }
          }, 100);
          return true;
        },
        eventResizeHandling: "Disabled",
        eventDeleteHandling: "Disabled",
        eventClickHandling: "Disabled",
        eventHoverHandling: "Bubble",
        bubble: new DayPilot.Bubble(),
        // },
      },
    };
  }

  componentDidMount() {
    // load resource and event data
    const mockResources = [
      { id: 1, name: "R1" },
      { id: 2, name: "R2" },
      { id: 3, name: "R3" },
    ];
    const mockEvents = mockResources.map((resource, id) => {
      return {
        id: id,
        text: id,
        start: DayPilot.Date.today(),
        end: DayPilot.Date.today().addDays(2),
        resource: resource.id,
      };
    });
    this.setState({
      schedulerConfig: {
        ...this.state.schedulerConfig,
        resources: mockResources,
        events: mockEvents,
      },
    });
  }

  render() {
    console.log(this.state);
    var { ...config } = this.state.schedulerConfig;
    console.log("holdModalRef", this.holdModalRef);
    return (
      <div>
        <HoldModal ref={this.holdModalRef} />
        <div style={{ display: "flex", marginBottom: "30px" }}>
          <div style={{ flex: 1 }}>
            <DayPilotScheduler
              {...config}
              ref={(component) => {
                this.scheduler = component && component.control;
              }}
            />
          </div>
        </div>
      </div>
    );
  }
}

export default Scheduler;

Modal:

import ReactDOM from "react-dom";
import React, { Component, Fragment } from "react";
import { Row, Card, CardBody, Button } from "reactstrap";
import { Colxx } from "../../../components/common/CustomBootstrap";
import { AvForm } from "availity-reactstrap-validation";
import IntlMessages from "../../../helpers/IntlMessages";

class HoldModal extends Component {
  constructor(props) {
    super(props);
    this.state = {
      show: false,
    };
    this.promiseInfo = {};
  }

  componentDidMount() {
    this.addEvents();
  }

  componentWillUnmount() {
    this.removeEvents();
  }

  getContainer = () => {
    return ReactDOM.findDOMNode(this);
  };

  addEvents = () => {
    ["click", "touchstart"].forEach((event) =>
      document.addEventListener(event, this.handleDocumentClick, true)
    );
  };

  removeEvents = () => {
    ["click", "touchstart"].forEach((event) =>
      document.removeEventListener(event, this.handleDocumentClick, true)
    );
  };

  handleDocumentClick = (e) => {
    if (this.state.show) {
      const container = this.getContainer();
      if (container.contains(e.target) || container === e.target) {
        return;
      }
      const { resolve, reject } = this.promiseInfo;
      e.preventDefault();
      reject("foobar");
      this.hide();
    }
  };

  show = async (args) => {
    this.addEvents();
    return new Promise((resolve, reject) => {
      this.promiseInfo = {
        resolve,
        reject,
      };
      this.setState({
        show: true,
      });
    });
  };

  hide = () => {
    this.setState({
      show: false,
    });
    this.removeEvents();
  };

  handleSubmit = (event, errors, values) => {
    const { resolve, reject } = this.promiseInfo;
    this.hide();
    resolve({
      success: true,
    });
    console.log("MODAL SUBMITTED SUCCESSFULLY");
  };

  render() {
    // const { messages } = this.props.intl;
    const evaBaseUrl = process.env.REACT_APP_EVA_BASE_URL;
    const evaBasePort = process.env.REACT_APP_EVA_BASE_PORT;
    const baseUrlApi = evaBasePort
      ? `${evaBaseUrl}:${evaBasePort}/api`
      : `${evaBaseUrl}/api`;
    const { resolve, reject } = this.promiseInfo;
    return (
      <div
        className={"CustomModal"}
        style={{
          display: this.state.show ? "inline" : "none",
          position: "fixed",
          zIndex: "1030",
          left: "50%",
          top: "50%",
          transform: "translate(-50%, -50%)",
          minWidth: "50%",
        }}
      >
        <Card>
          <CardBody style={{ maxHeight: "100vh", overflowY: "auto" }}>
            <Fragment>
              <AvForm onSubmit={this.handleSubmit} id="NewHoldTime">
                <Row>
                  <Colxx style={{ textAlign: "right" }}>
                    <Button
                      type="submit"
                      color="primary"
                      onClick={(e) => {
                        console.log(e);
                      }}
                      style={{
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        whiteSpace: "nowrap",
                        marginTop: "0.5em",
                      }}
                    >
                      {<IntlMessages id="leica.newIssueModal.button.submit" />}
                    </Button>
                  </Colxx>
                  <Colxx>
                    <Button
                      type="cancel"
                      color="secondary"
                      onClick={(e) => {
                        e.preventDefault();
                        this.hide();
                        reject("error msg");
                      }}
                      style={{
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        whiteSpace: "nowrap",
                        marginTop: "0.5em",
                      }}
                    >
                      {<IntlMessages id="forms.cancel" />}
                    </Button>
                  </Colxx>
                </Row>
              </AvForm>
            </Fragment>
          </CardBody>
        </Card>
      </div>
    );
  }
}

export default HoldModal;

Thank you in advance and have a nice day!

Best,
Katharina Korfhage

Answer posted by Dan Letecky [DayPilot]
1 month ago.

By calling setTimeout() in onEventMove you create a new JavaScript message that will be processed later as the JavaScript event loop continues (see also https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop). The rest of the method will execute immediately and it will proceed to event update and onEventMoved events. If you want to cancel the move event processing, you need to call args.preventDefault() in the same "thread". Calling it later will have no effect.

Here is a schematic onEventMove that uses DayPilot.Modal.confirm() which also returns a Promise:

onEventMove: async args => {
    args.async = true;
    const modal = await DayPilot.Modal.confirm("Really?");
    if (modal.canceled) {
        args.preventDefault();
    }
    args.loaded();
}

I would recommend not using reject() for the "Cancel" button as it forces you to use "try {} catch" or catch() method. Also, it will stop the code execution when the browser developer tools are enabled.

New Reply
This reply is
Your name (optional):