Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Calendar User Edits API #38

Open
kewisch opened this issue Aug 20, 2024 · 3 comments
Open

Proposal: Calendar User Edits API #38

kewisch opened this issue Aug 20, 2024 · 3 comments
Labels
api: calendar This issue is about the calendar API experimeent proposal Issue contains a draft API proposal

Comments

@kewisch
Copy link
Member

kewisch commented Aug 20, 2024

@jobisoft I've put some thought into how an API for the event dialog and other user edits could look like. I've left a few TODOs in here that we can discuss in comments or meetings as needed and then we can update the description. If you prefer I stick this into the doc that works for me as well.

The user edit lifecycle is separate from the regular calendar operations, since they are not full blown objects, but a series of changes that could be inhibited. We want to separate those from the storage lifecycle.

The most obvious user edit is the event dialog. It can be opened, changed, and closed in various ways. There are also other user edits that might be a lot quicker, such as dragging an event in the calendar view, checking off a task in the task list.

TODO We could consider also offering a way for extensions to offer their own user edits, e.g. if
they have their own edits UI and want to trigger it for other extensions.

messenger.calendar.useredit.onBeforeEdit.addListener((editId, item, options) => {
  // editId is an id to associate the edit session, like tabTracker
  // options.mode is "view" or "edit"
  // options.source is "dialog" or "view", maybe some more.
  // If you're looking for the calendar, this should be in item.calendarId

 // TODO editId is an id to associate the edit session, like tabTracker. We could also consider
 // calling this a transaction id, which could be the start of an undo/redo API where changes could
 // be identified, but I haven't thought that through.

 // TODO dialog as a source has a very specific name, what would be better?

  // TODO This might be a good place to set per-item capabilities if we want to support that.

  // Return cancel to stop the edit from happening
  return { cancel: true };

  // Return the item after modifying it
  let vcalendar = new ICAL.Component(item.item);
  let vevent = vcalendar.getFirstSubcomponent("vevent");
  vevent.updatePropertyWithValue("summary", "changed");
  return { item };
});

messenger.calendar.useredit.onUpdate.addListener(async (editId, changeInfo) => {
  // This triggers when a change is made in the UI. We'll define which properties can change in the
  // change info.

  // We could either do a simple key-value:
  // One downside here is that we'd need to maintain a field for everything in the dialog, which
  // could change. That could be acceptable though, we can deprecate some and add others. On the
  // plus side, there is a very simple account on which field changed without having to parse jCal.
  // changeInfo = { "summary": "updatedValue" }

  // Or we serialize each change into an ical property:
  // Upside is that this could be a bit more universal. Downside is the need for parsing, or
  // slightly ugly way of accessing info (e.g. changeInfo[0][3] is "updatedValue".
  // changeInfo = [
  //   ["summary", {}, "text", "updatedValue"]
  // ]

  // TODO There may be some updates from the dialog that don't correspond to an ical property. How
  // to deal with those if we go with the property approach? (e.g. disallow counter, separate
  // invitation per attendee)

  // TODO We'd have to think about how to signal additions and removals for multi-props e.g.
  // attendees. Either serialize all attendees on each change, or have some add/remove logic that
  // signals a change within a change.

  // TODO is changing the calendar simply an onUpdate change, or does it need to be separate?

  // TODO we need a way to make it easy not to cause an infinite loop where a further edit is
  // triggered by the add-on, that again calls the listener, etc.

  // If the user needs the full serialized item, then can get it.
  let item = await messenger.calendar.useredit.getItem(editId);


  // Not ready to save
  return { valid: false, validationError: "no can do" };
});

messenger.calendar.useredit.onBeforeModeChange.addListener((editId, oldMode, mode) => {
  // Switching from "view" to "edit" mode.
  // Right now the workflow is open -> summary -> edit. This method might become obsolete, but we'd
  // stay forward compatible.

  // Alternative might be to consider a "view" and an "edit" two different edit sessions, this way
  // we could avoid this method overall.

});

