Updated the calendar to improve opportunity display and navigation

This commit is contained in:
2026-10-27 03:22:10 -03:00
parent 7606643ff4
commit 1d5b28e95b
51 changed files with 1486 additions and 1237 deletions

View File

@@ -25,9 +25,10 @@ function setup(items = [opportunity("A", "2026-11-15")]) {
const host = new Element("host");
document = { createElement: tag => new Element(tag), getElementById: () => host };
const state = { status: "ready", items };
const h = { state, filtered: items, version: 1, instances: [], loads: 0, host, document };
const h = { state, filtered: items, version: 1, range: { startCloseDate: "2026-09-01", endCloseDate: "2027-03-01" }, instances: [], loads: 0, host, document };
class Calendar {
constructor(element, options) { this.element = element; this.options = options; this.events = options.events; this.dates = []; h.instances.push(this); }
setOption(key, value) { this.options[key] = value; }
render() { this.rendered = true; }
destroy() { this.destroyed = true; }
updateSize() { this.resized = true; }
@@ -37,7 +38,7 @@ function setup(items = [opportunity("A", "2026-11-15")]) {
addEventSource(events) { this.events = events; }
}
h.api = factory({ environment: { document }, loadLibrary: () => { h.loads++; return { Calendar }; },
getState: () => state, getItems: () => h.filtered, getVersion: () => h.version,
getState: () => state, getItems: () => h.filtered, getVersion: () => h.version, getRange: () => h.range,
formatRevenue: () => "$100", formatDate: value => value.slice(0, 10), getOwnerName: () => "Owner",
createDetailLink: (text, href) => { const a = new Element("a"); a.textContent = text; a.href = href; return a; },
createCopyButton: () => new Element("button"), createCloseIcon: () => new Element("svg"),
@@ -57,10 +58,6 @@ test("calendar controls, accessibility labels and missing dates are English", ()
const h = setup([opportunity("A", "2026-11-15"), opportunity("B", null)]);
const { options } = h.activate();
assert.equal(options.locale, "en");
assert.deepEqual(plain(options.buttonText), { today: "Today", month: "Month", week: "Week" });
assert.deepEqual(plain(options.buttonHints), { prev: "Previous", next: "Next", today: "Today", dayGridMonth: "Month", dayGridWeek: "Week" });
assert.equal(options.moreLinkHint(1), "View 1 more opportunity");
assert.equal(options.moreLinkHint(3), "View 3 more opportunities");
assert.ok(h.nodes().some(node => node.attributes["aria-label"] === "Opportunity views"));
assert.ok(h.nodes().some(node => node.textContent === "No valid Close Date (1)"));
});
@@ -126,7 +123,7 @@ test("initial calendar date selects the next close date, last past date or today
assert.equal(api.initialDate([opportunity("A", null)], now), "2026-10-05");
});
test("overview is the default; tabs lazily initialize and reuse a monthly/weekly calendar", () => {
test("overview is the default; tabs lazily initialize and reuse a scrollable monthly calendar", () => {
const h = setup(); h.mount();
const surface = h.byClass("opportunities-extension-calendar");
assert.equal(h.table.hidden, false); assert.equal(surface.hidden, true); assert.equal(h.loads, 0);
@@ -137,13 +134,13 @@ test("overview is the default; tabs lazily initialize and reuse a monthly/weekly
tabs[1].click();
const instance = h.instances[0];
assert.equal(h.table.hidden, true); assert.equal(surface.hidden, false);
assert.equal(instance.options.initialView, "dayGridMonth");
assert.equal(instance.options.initialView, "referenceMonths");
assert.equal(instance.options.height, "100%");
assert.equal(instance.options.expandRows, true);
assert.equal(instance.options.headerToolbar.right, "dayGridMonth,dayGridWeek");
assert.equal(instance.options.editable, false);
assert.equal(instance.options.dayMaxEvents, 2);
assert.equal(instance.options.moreLinkText(7), "... +7");
assert.equal(instance.options.dayMaxEvents, false);
assert.equal(instance.options.headerToolbar, false);
assert.deepEqual(plain(instance.options.validRange), {start:"2026-09-01",end:"2027-03-01"});
assert.equal(instance.options.weekNumbers, true);
tabs[0].click(); tabs[1].click();
assert.equal(h.loads, 1); assert.equal(h.instances.length, 1); assert.equal(instance.resized, true);
@@ -151,24 +148,15 @@ test("overview is the default; tabs lazily initialize and reuse a monthly/weekly
assert.equal(h.table.hidden, false); assert.equal(h.document.activeElement, tabs[0]);
});
test("responsive event limit reserves room for every week and the overflow control", () => {
const { api } = setup();
assert.equal(api.visibleEventLimit(240, 6, 21), 0);
assert.equal(api.visibleEventLimit(500, 5, 21), 1);
assert.equal(api.visibleEventLimit(800, 5, 21), 2);
assert.equal(api.visibleEventLimit(500, 1, 21), 2);
assert.equal(api.visibleEventLimit(0, 6, 21), 0);
});
test("calendar follows filtered data; only a new Reference request changes its date", () => {
const h = setup([opportunity("A", "2026-11-15"), opportunity("B", "2026-12-12")]);
const calendar = h.activate();
h.filtered = [h.state.items[1]]; h.api.render(h.filtered, h.state);
assert.deepEqual(plain(calendar.events.map(event => event.id)), ["B"]);
assert.equal(calendar.dates.length, 0);
h.version++; h.state.items = [opportunity("C", "2027-02-20")]; h.filtered = h.state.items;
h.range = { startCloseDate: "2026-12-01", endCloseDate: "2027-03-01" }; h.version++; h.state.items = [opportunity("C", "2027-02-20")]; h.filtered = h.state.items;
h.api.render(h.filtered, h.state);
assert.deepEqual(calendar.dates, ["2027-02-20"]);
assert.deepEqual(calendar.dates, ["2026-12-01"]);
assert.deepEqual(plain(calendar.events.map(event => event.id)), ["C"]);
h.filtered = []; h.api.render(h.filtered, h.state);
assert.equal(calendar.events.length, 0);
@@ -194,7 +182,7 @@ test("event details close on changed data, loading/error hide stale calendar con
assert.ok(h.nodes().some(node => node.attributes.role === "dialog"));
h.api.render([], h.state);
assert.ok(!h.nodes().some(node => node.attributes.role === "dialog"));
for (const status of ["loading", "error", "empty"]) {
for (const status of ["loading", "error"]) {
h.api.render([], { status, items: [], message: "State message" });
assert.equal(h.byClass("opportunities-calendar-grid").hidden, true);
assert.equal(h.byClass("opportunities-calendar-undated").hidden, true);
@@ -202,29 +190,6 @@ test("event details close on changed data, loading/error hide stale calendar con
}
});
test("overflow uses our dialog instead of the FullCalendar offsetParent-dependent popover", () => {
const h = setup([opportunity("A", "2026-11-15"), opportunity("B", "2026-11-15"), opportunity("C", "2026-11-15")]);
const calendar = h.activate();
const trigger = h.document.createElement("a");
const allSegs = calendar.events.map(event => ({ event }));
const open = () => calendar.options.moreLinkClick({ date: new Date("2026-11-15T00:00:00Z"), allSegs,
jsEvent: { currentTarget: trigger, preventDefault() {} } });
assert.equal(open(), true, "truthy non-string result suppresses vendor popover");
const list = h.byClass("opportunities-calendar-day-list");
assert.equal(list.children.length, 3);
assert.ok(list.children.every(button => button.children.length === 2 && button.children[1].textContent.includes(" | FORECAST | $100")));
list.children[2].click();
assert.ok(h.nodes().some(node => node.tag === "h2" && node.textContent === "<Opportunity C>"));
h.api.closeDetails();
assert.equal(h.document.activeElement, trigger);
open(); h.api.selectView("overview");
assert.ok(!h.nodes().some(node => node.attributes.role === "dialog"));
h.api.selectView("calendar"); open();
const dialog = h.nodes().find(node => node.attributes.role === "dialog");
dialog.listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
assert.ok(!h.nodes().some(node => node.attributes.role === "dialog"));
});
test("calendar events show their stage and handle missing stage values", () => {
const h = setup();
const calendar = h.activate();
@@ -237,3 +202,13 @@ test("calendar events show their stage and handle missing stage values", () => {
assert.equal(domNodes[0].children[1].title, `A | ${label} | $100`);
}
});
test("Reference boundaries exclude the next period and keep empty months visible", () => {
const h = setup([opportunity("before", "2026-08-31"), opportunity("first", "2026-09-01"), opportunity("last", "2026-11-30"), opportunity("next", "2026-12-01")]);
h.range = {startCloseDate:"2026-09-01",endCloseDate:"2026-12-01"};
const calendar = h.activate();
assert.deepEqual(plain(calendar.events.map(e=>e.id)), ["first","last"]);
h.api.render([], {status:"empty",items:[]});
assert.equal(h.byClass("opportunities-calendar-grid").hidden,false);
assert.equal(calendar.events.length,0);
});