messenger.calendar.useredit.onBeforeSave.addListener((editId, item, options) => {
  // The item is passed in here, it is very likely developers will need it.
  // options.mode = "save" -- The user clicked save, no closing
  // options.mode = "close" -- The user clicked save and close
  // options.mode = "delete" -- The user clicked delete. There won't be actual saving, but this
  //                            saves us a method onBeforeDelete


  // Cancel the saving of the item, stay in the dialog or edit session
  return { cancel: true };

  // TODO Maybe also a mode to cancel and also close the edit session?
  return { cancel: true, close: true };

  // TODO we should probably pipe through the extra settings (e.g. disallow counter, notify
  // attendees) in options object and allow them to be changed.
  return { notify: false };

  // TODO do we need an onAfterSave here as well? In theory this is covered by
  // calendar.items.onUpdated.
});

/**
 * calendar_item_details allows an iframe in the event dialog, either in a tab or inline. In that
 * context there are a few things developers should be able to do
 */
async function withinCalendarItemDetails() {
  // Get the edit id of the current iframe (returns null in the background script)
  let id = await messenger.calendar.useredit.getCurrent();

  // Get the item for that edit id
  let item = await messenger.calendar.useredit.getItem(id);

  // Add a listener just for that edit id
  messenger.calendar.useredit.onUpdated.addListener((editId, changeInfo) => {
    console.log(`The item ${editId} was updated!`, changeInfo);
  }, { returnFormat: "ical", editId });

  // Update certain info in the user edit.
  messenger.calendar.useredit.update(editId, {
    // Update validation. The validation status needs to be saved per add-on, and we might need a way
    // to avoid add-ons disrupting the validation due to a programming error.
    valid: false,
    validationError: "no can do", // An optional validation message that we could display

    // TODO do we need these to be exposed?
    timezones: true, // Enables/disables the "Show timezone" option
    linkdates: true, // Enables/disables the link start/end date option


    // This is one way to handle it which is good if changeInfo is simple properties. The title
    // field should be different? This is how to change it.
    title: "changed",

    // A different approach that is more jCal property based. If we do this, we might also need to
    // specify what format properties is, it could also be an array or block of iCalendar string
    // properties.
    properties: [
      ["summary", {}, "text", "changed"]
    ]
    // TODO how would we signal properties should be removed, e.g. attendee lists?
    // TODO we could potentially allow adding/removing x-props and other properties not visible in
    // the event dialog. They'd be available to multiple add-ons this way and we'd avoid race
    // conditions where if multiple add-ons have an onBeforeSave handler they couldn't easily rely
    // on each other. If the change is made immediately, this could trigger an onUpdated for all
    // add-ons except the one making the change.
  });
}


/**
 * A few extra features that could be called e.g. from the background script
 */
async function withinBackground() {
  
  // The developer may want to trigger opening the event/task dialog or summary dialog
  // mode is the edit mode as above
  let editId = await messenger.calendar.useredit.open(tabId, item, mode);

  // TODO do we also want to offer a close? Could be too intrusive, or give too much control over
  // the experience.
}
@kewisch kewisch added api: calendar This issue is about the calendar API experimeent proposal Issue contains a draft API proposal labels Aug 20, 2024
@opto
Copy link
Contributor

opto commented Oct 30, 2024

useredit.open can be mimicked by:

          async openEditDialog(calendarId, id) {
            const calendar = getResolvedCalendarById(context.extension, calendarId);

            const item = await calendar.getItem(id);
            if (!item) {
              throw new ExtensionError("Could not find item " + id);
            }
            let w3p = Services.wm.getMostRecentWindow("mail:3pane");
            w3p.modifyEventWithDialog(item, true);
          },

@kewisch
Copy link
Member Author

kewisch commented Jan 2, 2025

Needed:

  • A way to open the event dialog to create a new event or task, not just to edit an existing item
  • A method (maybe on another more window/tab based API) to give the default calendar for that calendar tab.

@LouisJULIEN
Copy link
Contributor

As mentioned in #54 , I think that the proposals by @kewisch and @opto are relevant! It would avoid plugin developers the heavy lifting of (quickly thus poorly) reproducing Thunderbird's event edit window just to add a few features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: calendar This issue is about the calendar API experimeent proposal Issue contains a draft API proposal
Projects
None yet
Development

No branches or pull requests

3 participants