From 2d9d8c42e1832b1dfd4b4462940007eeb1f39405 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 31 Oct 2024 17:12:05 -0500 Subject: [PATCH 01/74] 0 css. Can create series now. --- .../event-format-component-controller.js | 4 +- ecc/blocks/series-console/series-console.css | 0 ecc/blocks/series-console/series-console.js | 65 ++++++ ecc/scripts/esp-controller.js | 207 ++++++++++++------ 4 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 ecc/blocks/series-console/series-console.css create mode 100644 ecc/blocks/series-console/series-console.js diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/form-handler/controllers/event-format-component-controller.js index 8ac4697c..c5072ab6 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import { getSeries } from '../../../scripts/esp-controller.js'; +import { getAllSeries } from '../../../scripts/esp-controller.js'; import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; import { LIBS } from '../../../scripts/scripts.js'; import { changeInputValue } from '../../../scripts/utils.js'; @@ -76,7 +76,7 @@ async function populateSeriesOptions(props, component) { const seriesSelect = component.querySelector('#series-select-input'); if (!seriesSelect) return; - const series = await getSeries(); + const series = await getAllSeries(); if (!series) { seriesSelect.pending = false; seriesSelect.disabled = true; diff --git a/ecc/blocks/series-console/series-console.css b/ecc/blocks/series-console/series-console.css new file mode 100644 index 00000000..e69de29b diff --git a/ecc/blocks/series-console/series-console.js b/ecc/blocks/series-console/series-console.js new file mode 100644 index 00000000..b0347502 --- /dev/null +++ b/ecc/blocks/series-console/series-console.js @@ -0,0 +1,65 @@ +import { createSeries, getAllSeries } from '../../scripts/esp-controller.js'; +import { LIBS } from '../../scripts/scripts.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); + +function buildSeriesInfoWrapper(props) { + const seriesInfoContainer = createTag('div', { class: 'series-info-container' }); + props.el.append(seriesInfoContainer); +} + +function listAllSeries(props) { + const seriesInfoContainer = props.el.querySelector('.series-info-container'); + if (!seriesInfoContainer) return; + seriesInfoContainer.innerHTML = ''; + + props.data.forEach((series) => { + const seriesInfoWrapper = createTag('div', { class: 'series-info-wrapper' }); + + Object.keys(series).forEach((key) => { + const seriesInfo = createTag('div', { class: 'series-info' }); + seriesInfo.textContent = `${key}: ${series[key]}`; + seriesInfoWrapper.append(seriesInfo); + }); + + seriesInfoContainer.append(seriesInfoWrapper, createTag('br')); + }); +} + +function buildNewSeriesForm(props) { + const newSeriesNameInput = createTag('input', { class: 'new-series-name-input', placeholder: 'Enter new series name' }, '', { parent: props.el }); + const createNewSeriesBtn = createTag('button', { class: 'con-button blue create-new-series-btn' }, 'Create New Series', { parent: props.el }); + + createNewSeriesBtn.addEventListener('click', async () => { + const newSeriesName = newSeriesNameInput.value; + if (!newSeriesName) return; + + const newSeries = await createSeries({ seriesName: newSeriesName }); + if (!newSeries) return; + + props.data = await getAllSeries(); + }); +} + +export default async function init(el) { + const allSeries = await getAllSeries(); + const props = { + el, + data: allSeries, + }; + + buildSeriesInfoWrapper(props); + + const dataHandler = { + set(target, prop, value, receiver) { + target[prop] = value; + listAllSeries(receiver); + return true; + }, + }; + + const proxyProps = new Proxy(props, dataHandler); + + listAllSeries(proxyProps); + buildNewSeriesForm(proxyProps); +} diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 5574f54b..a6289878 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -51,6 +51,9 @@ export const getCaasTags = (() => { })(); function waitForAdobeIMS() { + const urlParam = new URLSearchParams(window.location.search); + if (urlParam.has('devToken')) return Promise.resolve(); + return new Promise((resolve) => { const checkIMS = () => { if (window.adobeIMS && window.adobeIMS.getAccessToken) { @@ -210,14 +213,14 @@ export async function deleteImage(configs, imageId) { if (!response.ok) { const data = await response.json(); window.lana?.log('Failed to delete image. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } // 204 no content. Return true if no error. return true; } catch (error) { window.lana?.log('Failed to delete image. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -232,13 +235,13 @@ export async function createVenue(eventId, venueData) { if (!response.ok) { window.lana?.log('Failed to create venue. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log('Failed to create venue. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -253,13 +256,13 @@ export async function replaceVenue(eventId, venueId, venueData) { if (!response.ok) { window.lana?.log('Failed to replace venue. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log('Failed to replace venue. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -274,13 +277,13 @@ export async function createEvent(payload) { if (!response.ok) { window.lana?.log('Failed to create event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log('Failed to create event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -297,13 +300,13 @@ export async function createSpeaker(profile, seriesId) { if (!response.ok) { window.lana?.log('Failed to create speaker. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to create speaker. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -318,13 +321,13 @@ export async function createSponsor(sponsorData, seriesId) { if (!response.ok) { window.lana?.log('Failed to create sponsor. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to create sponsor. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -339,13 +342,13 @@ export async function updateSponsor(sponsorData, sponsorId, seriesId) { if (!response.ok) { window.lana?.log('Failed to update sponsor. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to update sponsor. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -360,13 +363,13 @@ export async function addSponsorToEvent(sponsorData, eventId) { if (!response.ok) { window.lana?.log('Failed to add sponsor to event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to add sponsor to event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -381,13 +384,13 @@ export async function updateSponsorInEvent(sponsorData, sponsorId, eventId) { if (!response.ok) { window.lana?.log('Failed to update sponsor in event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to update sponsor in event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -401,13 +404,13 @@ export async function removeSponsorFromEvent(sponsorId, eventId) { if (!response.ok) { window.lana?.log('Failed to delete sponsor from event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to delete sponsor from event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -421,13 +424,13 @@ export async function getSponsor(seriesId, sponsorId) { if (!response.ok) { window.lana?.log('Failed to get sponsor. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to get sponsor. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -441,13 +444,13 @@ export async function getSponsors(seriesId) { if (!response.ok) { window.lana?.log('Failed to get sponsors. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to get sponsors. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -461,13 +464,13 @@ export async function getSponsorImages(seriesId, sponsorId) { if (!response.ok) { window.lana?.log('Failed to get sponsor images. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to get sponsor images. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -482,13 +485,13 @@ export async function addSpeakerToEvent(speakerData, eventId) { if (!response.ok) { window.lana?.log('Failed to add speaker to event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to add speaker to event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -503,13 +506,13 @@ export async function updateSpeakerInEvent(speakerData, speakerId, eventId) { if (!response.ok) { window.lana?.log('Failed to update speaker in event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to update speaker in event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -523,13 +526,13 @@ export async function removeSpeakerFromEvent(speakerId, eventId) { if (!response.ok) { window.lana?.log('Failed to delete speaker from event. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to delete speaker from event. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -545,13 +548,13 @@ export async function updateSpeaker(profile, seriesId) { if (!response.ok) { window.lana?.log('Failed to update speaker. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to update speaker. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -566,13 +569,13 @@ export async function updateEvent(eventId, payload) { if (!response.ok) { window.lana?.log(`Failed to update event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log(`Failed to update event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -587,13 +590,13 @@ export async function publishEvent(eventId, payload) { if (!response.ok) { window.lana?.log(`Failed to publish event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log(`Failed to publish event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -608,13 +611,13 @@ export async function unpublishEvent(eventId, payload) { if (!response.ok) { window.lana?.log(`Failed to unpublish event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.espProvider || data; } catch (error) { window.lana?.log(`Failed to unpublish event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -628,14 +631,14 @@ export async function deleteEvent(eventId) { if (!response.ok) { const data = await response.json(); window.lana?.log(`Failed to delete event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } // 204 no content. Return true if no error. return true; } catch (error) { window.lana?.log(`Failed to delete event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -649,13 +652,13 @@ export async function getEvents() { if (!response.ok) { window.lana?.log('Failed to get list of events. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to get list of events. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -669,13 +672,13 @@ export async function getEvent(eventId) { if (!response.ok) { window.lana?.log(`Failed to get details for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to get details for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -689,13 +692,13 @@ export async function getVenue(eventId) { if (!response.ok) { window.lana?.log('Failed to get venue details. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log('Failed to get venue details. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -709,13 +712,13 @@ export async function getSpeaker(seriesId, speakerId) { if (!response.ok) { window.lana?.log('Failed to get speaker details. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return convertToSpeaker(data); } catch (error) { window.lana?.log('Failed to get speaker details. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -731,7 +734,7 @@ export async function getClouds() { return null; } -export async function getSeries() { +export async function getAllSeries() { const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); @@ -741,13 +744,75 @@ export async function getSeries() { if (!response.ok) { window.lana?.log('Failed to fetch series. Status:', response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data.series; } catch (error) { window.lana?.log('Failed to fetch series. Error:', error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; + } +} + +export async function getSeriesById(seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const options = await constructRequestOptions('GET'); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to fetch series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to fetch series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function createSeries(seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify(seriesData); + const options = await constructRequestOptions('POST', raw); + + try { + const response = await fetch(`${host}/v1/series`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log('Failed to create series. Status:', response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log('Failed to create series. Error:', error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function updateSeries(seriesData, seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify(seriesData); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to update series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to update series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; } } @@ -764,13 +829,13 @@ export async function createAttendee(eventId, attendeeData) { if (!response.ok) { window.lana?.log(`Failed to create attendee for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to create attendee for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -787,13 +852,13 @@ export async function updateAttendee(eventId, attendeeId, attendeeData) { if (!response.ok) { window.lana?.log(`Failed to update attendee ${attendeeId} for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to update attendee ${attendeeId} for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -809,13 +874,13 @@ export async function removeAttendeeFromEvent(eventId, attendeeId) { if (!response.ok) { window.lana?.log(`Failed to delete attendee ${attendeeId} for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to delete attendee ${attendeeId} for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -831,13 +896,13 @@ export async function getEventAttendees(eventId) { if (!response.ok) { window.lana?.log(`Failed to fetch attendees for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to fetch attendees for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -851,7 +916,7 @@ export async function getAllEventAttendees(eventId) { .then((response) => { if (!response.ok) { window.lana?.log(`Failed to fetch attendees for event ${eventId}. Status:`, response.status); - return { ok: response.ok, status: response.status, error: response.statusText }; + return { status: response.status, error: response.statusText }; } return response.json(); @@ -865,7 +930,7 @@ export async function getAllEventAttendees(eventId) { }) .catch((error) => { window.lana?.log(`Failed to fetch attendees for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; }); }; @@ -884,13 +949,13 @@ export async function getAttendee(eventId, attendeeId) { if (!response.ok) { window.lana?.log(`Failed to get details of attendee ${attendeeId} for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to get details of attendee ${attendeeId} for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -906,13 +971,13 @@ export async function getSpeakers(seriesId) { if (!response.ok) { window.lana?.log(`Failed to get details of speakers for series ${seriesId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return { speakers: data.speakers.map(convertToSpeaker) }; } catch (error) { window.lana?.log(`Failed to get details of speakers for series ${seriesId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -928,13 +993,13 @@ export async function getEventImages(eventId) { if (!response.ok) { window.lana?.log(`Failed to get event images for event ${eventId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to get event images for event ${eventId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } @@ -950,12 +1015,12 @@ export async function deleteSpeakerImage(speakerId, seriesId, imageId) { if (!response.ok) { window.lana?.log(`Failed to delete speaker images for speaker ${speakerId}. Status:`, response.status, 'Error:', data); - return { ok: response.ok, status: response.status, error: data }; + return { status: response.status, error: data }; } return data; } catch (error) { window.lana?.log(`Failed to delete speaker images for speaker ${speakerId}. Error:`, error); - return { ok: false, status: 'Network Error', error: error.message }; + return { status: 'Network Error', error: error.message }; } } From 9f085323507735ca9904e01add00e9ab79896de3 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 1 Nov 2024 12:04:53 -0500 Subject: [PATCH 02/74] POC done --- ecc/blocks/series-console/series-console.css | 127 ++++++++++ ecc/blocks/series-console/series-console.js | 249 ++++++++++++++++++- ecc/scripts/esp-controller.js | 23 +- 3 files changed, 388 insertions(+), 11 deletions(-) diff --git a/ecc/blocks/series-console/series-console.css b/ecc/blocks/series-console/series-console.css index e69de29b..68637376 100644 --- a/ecc/blocks/series-console/series-console.css +++ b/ecc/blocks/series-console/series-console.css @@ -0,0 +1,127 @@ +.series-console { + width: var(--grid-container-width); + margin: 40px auto; + font-family: var(--body-font-family); +} + +.series-console.loading { + opacity: 0.5; + pointer-events: none; +} + +.series-console .new-series-form { + padding: 0 0 20px; + display: flex; + align-items: center; + gap: 1rem; +} + +.series-console label { + font-size: var(--type-body-xs-size); + width: max-content; +} + +.series-console input, +.series-console select { + font-family: var(--body-font-family); + padding: 8px; + border: none; + border-bottom: 1px solid var(--color-black); + flex-grow: 1; + max-width: 240px; +} + +.series-console input:disabled { + cursor: not-allowed; + border-bottom: 1px solid var(--color-gray-300); + background: none; + color: var(--color-black); +} + +.series-console a { + cursor: pointer; + text-decoration: none; +} + +.series-console .series-info-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 2rem; +} + +.series-console a:not(:any-link):not(.con-button) { + color: var(--color-primary); +} + +.series-console .series-info-wrapper { + padding: 16px; + border: 1px solid var(--color-gray-300); + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1rem; +} + +.series-console .series-info-wrapper .actions-wrapper { + display: flex; + gap: 1rem; +} + +.series-console .field-wrapper { + max-width: 100%; + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem; +} + +.series-console .preview-list-overlay { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgb(0 0 0 / 40%); + z-index: 1; +} + +.series-console .preview-list-modal { + position: relative; + background-color: var(--color-white); + padding: 40px; + border-radius: 24px; + max-height: 80%; + width: 800px; + max-width: 80%; +} + +.series-console .preview-list-modal img { + max-height: 600px; +} + +.series-console .preview-list-overlay.hidden { + display: none; +} + +.series-console .preview-list-items { + max-height: 600px; + display: grid; + overflow: auto; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 20px; +} + +.series-console sp-theme.toast-area { + position: fixed; + right: calc((100% - var(--grid-container-width)) / 2); + bottom: 67px; + display: flex; + flex-direction: column; + gap: 16px; + z-index: 9; +} diff --git a/ecc/blocks/series-console/series-console.js b/ecc/blocks/series-console/series-console.js index b0347502..2e619a22 100644 --- a/ecc/blocks/series-console/series-console.js +++ b/ecc/blocks/series-console/series-console.js @@ -1,13 +1,139 @@ -import { createSeries, getAllSeries } from '../../scripts/esp-controller.js'; +import { createSeries, deleteSeries, getAllSeries, updateSeries } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); +const ATTR_MAP = { + seriesId: { + handle: 'series-id', + label: 'Series ID', + type: 'string', + readonly: true, + }, + seriesName: { + handle: 'series-name', + label: 'Series Name', + type: 'string', + readonly: false, + }, + externalThemeId: { + handle: 'external-theme-id', + label: 'External Theme ID', + type: 'string', + readonly: true, + }, + cloudType: { + handle: 'cloud-type', + label: 'Cloud Type', + type: 'list', + readonly: false, + staticOptions: ['CreativeCloud', 'DX'], + }, + templateId: { + handle: 'template-id', + label: 'Events Template', + type: 'preview-list', + readonly: false, + optionsSource: '/ecc/system/series-templates.json', + }, + relatedDomain: { + handle: 'related-domain', + label: 'Related Domain', + type: 'string', + readonly: false, + }, + emailTemplate: { + handle: 'email-template', + label: 'Email Template', + type: 'string', + readonly: false, + }, + modificationTime: { + handle: 'modification-time', + label: 'Last Modified', + type: 'timestamp', + readonly: true, + }, + creationTime: { + handle: 'creation-time', + label: 'Creation Time', + type: 'timestamp', + readonly: true, + }, +}; + function buildSeriesInfoWrapper(props) { const seriesInfoContainer = createTag('div', { class: 'series-info-container' }); props.el.append(seriesInfoContainer); } +function showToast(props, msg, options = {}) { + const toastArea = props.el.querySelector('sp-theme.toast-area'); + const toast = createTag('sp-toast', { open: true, ...options }, msg, { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); +} + +async function buildPreviewListOptionsFromSource(previewList, source, attr) { + const valueHolder = previewList.closest('.series-info-wrapper').querySelector(`.${attr.handle}-input`); + const previewListItems = previewList.querySelector('.preview-list-items'); + const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + + const jsonResp = await fetch(source).then((res) => { + if (!res.ok) throw new Error('Failed to fetch series templates'); + return res.json(); + }); + + const options = jsonResp.data; + if (!options) return; + + if (options.length > 3) { + previewListItems.classList.add('show-3'); + } else { + previewListItems.classList.remove('show-3'); + } + + options.forEach((option) => { + const previewListItem = createTag('div', { class: 'preview-list-item' }); + const previewListItemImage = createTag('img', { src: option['template-image'] }); + const previewListItemTitle = createTag('h5', {}, option['template-name']); + const selectItemBtn = createTag('a', { class: 'con-button blue select-item-btn' }, 'Select', { parent: previewListItem }); + previewListItem.append(previewListItemImage, previewListItemTitle, selectItemBtn); + previewListItems.append(previewListItem); + + selectItemBtn.addEventListener('click', () => { + valueHolder.value = option['template-path']; + previewListOverlay.classList.add('hidden'); + }); + }); +} + +function buildPreviewList(attrObj) { + const { optionsSource } = attrObj; + + const previewList = createTag('div', { class: 'preview-list' }); + const previewListTitle = createTag('h4', {}, 'Select a template'); + const previewListItems = createTag('div', { class: 'preview-list-items' }); + const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, 'Select'); + const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); + const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); + const previewListCloseBtn = createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); + + previewListBtn.addEventListener('click', () => { + previewListOverlay.classList.remove('hidden'); + buildPreviewListOptionsFromSource(previewList, optionsSource, attrObj); + }); + + previewListCloseBtn.addEventListener('click', () => { + previewListOverlay.classList.add('hidden'); + }); + + previewListModal.append(previewListTitle, previewListItems); + previewList.append(previewListBtn, previewListOverlay); + return previewList; +} + function listAllSeries(props) { const seriesInfoContainer = props.el.querySelector('.series-info-container'); if (!seriesInfoContainer) return; @@ -16,19 +142,113 @@ function listAllSeries(props) { props.data.forEach((series) => { const seriesInfoWrapper = createTag('div', { class: 'series-info-wrapper' }); - Object.keys(series).forEach((key) => { - const seriesInfo = createTag('div', { class: 'series-info' }); - seriesInfo.textContent = `${key}: ${series[key]}`; - seriesInfoWrapper.append(seriesInfo); + Object.keys(ATTR_MAP).forEach(async (attr) => { + const fieldWrapper = createTag('div', { class: 'field-wrapper' }); + const attrValue = series[attr] || ''; + const attrType = ATTR_MAP[attr].type; + const attrReadonly = ATTR_MAP[attr].readonly; + const attrHandle = ATTR_MAP[attr].handle; + const attrSentence = ATTR_MAP[attr].label; + + if (attrType === 'list') { + const attrOptions = ATTR_MAP[attr].staticOptions || series[attr]; + const attrSelect = createTag('select', { class: `${attrHandle}-select` }); + if (attrReadonly) attrSelect.disabled = true; + attrOptions.forEach((option) => { + const opt = createTag('option', { value: option }, option); + attrSelect.append(opt); + }); + + fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrSelect); + } + + if (attrType === 'timestamp') { + const attrInput = createTag('input', { class: `${attrHandle}-input`, value: new Date(attrValue).toLocaleString() }); + attrInput.disabled = true; + fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrInput); + } + + if (attrType === 'preview-list') { + const label = createTag('label', {}, `${attrSentence}:`); + const valueHolder = createTag('input', { class: `${attrHandle}-input`, value: attrValue, disabled: true }); + const previewList = buildPreviewList(ATTR_MAP[attr]); + fieldWrapper.append(label, valueHolder, previewList); + } + + if (attrType === 'string') { + const attrInput = createTag('input', { class: `${attrHandle}-input`, value: attrValue }); + if (attrReadonly) attrInput.disabled = true; + fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrInput); + } + + seriesInfoWrapper.append(fieldWrapper); + }); + + const actionsWrapper = createTag('div', { class: 'actions-wrapper' }); + const updateSeriesBtn = createTag('a', { class: 'con-button fill update-series-btn' }, 'Update Series'); + const deleteSeriesBtn = createTag('a', { class: 'con-button fill delete-series-btn' }, 'Delete Series'); + actionsWrapper.append(updateSeriesBtn, deleteSeriesBtn); + seriesInfoWrapper.append(actionsWrapper); + + updateSeriesBtn.addEventListener('click', async (e) => { + e.preventDefault(); + const updatedSeries = {}; + + Object.keys(ATTR_MAP).forEach((attr) => { + const readOnly = ATTR_MAP[attr].readonly; + + if (readOnly) return; + + const attrType = ATTR_MAP[attr].type; + const attrHandle = ATTR_MAP[attr].handle; + + if (attrType === 'list') { + const attrSelect = seriesInfoWrapper.querySelector(`.${attrHandle}-select`); + if (attrSelect && attrSelect.value) updatedSeries[attr] = attrSelect.value; + } else { + const attrInput = seriesInfoWrapper.querySelector(`.${attrHandle}-input`); + if (attrInput && attrInput.value) updatedSeries[attr] = attrInput.value; + } + }); + + props.el.classList.add('loading'); + const resp = await updateSeries( + { ...updatedSeries, modificationTime: series.modificationTime }, + series.seriesId, + ); + + if (!resp.error) { + props.data = await getAllSeries(); + showToast(props, 'Series updated', { variant: 'positive', timeout: 6000 }); + } else { + showToast(props, 'Update failed. Please try again later.', { variant: 'negative', timeout: 6000 }); + } + props.el.classList.remove('loading'); + }); + + deleteSeriesBtn.addEventListener('click', async (e) => { + e.preventDefault(); + props.el.classList.add('loading'); + const { seriesId } = series; + const resp = await deleteSeries(seriesId); + + if (!resp.error) { + props.data = await getAllSeries(); + showToast(props, 'Series deleted', { variant: 'positive', timeout: 6000 }); + } else { + showToast(props, 'Delete failed. Please try again later.', { variant: 'negative', timeout: 6000 }); + } + props.el.classList.remove('loading'); }); - seriesInfoContainer.append(seriesInfoWrapper, createTag('br')); + seriesInfoContainer.append(seriesInfoWrapper); }); } function buildNewSeriesForm(props) { - const newSeriesNameInput = createTag('input', { class: 'new-series-name-input', placeholder: 'Enter new series name' }, '', { parent: props.el }); - const createNewSeriesBtn = createTag('button', { class: 'con-button blue create-new-series-btn' }, 'Create New Series', { parent: props.el }); + const newSeriesForm = createTag('div', { class: 'new-series-form' }); + const newSeriesNameInput = createTag('input', { class: 'new-series-name-input', placeholder: 'Enter new series name' }, '', { parent: newSeriesForm }); + const createNewSeriesBtn = createTag('a', { class: 'con-button fill create-new-series-btn' }, 'Create New Series', { parent: newSeriesForm }); createNewSeriesBtn.addEventListener('click', async () => { const newSeriesName = newSeriesNameInput.value; @@ -39,9 +259,19 @@ function buildNewSeriesForm(props) { props.data = await getAllSeries(); }); + + props.el.prepend(newSeriesForm); } export default async function init(el) { + const miloLibs = LIBS; + await Promise.all([ + import(`${miloLibs}/deps/lit-all.min.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/theme.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/toast.js`), + ]); + createTag('sp-theme', { color: 'light', scale: 'medium', class: 'toast-area' }, '', { parent: el }); + const allSeries = await getAllSeries(); const props = { el, @@ -59,7 +289,6 @@ export default async function init(el) { }; const proxyProps = new Proxy(props, dataHandler); - - listAllSeries(proxyProps); buildNewSeriesForm(proxyProps); + listAllSeries(proxyProps); } diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index a6289878..852c4304 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -797,7 +797,7 @@ export async function createSeries(seriesData) { export async function updateSeries(seriesData, seriesId) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const raw = JSON.stringify(seriesData); + const raw = JSON.stringify({ ...seriesData, seriesId }); const options = await constructRequestOptions('PUT', raw); try { @@ -816,6 +816,27 @@ export async function updateSeries(seriesData, seriesId) { } } +export async function deleteSeries(seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const options = await constructRequestOptions('DELETE'); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + + if (!response.ok) { + const data = await response.json(); + window.lana?.log(`Failed to delete series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + // 204 no content. Return true if no error. + return true; + } catch (error) { + window.lana?.log(`Failed to delete series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + export async function createAttendee(eventId, attendeeData) { if (!eventId || !attendeeData) return false; From 7ddee3ff2e5f92158a0b806d4e58a3ed337bc1ba Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 1 Nov 2024 12:07:16 -0500 Subject: [PATCH 03/74] Update series-console.js --- ecc/blocks/series-console/series-console.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/series-console/series-console.js b/ecc/blocks/series-console/series-console.js index 2e619a22..a4166109 100644 --- a/ecc/blocks/series-console/series-console.js +++ b/ecc/blocks/series-console/series-console.js @@ -221,7 +221,7 @@ function listAllSeries(props) { props.data = await getAllSeries(); showToast(props, 'Series updated', { variant: 'positive', timeout: 6000 }); } else { - showToast(props, 'Update failed. Please try again later.', { variant: 'negative', timeout: 6000 }); + showToast(props, 'Update action failed. Please try again later.', { variant: 'negative', timeout: 6000 }); } props.el.classList.remove('loading'); }); @@ -236,7 +236,7 @@ function listAllSeries(props) { props.data = await getAllSeries(); showToast(props, 'Series deleted', { variant: 'positive', timeout: 6000 }); } else { - showToast(props, 'Delete failed. Please try again later.', { variant: 'negative', timeout: 6000 }); + showToast(props, 'Delete action failed. Please try again later.', { variant: 'negative', timeout: 6000 }); } props.el.classList.remove('loading'); }); From adaaa481d2934be4bf855de4d99ee05bb430aaab Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 12:12:00 -0500 Subject: [PATCH 04/74] test user events relationship --- .../attendee-management-table.js | 4 +- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 12 ++--- ecc/blocks/form-handler/form-handler.js | 4 +- ecc/scripts/esp-controller.js | 37 ++++++++++++++ ecc/scripts/{event-apis.js => profile.js} | 49 +++++++++++++++++-- ecc/scripts/scripts.js | 2 +- 6 files changed, 93 insertions(+), 15 deletions(-) rename ecc/scripts/{event-apis.js => profile.js} (61%) diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index 106376ee..7707be54 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -11,7 +11,7 @@ import { } from '../../scripts/utils.js'; import { SearchablePicker } from '../../components/searchable-picker/searchable-picker.js'; import { FilterMenu } from '../../components/filter-menu/filter-menu.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -720,7 +720,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + await initProfileLogicTree({ noProfile: () => { signIn(); }, diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index f4d71afd..bed14939 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -2,7 +2,7 @@ import { createEvent, deleteEvent, getEventImages, - getEvents, + getEventsForUser, publishEvent, unpublishEvent, } from '../../scripts/esp-controller.js'; @@ -16,7 +16,7 @@ import { getEventServiceEnv, } from '../../scripts/utils.js'; import { quickFilter } from '../form-handler/data-handler.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -315,7 +315,7 @@ function initMoreOptions(props, config, eventObj, row) { return; } - const newJson = await getEvents(); + const newJson = await getEventsForUser(); props.data = newJson.events; props.filteredData = newJson.events; props.paginatedData = newJson.events; @@ -360,7 +360,7 @@ function initMoreOptions(props, config, eventObj, row) { return; } - const newJson = await getEvents(); + const newJson = await getEventsForUser(); props.data = newJson.events; props.filteredData = newJson.events; props.paginatedData = newJson.events; @@ -647,7 +647,7 @@ function buildDashboardTable(props, config) { } async function getEventsArray() { - const resp = await getEvents(); + const resp = await getEventsForUser(); if (resp.error) { return []; @@ -738,7 +738,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + await initProfileLogicTree({ noProfile: () => { signIn(); }, diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 979bedf4..1c60c7b6 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -27,7 +27,7 @@ import PartnerSelector from '../../components/partner-selector/partner-selector. import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { CustomSearch } from '../../components/custom-search/custom-search.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); @@ -870,7 +870,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + await initProfileLogicTree({ noProfile: () => { signIn(); }, diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 5574f54b..2bb99f38 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -1,5 +1,6 @@ import { LIBS } from './scripts.js'; import { getEventServiceEnv, getSecret } from './utils.js'; +import { getUser, userHasAccessToBU, userHasAccessToEvent, userHasAccessToSerie } from './profile.js'; const API_CONFIG = { esl: { @@ -659,6 +660,24 @@ export async function getEvents() { } } +export async function getEventsForUser() { + const user = await getUser(); + + if (!user) return []; + + const events = await getEvents(); + if (!events.error) { + const { role } = user; + + if (role === 'admin') return events; + if (role === 'manager') return events.filter((e) => userHasAccessToBU(user, e.cloudType)); + if (role === 'creator') return events.filter((e) => userHasAccessToSerie(user, e.serieId)); + if (role === 'editor') return events.filter((e) => userHasAccessToEvent(user, e.eventId)); + } + + return []; +} + export async function getEvent(eventId) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); @@ -751,6 +770,24 @@ export async function getSeries() { } } +export async function getSeriesForUser() { + const user = await getUser(); + + if (!user) return []; + + const series = await getSeries(); + + if (!series.error) { + const { role } = user; + + if (role === 'admin') return series; + if (role === 'manager') return series.filter((e) => userHasAccessToBU(user, e.cloudType)); + if (role === 'creator') return series.filter((e) => userHasAccessToSerie(user, e.serieId)); + } + + return []; +} + export async function createAttendee(eventId, attendeeData) { if (!eventId || !attendeeData) return false; diff --git a/ecc/scripts/event-apis.js b/ecc/scripts/profile.js similarity index 61% rename from ecc/scripts/event-apis.js rename to ecc/scripts/profile.js index 5bc41b3a..1a38353c 100644 --- a/ecc/scripts/event-apis.js +++ b/ecc/scripts/profile.js @@ -1,5 +1,6 @@ import BlockMediator from './deps/block-mediator.min.js'; -import { ALLOWED_ACCOUNT_TYPES } from '../constants/constants.js'; + +let usersCache = []; export async function getProfile() { const { feds, adobeProfile, fedsConfig, adobeIMS } = window; @@ -63,15 +64,55 @@ export function lazyCaptureProfile() { }, 1000); } -export function initProfileLogicTree(callbacks) { +export async function getUser() { + const profile = BlockMediator.get('imsProfile'); + if (!profile || profile.noProfile) return null; + + const { email } = profile; + + if (usersCache.length === 0) { + const resp = await fetch('/ecc/system/users.json') + .then((r) => r) + .catch((e) => window.lana?.log(`Failed to fetch Google Places API key: ${e}`)); + + if (!resp.ok) return null; + + const json = await resp.json(); + usersCache = json.data; + } + + const user = usersCache.find((s) => s.email === email); + return user; +} + +export async function userHasAccessToBU(user, bu) { + if (!user) return false; + const businessUnits = user['business-units'].split(',').map((b) => b.trim()); + return businessUnits.length === 0 || businessUnits.includes(bu); +} + +export async function userHasAccessToSerie(user, serieId) { + if (!user) return false; + const series = user.series.split(',').map((b) => b.trim()); + return series.length === 0 || series.includes(serieId); +} + +export async function userHasAccessToEvent(user, eventId) { + if (!user) return false; + const events = user.events.split(',').map((b) => b.trim()); + return events.length === 0 || events.includes(eventId); +} + +export async function initProfileLogicTree(callbacks) { const { noProfile, noAccessProfile, validProfile } = callbacks; const profile = BlockMediator.get('imsProfile'); + const user = await getUser(); if (profile) { if (profile.noProfile) { noProfile(); - } else if (!ALLOWED_ACCOUNT_TYPES.includes(profile.account_type)) { + } else if (!user) { noAccessProfile(); } else { validProfile(profile); @@ -85,7 +126,7 @@ export function initProfileLogicTree(callbacks) { if (newValue) { if (newValue.noProfile) { noProfile(); - } else if (!ALLOWED_ACCOUNT_TYPES.includes(newValue.account_type)) { + } else if (!user) { noAccessProfile(); } else { validProfile(newValue); diff --git a/ecc/scripts/scripts.js b/ecc/scripts/scripts.js index 5714e828..4bc2c711 100644 --- a/ecc/scripts/scripts.js +++ b/ecc/scripts/scripts.js @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import { lazyCaptureProfile } from './event-apis.js'; +import { lazyCaptureProfile } from './profile.js'; function convertEccIcon(n) { const createSVGIcon = (iconName) => { From a7af980f59e893dcb9837f7fc374d2c6e1a84ea0 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 12:15:04 -0500 Subject: [PATCH 05/74] Update profile.js --- ecc/scripts/profile.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 1a38353c..121446d5 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -107,9 +107,10 @@ export async function initProfileLogicTree(callbacks) { const { noProfile, noAccessProfile, validProfile } = callbacks; const profile = BlockMediator.get('imsProfile'); - const user = await getUser(); + let user; if (profile) { + user = await getUser(); if (profile.noProfile) { noProfile(); } else if (!user) { @@ -123,17 +124,19 @@ export async function initProfileLogicTree(callbacks) { if (!profile) { const unsubscribe = BlockMediator.subscribe('imsProfile', ({ newValue }) => { - if (newValue) { - if (newValue.noProfile) { - noProfile(); - } else if (!user) { - noAccessProfile(); - } else { - validProfile(newValue); + getUser().then((u) => { + if (newValue) { + if (newValue.noProfile) { + noProfile(); + } else if (!u) { + noAccessProfile(); + } else { + validProfile(newValue); + } } - } - unsubscribe(); + unsubscribe(); + }); }); } } From 4dbabb714ba53559de47baa63584297651226aa0 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 12:16:33 -0500 Subject: [PATCH 06/74] Update esp-controller.js --- ecc/scripts/esp-controller.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 2bb99f38..60c7ddd5 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -665,14 +665,14 @@ export async function getEventsForUser() { if (!user) return []; - const events = await getEvents(); - if (!events.error) { + const resp = await getEvents(); + if (!resp.error) { const { role } = user; - if (role === 'admin') return events; - if (role === 'manager') return events.filter((e) => userHasAccessToBU(user, e.cloudType)); - if (role === 'creator') return events.filter((e) => userHasAccessToSerie(user, e.serieId)); - if (role === 'editor') return events.filter((e) => userHasAccessToEvent(user, e.eventId)); + if (role === 'admin') return resp.events; + if (role === 'manager') return resp.events.filter((e) => userHasAccessToBU(user, e.cloudType)); + if (role === 'creator') return resp.events.filter((e) => userHasAccessToSerie(user, e.serieId)); + if (role === 'editor') return resp.events.filter((e) => userHasAccessToEvent(user, e.eventId)); } return []; From ce92895695fedc226ea8bdb0bfe1803b3a808bbb Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 12:32:57 -0500 Subject: [PATCH 07/74] test 3 --- ecc/scripts/profile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 121446d5..30f63f85 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -85,19 +85,19 @@ export async function getUser() { return user; } -export async function userHasAccessToBU(user, bu) { +export function userHasAccessToBU(user, bu) { if (!user) return false; const businessUnits = user['business-units'].split(',').map((b) => b.trim()); return businessUnits.length === 0 || businessUnits.includes(bu); } -export async function userHasAccessToSerie(user, serieId) { +export function userHasAccessToSerie(user, serieId) { if (!user) return false; const series = user.series.split(',').map((b) => b.trim()); return series.length === 0 || series.includes(serieId); } -export async function userHasAccessToEvent(user, eventId) { +export function userHasAccessToEvent(user, eventId) { if (!user) return false; const events = user.events.split(',').map((b) => b.trim()); return events.length === 0 || events.includes(eventId); From 57dc2fd5b8ff960c466489e9635f73cc4191143f Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 12:34:44 -0500 Subject: [PATCH 08/74] Update ecc-dashboard.js --- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index bed14939..50a92468 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -316,9 +316,9 @@ function initMoreOptions(props, config, eventObj, row) { } const newJson = await getEventsForUser(); - props.data = newJson.events; - props.filteredData = newJson.events; - props.paginatedData = newJson.events; + props.data = newJson; + props.filteredData = newJson; + props.paginatedData = newJson; const modTimeHeader = props.el.querySelector('th.sortable.modificationTime'); if (modTimeHeader) { props.currentSort = { field: 'modificationTime', el: modTimeHeader }; @@ -646,16 +646,6 @@ function buildDashboardTable(props, config) { } } -async function getEventsArray() { - const resp = await getEventsForUser(); - - if (resp.error) { - return []; - } - - return resp.events; -} - function buildNoEventScreen(el, config) { el.classList.add('no-events'); @@ -680,7 +670,7 @@ async function buildDashboard(el, config) { currentSort: {}, }; - const data = await getEventsArray(); + const data = await getEventsForUser(); if (!data?.length) { buildNoEventScreen(el, config); } else { From ddab0b95b60ca527675550cddb3c294dd5f6ecf9 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:18:53 -0500 Subject: [PATCH 09/74] Update attendee-management-table.js --- .../attendee-management-table.js | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index 7707be54..720265c1 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -1,5 +1,5 @@ /* eslint-disable max-len */ -import { getAllEventAttendees, getEvents } from '../../scripts/esp-controller.js'; +import { getAllEventAttendees, getEventsForUser } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; import { getIcon, @@ -11,7 +11,7 @@ import { } from '../../scripts/utils.js'; import { SearchablePicker } from '../../components/searchable-picker/searchable-picker.js'; import { FilterMenu } from '../../components/filter-menu/filter-menu.js'; -import { initProfileLogicTree } from '../../scripts/profile.js'; +import { getUser, initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -509,16 +509,6 @@ function buildDashboardTable(props, config) { populateTable(props, config); } -async function getEventsArray() { - const resp = await getEvents(); - - if (resp.error) { - return []; - } - - return resp.events; -} - function renderTableLoadingOverlay(props) { const tableContainer = props.el.querySelector('.dashboard-table-container'); const loadingOverlay = createTag('div', { class: 'loading-overlay' }); @@ -625,7 +615,7 @@ async function buildDashboard(el, config) { createTag('div', { class: 'dashboard-body-container' }, '', { parent: mainContainer }); const uspEventId = new URLSearchParams(window.location.search).get('eventId'); - const events = await getEventsArray(); + const events = await getEventsForUser(); const props = { el, @@ -641,8 +631,13 @@ async function buildDashboard(el, config) { let data = []; if (props.currentEventId) { - const resp = await getAllEventAttendees(props.currentEventId); - if (resp && !resp.error) data = resp; + if (userHasAccessToEvent(await getUser(), props.currentEventId)) { + const resp = await getAllEventAttendees(props.currentEventId); + if (resp && !resp.error) data = resp; + } else { + buildNoAccessScreen(el); + return; + } } props.data = data; From 660b2eb7c1c520f0e4310a82a6c9e63686552ce3 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:29:46 -0500 Subject: [PATCH 10/74] Update form-handler.js --- ecc/blocks/form-handler/form-handler.js | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 1c60c7b6..378b5451 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -27,7 +27,7 @@ import PartnerSelector from '../../components/partner-selector/partner-selector. import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { CustomSearch } from '../../components/custom-search/custom-search.js'; -import { initProfileLogicTree } from '../../scripts/profile.js'; +import { initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); @@ -233,22 +233,28 @@ async function initComponents(props) { const eventId = urlParams.get('eventId'); if (eventId) { - setTimeout(() => { - if (!props.eventDataResp.eventId) { - const toastArea = props.el.querySelector('.toast-area'); - if (!toastArea) return; - - const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); - toast.addEventListener('close', () => { - toast.remove(); - }); - } - }, 5000); - - props.el.classList.add('disabled'); - const eventData = await getEvent(eventId); - props.eventDataResp = { ...props.eventDataResp, ...eventData }; - props.el.classList.remove('disabled'); + const user = await getUser(); + if (userHasAccessToEvent(user, eventId)) { + setTimeout(() => { + if (!props.eventDataResp.eventId) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); + } + }, 5000); + + props.el.classList.add('disabled'); + const eventData = await getEvent(eventId); + props.eventDataResp = { ...props.eventDataResp, ...eventData }; + props.el.classList.remove('disabled'); + } else { + buildNoAccessScreen(props.el); + return; + } } const componentPromises = VANILLA_COMPONENTS.map(async (comp) => { From 70e87fc1a12ae1ab8962c035685dcd97f0d10c0e Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:29:52 -0500 Subject: [PATCH 11/74] Update form-handler.js --- ecc/blocks/form-handler/form-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 378b5451..55b52e82 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -27,7 +27,7 @@ import PartnerSelector from '../../components/partner-selector/partner-selector. import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { CustomSearch } from '../../components/custom-search/custom-search.js'; -import { initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; +import { getUser, initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); From 7a02408be83193c98bd3686a2c20e1a31bc6f49e Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:31:08 -0500 Subject: [PATCH 12/74] Update form-handler.js --- ecc/blocks/form-handler/form-handler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 55b52e82..3446088b 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -239,20 +239,21 @@ async function initComponents(props) { if (!props.eventDataResp.eventId) { const toastArea = props.el.querySelector('.toast-area'); if (!toastArea) return; - + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); toast.addEventListener('close', () => { toast.remove(); }); } }, 5000); - + props.el.classList.add('disabled'); const eventData = await getEvent(eventId); props.eventDataResp = { ...props.eventDataResp, ...eventData }; props.el.classList.remove('disabled'); } else { buildNoAccessScreen(props.el); + props.el.classList.remove('loading'); return; } } From b959a6f2a732aa222582a8d242e03412b94acef7 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:32:41 -0500 Subject: [PATCH 13/74] Update utils.js --- ecc/scripts/utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index 8c10687d..14070406 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -117,10 +117,11 @@ export function buildNoAccessScreen(el) { const h1 = createTag('h1', {}, 'You do not have sufficient access to view.'); const area = createTag('div', { class: 'no-access-area' }); - const noAccessDescription = createTag('p', {}, 'An Adobe corporate account is required to access this feature.'); + const noAccessDescription = createTag('p', {}, 'If you have another authorized account, please sign in with that account to access this page.'); + const requestAccessButton = createTag('sp-button', { variant: 'cta', size: 'xl' }, 'Request Access'); el.append(h1, area); - area.append(getIcon('browser-access-forbidden-lg'), noAccessDescription); + area.append(getIcon('browser-access-forbidden-lg'), noAccessDescription, requestAccessButton); } export function querySelectorAllDeep(selector, root = document) { From 61f8dbef2d502524f2a3a47dd406e4da60b74cf0 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 13:33:19 -0500 Subject: [PATCH 14/74] Update utils.js --- ecc/scripts/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index 14070406..a7c32159 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -118,7 +118,7 @@ export function buildNoAccessScreen(el) { const h1 = createTag('h1', {}, 'You do not have sufficient access to view.'); const area = createTag('div', { class: 'no-access-area' }); const noAccessDescription = createTag('p', {}, 'If you have another authorized account, please sign in with that account to access this page.'); - const requestAccessButton = createTag('sp-button', { variant: 'cta', size: 'xl' }, 'Request Access'); + const requestAccessButton = createTag('a', { class: 'con-button primary' }, 'Request Access'); el.append(h1, area); area.append(getIcon('browser-access-forbidden-lg'), noAccessDescription, requestAccessButton); From 3f5ee576a91c5f6c9318d9a48f356639952f0e15 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 14:03:28 -0500 Subject: [PATCH 15/74] Update esp-controller.js --- ecc/scripts/esp-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 60c7ddd5..76e84b05 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -781,8 +781,8 @@ export async function getSeriesForUser() { const { role } = user; if (role === 'admin') return series; - if (role === 'manager') return series.filter((e) => userHasAccessToBU(user, e.cloudType)); - if (role === 'creator') return series.filter((e) => userHasAccessToSerie(user, e.serieId)); + if (role === 'manager') return series.filter((s) => userHasAccessToBU(user, s.cloudType)); + if (role === 'creator') return series.filter((s) => userHasAccessToSerie(user, s.serieId)); } return []; From 97dadb457c29d5fb0ce807a88bfa2135c24881f5 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 14:04:42 -0500 Subject: [PATCH 16/74] Update event-format-component-controller.js --- .../controllers/event-format-component-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/form-handler/controllers/event-format-component-controller.js index 8ac4697c..f0f9292c 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import { getSeries } from '../../../scripts/esp-controller.js'; +import { getSeries, getSeriesForUser } from '../../../scripts/esp-controller.js'; import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; import { LIBS } from '../../../scripts/scripts.js'; import { changeInputValue } from '../../../scripts/utils.js'; @@ -76,7 +76,7 @@ async function populateSeriesOptions(props, component) { const seriesSelect = component.querySelector('#series-select-input'); if (!seriesSelect) return; - const series = await getSeries(); + const series = await getSeriesForUser(); if (!series) { seriesSelect.pending = false; seriesSelect.disabled = true; From 4280c7b1034255f8a866908e1d99579796c2e12a Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 14:07:53 -0500 Subject: [PATCH 17/74] fix series access for editors --- .../controllers/event-format-component-controller.js | 2 +- ecc/scripts/esp-controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/form-handler/controllers/event-format-component-controller.js index f0f9292c..2bbddcdd 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import { getSeries, getSeriesForUser } from '../../../scripts/esp-controller.js'; +import { getSeriesForUser } from '../../../scripts/esp-controller.js'; import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; import { LIBS } from '../../../scripts/scripts.js'; import { changeInputValue } from '../../../scripts/utils.js'; diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 76e84b05..9bc52a30 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -782,7 +782,7 @@ export async function getSeriesForUser() { if (role === 'admin') return series; if (role === 'manager') return series.filter((s) => userHasAccessToBU(user, s.cloudType)); - if (role === 'creator') return series.filter((s) => userHasAccessToSerie(user, s.serieId)); + if (role === 'creator' || role === 'editor') return series.filter((s) => userHasAccessToSerie(user, s.serieId)); } return []; From c45967d02cc167662bb27c85cdd8e1df1396e8af Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 14:26:06 -0500 Subject: [PATCH 18/74] seriesId fix --- .../controllers/event-format-component-controller.js | 1 + ecc/scripts/esp-controller.js | 6 +++--- ecc/scripts/profile.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/form-handler/controllers/event-format-component-controller.js index 2bbddcdd..9de25533 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -77,6 +77,7 @@ async function populateSeriesOptions(props, component) { if (!seriesSelect) return; const series = await getSeriesForUser(); + console.log(series); if (!series) { seriesSelect.pending = false; seriesSelect.disabled = true; diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 9bc52a30..37cc4338 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -1,6 +1,6 @@ import { LIBS } from './scripts.js'; import { getEventServiceEnv, getSecret } from './utils.js'; -import { getUser, userHasAccessToBU, userHasAccessToEvent, userHasAccessToSerie } from './profile.js'; +import { getUser, userHasAccessToBU, userHasAccessToEvent, userHasAccessToSeries } from './profile.js'; const API_CONFIG = { esl: { @@ -671,7 +671,7 @@ export async function getEventsForUser() { if (role === 'admin') return resp.events; if (role === 'manager') return resp.events.filter((e) => userHasAccessToBU(user, e.cloudType)); - if (role === 'creator') return resp.events.filter((e) => userHasAccessToSerie(user, e.serieId)); + if (role === 'creator') return resp.events.filter((e) => userHasAccessToSeries(user, e.seriesId)); if (role === 'editor') return resp.events.filter((e) => userHasAccessToEvent(user, e.eventId)); } @@ -782,7 +782,7 @@ export async function getSeriesForUser() { if (role === 'admin') return series; if (role === 'manager') return series.filter((s) => userHasAccessToBU(user, s.cloudType)); - if (role === 'creator' || role === 'editor') return series.filter((s) => userHasAccessToSerie(user, s.serieId)); + if (role === 'creator' || role === 'editor') return series.filter((s) => userHasAccessToSeries(user, s.seriesId)); } return []; diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 30f63f85..a23725f6 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -91,10 +91,10 @@ export function userHasAccessToBU(user, bu) { return businessUnits.length === 0 || businessUnits.includes(bu); } -export function userHasAccessToSerie(user, serieId) { +export function userHasAccessToSeries(user, seriesId) { if (!user) return false; const series = user.series.split(',').map((b) => b.trim()); - return series.length === 0 || series.includes(serieId); + return series.length === 0 || series.includes(seriesId); } export function userHasAccessToEvent(user, eventId) { From fec139544fa247d3795f8ebed350512c6d60a08d Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 25 Oct 2024 14:27:13 -0500 Subject: [PATCH 19/74] Update event-format-component-controller.js --- .../controllers/event-format-component-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/form-handler/controllers/event-format-component-controller.js index 9de25533..2bbddcdd 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -77,7 +77,6 @@ async function populateSeriesOptions(props, component) { if (!seriesSelect) return; const series = await getSeriesForUser(); - console.log(series); if (!series) { seriesSelect.pending = false; seriesSelect.disabled = true; From 9ccfdabd19d4722cc096ceaa4d92990f94a7379b Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 21 Nov 2024 12:27:42 -0600 Subject: [PATCH 20/74] Refactored to modularize form components into higher level --- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 2 +- .../controller.js} | 0 .../controller.js} | 2 +- .../data-handler.js | 0 .../event-creation-form.css | 495 ++++++++ .../event-creation-form.js | 1073 +++++++++++++++++ .../controller.js} | 8 +- .../controller.js} | 8 +- .../event-materials-component.css | 96 -- .../event-materials-component.js | 80 -- .../controller.js} | 4 +- .../controller.js} | 0 .../checkbox-component-controller.js | 57 - ecc/blocks/form-handler/form-handler.js | 12 +- .../controller.js} | 6 +- .../controller.js} | 4 +- .../controller.js} | 4 +- .../controller.js} | 0 .../controller.js} | 0 .../controller.js} | 6 +- .../controller.js} | 8 +- ecc/scripts/event-data-handler.js | 166 +++ 22 files changed, 1766 insertions(+), 265 deletions(-) rename ecc/blocks/{form-handler/controllers/event-agenda-component-controller.js => event-agenda-component/controller.js} (100%) rename ecc/blocks/{form-handler/controllers/event-community-link-component-controller.js => event-community-link-component/controller.js} (95%) rename ecc/blocks/{form-handler => event-creation-form}/data-handler.js (100%) create mode 100644 ecc/blocks/event-creation-form/event-creation-form.css create mode 100644 ecc/blocks/event-creation-form/event-creation-form.js rename ecc/blocks/{form-handler/controllers/event-format-component-controller.js => event-format-component/controller.js} (94%) rename ecc/blocks/{form-handler/controllers/event-info-component-controller.js => event-info-component/controller.js} (98%) delete mode 100644 ecc/blocks/event-materials-component/event-materials-component.css delete mode 100644 ecc/blocks/event-materials-component/event-materials-component.js rename ecc/blocks/{form-handler/controllers/event-partners-component-controller.js => event-partners-component/controller.js} (97%) rename ecc/blocks/{form-handler/controllers/event-topics-component-controller.js => event-topics-component/controller.js} (100%) delete mode 100644 ecc/blocks/form-handler/controllers/checkbox-component-controller.js rename ecc/blocks/{form-handler/controllers/img-upload-component-controller.js => img-upload-component/controller.js} (97%) rename ecc/blocks/{form-handler/controllers/product-promotion-component-controller.js => product-promotion-component/controller.js} (96%) rename ecc/blocks/{form-handler/controllers/profile-component-controller.js => profile-component/controller.js} (97%) rename ecc/blocks/{form-handler/controllers/registration-details-component-controller.js => registration-details-component/controller.js} (100%) rename ecc/blocks/{form-handler/controllers/registration-fields-component-controller.js => registration-fields-component/controller.js} (100%) rename ecc/blocks/{form-handler/controllers/terms-conditions-component-controller.js => terms-conditions-component/controller.js} (94%) rename ecc/blocks/{form-handler/controllers/venue-info-component-controller.js => venue-info-component/controller.js} (97%) create mode 100644 ecc/scripts/event-data-handler.js diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index b46284b5..061c8232 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -15,7 +15,7 @@ import { signIn, getEventServiceEnv, } from '../../scripts/utils.js'; -import { quickFilter } from '../form-handler/data-handler.js'; +import { quickFilter } from '../../scripts/event-data-handler.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/form-handler/controllers/event-agenda-component-controller.js b/ecc/blocks/event-agenda-component/controller.js similarity index 100% rename from ecc/blocks/form-handler/controllers/event-agenda-component-controller.js rename to ecc/blocks/event-agenda-component/controller.js diff --git a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js b/ecc/blocks/event-community-link-component/controller.js similarity index 95% rename from ecc/blocks/form-handler/controllers/event-community-link-component-controller.js rename to ecc/blocks/event-community-link-component/controller.js index 2156bc87..645db3a8 100644 --- a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js +++ b/ecc/blocks/event-community-link-component/controller.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import { changeInputValue } from '../../../scripts/utils.js'; +import { changeInputValue } from '../../scripts/utils.js'; export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; diff --git a/ecc/blocks/form-handler/data-handler.js b/ecc/blocks/event-creation-form/data-handler.js similarity index 100% rename from ecc/blocks/form-handler/data-handler.js rename to ecc/blocks/event-creation-form/data-handler.js diff --git a/ecc/blocks/event-creation-form/event-creation-form.css b/ecc/blocks/event-creation-form/event-creation-form.css new file mode 100644 index 00000000..20c4591b --- /dev/null +++ b/ecc/blocks/event-creation-form/event-creation-form.css @@ -0,0 +1,495 @@ +.event-creation-form { + display: block; + padding: 0 40px; + + --mod-textfield-icon-size-invalid: 0; + --stroke-color-divider: #6E6E6E; + --color-red: #EB1000; + --mod-textfield-focus-indicator-width: 0; + --mod-textfield-text-color-disabled: #000; + --mod-textfield-border-color-invalid-default: #000; + --mod-textfield-border-color-invalid-focus: #000; + --mod-textfield-border-color-invalid-focus-hover: #000; + --mod-textfield-border-color-invalid-hover: #000; + --mod-textfield-border-color-invalid-keyboard-focus: #000; + --mod-textfield-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; + --mod-textfield-font-weight: 700; + --mod-textfield-spacing-block-start: 8px; +} + +.event-creation-form .loading-screen { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 20; + background-color: var(--color-white); + opacity: 1; +} + +.event-creation-form .loading-screen sp-field-label { + font-size: var(--type-body-s-size); +} + +.event-creation-form .img-upload-text p { + margin: 0; + font-size: var(--type-body-xs-size); + line-height: normal; +} + +.event-creation-form .main-frame sp-theme sp-underlay { + z-index: 2; +} + +.event-creation-form .main-frame sp-theme sp-underlay + sp-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 3; + background: var(--spectrum-gray-100); + min-width: 480px; +} + +.event-creation-form .main-frame sp-theme sp-underlay + sp-dialog h1 { + font-size: var(--type-heading-s-size); +} + +.event-creation-form .main-frame sp-theme sp-underlay + sp-dialog p { + font-size: var(--type-body-s-size); +} + +.event-creation-form .main-frame sp-theme sp-underlay + sp-dialog .button-container { + display: flex; + justify-content: flex-end; + gap: 16px; +} + +.event-creation-form .main-frame sp-theme sp-underlay:not([open]) + sp-dialog { + display: none; +} + +.event-creation-form.show-error { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.event-creation-form.show-dup-event-error #info-field-event-title { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.event-creation-form.show-dup-event-error #info-field-event-title sp-help-text { + display: flex; +} + +.event-creation-form .main-frame { + flex-grow: 1; + min-height: 100%; +} + +.event-creation-form .event-creation-form-ctas-panel { + position: sticky; + transform: translateX(-40px); + box-sizing: border-box; + bottom: 0; + padding: 16px 60px; + background-color: var(--color-red); + width: calc(100% + 80px); + z-index: 1; + display: flex; + justify-content: center; +} + +.event-creation-form .side-menu, +.event-creation-form .main-frame, +.event-creation-form .event-creation-form-ctas-panel, +.event-creation-form .loading-screen { + transition: opacity 0.5s; +} + +.event-creation-form.disabled .main-frame, +.event-creation-form.disabled .event-creation-form-ctas-panel { + pointer-events: none; +} + +.event-creation-form .side-menu { + transition: opacity 0.2s; +} + +.event-creation-form.loading div:first-of-type, +.event-creation-form.loading .side-menu, +.event-creation-form.loading .main-frame, +.event-creation-form.loading .event-creation-form-ctas-panel { + opacity: 0; +} + +.event-creation-form .side-menu button { + font-family: var(--body-font-family); +} + +.event-creation-form sp-textfield { + outline: none; +} + +.event-creation-form sp-textfield[quiet]:not(:read-only):focus { + outline: 1px var(--color-gray-500) solid; + border-radius: 4px; +} + +.event-creation-form > div.form-body { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 203px); +} + +.event-creation-form .side-menu.disabled { + opacity: 0.5; + pointer-events: none; +} + +.event-creation-form .side-menu h3 { + font-size: var(--type-body-xs-size); + color: var(--color-gray-400); + margin-bottom: 0; + margin-top: 24px; + padding: 0 24px; +} + +.event-creation-form .side-menu ul { + margin-top: 0; + padding: 0; +} + +.event-creation-form .side-menu ul li { + list-style: none; + font-size: var(--type-body-xs-size); + line-height: normal; + border-radius: 8px; + padding-left: 24px; + padding-right: 24px; +} + +.event-creation-form .event-creation-form-ctas-panel a { + font-size: var(--type-body-s-size); + display: inline-flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s, filter 0.2s; +} + +.event-creation-form .side-menu ul li a { + color: var(--color-black); +} + +.event-creation-form .side-menu ul li a, +.event-creation-form .side-menu ul li button { + text-align: left; + border: none; + background-color: transparent; + padding-left: 24px; + padding-right: 24px; + width: 100%; +} + +.event-creation-form .side-menu ul li:not(:has(ul)) { + padding-left: 0; + padding-right: 0; + margin: 12px 0; + display: flex; +} + +.event-creation-form .side-menu ul li ul { + margin-top: 12px; +} + +.event-creation-form .side-menu ul li ul li:not(:has(ul)) { + margin: 4px 0; +} + +.event-creation-form .side-menu ul li:not(:has(ul)) a, +.event-creation-form .side-menu ul li:not(:has(ul)) button { + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; +} + +.event-creation-form .side-menu ul li:not(:has(ul)):has(a):hover, +.event-creation-form .side-menu ul li:not(:has(ul)):has(a).active, +.event-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover, +.event-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active { + background-color: var(--color-red); + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.event-creation-form .side-menu ul li:not(:has(ul)):has(a):hover a, +.event-creation-form .side-menu ul li:not(:has(ul)):has(a).active a, +.event-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active button, +.event-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover button { + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.event-creation-form .side-menu .nav-item { + cursor: pointer; +} + +.event-creation-form .side-menu .nav-item:disabled { + pointer-events: none; + cursor: unset; +} + +.event-creation-form .side-menu .nav-item.disabled { + pointer-events: none; + cursor: unset; + opacity: 0.5; +} + +.event-creation-form .main-frame sp-theme { + min-height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.event-creation-form .main-frame .section .content { + max-width: none; +} + +.event-creation-form .main-frame .section:first-of-type .content { + margin: 16px 24px; + max-width: none; + display: grid; + align-items: center; + justify-content: space-between; + grid-template-columns: 1fr 1fr; +} + +.event-creation-form .main-frame .section:first-of-type .content p { + font-size: var(--type-body-xs-size); +} + +.event-creation-form .main-frame .section:first-of-type .content p:first-of-type { + display: flex; + flex-direction: row-reverse; +} + +.event-creation-form .form-component > div:first-of-type > div > h2 { + font-size: var(--type-heading-xl-size); + line-height: var(--type-heading-xl-lh); +} + +.event-creation-form .form-component > div:first-of-type > div > h2, +.event-creation-form .form-component > div:first-of-type > div > h3 { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + margin-bottom: 32px; +} + +.event-creation-form .main-frame .section:first-of-type h2 { + margin: 0; + font-weight: 900; + color: var(--color-red); +} + +.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper { + display: flex; + align-items: center; + gap: 16px; +} + +.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag { + padding: 0 8px; + background-color: var(--color-white); + border-radius: 4px; +} + +.event-creation-form .main-frame .section:not(:first-of-type) { + padding: 24px 56px; + border-radius: 10px; + margin: 24px; + box-shadow: 0 3px 6px 0 rgb(0 0 0 / 16%); + background-color: var(--color-white); +} + +.event-creation-form .fragment.hidden { + display: none; +} + +.event-creation-form .form-component { + padding: 32px 12px; +} + +.event-creation-form .form-component:not(:last-of-type):not(.no-divider) { + border-bottom: 3px solid var(--stroke-color-divider); +} + +.event-creation-form .event-heading-tooltip-wrapper .event-heading-tooltip-icon { + height: 16px; + width: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: help; +} + +.event-creation-form .section:not(:first-of-type) > div.content > h2, +.event-creation-form .section:not(:first-of-type) > div.content > h3 { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 0; + padding: 0 12px; +} + +.event-creation-form .form-component > div:first-of-type > div > h2 sp-action-button, +.event-creation-form .form-component > div:first-of-type > div > h3 sp-action-button, +.event-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button, +.event-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button { + padding: 0; + background: none; + border: none; + cursor: help +} + +.event-creation-form .form-component > div:first-of-type > div > h2 sp-action-button .icon-info, +.event-creation-form .form-component > div:first-of-type > div > h3 sp-action-button .icon-info, +.event-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button .icon-info, +.event-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button .icon-info { + display: block; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-panel-wrapper { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 1440px; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-panel-wrapper > div { + display: flex; + align-items: center; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-backward-wrapper .back-btn { + padding: 8px; + border: 2px solid var(--color-white); + border-radius: 24px; + cursor: pointer; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-backward-wrapper .back-btn .icon { + display: block; + height: 20px; + width: 20px; +} + +.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag .icon { + margin-right: 4px; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-forward-wrapper > div:first-of-type { + padding-right: 64px; + border-right: 1px solid var(--color-black); + margin-right: 104px; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-forward-wrapper .action-area { + display: flex; + align-items: center; + gap: 16px; +} + +.event-creation-form .event-creation-form-ctas-panel a.disabled, +.event-creation-form .event-creation-form-ctas-panel a.preview-not-ready, +.event-creation-form .event-creation-form-ctas-panel a.submitting { + pointer-events: none; + opacity: 0.5; +} + +.event-creation-form .event-creation-form-ctas-panel a.next-button { + background-color: var(--color-gray-800); + border-color: var(--color-gray-800); +} + +.event-creation-form .event-creation-form-ctas-panel a.next-button:hover { + background-color: var(--color-black) +} + +.event-creation-form .event-creation-form-ctas-panel a.fill { + background-color: var(--color-gray-200); + color: var(--color-black); + font-weight: 700; + border-radius: 20px; + line-height: 20px; + min-height: 21px; + padding: 7px 18px 8px; + border: 2px solid var(--color-white); +} + +.event-creation-form .event-creation-form-ctas-panel a.fill:hover { + text-decoration: none; + filter: invert(); +} + +.event-creation-form .event-creation-form-ctas-panel a.preview-btns svg { + height: 20px; + width: 28px; +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-panel-wrapper .con-button.outline { + color: var(--color-white); + border-color: var(--color-white); +} + +.event-creation-form .event-creation-form-ctas-panel .event-creation-form-panel-wrapper .con-button.outline:hover { + background-color: var(--color-white); + color: var(--color-red); +} + +.event-creation-form.hidden, +.event-creation-form .hidden { + display: none; +} + +.event-creation-form:not(.loading) .loading-screen { + opacity: 0; + z-index: -1; +} + +.event-creation-form .toast-parent { + position: absolute; + bottom: 100%; + right: 60px; +} + +.event-creation-form .toast-area { + margin-bottom: 16px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 16px; +} + +@media screen and (min-width: 900px) { + .event-creation-form > div.form-body { + flex-direction: row; + } + + .event-creation-form .main-frame { + max-width: var(--grid-container-width); + } +} diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js new file mode 100644 index 00000000..8b01f6ce --- /dev/null +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -0,0 +1,1073 @@ +import { LIBS } from '../../scripts/scripts.js'; +import { + getIcon, + buildNoAccessScreen, + generateToolTip, + camelToSentenceCase, + getEventPageHost, + signIn, + getEventServiceEnv, +} from '../../scripts/utils.js'; +import { + createEvent, + updateEvent, + publishEvent, + getEvent, +} from '../../scripts/esp-controller.js'; +import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; +import { Profile } from '../../components/profile/profile.js'; +import { Repeater } from '../../components/repeater/repeater.js'; +import AgendaFieldset from '../../components/agenda-fieldset/agenda-fieldset.js'; +import AgendaFieldsetGroup from '../../components/agenda-fieldset-group/agenda-fieldset-group.js'; +import { ProfileContainer } from '../../components/profile-container/profile-container.js'; +import { CustomTextfield } from '../../components/custom-textfield/custom-textfield.js'; +import ProductSelector from '../../components/product-selector/product-selector.js'; +import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; +import PartnerSelector from '../../components/partner-selector/partner-selector.js'; +import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; +import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; +import { CustomSearch } from '../../components/custom-search/custom-search.js'; +import { initProfileLogicTree } from '../../scripts/event-apis.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); +const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); + +// list of controllers for the handler to load +const VANILLA_COMPONENTS = [ + 'event-format', + 'event-info', + 'img-upload', + 'venue-info', + 'profile', + 'event-agenda', + 'event-community-link', + 'event-partners', + 'terms-conditions', + 'product-promotion', + 'event-topics', + 'registration-details', + 'registration-fields', +]; + +const INPUT_TYPES = [ + 'input[required]', + 'select[required]', + 'textarea[required]', + 'sp-textfield[required]', + 'sp-checkbox[required]', + 'sp-picker[required]', +]; + +const SPECTRUM_COMPONENTS = [ + 'theme', + 'textfield', + 'picker', + 'menu', + 'checkbox', + 'field-label', + 'divider', + 'button', + 'progress-circle', + 'overlay', + 'dialog', + 'button-group', + 'tooltip', + 'popover', + 'search', + 'toast', + 'icon', + 'action-button', + 'progress-circle', +]; + +export function buildErrorMessage(props, resp) { + if (!resp) return; + + const toastArea = resp.targetEl ? resp.targetEl.querySelector('.toast-area') : props.el.querySelector('.toast-area'); + + if (resp.error) { + const messages = []; + const errorBag = resp.error.errors; + const errorMessage = resp.error.message; + + if (errorBag) { + errorBag.forEach((error) => { + const errorPathSegments = error.path.split('/'); + const text = `${camelToSentenceCase(errorPathSegments[errorPathSegments.length - 1])} ${error.message}`; + messages.push(text); + }); + + messages.forEach((msg, i) => { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 + (i * 3000) }, msg, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + }); + } else if (errorMessage) { + if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Event update invalid, event has been modified since last fetch"}') { + const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The event has been updated by a different session since your last save.', { parent: toastArea }); + const url = new URL(window.location.href); + url.searchParams.set('eventId', getFilteredCachedResponse().eventId); + + createTag('sp-button', { + slot: 'action', + variant: 'overBackground', + href: `${url.toString()}`, + }, 'See the latest version', { parent: toast }); + + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } else { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 }, errorMessage, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } + } + } +} + +function replaceAnchorWithButton(anchor) { + if (!anchor || anchor.tagName !== 'A') { + return null; + } + + const attributes = {}; + for (let i = 0; i < anchor.attributes.length; i += 1) { + const attr = anchor.attributes[i]; + attributes[attr.name] = attr.value; + } + + const button = createTag('button', attributes, anchor.innerHTML); + + anchor.parentNode.replaceChild(button, anchor); + return button; +} + +function getCurrentFragment(props) { + const frags = props.el.querySelectorAll('.fragment'); + const currentFrag = frags[props.currentStep]; + return currentFrag; +} + +function validateRequiredFields(fields) { + return fields.length === 0 || Array.from(fields).every((f) => f.value && !f.invalid); +} + +function onStepValidate(props) { + return function updateCtaStatus() { + const currentFrag = getCurrentFragment(props); + const stepValid = validateRequiredFields(props[`required-fields-in-${currentFrag.id}`]); + const ctas = props.el.querySelectorAll('.event-creation-form-panel-wrapper a'); + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + ctas.forEach((cta) => { + if (cta.classList.contains('back-btn')) { + cta.classList.toggle('disabled', props.currentStep === 0); + } else { + cta.classList.toggle('disabled', !stepValid); + } + }); + + sideNavs.forEach((nav, i) => { + if (i !== props.currentStep) { + nav.disabled = !stepValid; + } + }); + }; +} + +function initRequiredFieldsValidation(props) { + const currentFrag = getCurrentFragment(props); + + const inputValidationCB = onStepValidate(props); + props[`required-fields-in-${currentFrag.id}`].forEach((field) => { + field.removeEventListener('change', inputValidationCB); + field.addEventListener('change', inputValidationCB, { bubbles: true }); + }); + + inputValidationCB(); +} + +function enableSideNavForEditFlow(props) { + const frags = props.el.querySelectorAll('.fragment'); + const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component:not(.event-agenda-component)')) + .every((fc) => fc.classList.contains('prefilled')); + + if (!completeFirstStep) return; + + frags.forEach((frag, i) => { + const prefilledOtherSteps = i !== 0 && frag.querySelector('.form-component.prefilled'); + + if (completeFirstStep || prefilledOtherSteps) { + props.farthestStep = Math.max(props.farthestStep, i); + } + }); + + initRequiredFieldsValidation(props); +} + +function initCustomLitComponents() { + customElements.define('image-dropzone', ImageDropzone); + customElements.define('profile-ui', Profile); + customElements.define('repeater-element', Repeater); + customElements.define('partner-selector', PartnerSelector); + customElements.define('partner-selector-group', PartnerSelectorGroup); + customElements.define('agenda-fieldset', AgendaFieldset); + customElements.define('agenda-fieldset-group', AgendaFieldsetGroup); + customElements.define('product-selector', ProductSelector); + customElements.define('product-selector-group', ProductSelectorGroup); + customElements.define('profile-container', ProfileContainer); + customElements.define('custom-textfield', CustomTextfield); + customElements.define('custom-search', CustomSearch); +} + +async function loadEventData(props) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const eventId = urlParams.get('eventId'); + + if (eventId) { + setTimeout(() => { + if (!props.eventDataResp.eventId) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); + } + }, 5000); + + props.el.classList.add('disabled'); + const eventData = await getEvent(eventId); + props.eventDataResp = { ...props.eventDataResp, ...eventData }; + props.el.classList.remove('disabled'); + } +} + +async function initComponents(props) { + initCustomLitComponents(); + + const componentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents?.length) return; + + const componentInitPromises = Array.from(mappedComponents).map(async (component) => { + const { default: initComponent } = await import(`../${comp}-component/controller.js`); + await initComponent(component, props); + }); + + await Promise.all(componentInitPromises); + }); + + await Promise.all(componentPromises); +} + +async function gatherValues(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onSubmit } = await import(`../${comp}-component/controller.js`); + return onSubmit(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function handleEventUpdate(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onEventUpdate } = await import(`../${comp}-component/controller.js`); + return onEventUpdate(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnPayloadChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onPayloadUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onPayloadUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnRespChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onRespUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onRespUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +function decorateForm(el) { + const ctaRow = el.querySelector(':scope > div:last-of-type'); + const formBodyRow = el.querySelector(':scope > div:first-of-type'); + + if (ctaRow) { + const toastParent = createTag('sp-theme', { class: 'toast-parent', color: 'light', scale: 'medium' }, '', { parent: ctaRow }); + createTag('div', { class: 'toast-area' }, '', { parent: toastParent }); + } + + if (!formBodyRow) return; + + formBodyRow.classList.add('form-body'); + + const app = createTag('sp-theme', { color: 'light', scale: 'medium', id: 'form-app' }); + createTag('sp-underlay', {}, '', { parent: app }); + createTag('sp-dialog', { size: 's' }, '', { parent: app }); + const form = createTag('form', {}, '', { parent: app }); + const formDivs = el.querySelectorAll('.fragment'); + + if (!formDivs.length) { + el.remove(); + return; + } + + formDivs.forEach((formDiv) => { + formDiv.parentElement.parentElement.replaceChild(app, formDiv.parentElement); + form.append(formDiv.parentElement); + }); + + const cols = formBodyRow.querySelectorAll(':scope > div'); + + cols.forEach((col, i) => { + if (i === 0) { + col.classList.add('side-menu'); + const navItems = col.querySelectorAll('a[href*="#"]'); + navItems.forEach((nav, index) => { + const btn = replaceAnchorWithButton(nav); + btn.classList.add('nav-item'); + + if (index !== 0) { + btn.disabled = true; + } else { + btn.closest('li')?.classList.add('active'); + } + }); + } + + if (i === 1) { + col.classList.add('main-frame'); + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + const fragPathSegments = frag.dataset.path.split('/'); + const fragId = `form-step-${fragPathSegments[fragPathSegments.length - 1]}`; + frag.id = fragId; + }); + } + }); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + }); +} + +function showSaveSuccessMessage(props, detail = { message: 'Edits saved successfully' }) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const previousMsgs = toastArea.querySelectorAll('.save-success-msg'); + + previousMsgs.forEach((msg) => { + msg.remove(); + }); + + const toast = createTag('sp-toast', { class: 'save-success-msg', open: true, variant: 'positive', timeout: 6000 }, detail.message || 'Edits saved successfully', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); +} + +function updateDashboardLink(props) { + // FIXME: presuming first link is dashboard link is not good. + if (!getFilteredCachedResponse().eventId) return; + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + if (!dashboardLink) return; + + const url = new URL(dashboardLink.href); + + if (url.searchParams.has('eventId')) return; + + url.searchParams.set('newEventId', getFilteredCachedResponse().eventId); + dashboardLink.href = url.toString(); +} + +async function saveEvent(props, toPublish = false) { + try { + await gatherValues(props); + } catch (e) { + return { error: { message: e.message } }; + } + + let resp; + + const onEventSave = async () => { + if (resp?.eventId) await handleEventUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } + }; + + if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { + resp = await createEvent(quickFilter(props.payload)); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + updateDashboardLink(props); + await onEventSave(); + } else if (props.currentStep <= props.maxStep && !toPublish) { + resp = await updateEvent( + getFilteredCachedResponse().eventId, + getJoinedData(), + ); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + await onEventSave(); + } else if (toPublish) { + resp = await publishEvent( + getFilteredCachedResponse().eventId, + getJoinedData(), + ); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + if (resp?.eventId) await handleEventUpdate(props); + } + + return resp; +} + +function updateSideNav(props) { + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + sideNavs.forEach((n, i) => { + n.closest('li')?.classList.remove('active'); + if (i <= props.farthestStep) n.disabled = false; + if (i === props.currentStep) n.closest('li')?.classList.add('active'); + }); +} + +function updateRequiredFields(props) { + const currentFrag = getCurrentFragment(props); + props[`required-fields-in-${currentFrag.id}`] = currentFrag.querySelectorAll(INPUT_TYPES.join()); +} + +function renderFormNavigation(props, prevStep, currentStep) { + const nextBtn = props.el.querySelector('.event-creation-form-ctas-panel .next-button'); + const backBtn = props.el.querySelector('.event-creation-form-ctas-panel .back-btn'); + const frags = props.el.querySelectorAll('.fragment'); + + frags[prevStep].classList.add('hidden'); + frags[currentStep].classList.remove('hidden'); + + if (props.currentStep === props.maxStep) { + if (props.eventDataResp.published) { + nextBtn.textContent = nextBtn.dataset.republishStateText; + } else { + nextBtn.textContent = nextBtn.dataset.finalStateText; + } + nextBtn.prepend(getIcon('golden-rocket')); + } else { + nextBtn.textContent = nextBtn.dataset.nextStateText; + nextBtn.append(getIcon('chev-right-white')); + } + + backBtn.classList.toggle('disabled', currentStep === 0); +} + +function navigateForm(props, stepIndex) { + const index = stepIndex || stepIndex === 0 ? stepIndex : props.currentStep + 1; + const frags = props.el.querySelectorAll('.fragment'); + + if (index >= frags.length || index < 0) return; + + props.currentStep = index; + props.farthestStep = Math.max(props.farthestStep, index); + + window.scrollTo(0, 0); + updateRequiredFields(props); +} + +function closeDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (underlay) underlay.open = false; + if (dialog) dialog.innerHTML = ''; +} + +function buildPreviewLoadingDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return null; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (!underlay || !dialog) return null; + + underlay.open = false; + dialog.innerHTML = ''; + + createTag('h1', { slot: 'heading' }, 'Generating your preview...', { parent: dialog }); + createTag('p', {}, 'This usually takes 10-30 seconds, but it might take up to 10 minutes in rare cases. Please wait, and the preview will open in a new tab when it’s ready.', { parent: dialog }); + createTag('p', {}, 'Note: Please make sure pop-up is allowed in your browser settings.', { parent: dialog }); + const style = createTag('style', {}, ` + @keyframes progress-bar-indeterminate { + 0% { + transform: translateX(-100%); + } + 50% { + transform: translateX(0%); + } + 100% { + transform: translateX(200%); + } + } + `); + + // Create the progress bar container + const progressBar = createTag('div', { + style: ` + position: relative; + width: 100%; + height: 8px; + background: #e6e6e6; + border-radius: 4px; + overflow: hidden; + margin-bottom: 1rem; + `, + }); + + // Create the progress bar indicator + const progressBarIndicator = createTag('div', { + style: ` + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: #1473e6; + transform: translateX(0); + animation: progress-bar-indeterminate 1.5s linear infinite; + `, + }); + + // Append the elements to the shadow root + progressBar.appendChild(progressBarIndicator); + dialog.appendChild(style); + dialog.appendChild(progressBar); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'Cancel', { parent: buttonContainer }); + + underlay.open = true; + + return dialog; +} + +function buildPreviewLoadingFailedDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (!underlay || !dialog) return; + + underlay.open = false; + dialog.innerHTML = ''; + + createTag('h1', { slot: 'heading' }, 'Preview generation failed.', { parent: dialog }); + createTag('p', {}, 'Your changes have been saved. Our system is working in the background to update the page.', { parent: dialog }); + const slackLink = createTag('a', { href: 'https://adobe.enterprise.slack.com/archives/C07KPJYA760' }, 'Slack'); + const emailLink = createTag('a', { href: 'mailto:Grp-acom-milo-events-support@adobe.com' }, 'Grp-acom-milo-events-support@adobe.com'); + createTag('p', {}, `Please try again later. If the issue persists, please feel free to contact us on ${slackLink.outerHTML} or email ${emailLink.outerHTML}`, { parent: dialog }); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + const cancelButton = createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'OK', { parent: buttonContainer }); + + underlay.open = true; + + cancelButton.addEventListener('click', () => { + closeDialog(props); + dialog.innerHTML = ''; + }); +} + +async function getNonProdPreviewDataById(props) { + if (!props.eventDataResp) return null; + + const { eventId } = props.eventDataResp; + + if (!eventId) return null; + + const esEnv = getEventServiceEnv(); + const resp = await fetch(`${getEventPageHost()}/events/default/${esEnv === 'prod' ? '' : `${esEnv}/`}metadata-preview.json`); + if (resp.ok) { + const json = await resp.json(); + const pageData = json.data.find((d) => d['event-id'] === eventId); + + if (pageData) return pageData; + + window.lana?.log('Failed to find non-prod metadata for current page'); + return null; + } + + window.lana?.log('Failed to fetch non-prod metadata:', resp); + return null; +} + +async function validatePreview(props, oldResp, cta) { + let retryCount = 0; + + const currentData = { ...props.eventDataResp }; + const oldData = { ...oldResp }; + + if (!hasContentChanged(currentData, oldData) || !Object.keys(oldData).length) { + window.open(cta.href); + return Promise.resolve(); + } + + const modificationTimeMatch = (metadataObj) => { + const metadataModTimestamp = new Date(metadataObj['modification-time']).getTime(); + return metadataModTimestamp === props.eventDataResp.modificationTime; + }; + + return new Promise((resolve) => { + const interval = setInterval(async () => { + try { + retryCount += 1; + const metadataJson = await getNonProdPreviewDataById(props); + + if (metadataJson && modificationTimeMatch(metadataJson)) { + clearInterval(interval); + closeDialog(props); + window.open(cta.href); + resolve(); + } else if (retryCount >= 30) { + clearInterval(interval); + buildPreviewLoadingFailedDialog(props); + window.lana?.log('Error: Failed to match metadata after 30 retries'); + resolve(); + } + } catch (error) { + window.lana?.log('Error in interval fetch:', error); + clearInterval(interval); + resolve(); + } + }, Math.floor(Math.random() * (2000 - 1000 + 1)) + 1000); + + const dialog = buildPreviewLoadingDialog(props, interval); + + if (dialog) { + const cancelButton = dialog.querySelector('#cancel-preview'); + cancelButton.addEventListener('click', () => { + closeDialog(props); + if (interval) clearInterval(interval); + resolve(); + }); + } + }); +} + +function initFormCtas(props) { + const ctaRow = props.el.querySelector(':scope > div:last-of-type'); + decorateButtons(ctaRow, 'button-l'); + const ctas = ctaRow.querySelectorAll('a'); + ctaRow.classList.add('event-creation-form-ctas-panel'); + + const forwardActionsWrappers = ctaRow.querySelectorAll(':scope > div'); + + const panelWrapper = createTag('div', { class: 'event-creation-form-panel-wrapper' }, '', { parent: ctaRow }); + const backwardWrapper = createTag('div', { class: 'event-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); + const forwardWrapper = createTag('div', { class: 'event-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); + + forwardActionsWrappers.forEach((w) => { + w.classList.add('action-area'); + forwardWrapper.append(w); + }); + + const backBtn = createTag('a', { class: 'back-btn' }, getIcon('chev-left-white')); + + backwardWrapper.append(backBtn); + + const toggleBtnsSubmittingState = (submitting) => { + [...ctas, backBtn].forEach((c) => { + c.classList.toggle('submitting', submitting); + }); + }; + + let oldResp = { ...props.eventDataResp }; + ctas.forEach((cta) => { + if (cta.href) { + const ctaUrl = new URL(cta.href); + + if (['#pre-event', '#post-event'].includes(ctaUrl.hash)) { + cta.classList.add('fill', 'preview-btns', 'preview-not-ready', ctaUrl.hash.replace('#', '')); + cta.addEventListener('click', async (e) => { + e.preventDefault(); + toggleBtnsSubmittingState(true); + if (cta.classList.contains('preview-not-ready')) return; + validatePreview(props, oldResp, cta).then(() => { + toggleBtnsSubmittingState(false); + }); + }); + } + + if (['#save', '#next'].includes(ctaUrl.hash)) { + if (ctaUrl.hash === '#next') { + cta.classList.add('next-button'); + const [nextStateText, finalStateText, doneStateText, republishStateText] = cta.textContent.split('||'); + + cta.textContent = nextStateText; + cta.append(getIcon('chev-right-white')); + cta.dataset.nextStateText = nextStateText; + cta.dataset.finalStateText = finalStateText; + cta.dataset.doneStateText = doneStateText; + cta.dataset.republishStateText = republishStateText; + } + + cta.addEventListener('click', async (e) => { + e.preventDefault(); + toggleBtnsSubmittingState(true); + + if (ctaUrl.hash === '#next') { + let resp; + if (props.currentStep === props.maxStep) { + oldResp = { ...props.eventDataResp }; + resp = await saveEvent(props, true); + } else { + oldResp = { ...props.eventDataResp }; + resp = await saveEvent(props); + } + + if (resp?.error) { + buildErrorMessage(props, resp); + } else if (props.currentStep === props.maxStep) { + const toastArea = props.el.querySelector('.toast-area'); + cta.textContent = cta.dataset.doneStateText; + cta.classList.add('disabled'); + + if (toastArea) { + const toast = createTag('sp-toast', { open: true, variant: 'positive' }, 'Success! This event has been published.', { parent: toastArea }); + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + createTag( + 'sp-button', + { + slot: 'action', + variant: 'overBackground', + treatment: 'outline', + href: dashboardLink.href, + }, + 'Go to dashboard', + { parent: toast }, + ); + + toast.addEventListener('close', () => { + toast.remove(); + }); + } + } else { + navigateForm(props); + } + } else { + oldResp = { ...props.eventDataResp }; + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } + } + + toggleBtnsSubmittingState(false); + }); + } + } + }); + + backBtn.addEventListener('click', async () => { + toggleBtnsSubmittingState(true); + oldResp = { ...props.eventDataResp }; + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } else { + props.currentStep -= 1; + } + + toggleBtnsSubmittingState(false); + }); +} + +function updateCtas(props) { + const formCtas = props.el.querySelectorAll('.event-creation-form-ctas-panel a'); + const { eventDataResp } = props; + + formCtas.forEach((a) => { + if (a.classList.contains('preview-btns')) { + const testTime = a.classList.contains('pre-event') ? +props.eventDataResp.localEndTimeMillis - 10 : +props.eventDataResp.localEndTimeMillis + 10; + if (eventDataResp.detailPagePath) { + a.href = `${getEventPageHost()}${eventDataResp.detailPagePath}?previewMode=true&cachebuster=${Date.now()}&timing=${testTime}`; + a.classList.remove('preview-not-ready'); + } + } + + if (a.classList.contains('next-button')) { + if (props.currentStep === props.maxStep) { + if (props.eventDataResp.published) { + a.textContent = a.dataset.republishStateText; + } else { + a.textContent = a.dataset.finalStateText; + } + a.prepend(getIcon('golden-rocket')); + } else { + a.textContent = a.dataset.nextStateText; + a.append(getIcon('chev-right-white')); + } + } + }); +} + +function initNavigation(props) { + const frags = props.el.querySelectorAll('.fragment'); + const sideMenu = props.el.querySelector('.side-menu'); + const navItems = sideMenu.querySelectorAll('.nav-item'); + + frags.forEach((frag, i) => { + if (i !== 0) { + frag.classList.add('hidden'); + } + }); + + navItems.forEach((nav, i) => { + nav.addEventListener('click', async () => { + if (nav.closest('li').classList.contains('active')) return; + if (!nav.disabled && !sideMenu.classList.contains('disabled')) { + sideMenu.classList.add('disabled'); + + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } else { + navigateForm(props, i); + } + + sideMenu.classList.remove('disabled'); + } + }); + }); +} + +function initDeepLink(props) { + const { hash } = window.location; + + if (hash) { + const frags = props.el.querySelectorAll('.fragment'); + + const targetFragindex = Array.from(frags).findIndex((frag) => `#${frag.id}` === hash); + + if (targetFragindex && targetFragindex <= props.farthestStep) { + navigateForm(props, targetFragindex); + } + } +} + +function updateStatusTag(props) { + const { eventDataResp } = props; + + if (eventDataResp?.published === undefined) return; + + const currentFragment = getCurrentFragment(props); + + const headingSection = currentFragment.querySelector(':scope > .section:first-child'); + + const eixstingStatusTag = headingSection.querySelector('.event-status-tag'); + if (eixstingStatusTag) eixstingStatusTag.remove(); + + const heading = headingSection.querySelector('h2', 'h3', 'h3', 'h4'); + const headingWrapper = createTag('div', { class: 'step-heading-wrapper' }); + const dot = eventDataResp.published ? getIcon('dot-purple') : getIcon('dot-green'); + const text = eventDataResp.published ? 'Published' : 'Draft'; + const statusTag = createTag('span', { class: 'event-status-tag' }); + + statusTag.append(dot, text); + heading.parentElement?.replaceChild(headingWrapper, heading); + headingWrapper.append(heading, statusTag); +} + +async function buildECCForm(el) { + const props = { + el, + currentStep: 0, + farthestStep: 0, + maxStep: el.querySelectorAll('.fragment').length - 1, + payload: {}, + eventDataResp: {}, + }; + + const dataHandler = { + set(target, prop, value) { + const oldValue = target[prop]; + target[prop] = value; + + if (prop.startsWith('required-fields-in-')) { + initRequiredFieldsValidation(target); + } + + switch (prop) { + case 'currentStep': + { + renderFormNavigation(target, oldValue, value); + updateSideNav(target); + initRequiredFieldsValidation(target); + updateStatusTag(target); + break; + } + + case 'farthestStep': { + updateSideNav(target); + break; + } + + case 'payload': { + setPayloadCache(value); + updateComponentsOnPayloadChange(target); + initRequiredFieldsValidation(target); + break; + } + + case 'eventDataResp': { + setResponseCache(value); + updateComponentsOnRespChange(target); + updateCtas(target); + if (value.error) { + props.el.classList.add('show-error'); + } else { + props.el.classList.remove('show-error'); + } + break; + } + + default: + break; + } + + return true; + }, + }; + + const proxyProps = new Proxy(props, dataHandler); + + decorateForm(el); + + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + props[`required-fields-in-${frag.id}`] = []; + + frag.querySelectorAll(':scope > .section > .content').forEach((c) => { + generateToolTip(c); + }); + }); + + await loadEventData(proxyProps); + initFormCtas(proxyProps); + initNavigation(proxyProps); + await initComponents(proxyProps); + updateRequiredFields(proxyProps); + enableSideNavForEditFlow(proxyProps); + initDeepLink(proxyProps); + updateStatusTag(proxyProps); + + el.addEventListener('show-error-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + buildErrorMessage(proxyProps, e.detail); + }); + + el.addEventListener('show-success-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + showSaveSuccessMessage(proxyProps, e.detail); + }); +} + +function buildLoadingScreen(el) { + el.classList.add('loading'); + const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); + createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Adobe Event Creation Console form...', { parent: loadingScreen }); + + el.prepend(loadingScreen); +} + +export default async function init(el) { + buildLoadingScreen(el); + const miloLibs = LIBS; + const promises = Array.from(SPECTRUM_COMPONENTS).map(async (component) => { + await import(`${miloLibs}/features/spectrum-web-components/dist/${component}.js`); + }); + await Promise.all([ + import(`${miloLibs}/deps/lit-all.min.js`), + ...promises, + ]); + + const sp = new URLSearchParams(window.location.search); + const devToken = sp.get('devToken'); + if (devToken && getEventServiceEnv() === 'local') { + buildECCForm(el).then(() => { + el.classList.remove('loading'); + }); + return; + } + + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { + buildNoAccessScreen(el); + el.classList.remove('loading'); + }, + validProfile: () => { + buildECCForm(el).then(() => { + el.classList.remove('loading'); + }); + }, + }); +} diff --git a/ecc/blocks/form-handler/controllers/event-format-component-controller.js b/ecc/blocks/event-format-component/controller.js similarity index 94% rename from ecc/blocks/form-handler/controllers/event-format-component-controller.js rename to ecc/blocks/event-format-component/controller.js index 8ac4697c..8401ed9a 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/event-format-component/controller.js @@ -1,8 +1,8 @@ /* eslint-disable no-unused-vars */ -import { getSeries } from '../../../scripts/esp-controller.js'; -import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; -import { LIBS } from '../../../scripts/scripts.js'; -import { changeInputValue } from '../../../scripts/utils.js'; +import { getSeries } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { changeInputValue } from '../../scripts/utils.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/form-handler/controllers/event-info-component-controller.js b/ecc/blocks/event-info-component/controller.js similarity index 98% rename from ecc/blocks/form-handler/controllers/event-info-component-controller.js rename to ecc/blocks/event-info-component/controller.js index 1edd4ce4..40ef879d 100644 --- a/ecc/blocks/form-handler/controllers/event-info-component-controller.js +++ b/ecc/blocks/event-info-component/controller.js @@ -1,9 +1,9 @@ /* eslint-disable no-unused-vars */ /* eslint-disable no-use-before-define */ -import { getEvents } from '../../../scripts/esp-controller.js'; -import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; -import { LIBS } from '../../../scripts/scripts.js'; -import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../../scripts/utils.js'; +import { getEvents } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/event-materials-component/event-materials-component.css b/ecc/blocks/event-materials-component/event-materials-component.css deleted file mode 100644 index 1439fab0..00000000 --- a/ecc/blocks/event-materials-component/event-materials-component.css +++ /dev/null @@ -1,96 +0,0 @@ -.event-materials-component > div { - margin-bottom: 32px; -} - -.event-materials-component .image-dropzones { - display: grid; - gap: 16px; - margin-bottom: 24px; -} - -.event-materials-component sp-textfield { - width: 100%; -} - -.event-materials-component .material-file-input-wrapper { - border: 2px dashed var(--color-gray-400); - border-radius: 8px; - height: 124px; - width: 124px; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; - align-items: center; -} - -.event-materials-component .material-file-input-wrapper .preview-wrapper { - width: 100%; - height: 100%; - position: relative; - z-index: 1; -} - -.event-materials-component .material-file-input-wrapper .preview-wrapper .icon-delete { - position: absolute; - top: 8px; - right: 8px; - cursor: pointer; -} - -.event-materials-component .material-file-input-wrapper .preview-material-placeholder { - height: 100%; - overflow: hidden; - border-radius: 8px; - -} - -.event-materials-component .material-file-input-wrapper .preview-material-placeholder img { - height: 100%; - width: 100%; - object-fit: cover; -} - -.event-materials-component .material-file-input-wrapper label { - border-radius: 8px; - display: flex; - flex-direction: column; - box-sizing: border-box; - align-items: center; - padding: 16px; - height: 100%; - width: 100%; - line-height: var(--type-body-xxs-lh); -} - -.event-materials-component .material-file-input-wrapper label:hover { - background-color: var(--color-gray-100); -} - -.event-materials-component .material-file-input-wrapper label input.material-file-input { - display: none; -} - -.event-materials-component .material-file-input-wrapper label .icon { - width: 40px; - opacity: 0.5; -} - -.event-materials-component .material-file-input-wrapper label p { - margin: 0; - font-size: var(--type-body-xs-size);; -} - -.event-materials-component .material-file-input-wrapper .hidden { - display: none; -} - -.event-materials-component .attr-text { - text-align: right; -} - -@media (min-width: 900px) { - .event-materials-component .file-dropzones { - grid-template-columns: 1fr 1fr 1fr; - } -} diff --git a/ecc/blocks/event-materials-component/event-materials-component.js b/ecc/blocks/event-materials-component/event-materials-component.js deleted file mode 100644 index 99291fc9..00000000 --- a/ecc/blocks/event-materials-component/event-materials-component.js +++ /dev/null @@ -1,80 +0,0 @@ -import { LIBS } from '../../scripts/scripts.js'; -import { getIcon, generateToolTip } from '../../scripts/utils.js'; - -const { createTag } = await import(`${LIBS}/utils/utils.js`); - -async function decorateSWCTextField(row, extraOptions) { - row.classList.add('text-field-row'); - - const cols = row.querySelectorAll(':scope > div'); - if (!cols.length) return; - const [placeholderCol, maxLengthCol] = cols; - const text = placeholderCol.textContent.trim(); - - let maxCharNum; let - attrTextEl; - if (maxLengthCol) { - attrTextEl = createTag('div', { class: 'attr-text' }, maxLengthCol.textContent.trim()); - maxCharNum = maxLengthCol.querySelector('strong')?.textContent.trim(); - } - - const isRequired = attrTextEl?.textContent.trim().endsWith('*'); - - const input = createTag('sp-textfield', { ...extraOptions, class: 'text-input', placeholder: text }); - - if (isRequired) input.required = true; - - if (maxCharNum) input.setAttribute('maxlength', maxCharNum); - - const wrapper = createTag('div', { class: 'info-field-wrapper' }); - row.innerHTML = ''; - wrapper.append(input); - if (attrTextEl) wrapper.append(attrTextEl); - row.append(wrapper); -} - -function decorateFileDropzone(row) { - row.classList.add('file-dropzones'); - const cols = row.querySelectorAll(':scope > div'); - const dropzones = []; - - cols.forEach((c, i) => { - c.classList.add('file-dropzone'); - const text = c.textContent.trim(); - const existingFileInputs = document.querySelectorAll('.material-file-input'); - const inputId = `material-file-input-${existingFileInputs.length + i + 1}`; - const fileInput = createTag('input', { id: inputId, type: 'file', class: 'material-file-input' }); - const inputWrapper = createTag('div', { class: 'material-file-input-wrapper' }); - const inputLabel = createTag('label', { class: 'material-file-input-label' }); - - const previewWrapper = createTag('div', { class: 'preview-wrapper hidden' }); - const previewImg = createTag('div', { class: 'preview-img-placeholder' }); - const previewDeleteButton = getIcon('delete'); - - previewWrapper.append(previewImg, previewDeleteButton); - - inputWrapper.append(previewWrapper, inputLabel); - inputLabel.append(fileInput, getIcon('upload-cloud'), text); - dropzones.push(inputWrapper); - }); - - row.innerHTML = ''; - dropzones.forEach((dz) => { - row.append(dz); - }); -} - -export default function init(el) { - const existingFileInputs = document.querySelectorAll('.event-materials-component'); - const blockIndex = Array.from(existingFileInputs).findIndex((b) => b === el); - - el.classList.add('form-component'); - generateToolTip(el); - - const rows = el.querySelectorAll(':scope > div'); - rows.forEach(async (r, i) => { - if (i === 0) decorateFileDropzone(r); - if (i === 1) await decorateSWCTextField(r, { id: `event-material-url-${blockIndex}` }); - if (i === 2) await decorateSWCTextField(r, { id: `event-material-name-${blockIndex}`, quiet: true, size: 'xl' }); - }); -} diff --git a/ecc/blocks/form-handler/controllers/event-partners-component-controller.js b/ecc/blocks/event-partners-component/controller.js similarity index 97% rename from ecc/blocks/form-handler/controllers/event-partners-component-controller.js rename to ecc/blocks/event-partners-component/controller.js index a250fdeb..e2529c11 100644 --- a/ecc/blocks/form-handler/controllers/event-partners-component-controller.js +++ b/ecc/blocks/event-partners-component/controller.js @@ -6,8 +6,8 @@ import { getSponsors, removeSponsorFromEvent, updateSponsorInEvent, -} from '../../../scripts/esp-controller.js'; -import { getFilteredCachedResponse } from '../data-handler.js'; +} from '../../scripts/esp-controller.js'; +import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; let PARTNERS_SERIES_ID; diff --git a/ecc/blocks/form-handler/controllers/event-topics-component-controller.js b/ecc/blocks/event-topics-component/controller.js similarity index 100% rename from ecc/blocks/form-handler/controllers/event-topics-component-controller.js rename to ecc/blocks/event-topics-component/controller.js diff --git a/ecc/blocks/form-handler/controllers/checkbox-component-controller.js b/ecc/blocks/form-handler/controllers/checkbox-component-controller.js deleted file mode 100644 index 59674058..00000000 --- a/ecc/blocks/form-handler/controllers/checkbox-component-controller.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-unused-vars */ -export default function init(component, props) { - // TODO: init function and repopulate data from props if exists - const checkboxes = component.querySelectorAll('input[type="checkbox"]'); - const minReg = component.className.match(/min-(.*?)( |$)/); - const maxReg = component.className.match(/max-(.*?)( |$)/); - const required = !!minReg; - const min = minReg ? parseInt(minReg[1], 10) : 0; - const max = maxReg ? parseInt(maxReg[1], 10) : 0; - - const configs = { - required, - min, - max, - }; - - let boxesChecked = 0; - checkboxes.forEach((cb) => { - cb.addEventListener('change', () => { - if (cb.checked) { - boxesChecked += 1; - } else { - boxesChecked -= 1; - } - - checkboxes.forEach((c) => { - c.required = boxesChecked < configs.min; - }); - - if (!!configs.max && boxesChecked === configs.max) { - checkboxes.forEach((c) => { - if (!c.checked) c.disabled = true; - }); - } else { - checkboxes.forEach((c) => { - c.disabled = false; - }); - } - }); - }); -} - -export async function onPayloadUpdate(_component, _props) { - // Do nothing -} - -export async function onRespUpdate(_component, _props) { - // Do nothing -} - -export function onSubmit(component, props) { - // Do nothing -} - -export function onEventUpdate(component, props) { - // Do nothing -} diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 26f4715e..1e448296 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -25,7 +25,7 @@ import ProductSelector from '../../components/product-selector/product-selector. import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; -import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; +import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; import { CustomSearch } from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; @@ -259,7 +259,7 @@ async function initComponents(props) { if (!mappedComponents?.length) return; const componentInitPromises = Array.from(mappedComponents).map(async (component) => { - const { default: initComponent } = await import(`./controllers/${comp}-component-controller.js`); + const { default: initComponent } = await import(`../${comp}-component/controller.js`); await initComponent(component, props); }); @@ -275,7 +275,7 @@ async function gatherValues(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onSubmit } = await import(`./controllers/${comp}-component-controller.js`); + const { onSubmit } = await import(`../${comp}-component/controller.js`); return onSubmit(component, props); }); @@ -291,7 +291,7 @@ async function handleEventUpdate(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onEventUpdate } = await import(`./controllers/${comp}-component-controller.js`); + const { onEventUpdate } = await import(`../${comp}-component/controller.js`); return onEventUpdate(component, props); }); @@ -307,7 +307,7 @@ async function updateComponentsOnPayloadChange(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onPayloadUpdate } = await import(`./controllers/${comp}-component-controller.js`); + const { onPayloadUpdate } = await import(`../${comp}-component/controller.js`); const componentPayload = await onPayloadUpdate(component, props); return componentPayload; }); @@ -324,7 +324,7 @@ async function updateComponentsOnRespChange(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onRespUpdate } = await import(`./controllers/${comp}-component-controller.js`); + const { onRespUpdate } = await import(`../${comp}-component/controller.js`); const componentPayload = await onRespUpdate(component, props); return componentPayload; }); diff --git a/ecc/blocks/form-handler/controllers/img-upload-component-controller.js b/ecc/blocks/img-upload-component/controller.js similarity index 97% rename from ecc/blocks/form-handler/controllers/img-upload-component-controller.js rename to ecc/blocks/img-upload-component/controller.js index 9b2f1c47..56b0fcbc 100644 --- a/ecc/blocks/form-handler/controllers/img-upload-component-controller.js +++ b/ecc/blocks/img-upload-component/controller.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ -import { deleteImage, getEventImages, uploadImage } from '../../../scripts/esp-controller.js'; -import { LIBS } from '../../../scripts/scripts.js'; -import { getFilteredCachedResponse } from '../data-handler.js'; +import { deleteImage, getEventImages, uploadImage } from '../../scripts/esp-controller.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/form-handler/controllers/product-promotion-component-controller.js b/ecc/blocks/product-promotion-component/controller.js similarity index 96% rename from ecc/blocks/form-handler/controllers/product-promotion-component-controller.js rename to ecc/blocks/product-promotion-component/controller.js index ff1e3566..5822897e 100644 --- a/ecc/blocks/form-handler/controllers/product-promotion-component-controller.js +++ b/ecc/blocks/product-promotion-component/controller.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ -import { getCaasTags } from '../../../scripts/esp-controller.js'; -import { handlize } from '../../../scripts/utils.js'; +import { getCaasTags } from '../../scripts/esp-controller.js'; +import { handlize } from '../../scripts/utils.js'; export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; diff --git a/ecc/blocks/form-handler/controllers/profile-component-controller.js b/ecc/blocks/profile-component/controller.js similarity index 97% rename from ecc/blocks/form-handler/controllers/profile-component-controller.js rename to ecc/blocks/profile-component/controller.js index 2cf96fb2..846f839d 100644 --- a/ecc/blocks/form-handler/controllers/profile-component-controller.js +++ b/ecc/blocks/profile-component/controller.js @@ -5,8 +5,8 @@ import { updateSpeakerInEvent, removeSpeakerFromEvent, getEventSpeaker, -} from '../../../scripts/esp-controller.js'; -import { getFilteredCachedResponse } from '../data-handler.js'; +} from '../../scripts/esp-controller.js'; +import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; export async function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; diff --git a/ecc/blocks/form-handler/controllers/registration-details-component-controller.js b/ecc/blocks/registration-details-component/controller.js similarity index 100% rename from ecc/blocks/form-handler/controllers/registration-details-component-controller.js rename to ecc/blocks/registration-details-component/controller.js diff --git a/ecc/blocks/form-handler/controllers/registration-fields-component-controller.js b/ecc/blocks/registration-fields-component/controller.js similarity index 100% rename from ecc/blocks/form-handler/controllers/registration-fields-component-controller.js rename to ecc/blocks/registration-fields-component/controller.js diff --git a/ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js b/ecc/blocks/terms-conditions-component/controller.js similarity index 94% rename from ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js rename to ecc/blocks/terms-conditions-component/controller.js index 0d36d1f6..4fccb2d4 100644 --- a/ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js +++ b/ecc/blocks/terms-conditions-component/controller.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ -import { LIBS } from '../../../scripts/scripts.js'; -import HtmlSanitizer from '../../../scripts/deps/html-sanitizer.js'; -import { fetchThrottledMemoizedText, getEventServiceEnv } from '../../../scripts/utils.js'; +import { LIBS } from '../../scripts/scripts.js'; +import HtmlSanitizer from '../../scripts/deps/html-sanitizer.js'; +import { fetchThrottledMemoizedText, getEventServiceEnv } from '../../scripts/utils.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/form-handler/controllers/venue-info-component-controller.js b/ecc/blocks/venue-info-component/controller.js similarity index 97% rename from ecc/blocks/form-handler/controllers/venue-info-component-controller.js rename to ecc/blocks/venue-info-component/controller.js index 1d8a4947..482191c0 100644 --- a/ecc/blocks/form-handler/controllers/venue-info-component-controller.js +++ b/ecc/blocks/venue-info-component/controller.js @@ -1,8 +1,8 @@ /* eslint-disable no-unused-vars */ -import { createVenue, replaceVenue } from '../../../scripts/esp-controller.js'; -import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; -import { changeInputValue, getEventServiceEnv, getSecret } from '../../../scripts/utils.js'; -import { buildErrorMessage } from '../form-handler.js'; +import { createVenue, replaceVenue } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { changeInputValue, getEventServiceEnv, getSecret } from '../../scripts/utils.js'; +import { buildErrorMessage } from '../form-handler/form-handler.js'; function togglePrefillableFieldsHiddenState(component) { const address = component.querySelector('#venue-info-venue-address'); diff --git a/ecc/scripts/event-data-handler.js b/ecc/scripts/event-data-handler.js new file mode 100644 index 00000000..40dd55c2 --- /dev/null +++ b/ecc/scripts/event-data-handler.js @@ -0,0 +1,166 @@ +/* eslint-disable no-use-before-define */ +// FIXME: this whole data handler thing can be done better +let responseCache = {}; +let payloadCache = {}; + +const submissionFilter = [ + // from payload and response + 'agenda', + 'topics', + 'eventType', + 'cloudType', + 'seriesId', + 'templateId', + 'communityTopicUrl', + 'title', + 'description', + 'localStartDate', + 'localEndDate', + 'localStartTime', + 'localEndTime', + 'timezone', + 'showAgendaPostEvent', + 'showVenuePostEvent', + 'showVenueImage', + 'showSponsors', + 'rsvpFormFields', + 'relatedProducts', + 'rsvpDescription', + 'attendeeLimit', + 'allowWaitlisting', + 'hostEmail', + 'eventId', + 'published', + 'creationTime', + 'modificationTime', +]; + +function isValidAttribute(attr) { + return attr !== undefined && attr !== null; +} + +export function quickFilter(obj) { + const output = {}; + + submissionFilter.forEach((attr) => { + if (isValidAttribute(obj[attr])) { + output[attr] = obj[attr]; + } + }); + + return output; +} + +export function setPayloadCache(payload) { + if (!payload) return; + payloadCache = quickFilter(payload); +} + +export function getFilteredCachedPayload() { + return payloadCache; +} + +export function setResponseCache(response) { + if (!response) return; + responseCache = quickFilter(response); +} + +export function getFilteredCachedResponse() { + return responseCache; +} + +/** + * Recursively compares two values to determine if they are different. + * + * @param {*} value1 - The first value to compare. + * @param {*} value2 - The second value to compare. + * @returns {boolean} - Returns true if the values are different, otherwise false. + */ +export function compareObjects(value1, value2, lengthOnly = false) { + if ( + typeof value1 === 'object' + && value1 !== null + && !Array.isArray(value1) + && typeof value2 === 'object' + && value2 !== null + && !Array.isArray(value2) + ) { + if (hasContentChanged(value1, value2)) { + return true; + } + } else if (Array.isArray(value1) && Array.isArray(value2)) { + if (value1.length !== value2.length) { + // Change detected due to different array lengths + return true; + } + + if (!lengthOnly) { + for (let i = 0; i < value1.length; i += 1) { + if (compareObjects(value1[i], value2[i])) { + return true; + } + } + } + } else if (value1 !== value2) { + // Change detected + return true; + } + return false; +} + +/** + * Determines if the content of two objects has changed. + * + * @param {Object} oldData - The original object. + * @param {Object} newData - The updated object. + * @returns {boolean} - Returns true if content has changed, otherwise false. + * @throws {TypeError} - Throws error if inputs are not objects. + */ +export function hasContentChanged(oldData, newData) { + // Ensure both inputs are objects + if ( + typeof oldData !== 'object' + || oldData === null + || typeof newData !== 'object' + || newData === null + ) { + throw new TypeError('Both oldData and newData must be objects'); + } + + const ignoreList = [ + 'modificationTime', + 'status', + 'platform', + 'platformCode', + 'liveUpdate', + ]; + + // Checking keys counts + const oldDataKeys = Object.keys(oldData).filter((key) => !ignoreList.includes(key)); + const newDataKeys = Object.keys(newData).filter((key) => !ignoreList.includes(key)); + + if (oldDataKeys.length !== newDataKeys.length) { + // Change detected due to different key counts + return true; + } + + // Check for differences in the actual values + return oldDataKeys.some( + (key) => { + const lengthOnly = key === 'speakers' && !oldData[key].ordinal; + + return !ignoreList.includes(key) && compareObjects(oldData[key], newData[key], lengthOnly); + }, + ); +} + +export default function getJoinedData() { + const filteredResponse = getFilteredCachedResponse(); + const filteredPayload = getFilteredCachedPayload(); + + return { + ...filteredResponse, + ...filteredPayload, + modificationTime: filteredResponse.modificationTime, + }; +} From c97105667e181c0e3cf567f3a2f9b6977234798d Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 21 Nov 2024 15:01:25 -0600 Subject: [PATCH 21/74] sessionalize devToken --- .../attendee-management-table.js | 4 ++-- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 4 ++-- ecc/blocks/form-handler/form-handler.js | 4 ++-- ecc/scripts/esp-controller.js | 8 +++----- ecc/scripts/utils.js | 8 ++++++++ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index 3579aff4..f235d48f 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -8,6 +8,7 @@ import { readBlockConfig, signIn, getEventServiceEnv, + getDevToken, } from '../../scripts/utils.js'; import { SearchablePicker } from '../../components/searchable-picker/searchable-picker.js'; import { FilterMenu } from '../../components/filter-menu/filter-menu.js'; @@ -715,8 +716,7 @@ export default async function init(el) { el.innerHTML = ''; buildLoadingScreen(el); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { buildDashboard(el, config); return; diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index b46284b5..8f4dc728 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -14,6 +14,7 @@ import { readBlockConfig, signIn, getEventServiceEnv, + getDevToken, } from '../../scripts/utils.js'; import { quickFilter } from '../form-handler/data-handler.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; @@ -731,8 +732,7 @@ export default async function init(el) { el.innerHTML = ''; buildLoadingScreen(el); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { buildDashboard(el, config); return; diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 26f4715e..e15f7a7e 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -7,6 +7,7 @@ import { getEventPageHost, signIn, getEventServiceEnv, + getDevToken, } from '../../scripts/utils.js'; import { createEvent, @@ -1047,8 +1048,7 @@ export default async function init(el) { ...promises, ]); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { buildECCForm(el).then(() => { el.classList.remove('loading'); diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index a2af21f5..93bfc3a0 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -1,5 +1,5 @@ import { LIBS } from './scripts.js'; -import { getEventServiceEnv, getSecret } from './utils.js'; +import { getDevToken, getEventServiceEnv, getSecret } from './utils.js'; const API_CONFIG = { esl: { @@ -77,8 +77,7 @@ export async function constructRequestOptions(method, body = null) { ]); const headers = new Headers(); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); const authToken = devToken && getEventServiceEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; if (!authToken) window.lana?.log('Error: Failed to get Adobe IMS auth token'); @@ -112,8 +111,7 @@ export async function uploadImage(file, configs, tracker, imageId = null) { const requestId = await getUuid(new Date().getTime()); const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); const authToken = devToken && getEventServiceEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; let respJson = null; diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index 0463ae24..1d328bfd 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -4,6 +4,14 @@ const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); let secretCache = []; +export function getDevToken() { + const sp = new URLSearchParams(window.location.search); + const sessionDevToken = sessionStorage.getItem('devToken'); + const devToken = sessionDevToken || sp.get('devToken'); + + return devToken; +} + export function getEventServiceEnv() { const validEnvs = ['dev', 'stage', 'prod']; const { host, search } = window.location; From bcd4a53b5cac98966643f999bd9777a1eb365a79 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 22 Nov 2024 11:18:38 -0600 Subject: [PATCH 22/74] use draft page for LHS test because .page is guarded --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 57e803cf..b4e7f955 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,8 +3,8 @@ Describe your specific features or fixes and provide a preview link for the feat Resolves: [MWPW-NUMBER](https://jira.corp.adobe.com/browse/MWPW-NUMBER) Test URLs: -- Before: https://main--ecc-milo--adobecom.hlx.page/ -- After: https://--ecc-milo--adobecom.hlx.page/ +- Before: https://main--ecc-milo--adobecom.hlx.live/drafts/ +- After: https://--ecc-milo--adobecom.hlx.live/drafts/ To test the feature, please load up the branch locally and run it against your local ESP and ESL server. For more information on how to set up ESL and ESP locally, please refer to: https://wiki.corp.adobe.com/display/adobedotcom/Events+Milo+FE+Dev+Wiki#EventsMiloFEDevWiki-Localdevelopmentsetup From b47ef5ad3e9514ced116543eb39142cd7d22f197 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 22 Nov 2024 12:27:28 -0600 Subject: [PATCH 23/74] refactoring --- .../series-dashboard/series-dashboard.css | 334 +++++++++ .../series-dashboard/series-dashboard.js | 668 ++++++++++++++++++ ecc/icons/dot-gray.svg | 3 + ecc/scripts/esp-controller.js | 56 +- 4 files changed, 1054 insertions(+), 7 deletions(-) create mode 100644 ecc/blocks/series-dashboard/series-dashboard.css create mode 100644 ecc/blocks/series-dashboard/series-dashboard.js create mode 100644 ecc/icons/dot-gray.svg diff --git a/ecc/blocks/series-dashboard/series-dashboard.css b/ecc/blocks/series-dashboard/series-dashboard.css new file mode 100644 index 00000000..9c8adc7c --- /dev/null +++ b/ecc/blocks/series-dashboard/series-dashboard.css @@ -0,0 +1,334 @@ +.series-dashboard { + font-family: var(--body-font-family); + padding: 40px; + width: var(--grid-container-width); + margin: auto; +} + +.series-dashboard .loading-screen { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 20; + background: var(--color-white); +} + +.series-dashboard .loading-screen sp-field-label { + font-size: var(--type-body-s-size); +} + +.series-dashboard .dashboard-header { + display: flex; + flex-direction: column; + gap: 16px; + justify-content: space-between; + align-items: flex-start; +} + +.series-dashboard .dashboard-table-container { + max-width: 100%; +} + +.series-dashboard .pagination-container { + margin: 40px auto 0; + display: flex; + width: max-content; + font-size: var(--type-body-xs-size); +} + +.series-dashboard .loading-screen, +.series-dashboard .dashboard-header, +.series-dashboard .dashboard-table-container, +.series-dashboard .pagination-container { + transition: opacity 0.5s; +} + +.series-dashboard.loading .dashboard-header, +.series-dashboard.loading .dashboard-table-container, +.series-dashboard.loading .pagination-container { + opacity: 0; +} + +.series-dashboard:not(.loading) .loading-screen { + opacity: 0; + z-index: -1; +} + +.series-dashboard.no-data .no-data-area { + margin: 64px; + display: flex; + flex-direction: column; + align-items: center; +} + +.series-dashboard sp-theme sp-underlay + sp-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; + background: var(--spectrum-gray-100); + min-width: 480px; +} + +.series-dashboard sp-theme sp-underlay + sp-dialog h1 { + font-size: var(--type-heading-s-size); +} + +.series-dashboard sp-theme sp-underlay + sp-dialog p { + font-size: var(--type-body-s-size); +} + +.series-dashboard sp-theme sp-underlay + sp-dialog .button-container { + display: flex; + justify-content: flex-end; + gap: 16px; +} + +.series-dashboard sp-theme sp-underlay:not([open]) + sp-dialog { + display: none; +} + +.series-dashboard .dashboard-header-text { + display: flex; + align-items: flex-end; + gap: 16px; +} + +.series-dashboard .dashboard-header-text h1 { + margin: 0; +} + +.series-dashboard .dashboard-header-text p { + margin: 6px 0; +} + +.series-dashboard .dashboard-actions-container { + display: flex; + align-items: center; + gap: 16px; +} + +.series-dashboard .dashboard-actions-container .search-input-wrapper { + position: relative; +} + +.series-dashboard .dashboard-actions-container .search-input-wrapper img.icon-search { + position: absolute; + display: block; + top: 50%; + transform: translateY(-50%); + right: 10px; + height: 1rem; + width: 1rem; +} + +.series-dashboard .dashboard-actions-container input { + border-radius: 16px; + border: 2px solid var(--color-gray-500); + height: 24px; + padding: 0 16px; + width: 140px; +} + +.series-dashboard .dashboard-actions-container input:not(:placeholder-shown) + img.icon-search { + display: none; +} + +.series-dashboard table { + margin: auto; + border-collapse: collapse; +} + +.series-dashboard table .table-header-row { + height: 80px; + border-bottom: 2px solid var(--color-gray-600); +} + +.series-dashboard table .table-header-row th { + padding: 0 16px; + font-weight: 700; + text-align: left; + font-size: var(--type-body-xxs-size); + color: var(--spectrum-color-gray-500); + user-select: none; + width: 100px; + white-space: nowrap; +} + +.series-dashboard table .table-header-row th span { + white-space: nowrap; + width: 60px; +} + +.series-dashboard table .table-header-row th.sortable { + cursor: pointer; +} + +.series-dashboard table .table-header-row th.active { + color: var(--color-black); +} + +.series-dashboard table .table-header-row th .icon { + transform: translateY(4px); +} + +.series-dashboard table .table-header-row th:not(.active) .icon, +.series-dashboard table .table-header-row th .icon-chev-down, +.series-dashboard table .table-header-row th.desc-sort .icon-chev-up { + display: none; +} + +.series-dashboard table .table-header-row th.active.desc-sort .icon-chev-down { + display: inline-block; + +} + +.series-dashboard table .row { + height: 140px; + border-bottom: 2px solid var(--color-gray-300); + transition: background-color 1s; +} + +.series-dashboard table .no-search-results-row td { + padding: 40px 0; + text-align: center; +} + +.series-dashboard table .row.highlight { + background-color: #EAEAEA; +} + +.series-dashboard table .row .title-link { + font-weight: 700; + text-decoration: none; +} + +.series-dashboard table .row .thumbnail-container img { + display: block; + height: 90px; + width: 90px; + min-width: 90px; + object-fit: cover; + border-radius: 6px; + background-color: var(--color-gray-400); +} + +.series-dashboard table .row td { + padding: 16px; + position: relative; + font-size: var(--type-body-s-size); +} + +.series-dashboard table .row td:not(.thumbnail-container) { + padding: 24px 16px; + vertical-align: top; +} + +.series-dashboard table .row td:not(.thumbnail-container) .td-wrapper { + max-height: 80px; + overflow: hidden; + /* stylelint-disable-next-line value-no-vendor-prefix */ + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; +} + +.series-dashboard table .row td:not(.thumbnail-container) .td-wrapper a.disabled, +.series-dashboard table .row .dash-tool.disabled { + opacity: 0.5; + pointer-events: none; +} + +.series-dashboard table .row .icon-more-small-list { + cursor: pointer; + height: 24px; + width: 24px; + transition: opacity 0.3s; +} + +.series-dashboard table .row .icon-more-small-list:hover { + opacity: 0.8; +} + +.series-dashboard .row .dashboard-tool-box { + position: absolute; + display: flex; + flex-direction: column; + background-color: var(--color-white); + border-radius: 4px; + padding: 4px; + left: 20px; + z-index: 1; + box-shadow: 0 3px 6px 0 rgb(0 0 0 / 16%); + width: 182px; +} + +.series-dashboard .row .dashboard-tool-box a.dash-tool { + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + margin: 2px; + text-decoration: none; + color: var(--color-black); + font-size: var(--type-body-xxs-size); + border-radius: 8px; +} + +.series-dashboard .row .dashboard-tool-box a.dash-tool:hover { + background-color: var(--color-gray-200); +} + +.series-dashboard .pagination-container input { + padding: 4px 12px; + width: 16px; + margin-right: 4px; +} + +.series-dashboard .pagination-container img.icon { + cursor: pointer; + width: 24px; + padding: 0 16px; +} + +.series-dashboard .pagination-container img.icon.disabled { + opacity: 0.5; + pointer-events: none; +} + +.series-dashboard table .row .status img.icon { + margin-right: 8px; +} + +.series-dashboard .row .dashboard-tool-box a.dash-tool img.icon { + width: 16px; +} + +.series-dashboard .row.pending { + opacity: 0.5; + pointer-events: none; +} + +.series-dashboard sp-theme.toast-area { + position: fixed; + right: calc((100% - var(--grid-container-width)) / 2); + bottom: 80px; + display: flex; + flex-direction: column; + gap: 16px; + z-index: 9; +} + +@media screen and (min-width: 900px) { + .series-dashboard .dashboard-header { + flex-direction: row; + } +} diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js new file mode 100644 index 00000000..3ab6f738 --- /dev/null +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -0,0 +1,668 @@ +import { + createSeries, + getAllSeries, + publishSeries, + unpublishSeries, + archiveSeries, +} from '../../scripts/esp-controller.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { + getIcon, + buildNoAccessScreen, + getEventPageHost, + readBlockConfig, + signIn, + getEventServiceEnv, +} from '../../scripts/utils.js'; +import { initProfileLogicTree } from '../../scripts/event-apis.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); + +function showToast(props, msg, options = {}) { + const toastArea = props.el.querySelector('sp-theme.toast-area'); + const toast = createTag('sp-toast', { open: true, ...options }, msg, { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); +} + +function formatLocaleDate(string) { + const options = { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + + return new Date(string).toLocaleString('en-US', options); +} + +function highlightRow(row) { + if (!row) return; + + row.classList.add('highlight'); + + setTimeout(() => { + row.classList.remove('highlight'); + }, 1000); +} + +function updateDashboardData(newPayload, props) { + if (!newPayload) return; + + props.data = props.data.map((series) => { + if (series.seriesId === newPayload.seriesId) { + return newPayload; + } + return series; + }); + + props.filteredData = props.data; + props.paginatedData = props.data; +} + +function paginateData(props, config, page) { + const ps = +config['page-size']; + if (Number.isNaN(ps) || ps <= 0) { + window.lana?.log('error', 'Invalid page size'); + } + const start = (page - 1) * ps; + const end = Math.min(page * ps, props.filteredData.length); + + props.paginatedData = props.filteredData.slice(start, end); +} + +function sortData(props, config, options = {}) { + const { field, el } = props.currentSort; + + let sortAscending = true; + + if (el?.classList.contains('active')) { + if (options.resort) { + sortAscending = !el.classList.contains('desc-sort'); + } else { + sortAscending = el.classList.contains('desc-sort'); + } + el.classList.toggle('desc-sort', !sortAscending); + } else { + el?.classList.remove('desc-sort'); + } + + if (options.direction) { + sortAscending = options.direction === 'asc'; + el?.classList.toggle('desc-sort', !sortAscending); + } + + props.filteredData = props.filteredData.sort((a, b) => { + let valA; + let valB; + + if ((field === 'title')) { + valA = a[field]?.toLowerCase() || ''; + valB = b[field]?.toLowerCase() || ''; + return sortAscending ? valA.localeCompare(valB) : valB.localeCompare(valA); + } + + if (field === 'startDate' || field === 'modificationTime') { + valA = new Date(a[field]); + valB = new Date(b[field]); + return sortAscending ? valA - valB : valB - valA; + } + + if (field === 'venueName') { + valA = a.venue?.venueName?.toLowerCase() || ''; + valB = b.venue?.venueName?.toLowerCase() || ''; + return sortAscending ? valA.localeCompare(valB) : valB.localeCompare(valA); + } + + if (typeof a[field] === typeof b[field] && typeof a[field] === 'number') { + valA = a[field] || 0; + valB = b[field] || 0; + return sortAscending ? valA - valB : valB - valA; + } + + valA = a[field]?.toString().toLowerCase() || ''; + valB = b[field]?.toString().toLowerCase() || ''; + return sortAscending ? valA.localeCompare(valB) : valB.localeCompare(valA); + }); + + el?.parentNode.querySelectorAll('th').forEach((header) => { + if (header !== el) { + header.classList.remove('active'); + header.classList.remove('desc-sort'); + } + }); + + props.currentPage = 1; + paginateData(props, config, 1); + el?.classList.add('active'); +} + +function buildToastMsgWithEventTitle(seriesTitle, configValue) { + const msgTemplate = configValue instanceof Array ? configValue.join('
') : configValue; + return msgTemplate.replace(/\[\[(.*?)\]\]/g, seriesTitle); +} + +function initMoreOptions(props, config, seriesObj, row) { + const moreOptionsCell = row.querySelector('.option-col'); + const moreOptionIcon = moreOptionsCell.querySelector('.icon-more-small-list'); + + const buildTool = (parent, text, icon) => { + const tool = createTag('a', { class: 'dash-tool', href: '#' }, text, { parent }); + tool.prepend(getIcon(icon)); + return tool; + }; + + moreOptionIcon.addEventListener('click', () => { + const toolBox = createTag('div', { class: 'dashboard-tool-box' }); + + if (seriesObj.published) { + const unpub = buildTool(toolBox, 'Unpublish', 'publish-remove'); + unpub.addEventListener('click', async (e) => { + e.prseriesDefault(); + toolBox.remove(); + row.classList.add('pending'); + const resp = await unpublishSeries(seriesObj.seriesId, seriesObj); + updateDashboardData(resp, props); + + sortData(props, config, { resort: true }); + showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['unpublished-msg']), { variant: 'positive' }); + }); + } else { + const pub = buildTool(toolBox, 'Publish', 'publish-rocket'); + if (!seriesObj.detailPagePath) pub.classList.add('disabled'); + pub.addEventListener('click', async (e) => { + e.prseriesDefault(); + toolBox.remove(); + row.classList.add('pending'); + const resp = await publishSeries(seriesObj.seriesId, seriesObj); + updateDashboardData(resp, props); + + sortData(props, config, { resort: true }); + + showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['published-msg']), { variant: 'positive' }); + }); + } + + const previewPre = buildTool(toolBox, 'Preview pre-series', 'preview-eye'); + const previewPost = buildTool(toolBox, 'Preview post-series', 'preview-eye'); + const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); + const clone = buildTool(toolBox, 'Clone', 'clone'); + const deleteBtn = buildTool(toolBox, 'Delete', 'delete-wire-round'); + + if (seriesObj.detailPagePath) { + previewPre.href = (() => { + const url = new URL(`${getEventPageHost()}${seriesObj.detailPagePath}`); + url.searchParams.set('previewMode', 'true'); + url.searchParams.set('cachebuster', Date.now()); + url.searchParams.set('timing', +seriesObj.localEndTimeMillis - 10); + return url.toString(); + })(); + previewPre.target = '_blank'; + previewPost.href = (() => { + const url = new URL(`${getEventPageHost()}${seriesObj.detailPagePath}`); + url.searchParams.set('previewMode', 'true'); + url.searchParams.set('cachebuster', Date.now()); + url.searchParams.set('timing', +seriesObj.localEndTimeMillis + 10); + return url.toString(); + })(); + previewPost.target = '_blank'; + } else { + previewPre.classList.add('disabled'); + previewPost.classList.add('disabled'); + } + + // edit + const url = new URL(`${window.location.origin}${config['create-form-url']}`); + url.searchParams.set('seriesId', seriesObj.seriesId); + edit.href = url.toString(); + + // clone + clone.addEventListener('click', async (e) => { + e.prseriesDefault(); + const payload = { ...seriesObj }; + payload.title = `${seriesObj.title} - copy`; + toolBox.remove(); + row.classList.add('pending'); + const newEventJSON = await createSeries(payload); + + if (newEventJSON.error) { + row.classList.remove('pending'); + showToast(props, newEventJSON.error, { variant: 'negative' }); + return; + } + + const newJson = await getAllSeries(); + props.data = newJson.series; + props.filteredData = newJson.series; + props.paginatedData = newJson.series; + const modTimeHeader = props.el.querySelector('th.sortable.modificationTime'); + if (modTimeHeader) { + props.currentSort = { field: 'modificationTime', el: modTimeHeader }; + sortData(props, config, { direction: 'desc' }); + } + + const newRow = props.el.querySelector(`tr[data-id="${newEventJSON.seriesId}"]`); + highlightRow(newRow); + showToast(props, buildToastMsgWithEventTitle(newEventJSON.title, config['clone-toast-msg']), { variant: 'info' }); + }); + + // delete + deleteBtn.addEventListener('click', async (e) => { + e.prseriesDefault(); + + const spTheme = props.el.querySelector('sp-theme.toast-area'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + createTag('h1', { slot: 'heading' }, 'You are deleting this series.', { parent: dialog }); + createTag('p', {}, 'Are you sure you want to do this? This cannot be undone.', { parent: dialog }); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + const dialogDeleteBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to delete this series', { parent: buttonContainer }); + const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not delete', { parent: buttonContainer }); + + underlay.open = true; + + dialogDeleteBtn.addEventListener('click', async () => { + toolBox.remove(); + underlay.open = false; + dialog.innerHTML = ''; + row.classList.add('pending'); + const resp = await archiveSeries(seriesObj.seriesId); + + if (resp.error) { + row.classList.remove('pending'); + showToast(props, resp.error, { variant: 'negative' }); + return; + } + + const newJson = await getAllSeries(); + props.data = newJson.series; + props.filteredData = newJson.series; + props.paginatedData = newJson.series; + + sortData(props, config, { resort: true }); + showToast(props, config['delete-toast-msg']); + }); + + dialogCancelBtn.addEventListener('click', () => { + toolBox.remove(); + underlay.open = false; + dialog.innerHTML = ''; + }); + }); + + if (!moreOptionsCell.querySelector('.dashboard-tool-box')) { + moreOptionsCell.append(toolBox); + } + }); + + document.addEventListener('click', (e) => { + if (!moreOptionsCell.contains(e.target) || moreOptionsCell === e.target) { + const toolBox = moreOptionsCell.querySelector('.dashboard-tool-box'); + toolBox?.remove(); + } + }); +} + +function getCountryName(seriesObj) { + if (!seriesObj.venue) return ''; + + const { venue } = seriesObj; + return venue.country || ''; +} + +function buildStatusTag(series) { + const dot = series.published ? getIcon('dot-purple') : getIcon('dot-green'); + const text = series.published ? 'Published' : 'Draft'; + + const statusTag = createTag('div', { class: 'status' }); + statusTag.append(dot, text); + return statusTag; +} + +function buildEventTitleTag(config, seriesObj) { + const url = new URL(`${window.location.origin}${config['create-form-url']}`); + url.searchParams.set('seriesId', seriesObj.seriesId); + const seriesTitleTag = createTag('a', { class: 'title-link', href: url.toString() }, seriesObj.title); + return seriesTitleTag; +} + +// TODO: to retire +function buildVenueTag(seriesObj) { + const { venue } = seriesObj; + if (!venue) return null; + + const venueTag = createTag('span', { class: 'vanue-name' }, venue.venueName); + return venueTag; +} + +function buildRSVPTag(config, seriesObj) { + const text = `${seriesObj.attendeeCount} / ${seriesObj.attendeeLimit}`; + + const url = new URL(`${window.location.origin}${config['attendee-dashboard-url']}`); + url.searchParams.set('seriesId', seriesObj.seriesId); + + const rsvpTag = createTag('a', { class: 'rsvp-tag', href: url }, text); + return rsvpTag; +} + +async function populateRow(props, config, index) { + const series = props.paginatedData[index]; + const tBody = props.el.querySelector('table.dashboard-table tbody'); + const sp = new URLSearchParams(window.location.search); + + // TODO: build each column's element specifically rather than just text + const row = createTag('tr', { class: 'row', 'data-id': series.seriesId }, '', { parent: tBody }); + const titleCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildEventTitleTag(config, series))); + const statusCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildStatusTag(series))); + const startDateCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, formatLocaleDate(series.startDate))); + const modDateCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, formatLocaleDate(series.modificationTime))); + const venueCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildVenueTag(series))); + const geoCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, getCountryName(series))); + const externalEventId = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildRSVPTag(config, series))); + const moreOptionsCell = createTag('td', { class: 'option-col' }, createTag('div', { class: 'td-wrapper' }, getIcon('more-small-list'))); + + row.append( + titleCell, + statusCell, + startDateCell, + modDateCell, + venueCell, + geoCell, + externalEventId, + moreOptionsCell, + ); + + initMoreOptions(props, config, series, row); + + if (series.seriesId === sp.get('newEventId')) { + if (!props.el.classList.contains('toast-shown')) { + showToast(props, buildToastMsgWithEventTitle(series.title, config['new-toast-msg']), { variant: 'positive' }); + + props.el.classList.add('toast-shown'); + } + + if (props.el.querySelector('.new-confirmation-toast')?.open === true) highlightRow(row); + } +} + +function updatePaginationControl(pagination, currentPage, totalPages) { + const input = pagination.querySelector('input'); + input.value = currentPage; + const leftChevron = pagination.querySelector('.icon-chev-left'); + const rightChevron = pagination.querySelector('.icon-chev-right'); + leftChevron.classList.toggle('disabled', currentPage === 1); + rightChevron.classList.toggle('disabled', currentPage === totalPages); +} + +function decoratePagination(props, config) { + if (!props.filteredData.length) return; + + const totalPages = Math.ceil(props.filteredData.length / +config['page-size']); + const paginationContainer = createTag('div', { class: 'pagination-container' }); + const chevLeft = getIcon('chev-left'); + const chevRight = getIcon('chev-right'); + const paginationText = createTag('div', { class: 'pagination-text' }, `of ${totalPages} pages`); + const pageInput = createTag('input', { type: 'text', class: 'page-input' }); + + paginationText.prepend(pageInput); + paginationContainer.append(chevLeft, paginationText, chevRight); + + pageInput.addEventListener('keypress', (series) => { + if (series.key === 'Enter') { + let page = parseInt(pageInput.value, +config['page-size']); + if (page > totalPages) { + page = totalPages; + } else if (page < 1) { + page = 1; + } + + updatePaginationControl(paginationContainer, props.currentPage = page, totalPages); + paginateData(props, config, page); + } + }); + + chevLeft.addEventListener('click', () => { + if (props.currentPage > 1) { + updatePaginationControl(paginationContainer, props.currentPage -= 1, totalPages); + paginateData(props, config, props.currentPage); + } + }); + + chevRight.addEventListener('click', () => { + if (props.currentPage < totalPages) { + updatePaginationControl(paginationContainer, props.currentPage += 1, totalPages); + paginateData(props, config, props.currentPage); + } + }); + + props.el.append(paginationContainer); + updatePaginationControl(paginationContainer, props.currentPage, totalPages); +} + +function initSorting(props, config) { + const thead = props.el.querySelector('thead'); + const thRow = thead.querySelector('tr'); + + const headers = { + thumbnail: '', + title: 'EVENT NAME', + published: 'PUBLISH STATUS', + startDate: 'DATE RUN', + modificationTime: 'LAST MODIFIED', + venueName: 'VENUE NAME', + timezone: 'GEO', + attendeeCount: 'RSVP DATA', + manage: 'MANAGE', + }; + + Object.entries(headers).forEach(([key, val]) => { + const thText = createTag('span', {}, val); + const th = createTag('th', {}, thText, { parent: thRow }); + + if (['thumbnail', 'manage'].includes(key)) return; + + th.append(getIcon('chev-down'), getIcon('chev-up')); + th.classList.add('sortable', key); + th.addEventListener('click', () => { + if (!props.filteredData.length) return; + + thead.querySelectorAll('th').forEach((h) => { + if (th !== h) { + h.classList.remove('active'); + } + }); + th.classList.add('active'); + props.currentSort = { + el: th, + field: key, + }; + sortData(props, config); + }); + }); +} + +function buildNoSearchResultsScreen(el, config) { + const noSearchResultsRow = createTag('tr', { class: 'no-search-results-row' }); + const noSearchResultsCol = createTag('td', { colspan: '100%' }, getIcon('empty-dashboard'), { parent: noSearchResultsRow }); + createTag('h2', {}, config['no-search-results-heading'], { parent: noSearchResultsCol }); + createTag('p', {}, config['no-search-results-text'], { parent: noSearchResultsCol }); + + el.append(noSearchResultsRow); +} + +function populateTable(props, config) { + const tBody = props.el.querySelector('table.dashboard-table tbody'); + tBody.innerHTML = ''; + + if (!props.paginatedData.length) { + buildNoSearchResultsScreen(tBody, config); + } else { + const endOfPage = Math.min(+config['page-size'], props.paginatedData.length); + + for (let i = 0; i < endOfPage; i += 1) { + populateRow(props, config, i); + } + + props.el.querySelector('.pagination-container')?.remove(); + decoratePagination(props, config); + } +} + +function filterData(props, config, query) { + const q = query.toLowerCase(); + props.filteredData = props.data.filter((e) => e.title.toLowerCase().includes(q)); + props.currentPage = 1; + paginateData(props, config, 1); + sortData(props, config, { resort: true }); +} + +function buildDashboardHeader(props, config) { + const dashboardHeader = createTag('div', { class: 'dashboard-header' }); + const textContainer = createTag('div', { class: 'dashboard-header-text' }); + const actionsContainer = createTag('div', { class: 'dashboard-actions-container' }); + + createTag('h1', { class: 'dashboard-header-heading' }, 'All Events', { parent: textContainer }); + createTag('p', { class: 'dashboard-header-series-count' }, `(${props.data.length} series)`, { parent: textContainer }); + + const searchInputWrapper = createTag('div', { class: 'search-input-wrapper' }, '', { parent: actionsContainer }); + const searchInput = createTag('input', { type: 'text', placeholder: 'Search' }, '', { parent: searchInputWrapper }); + searchInputWrapper.append(getIcon('search')); + createTag('a', { class: 'con-button blue', href: config['create-form-url'] }, config['create-cta-text'], { parent: actionsContainer }); + searchInput.addEventListener('input', () => filterData(props, config, searchInput.value)); + + dashboardHeader.append(textContainer, actionsContainer); + props.el.prepend(dashboardHeader); +} + +function updateEventsCount(props) { + const seriesCount = props.el.querySelector('.dashboard-header-series-count'); + seriesCount.textContent = `(${props.data.length} series)`; +} + +function buildDashboardTable(props, config) { + const tableContainer = createTag('div', { class: 'dashboard-table-container' }, '', { parent: props.el }); + const table = createTag('table', { class: 'dashboard-table' }, '', { parent: tableContainer }); + const thead = createTag('thead', {}, '', { parent: table }); + createTag('tbody', {}, '', { parent: table }); + createTag('tr', { class: 'table-header-row' }, '', { parent: thead }); + initSorting(props, config); + populateTable(props, config); + + const usp = new URLSearchParams(window.location.search); + if (usp.get('newEventId')) { + const modTimeHeader = props.el.querySelector('th.sortable.modificationTime'); + if (modTimeHeader) { + props.currentSort = { field: 'modificationTime', el: modTimeHeader }; + sortData(props, config, { direction: 'desc' }); + } + } +} + +async function getSeriesArray() { + const resp = await getAllSeries(); + + if (resp.error) { + return []; + } + + return resp.series; +} + +function buildNoEventScreen(el, config) { + el.classList.add('no-data'); + + const h1 = createTag('h1', {}, 'All Events'); + const area = createTag('div', { class: 'no-data-area' }); + const noEventHeading = createTag('h2', {}, config['no-heading']); + const noEventDescription = createTag('p', {}, config['no-description']); + const cta = createTag('a', { class: 'con-button blue', href: config['create-form-url'] }, config['create-cta-text']); + + el.append(h1, area); + area.append(getIcon('empty-dashboard'), noEventHeading, noEventDescription, cta); +} + +async function buildDashboard(el, config) { + const spTheme = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'toast-area' }, '', { parent: el }); + createTag('sp-underlay', {}, '', { parent: spTheme }); + createTag('sp-dialog', { size: 's' }, '', { parent: spTheme }); + + const props = { + el, + currentPage: 1, + currentSort: {}, + }; + + const data = await getSeriesArray(); + + if (!data?.length) { + buildNoEventScreen(el, config); + } else { + props.data = data; + props.filteredData = [...data]; + props.paginatedData = [...data]; + + const dataHandler = { + set(target, prop, value, receiver) { + target[prop] = value; + populateTable(receiver, config); + updateEventsCount(receiver); + return true; + }, + }; + const proxyProps = new Proxy(props, dataHandler); + buildDashboardHeader(proxyProps, config); + buildDashboardTable(proxyProps, config); + } + + setTimeout(() => { + el.classList.remove('loading'); + }, 10); +} + +function buildLoadingScreen(el) { + el.classList.add('loading'); + const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); + createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Adobe Event Creation Console dashboard...', { parent: loadingScreen }); + + el.prepend(loadingScreen); +} + +export default async function init(el) { + const miloLibs = LIBS; + await Promise.all([ + import(`${miloLibs}/deps/lit-all.min.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/theme.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/toast.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/button.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/dialog.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/underlay.js`), + import(`${miloLibs}/features/spectrum-web-components/dist/progress-circle.js`), + ]); + + const config = readBlockConfig(el); + el.innerHTML = ''; + buildLoadingScreen(el); + + const sp = new URLSearchParams(window.location.search); + const devToken = sp.get('devToken'); + if (devToken && getEventServiceEnv() === 'dev') { + buildDashboard(el, config); + return; + } + + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { + buildNoAccessScreen(el); + }, + validProfile: () => { + buildDashboard(el, config); + }, + }); +} diff --git a/ecc/icons/dot-gray.svg b/ecc/icons/dot-gray.svg new file mode 100644 index 00000000..2052a137 --- /dev/null +++ b/ecc/icons/dot-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index db521b84..d4a4d661 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -848,23 +848,65 @@ export async function updateSeries(seriesData, seriesId) { } } -export async function deleteSeries(seriesId) { +export async function publishSeries(seriesId, seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const options = await constructRequestOptions('DELETE'); + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'published' }); + const options = await constructRequestOptions('PUT', raw); try { const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); if (!response.ok) { - const data = await response.json(); - window.lana?.log(`Failed to delete series ${seriesId}. Status:`, response.status, 'Error:', data); + window.lana?.log(`Failed to publish series ${seriesId}. Status:`, response.status, 'Error:', data); return { status: response.status, error: data }; } - // 204 no content. Return true if no error. - return true; + return data; + } catch (error) { + window.lana?.log(`Failed to publish series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function unpublishSeries(seriesId, seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'unpublished' }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to unpublish series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to unpublish series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function archiveSeries(seriesId, seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'archived' }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to archive series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; } catch (error) { - window.lana?.log(`Failed to delete series ${seriesId}. Error:`, error); + window.lana?.log(`Failed to archive series ${seriesId}. Error:`, error); return { status: 'Network Error', error: error.message }; } } From c606d37ce42bfe55188ce382fb4b0e3e0ec1b4cf Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 22 Nov 2024 12:33:08 -0600 Subject: [PATCH 24/74] Update series-dashboard.js --- ecc/blocks/series-dashboard/series-dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 3ab6f738..de85e76c 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -649,7 +649,7 @@ export default async function init(el) { const sp = new URLSearchParams(window.location.search); const devToken = sp.get('devToken'); - if (devToken && getEventServiceEnv() === 'dev') { + if (devToken && getEventServiceEnv() === 'local') { buildDashboard(el, config); return; } From 2db73928c181a9c5cb1d4331f360e2ca7dada050 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 22 Nov 2024 15:19:55 -0600 Subject: [PATCH 25/74] Dashboard prototype done --- .../series-dashboard/series-dashboard.css | 13 +- .../series-dashboard/series-dashboard.js | 194 ++++++++---------- ecc/icons/archive.svg | 6 + ecc/icons/version-history.svg | 7 + ecc/scripts/esp-controller.js | 2 +- 5 files changed, 98 insertions(+), 124 deletions(-) create mode 100644 ecc/icons/archive.svg create mode 100644 ecc/icons/version-history.svg diff --git a/ecc/blocks/series-dashboard/series-dashboard.css b/ecc/blocks/series-dashboard/series-dashboard.css index 9c8adc7c..0767b4b1 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.css +++ b/ecc/blocks/series-dashboard/series-dashboard.css @@ -142,6 +142,7 @@ } .series-dashboard table { + width: 100%; margin: auto; border-collapse: collapse; } @@ -156,15 +157,10 @@ font-weight: 700; text-align: left; font-size: var(--type-body-xxs-size); + line-height: var(--type-body-xxs-lh); color: var(--spectrum-color-gray-500); user-select: none; width: 100px; - white-space: nowrap; -} - -.series-dashboard table .table-header-row th span { - white-space: nowrap; - width: 60px; } .series-dashboard table .table-header-row th.sortable { @@ -187,11 +183,10 @@ .series-dashboard table .table-header-row th.active.desc-sort .icon-chev-down { display: inline-block; - } .series-dashboard table .row { - height: 140px; + height: 120px; border-bottom: 2px solid var(--color-gray-300); transition: background-color 1s; } @@ -205,7 +200,7 @@ background-color: #EAEAEA; } -.series-dashboard table .row .title-link { +.series-dashboard table .row .name-link { font-weight: 700; text-decoration: none; } diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index de85e76c..4ed20250 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -4,12 +4,12 @@ import { publishSeries, unpublishSeries, archiveSeries, + getEvents, } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; import { getIcon, buildNoAccessScreen, - getEventPageHost, readBlockConfig, signIn, getEventServiceEnv, @@ -72,6 +72,10 @@ function paginateData(props, config, page) { props.paginatedData = props.filteredData.slice(start, end); } +function getSeriesEvents(seriesId, events) { + return events.filter((e) => e.seriesId === seriesId); +} + function sortData(props, config, options = {}) { const { field, el } = props.currentSort; @@ -97,25 +101,25 @@ function sortData(props, config, options = {}) { let valA; let valB; - if ((field === 'title')) { + if ((field === 'seriesName')) { valA = a[field]?.toLowerCase() || ''; valB = b[field]?.toLowerCase() || ''; return sortAscending ? valA.localeCompare(valB) : valB.localeCompare(valA); } - if (field === 'startDate' || field === 'modificationTime') { + if (field === 'modificationTime') { valA = new Date(a[field]); valB = new Date(b[field]); return sortAscending ? valA - valB : valB - valA; } - if (field === 'venueName') { - valA = a.venue?.venueName?.toLowerCase() || ''; - valB = b.venue?.venueName?.toLowerCase() || ''; - return sortAscending ? valA.localeCompare(valB) : valB.localeCompare(valA); + if (field === 'eventsCount') { + valA = getSeriesEvents(a.seriesId, props.events).length; + valB = getSeriesEvents(b.seriesId, props.events).length; + return sortAscending ? valA - valB : valB - valA; } - if (typeof a[field] === typeof b[field] && typeof a[field] === 'number') { + if ((!Number.isNaN(+a[field]) && !Number.isNaN(+b[field]))) { valA = a[field] || 0; valB = b[field] || 0; return sortAscending ? valA - valB : valB - valA; @@ -158,8 +162,9 @@ function initMoreOptions(props, config, seriesObj, row) { if (seriesObj.published) { const unpub = buildTool(toolBox, 'Unpublish', 'publish-remove'); + if (seriesObj.status === 'archived') unpub.classList.add('disabled'); unpub.addEventListener('click', async (e) => { - e.prseriesDefault(); + e.preventDefault(); toolBox.remove(); row.classList.add('pending'); const resp = await unpublishSeries(seriesObj.seriesId, seriesObj); @@ -170,9 +175,9 @@ function initMoreOptions(props, config, seriesObj, row) { }); } else { const pub = buildTool(toolBox, 'Publish', 'publish-rocket'); - if (!seriesObj.detailPagePath) pub.classList.add('disabled'); + if (seriesObj.status === 'archived') pub.classList.add('disabled'); pub.addEventListener('click', async (e) => { - e.prseriesDefault(); + e.preventDefault(); toolBox.remove(); row.classList.add('pending'); const resp = await publishSeries(seriesObj.seriesId, seriesObj); @@ -184,33 +189,11 @@ function initMoreOptions(props, config, seriesObj, row) { }); } - const previewPre = buildTool(toolBox, 'Preview pre-series', 'preview-eye'); - const previewPost = buildTool(toolBox, 'Preview post-series', 'preview-eye'); + const viewTemplate = buildTool(toolBox, 'View Template', 'preview-eye'); const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); const clone = buildTool(toolBox, 'Clone', 'clone'); - const deleteBtn = buildTool(toolBox, 'Delete', 'delete-wire-round'); - - if (seriesObj.detailPagePath) { - previewPre.href = (() => { - const url = new URL(`${getEventPageHost()}${seriesObj.detailPagePath}`); - url.searchParams.set('previewMode', 'true'); - url.searchParams.set('cachebuster', Date.now()); - url.searchParams.set('timing', +seriesObj.localEndTimeMillis - 10); - return url.toString(); - })(); - previewPre.target = '_blank'; - previewPost.href = (() => { - const url = new URL(`${getEventPageHost()}${seriesObj.detailPagePath}`); - url.searchParams.set('previewMode', 'true'); - url.searchParams.set('cachebuster', Date.now()); - url.searchParams.set('timing', +seriesObj.localEndTimeMillis + 10); - return url.toString(); - })(); - previewPost.target = '_blank'; - } else { - previewPre.classList.add('disabled'); - previewPost.classList.add('disabled'); - } + const archive = buildTool(toolBox, 'Archive', 'archive'); + const verHistory = buildTool(toolBox, 'Version History', 'version-history'); // edit const url = new URL(`${window.location.origin}${config['create-form-url']}`); @@ -219,16 +202,16 @@ function initMoreOptions(props, config, seriesObj, row) { // clone clone.addEventListener('click', async (e) => { - e.prseriesDefault(); + e.preventDefault(); const payload = { ...seriesObj }; payload.title = `${seriesObj.title} - copy`; toolBox.remove(); row.classList.add('pending'); - const newEventJSON = await createSeries(payload); + const newSeriesObj = await createSeries(payload); - if (newEventJSON.error) { + if (newSeriesObj.error) { row.classList.remove('pending'); - showToast(props, newEventJSON.error, { variant: 'negative' }); + showToast(props, newSeriesObj.error, { variant: 'negative' }); return; } @@ -242,14 +225,14 @@ function initMoreOptions(props, config, seriesObj, row) { sortData(props, config, { direction: 'desc' }); } - const newRow = props.el.querySelector(`tr[data-id="${newEventJSON.seriesId}"]`); + const newRow = props.el.querySelector(`tr[data-id="${newSeriesObj.seriesId}"]`); highlightRow(newRow); - showToast(props, buildToastMsgWithEventTitle(newEventJSON.title, config['clone-toast-msg']), { variant: 'info' }); + showToast(props, buildToastMsgWithEventTitle(newSeriesObj.seriesName, config['clone-toast-msg']), { variant: 'info' }); }); - // delete - deleteBtn.addEventListener('click', async (e) => { - e.prseriesDefault(); + // archive + archive.addEventListener('click', async (e) => { + e.preventDefault(); const spTheme = props.el.querySelector('sp-theme.toast-area'); if (!spTheme) return; @@ -298,6 +281,9 @@ function initMoreOptions(props, config, seriesObj, row) { } }); + // version history + + // close tool box document.addEventListener('click', (e) => { if (!moreOptionsCell.contains(e.target) || moreOptionsCell === e.target) { const toolBox = moreOptionsCell.querySelector('.dashboard-tool-box'); @@ -306,46 +292,39 @@ function initMoreOptions(props, config, seriesObj, row) { }); } -function getCountryName(seriesObj) { - if (!seriesObj.venue) return ''; - - const { venue } = seriesObj; - return venue.country || ''; -} - function buildStatusTag(series) { - const dot = series.published ? getIcon('dot-purple') : getIcon('dot-green'); - const text = series.published ? 'Published' : 'Draft'; + let dot; + + switch (series.status) { + case 'published': + dot = getIcon('dot-purple'); + break; + case 'unpublished': + dot = getIcon('dot-green'); + break; + case 'archived': + default: + dot = getIcon('dot-gray'); + break; + } const statusTag = createTag('div', { class: 'status' }); - statusTag.append(dot, text); + statusTag.append(dot, series.status); return statusTag; } -function buildEventTitleTag(config, seriesObj) { +function buildSeriesNameTag(config, seriesObj) { const url = new URL(`${window.location.origin}${config['create-form-url']}`); url.searchParams.set('seriesId', seriesObj.seriesId); - const seriesTitleTag = createTag('a', { class: 'title-link', href: url.toString() }, seriesObj.title); - return seriesTitleTag; + const nameTag = createTag('a', { class: 'name-link', href: url.toString() }, seriesObj.seriesName); + return nameTag; } -// TODO: to retire -function buildVenueTag(seriesObj) { - const { venue } = seriesObj; - if (!venue) return null; +function buildEventsCountTag(series, events) { + const seriesEvents = getSeriesEvents(series.seriesId, events); - const venueTag = createTag('span', { class: 'vanue-name' }, venue.venueName); - return venueTag; -} - -function buildRSVPTag(config, seriesObj) { - const text = `${seriesObj.attendeeCount} / ${seriesObj.attendeeLimit}`; - - const url = new URL(`${window.location.origin}${config['attendee-dashboard-url']}`); - url.searchParams.set('seriesId', seriesObj.seriesId); - - const rsvpTag = createTag('a', { class: 'rsvp-tag', href: url }, text); - return rsvpTag; + const eventsCountTag = createTag('span', { class: 'events-count' }, seriesEvents.length); + return eventsCountTag; } async function populateRow(props, config, index) { @@ -355,23 +334,21 @@ async function populateRow(props, config, index) { // TODO: build each column's element specifically rather than just text const row = createTag('tr', { class: 'row', 'data-id': series.seriesId }, '', { parent: tBody }); - const titleCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildEventTitleTag(config, series))); + const nameCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildSeriesNameTag(config, series))); const statusCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildStatusTag(series))); - const startDateCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, formatLocaleDate(series.startDate))); - const modDateCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, formatLocaleDate(series.modificationTime))); - const venueCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildVenueTag(series))); - const geoCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, getCountryName(series))); - const externalEventId = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildRSVPTag(config, series))); + const modificationTimeCall = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, formatLocaleDate(series.modificationTime))); + const createdByCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, series.createdBy)); + const modifiedByCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, series.modifiedBy)); + const eventsCountCell = createTag('td', {}, createTag('div', { class: 'td-wrapper' }, buildEventsCountTag(series, props.events))); const moreOptionsCell = createTag('td', { class: 'option-col' }, createTag('div', { class: 'td-wrapper' }, getIcon('more-small-list'))); row.append( - titleCell, + nameCell, statusCell, - startDateCell, - modDateCell, - venueCell, - geoCell, - externalEventId, + modificationTimeCall, + createdByCell, + modifiedByCell, + eventsCountCell, moreOptionsCell, ); @@ -379,7 +356,7 @@ async function populateRow(props, config, index) { if (series.seriesId === sp.get('newEventId')) { if (!props.el.classList.contains('toast-shown')) { - showToast(props, buildToastMsgWithEventTitle(series.title, config['new-toast-msg']), { variant: 'positive' }); + showToast(props, buildToastMsgWithEventTitle(series.seriesName, config['new-toast-msg']), { variant: 'positive' }); props.el.classList.add('toast-shown'); } @@ -442,19 +419,17 @@ function decoratePagination(props, config) { updatePaginationControl(paginationContainer, props.currentPage, totalPages); } -function initSorting(props, config) { +function initHeaderRow(props, config) { const thead = props.el.querySelector('thead'); const thRow = thead.querySelector('tr'); const headers = { - thumbnail: '', - title: 'EVENT NAME', - published: 'PUBLISH STATUS', - startDate: 'DATE RUN', + seriesName: 'SERIES NAME', + status: 'STATUS', modificationTime: 'LAST MODIFIED', - venueName: 'VENUE NAME', - timezone: 'GEO', - attendeeCount: 'RSVP DATA', + createdBy: 'CREATED BY', + modifiedBy: 'MODIFIED BY', + eventsCount: 'NUMBER OF EVENTS IN SERIES', manage: 'MANAGE', }; @@ -462,7 +437,7 @@ function initSorting(props, config) { const thText = createTag('span', {}, val); const th = createTag('th', {}, thText, { parent: thRow }); - if (['thumbnail', 'manage'].includes(key)) return; + if (['manage'].includes(key)) return; th.append(getIcon('chev-down'), getIcon('chev-up')); th.classList.add('sortable', key); @@ -513,7 +488,7 @@ function populateTable(props, config) { function filterData(props, config, query) { const q = query.toLowerCase(); - props.filteredData = props.data.filter((e) => e.title.toLowerCase().includes(q)); + props.filteredData = props.data.filter((s) => s.seriesName.toLowerCase().includes(q)); props.currentPage = 1; paginateData(props, config, 1); sortData(props, config, { resort: true }); @@ -548,7 +523,7 @@ function buildDashboardTable(props, config) { const thead = createTag('thead', {}, '', { parent: table }); createTag('tbody', {}, '', { parent: table }); createTag('tr', { class: 'table-header-row' }, '', { parent: thead }); - initSorting(props, config); + initHeaderRow(props, config); populateTable(props, config); const usp = new URLSearchParams(window.location.search); @@ -561,16 +536,6 @@ function buildDashboardTable(props, config) { } } -async function getSeriesArray() { - const resp = await getAllSeries(); - - if (resp.error) { - return []; - } - - return resp.series; -} - function buildNoEventScreen(el, config) { el.classList.add('no-data'); @@ -595,14 +560,15 @@ async function buildDashboard(el, config) { currentSort: {}, }; - const data = await getSeriesArray(); + const [{ series }, { events }] = await Promise.all([getAllSeries(), getEvents()]); - if (!data?.length) { + if (!series?.length) { buildNoEventScreen(el, config); } else { - props.data = data; - props.filteredData = [...data]; - props.paginatedData = [...data]; + props.events = events; + props.data = series; + props.filteredData = [...series]; + props.paginatedData = [...series]; const dataHandler = { set(target, prop, value, receiver) { @@ -626,7 +592,7 @@ function buildLoadingScreen(el) { el.classList.add('loading'); const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); - createTag('sp-field-label', {}, 'Loading Adobe Event Creation Console dashboard...', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Series dashboard...', { parent: loadingScreen }); el.prepend(loadingScreen); } diff --git a/ecc/icons/archive.svg b/ecc/icons/archive.svg new file mode 100644 index 00000000..6e8b0442 --- /dev/null +++ b/ecc/icons/archive.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ecc/icons/version-history.svg b/ecc/icons/version-history.svg new file mode 100644 index 00000000..772c1156 --- /dev/null +++ b/ecc/icons/version-history.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index c66ad675..a2f43062 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -783,7 +783,7 @@ export async function getAllSeries() { return { status: response.status, error: data }; } - return data.series; + return data; } catch (error) { window.lana?.log('Failed to fetch series. Error:', error); return { status: 'Network Error', error: error.message }; From 7447443eb0135ca3a8acfa423f1e278fdcf7745f Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Sun, 1 Dec 2024 16:21:45 -0600 Subject: [PATCH 26/74] series-creation-form WIP --- .../event-format-component.js | 7 - .../series-creation-form/data-handler.js | 166 +++ .../series-creation-form.css | 495 ++++++++ .../series-creation-form.js | 1073 +++++++++++++++++ .../series-details-component/controller.js | 600 +++++++++ .../series-details-component.css | 311 +++++ .../series-details-component.js | 141 +++ 7 files changed, 2786 insertions(+), 7 deletions(-) create mode 100644 ecc/blocks/series-creation-form/data-handler.js create mode 100644 ecc/blocks/series-creation-form/series-creation-form.css create mode 100644 ecc/blocks/series-creation-form/series-creation-form.js create mode 100644 ecc/blocks/series-details-component/controller.js create mode 100644 ecc/blocks/series-details-component/series-details-component.css create mode 100644 ecc/blocks/series-details-component/series-details-component.js diff --git a/ecc/blocks/event-format-component/event-format-component.js b/ecc/blocks/event-format-component/event-format-component.js index f9b0803f..3cab23a9 100644 --- a/ecc/blocks/event-format-component/event-format-component.js +++ b/ecc/blocks/event-format-component/event-format-component.js @@ -55,13 +55,6 @@ function decorateTimeZoneSelect(column) { column.append(tzWrapper); } -// FIXME: comment out for now. Might support other checkboxes later. -// function decorateCheckbox(column) { -// const checkbox = createTag('sp-checkbox', { id: 'rsvp-required-check' }, column.textContent.trim()); -// column.innerHTML = ''; -// column.append(checkbox); -// } - export default function init(el) { el.classList.add('form-component'); diff --git a/ecc/blocks/series-creation-form/data-handler.js b/ecc/blocks/series-creation-form/data-handler.js new file mode 100644 index 00000000..40dd55c2 --- /dev/null +++ b/ecc/blocks/series-creation-form/data-handler.js @@ -0,0 +1,166 @@ +/* eslint-disable no-use-before-define */ +// FIXME: this whole data handler thing can be done better +let responseCache = {}; +let payloadCache = {}; + +const submissionFilter = [ + // from payload and response + 'agenda', + 'topics', + 'eventType', + 'cloudType', + 'seriesId', + 'templateId', + 'communityTopicUrl', + 'title', + 'description', + 'localStartDate', + 'localEndDate', + 'localStartTime', + 'localEndTime', + 'timezone', + 'showAgendaPostEvent', + 'showVenuePostEvent', + 'showVenueImage', + 'showSponsors', + 'rsvpFormFields', + 'relatedProducts', + 'rsvpDescription', + 'attendeeLimit', + 'allowWaitlisting', + 'hostEmail', + 'eventId', + 'published', + 'creationTime', + 'modificationTime', +]; + +function isValidAttribute(attr) { + return attr !== undefined && attr !== null; +} + +export function quickFilter(obj) { + const output = {}; + + submissionFilter.forEach((attr) => { + if (isValidAttribute(obj[attr])) { + output[attr] = obj[attr]; + } + }); + + return output; +} + +export function setPayloadCache(payload) { + if (!payload) return; + payloadCache = quickFilter(payload); +} + +export function getFilteredCachedPayload() { + return payloadCache; +} + +export function setResponseCache(response) { + if (!response) return; + responseCache = quickFilter(response); +} + +export function getFilteredCachedResponse() { + return responseCache; +} + +/** + * Recursively compares two values to determine if they are different. + * + * @param {*} value1 - The first value to compare. + * @param {*} value2 - The second value to compare. + * @returns {boolean} - Returns true if the values are different, otherwise false. + */ +export function compareObjects(value1, value2, lengthOnly = false) { + if ( + typeof value1 === 'object' + && value1 !== null + && !Array.isArray(value1) + && typeof value2 === 'object' + && value2 !== null + && !Array.isArray(value2) + ) { + if (hasContentChanged(value1, value2)) { + return true; + } + } else if (Array.isArray(value1) && Array.isArray(value2)) { + if (value1.length !== value2.length) { + // Change detected due to different array lengths + return true; + } + + if (!lengthOnly) { + for (let i = 0; i < value1.length; i += 1) { + if (compareObjects(value1[i], value2[i])) { + return true; + } + } + } + } else if (value1 !== value2) { + // Change detected + return true; + } + return false; +} + +/** + * Determines if the content of two objects has changed. + * + * @param {Object} oldData - The original object. + * @param {Object} newData - The updated object. + * @returns {boolean} - Returns true if content has changed, otherwise false. + * @throws {TypeError} - Throws error if inputs are not objects. + */ +export function hasContentChanged(oldData, newData) { + // Ensure both inputs are objects + if ( + typeof oldData !== 'object' + || oldData === null + || typeof newData !== 'object' + || newData === null + ) { + throw new TypeError('Both oldData and newData must be objects'); + } + + const ignoreList = [ + 'modificationTime', + 'status', + 'platform', + 'platformCode', + 'liveUpdate', + ]; + + // Checking keys counts + const oldDataKeys = Object.keys(oldData).filter((key) => !ignoreList.includes(key)); + const newDataKeys = Object.keys(newData).filter((key) => !ignoreList.includes(key)); + + if (oldDataKeys.length !== newDataKeys.length) { + // Change detected due to different key counts + return true; + } + + // Check for differences in the actual values + return oldDataKeys.some( + (key) => { + const lengthOnly = key === 'speakers' && !oldData[key].ordinal; + + return !ignoreList.includes(key) && compareObjects(oldData[key], newData[key], lengthOnly); + }, + ); +} + +export default function getJoinedData() { + const filteredResponse = getFilteredCachedResponse(); + const filteredPayload = getFilteredCachedPayload(); + + return { + ...filteredResponse, + ...filteredPayload, + modificationTime: filteredResponse.modificationTime, + }; +} diff --git a/ecc/blocks/series-creation-form/series-creation-form.css b/ecc/blocks/series-creation-form/series-creation-form.css new file mode 100644 index 00000000..da565c1f --- /dev/null +++ b/ecc/blocks/series-creation-form/series-creation-form.css @@ -0,0 +1,495 @@ +.series-creation-form { + display: block; + padding: 0 40px; + + --mod-textfield-icon-size-invalid: 0; + --stroke-color-divider: #6E6E6E; + --color-red: #EB1000; + --mod-textfield-focus-indicator-width: 0; + --mod-textfield-text-color-disabled: #000; + --mod-textfield-border-color-invalid-default: #000; + --mod-textfield-border-color-invalid-focus: #000; + --mod-textfield-border-color-invalid-focus-hover: #000; + --mod-textfield-border-color-invalid-hover: #000; + --mod-textfield-border-color-invalid-keyboard-focus: #000; + --mod-textfield-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; + --mod-textfield-font-weight: 700; + --mod-textfield-spacing-block-start: 8px; +} + +.series-creation-form .loading-screen { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 20; + background-color: var(--color-white); + opacity: 1; +} + +.series-creation-form .loading-screen sp-field-label { + font-size: var(--type-body-s-size); +} + +.series-creation-form .img-upload-text p { + margin: 0; + font-size: var(--type-body-xs-size); + line-height: normal; +} + +.series-creation-form .main-frame sp-theme sp-underlay { + z-index: 2; +} + +.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 3; + background: var(--spectrum-gray-100); + min-width: 480px; +} + +.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog h1 { + font-size: var(--type-heading-s-size); +} + +.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog p { + font-size: var(--type-body-s-size); +} + +.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog .button-container { + display: flex; + justify-content: flex-end; + gap: 16px; +} + +.series-creation-form .main-frame sp-theme sp-underlay:not([open]) + sp-dialog { + display: none; +} + +.series-creation-form.show-error { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.series-creation-form.show-dup-event-error #info-field-event-title { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.series-creation-form.show-dup-event-error #info-field-event-title sp-help-text { + display: flex; +} + +.series-creation-form .main-frame { + flex-grow: 1; + min-height: 100%; +} + +.series-creation-form .series-creation-form-ctas-panel { + position: sticky; + transform: translateX(-40px); + box-sizing: border-box; + bottom: 0; + padding: 16px 60px; + background-color: var(--color-red); + width: calc(100% + 80px); + z-index: 1; + display: flex; + justify-content: center; +} + +.series-creation-form .side-menu, +.series-creation-form .main-frame, +.series-creation-form .series-creation-form-ctas-panel, +.series-creation-form .loading-screen { + transition: opacity 0.5s; +} + +.series-creation-form.disabled .main-frame, +.series-creation-form.disabled .series-creation-form-ctas-panel { + pointer-events: none; +} + +.series-creation-form .side-menu { + transition: opacity 0.2s; +} + +.series-creation-form.loading div:first-of-type, +.series-creation-form.loading .side-menu, +.series-creation-form.loading .main-frame, +.series-creation-form.loading .series-creation-form-ctas-panel { + opacity: 0; +} + +.series-creation-form .side-menu button { + font-family: var(--body-font-family); +} + +.series-creation-form sp-textfield { + outline: none; +} + +.series-creation-form sp-textfield[quiet]:not(:read-only):focus { + outline: 1px var(--color-gray-500) solid; + border-radius: 4px; +} + +.series-creation-form > div.form-body { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 203px); +} + +.series-creation-form .side-menu.disabled { + opacity: 0.5; + pointer-events: none; +} + +.series-creation-form .side-menu h3 { + font-size: var(--type-body-xs-size); + color: var(--color-gray-400); + margin-bottom: 0; + margin-top: 24px; + padding: 0 24px; +} + +.series-creation-form .side-menu ul { + margin-top: 0; + padding: 0; +} + +.series-creation-form .side-menu ul li { + list-style: none; + font-size: var(--type-body-xs-size); + line-height: normal; + border-radius: 8px; + padding-left: 24px; + padding-right: 24px; +} + +.series-creation-form .series-creation-form-ctas-panel a { + font-size: var(--type-body-s-size); + display: inline-flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s, filter 0.2s; +} + +.series-creation-form .side-menu ul li a { + color: var(--color-black); +} + +.series-creation-form .side-menu ul li a, +.series-creation-form .side-menu ul li button { + text-align: left; + border: none; + background-color: transparent; + padding-left: 24px; + padding-right: 24px; + width: 100%; +} + +.series-creation-form .side-menu ul li:not(:has(ul)) { + padding-left: 0; + padding-right: 0; + margin: 12px 0; + display: flex; +} + +.series-creation-form .side-menu ul li ul { + margin-top: 12px; +} + +.series-creation-form .side-menu ul li ul li:not(:has(ul)) { + margin: 4px 0; +} + +.series-creation-form .side-menu ul li:not(:has(ul)) a, +.series-creation-form .side-menu ul li:not(:has(ul)) button { + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; +} + +.series-creation-form .side-menu ul li:not(:has(ul)):has(a):hover, +.series-creation-form .side-menu ul li:not(:has(ul)):has(a).active, +.series-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover, +.series-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active { + background-color: var(--color-red); + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.series-creation-form .side-menu ul li:not(:has(ul)):has(a):hover a, +.series-creation-form .side-menu ul li:not(:has(ul)):has(a).active a, +.series-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active button, +.series-creation-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover button { + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.series-creation-form .side-menu .nav-item { + cursor: pointer; +} + +.series-creation-form .side-menu .nav-item:disabled { + pointer-events: none; + cursor: unset; +} + +.series-creation-form .side-menu .nav-item.disabled { + pointer-events: none; + cursor: unset; + opacity: 0.5; +} + +.series-creation-form .main-frame sp-theme { + min-height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.series-creation-form .main-frame .section .content { + max-width: none; +} + +.series-creation-form .main-frame .section:first-of-type .content { + margin: 16px 24px; + max-width: none; + display: grid; + align-items: center; + justify-content: space-between; + grid-template-columns: 1fr 1fr; +} + +.series-creation-form .main-frame .section:first-of-type .content p { + font-size: var(--type-body-xs-size); +} + +.series-creation-form .main-frame .section:first-of-type .content p:first-of-type { + display: flex; + flex-direction: row-reverse; +} + +.series-creation-form .form-component > div:first-of-type > div > h2 { + font-size: var(--type-heading-xl-size); + line-height: var(--type-heading-xl-lh); +} + +.series-creation-form .form-component > div:first-of-type > div > h2, +.series-creation-form .form-component > div:first-of-type > div > h3 { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + margin-bottom: 32px; +} + +.series-creation-form .main-frame .section:first-of-type h2 { + margin: 0; + font-weight: 900; + color: var(--color-red); +} + +.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper { + display: flex; + align-items: center; + gap: 16px; +} + +.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag { + padding: 0 8px; + background-color: var(--color-white); + border-radius: 4px; +} + +.series-creation-form .main-frame .section:not(:first-of-type) { + padding: 24px 56px; + border-radius: 10px; + margin: 24px; + box-shadow: 0 3px 6px 0 rgb(0 0 0 / 16%); + background-color: var(--color-white); +} + +.series-creation-form .fragment.hidden { + display: none; +} + +.series-creation-form .form-component { + padding: 32px 12px; +} + +.series-creation-form .form-component:not(:last-of-type):not(.no-divider) { + border-bottom: 3px solid var(--stroke-color-divider); +} + +.series-creation-form .event-heading-tooltip-wrapper .event-heading-tooltip-icon { + height: 16px; + width: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: help; +} + +.series-creation-form .section:not(:first-of-type) > div.content > h2, +.series-creation-form .section:not(:first-of-type) > div.content > h3 { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 0; + padding: 0 12px; +} + +.series-creation-form .form-component > div:first-of-type > div > h2 sp-action-button, +.series-creation-form .form-component > div:first-of-type > div > h3 sp-action-button, +.series-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button, +.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button { + padding: 0; + background: none; + border: none; + cursor: help +} + +.series-creation-form .form-component > div:first-of-type > div > h2 sp-action-button .icon-info, +.series-creation-form .form-component > div:first-of-type > div > h3 sp-action-button .icon-info, +.series-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button .icon-info, +.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button .icon-info { + display: block; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-panel-wrapper { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 1440px; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-panel-wrapper > div { + display: flex; + align-items: center; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-backward-wrapper .back-btn { + padding: 8px; + border: 2px solid var(--color-white); + border-radius: 24px; + cursor: pointer; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-backward-wrapper .back-btn .icon { + display: block; + height: 20px; + width: 20px; +} + +.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag .icon { + margin-right: 4px; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-forward-wrapper > div:first-of-type { + padding-right: 64px; + border-right: 1px solid var(--color-black); + margin-right: 104px; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-forward-wrapper .action-area { + display: flex; + align-items: center; + gap: 16px; +} + +.series-creation-form .series-creation-form-ctas-panel a.disabled, +.series-creation-form .series-creation-form-ctas-panel a.preview-not-ready, +.series-creation-form .series-creation-form-ctas-panel a.submitting { + pointer-events: none; + opacity: 0.5; +} + +.series-creation-form .series-creation-form-ctas-panel a.next-button { + background-color: var(--color-gray-800); + border-color: var(--color-gray-800); +} + +.series-creation-form .series-creation-form-ctas-panel a.next-button:hover { + background-color: var(--color-black) +} + +.series-creation-form .series-creation-form-ctas-panel a.fill { + background-color: var(--color-gray-200); + color: var(--color-black); + font-weight: 700; + border-radius: 20px; + line-height: 20px; + min-height: 21px; + padding: 7px 18px 8px; + border: 2px solid var(--color-white); +} + +.series-creation-form .series-creation-form-ctas-panel a.fill:hover { + text-decoration: none; + filter: invert(); +} + +.series-creation-form .series-creation-form-ctas-panel a.preview-btns svg { + height: 20px; + width: 28px; +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-panel-wrapper .con-button.outline { + color: var(--color-white); + border-color: var(--color-white); +} + +.series-creation-form .series-creation-form-ctas-panel .series-creation-form-panel-wrapper .con-button.outline:hover { + background-color: var(--color-white); + color: var(--color-red); +} + +.series-creation-form.hidden, +.series-creation-form .hidden { + display: none; +} + +.series-creation-form:not(.loading) .loading-screen { + opacity: 0; + z-index: -1; +} + +.series-creation-form .toast-parent { + position: absolute; + bottom: 100%; + right: 60px; +} + +.series-creation-form .toast-area { + margin-bottom: 16px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 16px; +} + +@media screen and (min-width: 900px) { + .series-creation-form > div.form-body { + flex-direction: row; + } + + .series-creation-form .main-frame { + max-width: var(--grid-container-width); + } +} diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js new file mode 100644 index 00000000..f9beb18a --- /dev/null +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -0,0 +1,1073 @@ +import { LIBS } from '../../scripts/scripts.js'; +import { + getIcon, + buildNoAccessScreen, + generateToolTip, + camelToSentenceCase, + getEventPageHost, + signIn, + getEventServiceEnv, + getDevToken, +} from '../../scripts/utils.js'; +import { + createEvent, + updateEvent, + publishEvent, + getEvent, +} from '../../scripts/esp-controller.js'; +import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; +import { Profile } from '../../components/profile/profile.js'; +import { Repeater } from '../../components/repeater/repeater.js'; +import AgendaFieldset from '../../components/agenda-fieldset/agenda-fieldset.js'; +import AgendaFieldsetGroup from '../../components/agenda-fieldset-group/agenda-fieldset-group.js'; +import { ProfileContainer } from '../../components/profile-container/profile-container.js'; +import { CustomTextfield } from '../../components/custom-textfield/custom-textfield.js'; +import ProductSelector from '../../components/product-selector/product-selector.js'; +import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; +import PartnerSelector from '../../components/partner-selector/partner-selector.js'; +import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; +import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; +import { CustomSearch } from '../../components/custom-search/custom-search.js'; +import { initProfileLogicTree } from '../../scripts/event-apis.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); +const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); + +// list of controllers for the handler to load +const VANILLA_COMPONENTS = [ + 'event-format', + 'event-info', + 'img-upload', + 'venue-info', + 'profile', + 'event-agenda', + 'event-community-link', + 'event-partners', + 'terms-conditions', + 'product-promotion', + 'event-topics', + 'registration-details', + 'registration-fields', +]; + +const REQUIRED_INPUT_TYPES = [ + 'input[required]', + 'select[required]', + 'textarea[required]', + 'sp-textfield[required]', + 'sp-checkbox[required]', + 'sp-picker[required]', +]; + +const SPECTRUM_COMPONENTS = [ + 'theme', + 'textfield', + 'picker', + 'menu', + 'checkbox', + 'field-label', + 'divider', + 'button', + 'progress-circle', + 'overlay', + 'dialog', + 'button-group', + 'tooltip', + 'popover', + 'search', + 'toast', + 'icon', + 'action-button', + 'progress-circle', +]; + +export function buildErrorMessage(props, resp) { + if (!resp) return; + + const toastArea = resp.targetEl ? resp.targetEl.querySelector('.toast-area') : props.el.querySelector('.toast-area'); + + if (resp.error) { + const messages = []; + const errorBag = resp.error.errors; + const errorMessage = resp.error.message; + + if (errorBag) { + errorBag.forEach((error) => { + const errorPathSegments = error.path.split('/'); + const text = `${camelToSentenceCase(errorPathSegments[errorPathSegments.length - 1])} ${error.message}`; + messages.push(text); + }); + + messages.forEach((msg, i) => { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 + (i * 3000) }, msg, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + }); + } else if (errorMessage) { + if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Event update invalid, event has been modified since last fetch"}') { + const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The event has been updated by a different session since your last save.', { parent: toastArea }); + const url = new URL(window.location.href); + url.searchParams.set('eventId', getFilteredCachedResponse().eventId); + + createTag('sp-button', { + slot: 'action', + variant: 'overBackground', + href: `${url.toString()}`, + }, 'See the latest version', { parent: toast }); + + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } else { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 }, errorMessage, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } + } + } +} + +function replaceAnchorWithButton(anchor) { + if (!anchor || anchor.tagName !== 'A') { + return null; + } + + const attributes = {}; + for (let i = 0; i < anchor.attributes.length; i += 1) { + const attr = anchor.attributes[i]; + attributes[attr.name] = attr.value; + } + + const button = createTag('button', attributes, anchor.innerHTML); + + anchor.parentNode.replaceChild(button, anchor); + return button; +} + +function getCurrentFragment(props) { + const frags = props.el.querySelectorAll('.fragment'); + const currentFrag = frags[props.currentStep]; + return currentFrag; +} + +function validateRequiredFields(fields) { + return fields.length === 0 || Array.from(fields).every((f) => f.value && !f.invalid); +} + +function onStepValidate(props) { + return function updateCtaStatus() { + const currentFrag = getCurrentFragment(props); + const stepValid = validateRequiredFields(props[`required-fields-in-${currentFrag.id}`]); + const ctas = props.el.querySelectorAll('.event-creation-form-panel-wrapper a'); + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + ctas.forEach((cta) => { + if (cta.classList.contains('back-btn')) { + cta.classList.toggle('disabled', props.currentStep === 0); + } else { + cta.classList.toggle('disabled', !stepValid); + } + }); + + sideNavs.forEach((nav, i) => { + if (i !== props.currentStep) { + nav.disabled = !stepValid; + } + }); + }; +} + +function initRequiredFieldsValidation(props) { + const currentFrag = getCurrentFragment(props); + + const inputValidationCB = onStepValidate(props); + props[`required-fields-in-${currentFrag.id}`].forEach((field) => { + field.removeEventListener('change', inputValidationCB); + field.addEventListener('change', inputValidationCB, { bubbles: true }); + }); + + inputValidationCB(); +} + +function enableSideNavForEditFlow(props) { + const frags = props.el.querySelectorAll('.fragment'); + const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component:not(.event-agenda-component)')) + .every((fc) => fc.classList.contains('prefilled')); + + if (!completeFirstStep) return; + + frags.forEach((frag, i) => { + const prefilledOtherSteps = i !== 0 && frag.querySelector('.form-component.prefilled'); + + if (completeFirstStep || prefilledOtherSteps) { + props.farthestStep = Math.max(props.farthestStep, i); + } + }); + + initRequiredFieldsValidation(props); +} + +function initCustomLitComponents() { + customElements.define('image-dropzone', ImageDropzone); + customElements.define('profile-ui', Profile); + customElements.define('repeater-element', Repeater); + customElements.define('partner-selector', PartnerSelector); + customElements.define('partner-selector-group', PartnerSelectorGroup); + customElements.define('agenda-fieldset', AgendaFieldset); + customElements.define('agenda-fieldset-group', AgendaFieldsetGroup); + customElements.define('product-selector', ProductSelector); + customElements.define('product-selector-group', ProductSelectorGroup); + customElements.define('profile-container', ProfileContainer); + customElements.define('custom-textfield', CustomTextfield); + customElements.define('custom-search', CustomSearch); +} + +async function loadEventData(props) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const eventId = urlParams.get('eventId'); + + if (eventId) { + setTimeout(() => { + if (!props.eventDataResp.eventId) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); + } + }, 5000); + + props.el.classList.add('disabled'); + const eventData = await getEvent(eventId); + props.eventDataResp = { ...props.eventDataResp, ...eventData }; + props.el.classList.remove('disabled'); + } +} + +async function initComponents(props) { + initCustomLitComponents(); + + const componentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents?.length) return; + + const componentInitPromises = Array.from(mappedComponents).map(async (component) => { + const { default: initComponent } = await import(`../${comp}-component/controller.js`); + await initComponent(component, props); + }); + + await Promise.all(componentInitPromises); + }); + + await Promise.all(componentPromises); +} + +async function gatherValues(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onSubmit } = await import(`../${comp}-component/controller.js`); + return onSubmit(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function handleEventUpdate(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onEventUpdate } = await import(`../${comp}-component/controller.js`); + return onEventUpdate(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnPayloadChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onPayloadUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onPayloadUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnRespChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onRespUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onRespUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +function decorateForm(el) { + const ctaRow = el.querySelector(':scope > div:last-of-type'); + const formBodyRow = el.querySelector(':scope > div:first-of-type'); + + if (ctaRow) { + const toastParent = createTag('sp-theme', { class: 'toast-parent', color: 'light', scale: 'medium' }, '', { parent: ctaRow }); + createTag('div', { class: 'toast-area' }, '', { parent: toastParent }); + } + + if (!formBodyRow) return; + + formBodyRow.classList.add('form-body'); + + const app = createTag('sp-theme', { color: 'light', scale: 'medium', id: 'form-app' }); + createTag('sp-underlay', {}, '', { parent: app }); + createTag('sp-dialog', { size: 's' }, '', { parent: app }); + const form = createTag('form', {}, '', { parent: app }); + const formDivs = el.querySelectorAll('.fragment'); + + if (!formDivs.length) { + el.remove(); + return; + } + + formDivs.forEach((formDiv) => { + formDiv.parentElement.parentElement.replaceChild(app, formDiv.parentElement); + form.append(formDiv.parentElement); + }); + + const cols = formBodyRow.querySelectorAll(':scope > div'); + + cols.forEach((col, i) => { + if (i === 0) { + col.classList.add('side-menu'); + const navItems = col.querySelectorAll('a[href*="#"]'); + navItems.forEach((nav, index) => { + const btn = replaceAnchorWithButton(nav); + btn.classList.add('nav-item'); + + if (index !== 0) { + btn.disabled = true; + } else { + btn.closest('li')?.classList.add('active'); + } + }); + } + + if (i === 1) { + col.classList.add('main-frame'); + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + const fragPathSegments = frag.dataset.path.split('/'); + const fragId = `form-step-${fragPathSegments[fragPathSegments.length - 1]}`; + frag.id = fragId; + }); + } + }); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + }); +} + +function showSaveSuccessMessage(props, detail = { message: 'Edits saved successfully' }) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const previousMsgs = toastArea.querySelectorAll('.save-success-msg'); + + previousMsgs.forEach((msg) => { + msg.remove(); + }); + + const toast = createTag('sp-toast', { class: 'save-success-msg', open: true, variant: 'positive', timeout: 6000 }, detail.message || 'Edits saved successfully', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); +} + +function updateDashboardLink(props) { + // FIXME: presuming first link is dashboard link is not good. + if (!getFilteredCachedResponse().eventId) return; + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + if (!dashboardLink) return; + + const url = new URL(dashboardLink.href); + + if (url.searchParams.has('eventId')) return; + + url.searchParams.set('newEventId', getFilteredCachedResponse().eventId); + dashboardLink.href = url.toString(); +} + +async function saveEvent(props, toPublish = false) { + try { + await gatherValues(props); + } catch (e) { + return { error: { message: e.message } }; + } + + let resp; + + const onEventSave = async () => { + if (resp?.eventId) await handleEventUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } + }; + + if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { + resp = await createEvent(quickFilter(props.payload)); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + updateDashboardLink(props); + await onEventSave(); + } else if (props.currentStep <= props.maxStep && !toPublish) { + resp = await updateEvent( + getFilteredCachedResponse().eventId, + getJoinedData(), + ); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + await onEventSave(); + } else if (toPublish) { + resp = await publishEvent( + getFilteredCachedResponse().eventId, + getJoinedData(), + ); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + if (resp?.eventId) await handleEventUpdate(props); + } + + return resp; +} + +function updateSideNav(props) { + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + sideNavs.forEach((n, i) => { + n.closest('li')?.classList.remove('active'); + if (i <= props.farthestStep) n.disabled = false; + if (i === props.currentStep) n.closest('li')?.classList.add('active'); + }); +} + +function updateRequiredFields(props) { + const currentFrag = getCurrentFragment(props); + props[`required-fields-in-${currentFrag.id}`] = currentFrag.querySelectorAll(REQUIRED_INPUT_TYPES.join()); +} + +function renderFormNavigation(props, prevStep, currentStep) { + const nextBtn = props.el.querySelector('.event-creation-form-ctas-panel .next-button'); + const backBtn = props.el.querySelector('.event-creation-form-ctas-panel .back-btn'); + const frags = props.el.querySelectorAll('.fragment'); + + frags[prevStep].classList.add('hidden'); + frags[currentStep].classList.remove('hidden'); + + if (props.currentStep === props.maxStep) { + if (props.eventDataResp.published) { + nextBtn.textContent = nextBtn.dataset.republishStateText; + } else { + nextBtn.textContent = nextBtn.dataset.finalStateText; + } + nextBtn.prepend(getIcon('golden-rocket')); + } else { + nextBtn.textContent = nextBtn.dataset.nextStateText; + nextBtn.append(getIcon('chev-right-white')); + } + + backBtn.classList.toggle('disabled', currentStep === 0); +} + +function navigateForm(props, stepIndex) { + const index = stepIndex || stepIndex === 0 ? stepIndex : props.currentStep + 1; + const frags = props.el.querySelectorAll('.fragment'); + + if (index >= frags.length || index < 0) return; + + props.currentStep = index; + props.farthestStep = Math.max(props.farthestStep, index); + + window.scrollTo(0, 0); + updateRequiredFields(props); +} + +function closeDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (underlay) underlay.open = false; + if (dialog) dialog.innerHTML = ''; +} + +function buildPreviewLoadingDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return null; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (!underlay || !dialog) return null; + + underlay.open = false; + dialog.innerHTML = ''; + + createTag('h1', { slot: 'heading' }, 'Generating your preview...', { parent: dialog }); + createTag('p', {}, 'This usually takes 10-30 seconds, but it might take up to 10 minutes in rare cases. Please wait, and the preview will open in a new tab when it’s ready.', { parent: dialog }); + createTag('p', {}, 'Note: Please make sure pop-up is allowed in your browser settings.', { parent: dialog }); + const style = createTag('style', {}, ` + @keyframes progress-bar-indeterminate { + 0% { + transform: translateX(-100%); + } + 50% { + transform: translateX(0%); + } + 100% { + transform: translateX(200%); + } + } + `); + + // Create the progress bar container + const progressBar = createTag('div', { + style: ` + position: relative; + width: 100%; + height: 8px; + background: #e6e6e6; + border-radius: 4px; + overflow: hidden; + margin-bottom: 1rem; + `, + }); + + // Create the progress bar indicator + const progressBarIndicator = createTag('div', { + style: ` + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: #1473e6; + transform: translateX(0); + animation: progress-bar-indeterminate 1.5s linear infinite; + `, + }); + + // Append the elements to the shadow root + progressBar.appendChild(progressBarIndicator); + dialog.appendChild(style); + dialog.appendChild(progressBar); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'Cancel', { parent: buttonContainer }); + + underlay.open = true; + + return dialog; +} + +function buildPreviewLoadingFailedDialog(props) { + const spTheme = props.el.querySelector('#form-app'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + + if (!underlay || !dialog) return; + + underlay.open = false; + dialog.innerHTML = ''; + + createTag('h1', { slot: 'heading' }, 'Preview generation failed.', { parent: dialog }); + createTag('p', {}, 'Your changes have been saved. Our system is working in the background to update the page.', { parent: dialog }); + const slackLink = createTag('a', { href: 'https://adobe.enterprise.slack.com/archives/C07KPJYA760' }, 'Slack'); + const emailLink = createTag('a', { href: 'mailto:Grp-acom-milo-events-support@adobe.com' }, 'Grp-acom-milo-events-support@adobe.com'); + createTag('p', {}, `Please try again later. If the issue persists, please feel free to contact us on ${slackLink.outerHTML} or email ${emailLink.outerHTML}`, { parent: dialog }); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + const cancelButton = createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'OK', { parent: buttonContainer }); + + underlay.open = true; + + cancelButton.addEventListener('click', () => { + closeDialog(props); + dialog.innerHTML = ''; + }); +} + +async function getNonProdPreviewDataById(props) { + if (!props.eventDataResp) return null; + + const { eventId } = props.eventDataResp; + + if (!eventId) return null; + + const esEnv = getEventServiceEnv(); + const resp = await fetch(`${getEventPageHost()}/events/default/${esEnv === 'prod' ? '' : `${esEnv}/`}metadata-preview.json`); + if (resp.ok) { + const json = await resp.json(); + const pageData = json.data.find((d) => d['event-id'] === eventId); + + if (pageData) return pageData; + + window.lana?.log('Failed to find non-prod metadata for current page'); + return null; + } + + window.lana?.log('Failed to fetch non-prod metadata:', resp); + return null; +} + +async function validatePreview(props, oldResp, cta) { + let retryCount = 0; + + const currentData = { ...props.eventDataResp }; + const oldData = { ...oldResp }; + + if (!hasContentChanged(currentData, oldData) || !Object.keys(oldData).length) { + window.open(cta.href); + return Promise.resolve(); + } + + const modificationTimeMatch = (metadataObj) => { + const metadataModTimestamp = new Date(metadataObj['modification-time']).getTime(); + return metadataModTimestamp === props.eventDataResp.modificationTime; + }; + + return new Promise((resolve) => { + const interval = setInterval(async () => { + try { + retryCount += 1; + const metadataJson = await getNonProdPreviewDataById(props); + + if (metadataJson && modificationTimeMatch(metadataJson)) { + clearInterval(interval); + closeDialog(props); + window.open(cta.href); + resolve(); + } else if (retryCount >= 30) { + clearInterval(interval); + buildPreviewLoadingFailedDialog(props); + window.lana?.log('Error: Failed to match metadata after 30 retries'); + resolve(); + } + } catch (error) { + window.lana?.log('Error in interval fetch:', error); + clearInterval(interval); + resolve(); + } + }, Math.floor(Math.random() * (2000 - 1000 + 1)) + 1000); + + const dialog = buildPreviewLoadingDialog(props, interval); + + if (dialog) { + const cancelButton = dialog.querySelector('#cancel-preview'); + cancelButton.addEventListener('click', () => { + closeDialog(props); + if (interval) clearInterval(interval); + resolve(); + }); + } + }); +} + +function initFormCtas(props) { + const ctaRow = props.el.querySelector(':scope > div:last-of-type'); + decorateButtons(ctaRow, 'button-l'); + const ctas = ctaRow.querySelectorAll('a'); + ctaRow.classList.add('event-creation-form-ctas-panel'); + + const forwardActionsWrappers = ctaRow.querySelectorAll(':scope > div'); + + const panelWrapper = createTag('div', { class: 'event-creation-form-panel-wrapper' }, '', { parent: ctaRow }); + const backwardWrapper = createTag('div', { class: 'event-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); + const forwardWrapper = createTag('div', { class: 'event-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); + + forwardActionsWrappers.forEach((w) => { + w.classList.add('action-area'); + forwardWrapper.append(w); + }); + + const backBtn = createTag('a', { class: 'back-btn' }, getIcon('chev-left-white')); + + backwardWrapper.append(backBtn); + + const toggleBtnsSubmittingState = (submitting) => { + [...ctas, backBtn].forEach((c) => { + c.classList.toggle('submitting', submitting); + }); + }; + + let oldResp = { ...props.eventDataResp }; + ctas.forEach((cta) => { + if (cta.href) { + const ctaUrl = new URL(cta.href); + + if (['#pre-event', '#post-event'].includes(ctaUrl.hash)) { + cta.classList.add('fill', 'preview-btns', 'preview-not-ready', ctaUrl.hash.replace('#', '')); + cta.addEventListener('click', async (e) => { + e.preventDefault(); + toggleBtnsSubmittingState(true); + if (cta.classList.contains('preview-not-ready')) return; + validatePreview(props, oldResp, cta).then(() => { + toggleBtnsSubmittingState(false); + }); + }); + } + + if (['#save', '#next'].includes(ctaUrl.hash)) { + if (ctaUrl.hash === '#next') { + cta.classList.add('next-button'); + const [nextStateText, finalStateText, doneStateText, republishStateText] = cta.textContent.split('||'); + + cta.textContent = nextStateText; + cta.append(getIcon('chev-right-white')); + cta.dataset.nextStateText = nextStateText; + cta.dataset.finalStateText = finalStateText; + cta.dataset.doneStateText = doneStateText; + cta.dataset.republishStateText = republishStateText; + } + + cta.addEventListener('click', async (e) => { + e.preventDefault(); + toggleBtnsSubmittingState(true); + + if (ctaUrl.hash === '#next') { + let resp; + if (props.currentStep === props.maxStep) { + oldResp = { ...props.eventDataResp }; + resp = await saveEvent(props, true); + } else { + oldResp = { ...props.eventDataResp }; + resp = await saveEvent(props); + } + + if (resp?.error) { + buildErrorMessage(props, resp); + } else if (props.currentStep === props.maxStep) { + const toastArea = props.el.querySelector('.toast-area'); + cta.textContent = cta.dataset.doneStateText; + cta.classList.add('disabled'); + + if (toastArea) { + const toast = createTag('sp-toast', { open: true, variant: 'positive' }, 'Success! This event has been published.', { parent: toastArea }); + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + createTag( + 'sp-button', + { + slot: 'action', + variant: 'overBackground', + treatment: 'outline', + href: dashboardLink.href, + }, + 'Go to dashboard', + { parent: toast }, + ); + + toast.addEventListener('close', () => { + toast.remove(); + }); + } + } else { + navigateForm(props); + } + } else { + oldResp = { ...props.eventDataResp }; + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } + } + + toggleBtnsSubmittingState(false); + }); + } + } + }); + + backBtn.addEventListener('click', async () => { + toggleBtnsSubmittingState(true); + oldResp = { ...props.eventDataResp }; + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } else { + props.currentStep -= 1; + } + + toggleBtnsSubmittingState(false); + }); +} + +function updateCtas(props) { + const formCtas = props.el.querySelectorAll('.event-creation-form-ctas-panel a'); + const { eventDataResp } = props; + + formCtas.forEach((a) => { + if (a.classList.contains('preview-btns')) { + const testTime = a.classList.contains('pre-event') ? +props.eventDataResp.localEndTimeMillis - 10 : +props.eventDataResp.localEndTimeMillis + 10; + if (eventDataResp.detailPagePath) { + a.href = `${getEventPageHost()}${eventDataResp.detailPagePath}?previewMode=true&cachebuster=${Date.now()}&timing=${testTime}`; + a.classList.remove('preview-not-ready'); + } + } + + if (a.classList.contains('next-button')) { + if (props.currentStep === props.maxStep) { + if (props.eventDataResp.published) { + a.textContent = a.dataset.republishStateText; + } else { + a.textContent = a.dataset.finalStateText; + } + a.prepend(getIcon('golden-rocket')); + } else { + a.textContent = a.dataset.nextStateText; + a.append(getIcon('chev-right-white')); + } + } + }); +} + +function initNavigation(props) { + const frags = props.el.querySelectorAll('.fragment'); + const sideMenu = props.el.querySelector('.side-menu'); + const navItems = sideMenu.querySelectorAll('.nav-item'); + + frags.forEach((frag, i) => { + if (i !== 0) { + frag.classList.add('hidden'); + } + }); + + navItems.forEach((nav, i) => { + nav.addEventListener('click', async () => { + if (nav.closest('li').classList.contains('active')) return; + if (!nav.disabled && !sideMenu.classList.contains('disabled')) { + sideMenu.classList.add('disabled'); + + const resp = await saveEvent(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } else { + navigateForm(props, i); + } + + sideMenu.classList.remove('disabled'); + } + }); + }); +} + +function initDeepLink(props) { + const { hash } = window.location; + + if (hash) { + const frags = props.el.querySelectorAll('.fragment'); + + const targetFragindex = Array.from(frags).findIndex((frag) => `#${frag.id}` === hash); + + if (targetFragindex && targetFragindex <= props.farthestStep) { + navigateForm(props, targetFragindex); + } + } +} + +function updateStatusTag(props) { + const { eventDataResp } = props; + + if (eventDataResp?.published === undefined) return; + + const currentFragment = getCurrentFragment(props); + + const headingSection = currentFragment.querySelector(':scope > .section:first-child'); + + const eixstingStatusTag = headingSection.querySelector('.event-status-tag'); + if (eixstingStatusTag) eixstingStatusTag.remove(); + + const heading = headingSection.querySelector('h2', 'h3', 'h3', 'h4'); + const headingWrapper = createTag('div', { class: 'step-heading-wrapper' }); + const dot = eventDataResp.published ? getIcon('dot-purple') : getIcon('dot-green'); + const text = eventDataResp.published ? 'Published' : 'Draft'; + const statusTag = createTag('span', { class: 'event-status-tag' }); + + statusTag.append(dot, text); + heading.parentElement?.replaceChild(headingWrapper, heading); + headingWrapper.append(heading, statusTag); +} + +async function buildECCForm(el) { + const props = { + el, + currentStep: 0, + farthestStep: 0, + maxStep: el.querySelectorAll('.fragment').length - 1, + payload: {}, + eventDataResp: {}, + }; + + const dataHandler = { + set(target, prop, value) { + const oldValue = target[prop]; + target[prop] = value; + + if (prop.startsWith('required-fields-in-')) { + initRequiredFieldsValidation(target); + } + + switch (prop) { + case 'currentStep': + { + renderFormNavigation(target, oldValue, value); + updateSideNav(target); + initRequiredFieldsValidation(target); + updateStatusTag(target); + break; + } + + case 'farthestStep': { + updateSideNav(target); + break; + } + + case 'payload': { + setPayloadCache(value); + updateComponentsOnPayloadChange(target); + initRequiredFieldsValidation(target); + break; + } + + case 'eventDataResp': { + setResponseCache(value); + updateComponentsOnRespChange(target); + updateCtas(target); + if (value.error) { + props.el.classList.add('show-error'); + } else { + props.el.classList.remove('show-error'); + } + break; + } + + default: + break; + } + + return true; + }, + }; + + const proxyProps = new Proxy(props, dataHandler); + + decorateForm(el); + + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + props[`required-fields-in-${frag.id}`] = []; + + frag.querySelectorAll(':scope > .section > .content').forEach((c) => { + generateToolTip(c); + }); + }); + + await loadEventData(proxyProps); + initFormCtas(proxyProps); + initNavigation(proxyProps); + await initComponents(proxyProps); + updateRequiredFields(proxyProps); + enableSideNavForEditFlow(proxyProps); + initDeepLink(proxyProps); + updateStatusTag(proxyProps); + + el.addEventListener('show-error-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + buildErrorMessage(proxyProps, e.detail); + }); + + el.addEventListener('show-success-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + showSaveSuccessMessage(proxyProps, e.detail); + }); +} + +function buildLoadingScreen(el) { + el.classList.add('loading'); + const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); + createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Adobe Event Creation Console form...', { parent: loadingScreen }); + + el.prepend(loadingScreen); +} + +export default async function init(el) { + buildLoadingScreen(el); + const miloLibs = LIBS; + const promises = Array.from(SPECTRUM_COMPONENTS).map(async (component) => { + await import(`${miloLibs}/features/spectrum-web-components/dist/${component}.js`); + }); + await Promise.all([ + import(`${miloLibs}/deps/lit-all.min.js`), + ...promises, + ]); + + const devToken = getDevToken(); + if (devToken && getEventServiceEnv() === 'local') { + buildECCForm(el).then(() => { + el.classList.remove('loading'); + }); + return; + } + + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { + buildNoAccessScreen(el); + el.classList.remove('loading'); + }, + validProfile: () => { + buildECCForm(el).then(() => { + el.classList.remove('loading'); + }); + }, + }); +} diff --git a/ecc/blocks/series-details-component/controller.js b/ecc/blocks/series-details-component/controller.js new file mode 100644 index 00000000..40ef879d --- /dev/null +++ b/ecc/blocks/series-details-component/controller.js @@ -0,0 +1,600 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-use-before-define */ +import { getEvents } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; + +const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); + +function formatDate(date) { + let month = `${date.getMonth() + 1}`; + let day = `${date.getDate()}`; + const year = date.getFullYear(); + + if (month.length < 2) month = `0${month}`; + if (day.length < 2) day = `0${day}`; + + return [year, month, day].join('-'); +} + +function parseFormatedDate(string) { + if (!string) return null; + + const [year, month, day] = string.split('-'); + const date = new Date(year, +month - 1, day); + + return date; +} + +// Function to generate a calendar +function updateCalendar(component, parent, state) { + parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); + if (state.currentView === 'days') { + updateDayView(component, parent, state); + } else if (state.currentView === 'months') { + updateMonthView(component, parent, state); + } else if (state.currentView === 'years') { + updateYearView(component, parent, state); + } + + if (state.selectedStartDate && state.selectedEndDate) { + updateSelectedDates(state); + } +} + +function updateDayView(component, parent, state) { + state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; + const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); + weekdays.forEach((day) => { + createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); + }); + + const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); + const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); + const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); + const todayDate = new Date(); + + for (let i = 0; i < firstDayOfMonth; i += 1) { + createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); + } + for (let day = 1; day <= daysInMonth; day += 1) { + const date = new Date(state.currentYear, state.currentMonth, day); + const dayElement = createTag('div', { + class: 'calendar-day', + tabindex: '0', + 'data-date': formatDate(date), + }, day.toString(), { parent: calendarGrid }); + + if (date < todayDate) { + dayElement.classList.add('disabled'); + } else { + dayElement.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + selectDate(component, state, date); + event.preventDefault(); + } + }); + dayElement.addEventListener('click', () => selectDate(component, state, date)); + } + + // Mark today's date + if (date === todayDate) { + dayElement.classList.add('today'); + } + } +} + +function updateMonthView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear}`; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + months.forEach((month, index) => { + const monthElement = createTag('div', { + class: 'calendar-month', + 'data-month': index, + }, month, { parent: calendarGrid }); + + // Disable past months in the current year + if ((state.currentYear === currentYear && index < currentMonth) + || state.currentYear < currentYear) { + monthElement.classList.add('disabled'); + } else { + monthElement.addEventListener('click', () => { + state.currentMonth = index; + state.currentView = 'days'; + updateCalendar(component, parent, state); + }); + } + }); +} + +function isSameDay(date1, date2) { + return date1.getFullYear() === date2.getFullYear() + && date1.getMonth() === date2.getMonth() + && date1.getDate() === date2.getDate(); +} + +function updateYearView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; + const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); + const currentYear = new Date().getFullYear(); + + for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { + const yearElement = createTag('div', { + class: 'calendar-year', + 'data-year': year, + }, year.toString(), { parent: calendarGrid }); + + // Disable past years + if (year < currentYear) { + yearElement.classList.add('disabled'); + } else { + yearElement.addEventListener('click', () => { + state.currentYear = year; + state.currentView = 'months'; + updateCalendar(component, parent, state); + }); + } + } +} + +function selectDate(component, state, date) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { + state.selectedStartDate = date; + state.selectedEndDate = date; + } else if ((state.selectedStartDate && !state.selectedEndDate) + || (state.selectedStartDate === state.selectedEndDate)) { + if (date < state.selectedStartDate) { + state.selectedEndDate = state.selectedStartDate; + state.selectedStartDate = date; + } else { + state.selectedEndDate = date; + } + } + + updateSelectedDates(state); + updateInput(component, state); + input.dispatchEvent(new Event('change')); +} + +function updateInput(component, state) { + const dateInput = component.querySelector('#event-info-date-picker'); + + if (dateInput) { + if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); + if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); + + if (dateInput.dataset.startDate && dateInput.dataset.endDate) { + const options = { year: 'numeric', month: 'long', day: 'numeric' }; + const dateLocale = getConfig().locale?.ietf || 'en-US'; + const startDateTime = state.selectedStartDate + .toLocaleDateString(dateLocale, options); + const endDateTime = state.selectedEndDate + .toLocaleDateString(dateLocale, options); + const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate + ? startDateTime : `${startDateTime} - ${endDateTime}`; + dateInput.value = dateValue; + } + } +} + +function updateSelectedDates(state) { + const { parent } = state; + parent.querySelectorAll('.calendar-day').forEach((dayElement) => { + if (!dayElement.getAttribute('data-date')) return; + + const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); + dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); + dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); + // Mark the start date and end date + + if (isSameDay(clickedDate, state.selectedStartDate) + && isSameDay(state.selectedStartDate, state.selectedEndDate)) { + dayElement.classList.add('start-date', 'end-date'); + } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { + dayElement.classList.remove('end-date'); + dayElement.classList.add('start-date'); + } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { + dayElement.classList.remove('start-date'); + dayElement.classList.add('end-date'); + } else { + dayElement.classList.remove('start-date', 'end-date'); + } + + parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); + }); +} + +function changeCalendarPage(component, state, delta) { + if (state.currentView === 'days') { + state.currentMonth += delta; + if (state.currentMonth < 0) { + state.currentMonth = 11; + state.currentYear -= 1; + } else if (state.currentMonth > 11) { + state.currentMonth = 0; + state.currentYear += 1; + } + } else if (state.currentView === 'months') { + state.currentYear += delta; + } else if (state.currentView === 'years') { + state.currentYear += delta * 10; + } + updateCalendar(component, state.parent, state); +} + +function initInputWatcher(input, onChange) { + const config = { attributes: true, childList: false, subtree: false }; + + const callback = (mutationList) => { + const [mutation] = mutationList; + if (mutation.target.disabled) { + onChange(); + } + }; + + const observer = new MutationObserver(callback); + observer.observe(input, config); +} + +function buildCalendar(component, parent) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + const state = { + currentView: 'days', + selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, + selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, + currentYear: new Date().getFullYear(), + currentMonth: new Date().getMonth(), + headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), + parent, + }; + + const header = createTag('div', { class: 'calendar-header' }, null, { parent }); + const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); + header.append(state.headerTitle); + const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); + + prevButton.onclick = () => changeCalendarPage(component, state, -1); + nextButton.onclick = () => changeCalendarPage(component, state, 1); + + state.headerTitle.addEventListener('click', () => { + // eslint-disable-next-line no-nested-ternary + state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; + updateCalendar(component, parent, state); + }); + + updateCalendar(component, parent, state); +} + +function initCalendar(component) { + let calendar; + const datePickerContainer = component.querySelector('.date-picker'); + const input = component.querySelector('#event-info-date-picker'); + + datePickerContainer.addEventListener('click', () => { + if (calendar || input.disabled) return; + calendar = createTag('div', { class: 'calendar-container' }); + datePickerContainer.append(calendar); + buildCalendar(component, calendar); + }); + + document.addEventListener('click', (e) => { + if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { + calendar.remove(); + calendar = ''; + } + }); + + initInputWatcher(input, () => { + calendar.remove(); + calendar = ''; + }); +} + +function dateTimeStringToTimestamp(dateString, timeString) { + const dateTimeString = `${dateString}T${timeString}`; + + const date = new Date(dateTimeString); + + return date.getTime(); +} + +export function onSubmit(component, props) { + if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const title = component.querySelector('#info-field-event-title').value; + const description = component.querySelector('#info-field-event-description').value; + const datePicker = component.querySelector('#event-info-date-picker'); + const localStartDate = datePicker.dataset.startDate; + const localEndDate = datePicker.dataset.endDate; + + const localStartTime = component.querySelector('#time-picker-start-time-value').value; + const localEndTime = component.querySelector('#time-picker-end-time-value').value; + + const timezone = component.querySelector('#time-zone-select-input').value; + + const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); + const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); + + const eventInfo = { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + localStartTimeMillis, + localEndTimeMillis, + timezone, + }; + + props.payload = { ...props.payload, ...eventInfo }; +} + +export async function onPayloadUpdate(component, props) { + // do nothing +} + +export async function onRespUpdate(_component, _props) { + // Do nothing +} + +function checkEventDuplication(event, compareMetrics) { + const titleMatch = event.title === compareMetrics.title; + const startDateMatch = event.localStartDate === compareMetrics.startDate; + const venueIdMatch = event.venue?.city === compareMetrics.city; + const eventIdNoMatch = event.eventId !== compareMetrics.eventId; + + return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; +} + +export default async function init(component, props) { + const allEventsResp = await getEvents(); + const allEvents = allEventsResp?.events; + const eventData = props.eventDataResp; + const sameSeriesEvents = allEvents?.filter((e) => { + const matchInPayload = e.seriesId === props.payload.seriesId; + const matchInResp = e.seriesId === eventData.seriesId; + return matchInPayload || matchInResp; + }) || []; + + const eventTitleInput = component.querySelector('#info-field-event-title'); + const startTimeInput = component.querySelector('#time-picker-start-time'); + const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); + const startAmpmInput = component.querySelector('#ampm-picker-start-time'); + const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); + const endTimeInput = component.querySelector('#time-picker-end-time'); + const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); + const endAmpmInput = component.querySelector('#ampm-picker-end-time'); + const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); + const startTime = component.querySelector('#time-picker-start-time-value'); + const endTime = component.querySelector('#time-picker-end-time-value'); + const datePicker = component.querySelector('#event-info-date-picker'); + + initCalendar(component); + + eventTitleInput.addEventListener('input', () => { + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); + }); + + const resetAllOptions = () => { + [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] + .forEach((options) => { + options.forEach((option) => { + option.disabled = false; + }); + }); + }; + + const sameDayEvent = () => datePicker.dataset.startDate + && datePicker.dataset.endDate + && datePicker.dataset.startDate === datePicker.dataset.endDate; + + const onEndTimeUpdate = () => { + if (endAmpmInput.value && endTimeInput.value) { + endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); + } else { + endTime.value = null; + } + + if (!sameDayEvent()) return; + + startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; + let onlyPossibleStartAmpm = startAmpmInput.value; + if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; + + if (startTimeInput.value) { + if (onlyPossibleStartAmpm) { + const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); + if (endAmpmInput.value) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + } + } + + if (endTime.value) { + if (onlyPossibleStartAmpm) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); + option.disabled = optionTime >= endTime.value; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= endTime.value; + }); + } + } + }; + + const onStartTimeUpdate = () => { + if (startAmpmInput.value && startTimeInput.value) { + startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); + } else { + startTime.value = null; + } + + if (!sameDayEvent()) return; + + endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; + let onlyPossibleEndAmpm = endAmpmInput.value; + if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; + + if (endTimeInput.value) { + if (onlyPossibleEndAmpm) { + const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); + if (startAmpmInput.value) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + } + } + + if (startTime.value) { + if (onlyPossibleEndAmpm) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); + option.disabled = optionTime <= startTime.value; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= startTime.value; + }); + } + } + }; + + const updateTimeOptionsBasedOnDate = () => { + if (!sameDayEvent()) { + resetAllOptions(); + } else { + startTimeInput.value = ''; + startAmpmInput.value = ''; + endTimeInput.value = ''; + endAmpmInput.value = ''; + startTime.value = null; + endTime.value = null; + + resetAllOptions(); + } + }; + + startTimeInput.addEventListener('change', onStartTimeUpdate); + endTimeInput.addEventListener('change', onEndTimeUpdate); + startAmpmInput.addEventListener('change', onStartTimeUpdate); + endAmpmInput.addEventListener('change', onEndTimeUpdate); + + datePicker.addEventListener('change', (e) => { + updateTimeOptionsBasedOnDate(e); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); + }); + + BlockMediator.subscribe('eventDupMetrics', (store) => { + const metrics = store.newValue; + const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); + + helpText.textContent = helpText.textContent + .replace('[[seriesName]]', metrics.seriesName) + .replace('[[eventName]]', metrics.title); + + const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); + if (isDup) { + props.el.classList.add('show-dup-event-error'); + eventTitleInput.invalid = true; + } else { + props.el.classList.remove('show-dup-event-error'); + eventTitleInput.invalid = false; + } + + eventTitleInput.dispatchEvent(new Event('change')); + }); + + const { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + timezone, + } = eventData; + + if (title + && description + && localStartDate + && localEndDate + && localStartTime + && localEndTime + && timezone) { + const startTimePieces = parse24HourFormat(localStartTime); + const endTimePieces = parse24HourFormat(localEndTime); + + datePicker.dataset.startDate = localStartDate || ''; + datePicker.dataset.endDate = localEndDate || ''; + updateInput(component, { + selectedStartDate: parseFormatedDate(localStartDate), + selectedEndDate: parseFormatedDate(localEndDate), + }); + + component.querySelector('#info-field-event-title').value = title || ''; + component.querySelector('#info-field-event-description').value = description || ''; + changeInputValue(startTime, 'value', `${localStartTime}` || ''); + changeInputValue(endTime, 'value', `${localEndTime}` || ''); + changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); + changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); + changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); + changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); + changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); + + BlockMediator.set('eventDupMetrics', { + ...BlockMediator.get('eventDupMetrics'), + ...{ + title, + startDate: localStartDate, + eventId: eventData.eventId, + }, + }); + + component.classList.add('prefilled'); + } +} + +export function onEventUpdate(component, props) { + // Do nothing +} diff --git a/ecc/blocks/series-details-component/series-details-component.css b/ecc/blocks/series-details-component/series-details-component.css new file mode 100644 index 00000000..d07cc76a --- /dev/null +++ b/ecc/blocks/series-details-component/series-details-component.css @@ -0,0 +1,311 @@ +.event-info-component .info-field-wrapper { + margin-bottom: 24px; +} + +.event-info-component .attr-text { + font-size: var(--type-body-xs-size); + text-align: right; +} + +.event-info-component sp-textfield { + width: 100%; +} + +.event-info-component sp-textfield#info-field-event-title sp-help-text { + position: absolute; + display: none; +} + +.event-info-component sp-textfield#info-field-event-title.show-negative-help-text sp-help-text { + display: flex; +} + +.event-info-component sp-textfield.textarea-input { + font-size: var(--type-body-m-size);; + width: 100%; + outline: none; + resize: vertical; +} + +.event-info-component label { + font-weight: 700; +} + +.event-info-component .date-time-row { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: 24px; + margin-bottom: 24px; +} + +.event-info-component .date-time-row > .date-picker { + position: relative; + border-bottom: 1px solid var(--color-black); + min-width: 300px; + display: flex; + align-items: center; + gap: 8px; + margin-top: 16px; + cursor: pointer; +} + +.event-info-component .date-time-row > .date-picker input::placeholder { + font-size: var(--type-heading-m-size); + font-weight: 700; + font-family: var(--body-font-family); +} + +.event-info-component .date-time-row > div:last-of-type { + flex-grow: 1; +} + +.event-info-component .date-time-row input { + border: none; + width: 100%; + max-width: 360px; + pointer-events: none; +} + +.event-info-component .date-time-row .icon { + display: block; + cursor: pointer; +} + +.event-info-component .date-time-row > p { + font-size: var(--type-heading-m-size); + font-weight: 700; +} + +.event-info-component .date-time-row > p::before { + content: ''; + display: inline-block; + background-image: url('../../icons/clock.svg'); + background-repeat: no-repeat; + background-position: center; + width: 32px; + height: 16px; +} + +.event-info-component .time-inputs-wrapper { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.event-info-component .time-pickers { + display: flex; + gap: 32px; +} + +.event-info-component .time-pickers .time-picker-wrapper { + flex: 1 1 0; +} + +.event-info-component .time-pickers .time-picker-wrapper .select-wrapper { + display: flex; + gap: 8px; +} + +.event-info-component .time-zone-picker sp-picker, +.event-info-component .time-pickers .time-picker-wrapper sp-picker { + flex-grow: 1; + border-radius: 4px; + padding: 4px 0; + height: 32px; + width: 100%; + box-sizing: border-box; +} + +.event-info-component .time-pickers .time-picker-wrapper .select-wrapper sp-picker:last-of-type { + width: max-content; +} + +.event-info-component .time-pickers .time-picker-wrapper label { + display: block; + font-size: var(--type-body-xs-size); +} + +.event-info-component .time-zone-picker { + display: flex; + justify-content: flex-end; +} + +.event-info-component .date-input { + height: 40px; +} + +/* Calendar container styling */ +.event-info-component .calendar-container { + top: calc(100% + 8px); + right: 50%; + transform: translateX(50%); + padding: 16px; + width: 317px; + position: absolute; + margin: auto; + background: white; + box-shadow: 0 4px 8px rgb(0 0 0 / 10%); + border-radius: 8px; + overflow: hidden; + font-size: var(--type-body-xs-size); + z-index: 2; +} + +/* Calendar header */ +.event-info-component .calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + font-weight: 700; + font-size: var(--type-body-s-size); +} + +.event-info-component .calendar-header a.next-button, +.event-info-component .calendar-header a.prev-button { + color: var(--color-black); + font-weight: 400; + cursor: pointer; + user-select: none; +} + +.event-info-component .calendar-header a.next-button:hover, +.event-info-component .calendar-header a.prev-button:hover, +.event-info-component .calendar-header a.next-button:focus, +.event-info-component .calendar-header a.prev-button:focus { + text-decoration: none; +} + +.event-info-component .calendar-header .header-title { + cursor: pointer; +} + +/* Calendar grid */ +.event-info-component .calendar-grid, +.event-info-component .weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + text-align: center; + justify-items: center; +} + +.event-info-component .calendar-grid.month-view, +.event-info-component .calendar-grid.year-view { + grid-template-columns: repeat(4, 1fr); + height: calc(100% - 64px); +} + +.event-info-component .calendar-grid .calendar-month, +.event-info-component .calendar-grid .calendar-year { + align-content: center; + cursor: pointer; +} + +.event-info-component .weekdays .weekday { + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.event-info-component .calendar-day { + height: 32px; + width: 32px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; +} + +.event-info-component .calendar-day:last-child { + border-right: none; +} + +.event-info-component .calendar-day.empty { + cursor: default; +} + +.event-info-component .calendar-day.disabled, +.event-info-component .calendar-month.disabled, +.event-info-component .calendar-year.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Accessibility focus */ +.event-info-component .calendar-day:focus { + box-shadow: 0 0 0 2px rgb(0 123 255 / 50%); + outline: none; +} + +.event-info-component .calendar-day:hover:not(.empty), +.event-info-component .calendar-day:focus:not(.empty) { + outline: none; +} + +.event-info-component .calendar-day.selected { + background-color: var(--link-color-dark); + color: white; +} + +.event-info-component .calendar-day.range { + background-color: var(--color-info-accent-light); + overflow: visible; + color: var(--color-black); +} + +.event-info-component .calendar-day.range::before { + content: ''; + position: absolute; + width: calc(100% + 16px); + height: 100%; + top: 0; + left: 50%; + transform: translateX(-50%); + background-color: var(--color-info-accent-light); + z-index: -1; +} + +.event-info-component .calendar-container.range-selected .calendar-day.start-date:not(.end-date)::before { + content: ''; + position: absolute; + width: calc(100% + 16px); + border-radius: 24px 0 0 24px; + height: 100%; + top: 0; + left: 0 ; + background-color: var(--color-info-accent-light); + z-index: -1; +} + +.event-info-component .calendar-container.range-selected .calendar-day.end-date:not(.start-date)::before { + content: ''; + position: absolute; + width: calc(100% + 16px); + border-radius: 0 24px 24px 0; + height: 100%; + top: 0; + right: 0 ; + background-color: var(--color-info-accent-light); + z-index: -1; +} + +@media screen and (min-width: 900px) { + .event-info-component .date-time-row { + flex-direction: row; + } + + .event-info-component .time-inputs-wrapper { + width: auto; + } + + .event-info-component .date-time-row > p { + margin-left: 24px; + } +} diff --git a/ecc/blocks/series-details-component/series-details-component.js b/ecc/blocks/series-details-component/series-details-component.js new file mode 100644 index 00000000..bfe6d70d --- /dev/null +++ b/ecc/blocks/series-details-component/series-details-component.js @@ -0,0 +1,141 @@ +import { LIBS } from '../../scripts/scripts.js'; +import { + getIcon, + generateToolTip, + decorateTextfield, + decorateTextarea, + miloReplaceKey, +} from '../../scripts/utils.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); + +function buildDatePicker(column) { + column.classList.add('date-picker'); + const datePicker = createTag('input', { id: 'event-info-date-picker', name: 'event-date', class: 'date-input', required: true, placeholder: column.textContent.trim() }); + const calendarIcon = getIcon('calendar-add'); + + column.innerHTML = ''; + column.append(datePicker, calendarIcon); +} + +function buildTimePicker(column, wrapper) { + column.classList.add('time-pickers'); + const header = column.querySelector(':scope > p'); + const rows = column.querySelectorAll('table tr'); + const timePickerWrappers = []; + + rows.forEach((r, i) => { + const timePickerWrapper = createTag('div', { class: 'time-picker-wrapper' }); + const cols = r.querySelectorAll('td'); + let pickerName; + let pickerHandle; + if (i === 0) pickerHandle = 'start-time'; + if (i === 1) pickerHandle = 'end-time'; + cols.forEach((c, j) => { + if (j === 0) { + pickerName = c.textContent.trim(); + + const label = createTag('label', { for: `time-picker-${pickerHandle}` }, pickerName); + timePickerWrapper.append(label); + } + + if (j === 1) { + const timeSlots = c.querySelectorAll('li'); + const selectWrapper = createTag('div', { class: 'select-wrapper' }); + const submitValueHolder = createTag('input', { type: 'hidden', name: `time-picker-${pickerHandle}`, id: `time-picker-${pickerHandle}-value`, value: '' }); + const timeSelect = createTag('sp-picker', { id: `time-picker-${pickerHandle}`, class: 'select-input', required: true, label: '-' }); + const ampmSelect = createTag('sp-picker', { id: `ampm-picker-${pickerHandle}`, class: 'select-input', required: true, label: '-' }); + + timeSlots.forEach((t) => { + const text = t.textContent.trim(); + const opt = createTag('sp-menu-item', { value: text }, text); + timeSelect.append(opt); + }); + + ['AM', 'PM'].forEach((t, ti) => { + const opt = createTag('sp-menu-item', { value: t }, t); + if (ti === 0) opt.selected = true; + ampmSelect.append(opt); + }); + + selectWrapper.append(timeSelect, ampmSelect, submitValueHolder); + timePickerWrapper.append(selectWrapper); + } + }); + + timePickerWrappers.push(timePickerWrapper); + }); + + column.innerHTML = ''; + if (header) wrapper.before(header); + timePickerWrappers.forEach((w) => { column.append(w); }); + + wrapper.append(column); +} + +function decorateTimeZoneSelect(cell, wrapper) { + const phText = cell.querySelector('p')?.textContent.trim(); + const select = createTag('sp-picker', { id: 'time-zone-select-input', class: 'select-input', required: true, label: phText }); + const timeZones = cell.querySelectorAll('li'); + timeZones.forEach((t) => { + const text = t.textContent.trim(); + const opt = createTag('sp-menu-item', { value: text.split(' - ')[1] }, text); + select.append(opt); + }); + cell.innerHTML = ''; + cell.className = 'time-zone-picker'; + cell.append(select); + + wrapper.append(cell); +} + +async function decorateCloudTagSelect(column) { + const phText = column.textContent.trim(); + const buSelectWrapper = createTag('div', { class: 'bu-picker-wrapper' }); + const select = createTag('sp-picker', { id: 'bu-select-input', pending: true, class: 'select-input', size: 'm', label: phText }); + + column.innerHTML = ''; + buSelectWrapper.append(select); + column.append(buSelectWrapper); + + // FIXME: cloulds shouldn't be hardcoded + // const clouds = await getClouds(); + // const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }]; + const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }]; + + Object.entries(clouds).forEach(([, val]) => { + const opt = createTag('sp-menu-item', { value: val.id }, val.name); + select.append(opt); + }); + + select.pending = false; +} + +export default function init(el) { + el.classList.add('form-component'); + + const rows = el.querySelectorAll(':scope > div'); + rows.forEach(async (r, ri) => { + const cols = r.querySelectorAll(':scope > div'); + if (ri === 0) generateToolTip(r); + + if (ri === 1) { + r.classList.add('series-fields-wrapper'); + + cols.forEach(async (c, ci) => { + if (ci === 0) decorateCloudTagSelect(c); + if (ci === 1) decorateSeriesFormatSelect(c); + // if (ci === 2) decorateNewSeriesBtnAndModal(c); + // if (ci === 2) decorateCheckbox(c); + }); + } + + if (ri === 2) { + await decorateTextfield(r, { id: 'info-field-series-name' }, await miloReplaceKey('duplicate-series--error')); + } + + if (ri === 3) { + await decorateTextarea(r, { id: 'info-field-series-description', grows: true, quiet: true }); + } + }); +} From b58f3195f31a8151592c75120abc099446b70209 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 2 Dec 2024 10:39:32 -0600 Subject: [PATCH 27/74] WIP --- .../event-creation-form.js | 4 +- .../series-creation-form.js | 39 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index 8b01f6ce..43d61569 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -7,6 +7,7 @@ import { getEventPageHost, signIn, getEventServiceEnv, + getDevToken, } from '../../scripts/utils.js'; import { createEvent, @@ -1047,8 +1048,7 @@ export default async function init(el) { ...promises, ]); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { buildECCForm(el).then(() => { el.classList.remove('loading'); diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index f9beb18a..9f31ef62 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -163,7 +163,7 @@ function onStepValidate(props) { return function updateCtaStatus() { const currentFrag = getCurrentFragment(props); const stepValid = validateRequiredFields(props[`required-fields-in-${currentFrag.id}`]); - const ctas = props.el.querySelectorAll('.event-creation-form-panel-wrapper a'); + const ctas = props.el.querySelectorAll('.series-creation-form-panel-wrapper a'); const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); ctas.forEach((cta) => { @@ -488,8 +488,8 @@ function updateRequiredFields(props) { } function renderFormNavigation(props, prevStep, currentStep) { - const nextBtn = props.el.querySelector('.event-creation-form-ctas-panel .next-button'); - const backBtn = props.el.querySelector('.event-creation-form-ctas-panel .back-btn'); + const nextBtn = props.el.querySelector('.series-creation-form-ctas-panel .next-button'); + const backBtn = props.el.querySelector('.series-creation-form-ctas-panel .back-btn'); const frags = props.el.querySelectorAll('.fragment'); frags[prevStep].classList.add('hidden'); @@ -501,10 +501,9 @@ function renderFormNavigation(props, prevStep, currentStep) { } else { nextBtn.textContent = nextBtn.dataset.finalStateText; } - nextBtn.prepend(getIcon('golden-rocket')); } else { - nextBtn.textContent = nextBtn.dataset.nextStateText; - nextBtn.append(getIcon('chev-right-white')); + nextBtn.textContent = nextBtn.dataset.finalStateText; + nextBtn.prepend(getIcon('golden-rocket')); } backBtn.classList.toggle('disabled', currentStep === 0); @@ -710,13 +709,13 @@ function initFormCtas(props) { const ctaRow = props.el.querySelector(':scope > div:last-of-type'); decorateButtons(ctaRow, 'button-l'); const ctas = ctaRow.querySelectorAll('a'); - ctaRow.classList.add('event-creation-form-ctas-panel'); + ctaRow.classList.add('series-creation-form-ctas-panel'); const forwardActionsWrappers = ctaRow.querySelectorAll(':scope > div'); - const panelWrapper = createTag('div', { class: 'event-creation-form-panel-wrapper' }, '', { parent: ctaRow }); - const backwardWrapper = createTag('div', { class: 'event-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); - const forwardWrapper = createTag('div', { class: 'event-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); + const panelWrapper = createTag('div', { class: 'series-creation-form-panel-wrapper' }, '', { parent: ctaRow }); + const backwardWrapper = createTag('div', { class: 'series-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); + const forwardWrapper = createTag('div', { class: 'series-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); forwardActionsWrappers.forEach((w) => { w.classList.add('action-area'); @@ -753,11 +752,10 @@ function initFormCtas(props) { if (['#save', '#next'].includes(ctaUrl.hash)) { if (ctaUrl.hash === '#next') { cta.classList.add('next-button'); - const [nextStateText, finalStateText, doneStateText, republishStateText] = cta.textContent.split('||'); + const [finalStateText, doneStateText, republishStateText] = cta.textContent.split('||'); - cta.textContent = nextStateText; - cta.append(getIcon('chev-right-white')); - cta.dataset.nextStateText = nextStateText; + cta.textContent = finalStateText; + cta.prepend(getIcon('golden-rocket')); cta.dataset.finalStateText = finalStateText; cta.dataset.doneStateText = doneStateText; cta.dataset.republishStateText = republishStateText; @@ -836,7 +834,7 @@ function initFormCtas(props) { } function updateCtas(props) { - const formCtas = props.el.querySelectorAll('.event-creation-form-ctas-panel a'); + const formCtas = props.el.querySelectorAll('.series-creation-form-ctas-panel a'); const { eventDataResp } = props; formCtas.forEach((a) => { @@ -855,10 +853,9 @@ function updateCtas(props) { } else { a.textContent = a.dataset.finalStateText; } - a.prepend(getIcon('golden-rocket')); } else { - a.textContent = a.dataset.nextStateText; - a.append(getIcon('chev-right-white')); + a.textContent = a.dataset.finalStateText; + a.prepend(getIcon('golden-rocket')); } } }); @@ -931,7 +928,7 @@ function updateStatusTag(props) { headingWrapper.append(heading, statusTag); } -async function buildECCForm(el) { +async function buildForm(el) { const props = { el, currentStep: 0, @@ -1050,7 +1047,7 @@ export default async function init(el) { const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { - buildECCForm(el).then(() => { + buildForm(el).then(() => { el.classList.remove('loading'); }); return; @@ -1065,7 +1062,7 @@ export default async function init(el) { el.classList.remove('loading'); }, validProfile: () => { - buildECCForm(el).then(() => { + buildForm(el).then(() => { el.classList.remove('loading'); }); }, From 3965f116f8a2bce08b40692a90ebb74baca264f9 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 2 Dec 2024 14:56:37 -0600 Subject: [PATCH 28/74] WIP --- .../controller.js | 600 ++++++++++++++++++ .../series-additional-info-component.css | 39 ++ .../series-additional-info-component.js | 25 + .../series-creation-form.css | 35 +- .../series-creation-form.js | 2 +- .../series-details-component.css | 302 +-------- .../series-details-component.js | 102 +-- .../series-templates-component/controller.js | 600 ++++++++++++++++++ .../series-templates-component.css | 35 + .../series-templates-component.js | 17 + ecc/scripts/utils.js | 38 +- 11 files changed, 1410 insertions(+), 385 deletions(-) create mode 100644 ecc/blocks/series-additional-info-component/controller.js create mode 100644 ecc/blocks/series-additional-info-component/series-additional-info-component.css create mode 100644 ecc/blocks/series-additional-info-component/series-additional-info-component.js create mode 100644 ecc/blocks/series-templates-component/controller.js create mode 100644 ecc/blocks/series-templates-component/series-templates-component.css create mode 100644 ecc/blocks/series-templates-component/series-templates-component.js diff --git a/ecc/blocks/series-additional-info-component/controller.js b/ecc/blocks/series-additional-info-component/controller.js new file mode 100644 index 00000000..40ef879d --- /dev/null +++ b/ecc/blocks/series-additional-info-component/controller.js @@ -0,0 +1,600 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-use-before-define */ +import { getEvents } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; + +const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); + +function formatDate(date) { + let month = `${date.getMonth() + 1}`; + let day = `${date.getDate()}`; + const year = date.getFullYear(); + + if (month.length < 2) month = `0${month}`; + if (day.length < 2) day = `0${day}`; + + return [year, month, day].join('-'); +} + +function parseFormatedDate(string) { + if (!string) return null; + + const [year, month, day] = string.split('-'); + const date = new Date(year, +month - 1, day); + + return date; +} + +// Function to generate a calendar +function updateCalendar(component, parent, state) { + parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); + if (state.currentView === 'days') { + updateDayView(component, parent, state); + } else if (state.currentView === 'months') { + updateMonthView(component, parent, state); + } else if (state.currentView === 'years') { + updateYearView(component, parent, state); + } + + if (state.selectedStartDate && state.selectedEndDate) { + updateSelectedDates(state); + } +} + +function updateDayView(component, parent, state) { + state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; + const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); + weekdays.forEach((day) => { + createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); + }); + + const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); + const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); + const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); + const todayDate = new Date(); + + for (let i = 0; i < firstDayOfMonth; i += 1) { + createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); + } + for (let day = 1; day <= daysInMonth; day += 1) { + const date = new Date(state.currentYear, state.currentMonth, day); + const dayElement = createTag('div', { + class: 'calendar-day', + tabindex: '0', + 'data-date': formatDate(date), + }, day.toString(), { parent: calendarGrid }); + + if (date < todayDate) { + dayElement.classList.add('disabled'); + } else { + dayElement.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + selectDate(component, state, date); + event.preventDefault(); + } + }); + dayElement.addEventListener('click', () => selectDate(component, state, date)); + } + + // Mark today's date + if (date === todayDate) { + dayElement.classList.add('today'); + } + } +} + +function updateMonthView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear}`; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + months.forEach((month, index) => { + const monthElement = createTag('div', { + class: 'calendar-month', + 'data-month': index, + }, month, { parent: calendarGrid }); + + // Disable past months in the current year + if ((state.currentYear === currentYear && index < currentMonth) + || state.currentYear < currentYear) { + monthElement.classList.add('disabled'); + } else { + monthElement.addEventListener('click', () => { + state.currentMonth = index; + state.currentView = 'days'; + updateCalendar(component, parent, state); + }); + } + }); +} + +function isSameDay(date1, date2) { + return date1.getFullYear() === date2.getFullYear() + && date1.getMonth() === date2.getMonth() + && date1.getDate() === date2.getDate(); +} + +function updateYearView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; + const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); + const currentYear = new Date().getFullYear(); + + for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { + const yearElement = createTag('div', { + class: 'calendar-year', + 'data-year': year, + }, year.toString(), { parent: calendarGrid }); + + // Disable past years + if (year < currentYear) { + yearElement.classList.add('disabled'); + } else { + yearElement.addEventListener('click', () => { + state.currentYear = year; + state.currentView = 'months'; + updateCalendar(component, parent, state); + }); + } + } +} + +function selectDate(component, state, date) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { + state.selectedStartDate = date; + state.selectedEndDate = date; + } else if ((state.selectedStartDate && !state.selectedEndDate) + || (state.selectedStartDate === state.selectedEndDate)) { + if (date < state.selectedStartDate) { + state.selectedEndDate = state.selectedStartDate; + state.selectedStartDate = date; + } else { + state.selectedEndDate = date; + } + } + + updateSelectedDates(state); + updateInput(component, state); + input.dispatchEvent(new Event('change')); +} + +function updateInput(component, state) { + const dateInput = component.querySelector('#event-info-date-picker'); + + if (dateInput) { + if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); + if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); + + if (dateInput.dataset.startDate && dateInput.dataset.endDate) { + const options = { year: 'numeric', month: 'long', day: 'numeric' }; + const dateLocale = getConfig().locale?.ietf || 'en-US'; + const startDateTime = state.selectedStartDate + .toLocaleDateString(dateLocale, options); + const endDateTime = state.selectedEndDate + .toLocaleDateString(dateLocale, options); + const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate + ? startDateTime : `${startDateTime} - ${endDateTime}`; + dateInput.value = dateValue; + } + } +} + +function updateSelectedDates(state) { + const { parent } = state; + parent.querySelectorAll('.calendar-day').forEach((dayElement) => { + if (!dayElement.getAttribute('data-date')) return; + + const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); + dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); + dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); + // Mark the start date and end date + + if (isSameDay(clickedDate, state.selectedStartDate) + && isSameDay(state.selectedStartDate, state.selectedEndDate)) { + dayElement.classList.add('start-date', 'end-date'); + } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { + dayElement.classList.remove('end-date'); + dayElement.classList.add('start-date'); + } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { + dayElement.classList.remove('start-date'); + dayElement.classList.add('end-date'); + } else { + dayElement.classList.remove('start-date', 'end-date'); + } + + parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); + }); +} + +function changeCalendarPage(component, state, delta) { + if (state.currentView === 'days') { + state.currentMonth += delta; + if (state.currentMonth < 0) { + state.currentMonth = 11; + state.currentYear -= 1; + } else if (state.currentMonth > 11) { + state.currentMonth = 0; + state.currentYear += 1; + } + } else if (state.currentView === 'months') { + state.currentYear += delta; + } else if (state.currentView === 'years') { + state.currentYear += delta * 10; + } + updateCalendar(component, state.parent, state); +} + +function initInputWatcher(input, onChange) { + const config = { attributes: true, childList: false, subtree: false }; + + const callback = (mutationList) => { + const [mutation] = mutationList; + if (mutation.target.disabled) { + onChange(); + } + }; + + const observer = new MutationObserver(callback); + observer.observe(input, config); +} + +function buildCalendar(component, parent) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + const state = { + currentView: 'days', + selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, + selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, + currentYear: new Date().getFullYear(), + currentMonth: new Date().getMonth(), + headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), + parent, + }; + + const header = createTag('div', { class: 'calendar-header' }, null, { parent }); + const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); + header.append(state.headerTitle); + const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); + + prevButton.onclick = () => changeCalendarPage(component, state, -1); + nextButton.onclick = () => changeCalendarPage(component, state, 1); + + state.headerTitle.addEventListener('click', () => { + // eslint-disable-next-line no-nested-ternary + state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; + updateCalendar(component, parent, state); + }); + + updateCalendar(component, parent, state); +} + +function initCalendar(component) { + let calendar; + const datePickerContainer = component.querySelector('.date-picker'); + const input = component.querySelector('#event-info-date-picker'); + + datePickerContainer.addEventListener('click', () => { + if (calendar || input.disabled) return; + calendar = createTag('div', { class: 'calendar-container' }); + datePickerContainer.append(calendar); + buildCalendar(component, calendar); + }); + + document.addEventListener('click', (e) => { + if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { + calendar.remove(); + calendar = ''; + } + }); + + initInputWatcher(input, () => { + calendar.remove(); + calendar = ''; + }); +} + +function dateTimeStringToTimestamp(dateString, timeString) { + const dateTimeString = `${dateString}T${timeString}`; + + const date = new Date(dateTimeString); + + return date.getTime(); +} + +export function onSubmit(component, props) { + if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const title = component.querySelector('#info-field-event-title').value; + const description = component.querySelector('#info-field-event-description').value; + const datePicker = component.querySelector('#event-info-date-picker'); + const localStartDate = datePicker.dataset.startDate; + const localEndDate = datePicker.dataset.endDate; + + const localStartTime = component.querySelector('#time-picker-start-time-value').value; + const localEndTime = component.querySelector('#time-picker-end-time-value').value; + + const timezone = component.querySelector('#time-zone-select-input').value; + + const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); + const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); + + const eventInfo = { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + localStartTimeMillis, + localEndTimeMillis, + timezone, + }; + + props.payload = { ...props.payload, ...eventInfo }; +} + +export async function onPayloadUpdate(component, props) { + // do nothing +} + +export async function onRespUpdate(_component, _props) { + // Do nothing +} + +function checkEventDuplication(event, compareMetrics) { + const titleMatch = event.title === compareMetrics.title; + const startDateMatch = event.localStartDate === compareMetrics.startDate; + const venueIdMatch = event.venue?.city === compareMetrics.city; + const eventIdNoMatch = event.eventId !== compareMetrics.eventId; + + return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; +} + +export default async function init(component, props) { + const allEventsResp = await getEvents(); + const allEvents = allEventsResp?.events; + const eventData = props.eventDataResp; + const sameSeriesEvents = allEvents?.filter((e) => { + const matchInPayload = e.seriesId === props.payload.seriesId; + const matchInResp = e.seriesId === eventData.seriesId; + return matchInPayload || matchInResp; + }) || []; + + const eventTitleInput = component.querySelector('#info-field-event-title'); + const startTimeInput = component.querySelector('#time-picker-start-time'); + const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); + const startAmpmInput = component.querySelector('#ampm-picker-start-time'); + const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); + const endTimeInput = component.querySelector('#time-picker-end-time'); + const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); + const endAmpmInput = component.querySelector('#ampm-picker-end-time'); + const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); + const startTime = component.querySelector('#time-picker-start-time-value'); + const endTime = component.querySelector('#time-picker-end-time-value'); + const datePicker = component.querySelector('#event-info-date-picker'); + + initCalendar(component); + + eventTitleInput.addEventListener('input', () => { + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); + }); + + const resetAllOptions = () => { + [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] + .forEach((options) => { + options.forEach((option) => { + option.disabled = false; + }); + }); + }; + + const sameDayEvent = () => datePicker.dataset.startDate + && datePicker.dataset.endDate + && datePicker.dataset.startDate === datePicker.dataset.endDate; + + const onEndTimeUpdate = () => { + if (endAmpmInput.value && endTimeInput.value) { + endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); + } else { + endTime.value = null; + } + + if (!sameDayEvent()) return; + + startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; + let onlyPossibleStartAmpm = startAmpmInput.value; + if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; + + if (startTimeInput.value) { + if (onlyPossibleStartAmpm) { + const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); + if (endAmpmInput.value) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + } + } + + if (endTime.value) { + if (onlyPossibleStartAmpm) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); + option.disabled = optionTime >= endTime.value; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= endTime.value; + }); + } + } + }; + + const onStartTimeUpdate = () => { + if (startAmpmInput.value && startTimeInput.value) { + startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); + } else { + startTime.value = null; + } + + if (!sameDayEvent()) return; + + endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; + let onlyPossibleEndAmpm = endAmpmInput.value; + if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; + + if (endTimeInput.value) { + if (onlyPossibleEndAmpm) { + const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); + if (startAmpmInput.value) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + } + } + + if (startTime.value) { + if (onlyPossibleEndAmpm) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); + option.disabled = optionTime <= startTime.value; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= startTime.value; + }); + } + } + }; + + const updateTimeOptionsBasedOnDate = () => { + if (!sameDayEvent()) { + resetAllOptions(); + } else { + startTimeInput.value = ''; + startAmpmInput.value = ''; + endTimeInput.value = ''; + endAmpmInput.value = ''; + startTime.value = null; + endTime.value = null; + + resetAllOptions(); + } + }; + + startTimeInput.addEventListener('change', onStartTimeUpdate); + endTimeInput.addEventListener('change', onEndTimeUpdate); + startAmpmInput.addEventListener('change', onStartTimeUpdate); + endAmpmInput.addEventListener('change', onEndTimeUpdate); + + datePicker.addEventListener('change', (e) => { + updateTimeOptionsBasedOnDate(e); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); + }); + + BlockMediator.subscribe('eventDupMetrics', (store) => { + const metrics = store.newValue; + const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); + + helpText.textContent = helpText.textContent + .replace('[[seriesName]]', metrics.seriesName) + .replace('[[eventName]]', metrics.title); + + const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); + if (isDup) { + props.el.classList.add('show-dup-event-error'); + eventTitleInput.invalid = true; + } else { + props.el.classList.remove('show-dup-event-error'); + eventTitleInput.invalid = false; + } + + eventTitleInput.dispatchEvent(new Event('change')); + }); + + const { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + timezone, + } = eventData; + + if (title + && description + && localStartDate + && localEndDate + && localStartTime + && localEndTime + && timezone) { + const startTimePieces = parse24HourFormat(localStartTime); + const endTimePieces = parse24HourFormat(localEndTime); + + datePicker.dataset.startDate = localStartDate || ''; + datePicker.dataset.endDate = localEndDate || ''; + updateInput(component, { + selectedStartDate: parseFormatedDate(localStartDate), + selectedEndDate: parseFormatedDate(localEndDate), + }); + + component.querySelector('#info-field-event-title').value = title || ''; + component.querySelector('#info-field-event-description').value = description || ''; + changeInputValue(startTime, 'value', `${localStartTime}` || ''); + changeInputValue(endTime, 'value', `${localEndTime}` || ''); + changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); + changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); + changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); + changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); + changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); + + BlockMediator.set('eventDupMetrics', { + ...BlockMediator.get('eventDupMetrics'), + ...{ + title, + startDate: localStartDate, + eventId: eventData.eventId, + }, + }); + + component.classList.add('prefilled'); + } +} + +export function onEventUpdate(component, props) { + // Do nothing +} diff --git a/ecc/blocks/series-additional-info-component/series-additional-info-component.css b/ecc/blocks/series-additional-info-component/series-additional-info-component.css new file mode 100644 index 00000000..00db03d4 --- /dev/null +++ b/ecc/blocks/series-additional-info-component/series-additional-info-component.css @@ -0,0 +1,39 @@ +.series-additional-info-component .labeled-text-field-wrapper { + margin-bottom: 40px; +} + +.series-additional-info-component .attr-text { + font-size: var(--type-body-xs-size); + text-align: right; +} + +.series-additional-info-component sp-field-label { + width: 180px; + font-family: var(--body-font-family); + font-size: var(--type-body-s-size); + font-weight: 700; +} + +.series-additional-info-component sp-textfield { + width: 493px; +} + +.series-additional-info-component sp-textfield#info-field-event-title sp-help-text { + position: absolute; + display: none; +} + +.series-additional-info-component sp-textfield#info-field-event-title.show-negative-help-text sp-help-text { + display: flex; +} + +.series-additional-info-component sp-textfield.textarea-input { + font-size: var(--type-body-m-size);; + width: 100%; + outline: none; + resize: vertical; +} + +.series-additional-info-component label { + font-weight: 700; +} diff --git a/ecc/blocks/series-additional-info-component/series-additional-info-component.js b/ecc/blocks/series-additional-info-component/series-additional-info-component.js new file mode 100644 index 00000000..eb7ddb3e --- /dev/null +++ b/ecc/blocks/series-additional-info-component/series-additional-info-component.js @@ -0,0 +1,25 @@ +import { + generateToolTip, + decorateLabeledTextfield, +} from '../../scripts/utils.js'; + +export default function init(el) { + el.classList.add('form-component'); + + const rows = el.querySelectorAll(':scope > div'); + rows.forEach(async (r, ri) => { + if (ri === 0) generateToolTip(r); + + if (ri === 1) { + await decorateLabeledTextfield(r, { id: 'info-field-series-susi' }); + } + + if (ri === 2) { + await decorateLabeledTextfield(r, { id: 'info-field-series-related-domain' }); + } + + if (ri === 3) { + await decorateLabeledTextfield(r, { id: 'info-field-series-ext-id' }); + } + }); +} diff --git a/ecc/blocks/series-creation-form/series-creation-form.css b/ecc/blocks/series-creation-form/series-creation-form.css index da565c1f..8507378c 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.css +++ b/ecc/blocks/series-creation-form/series-creation-form.css @@ -42,11 +42,11 @@ line-height: normal; } -.series-creation-form .main-frame sp-theme sp-underlay { +.series-creation-form .main-frame sp-underlay { z-index: 2; } -.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog { +.series-creation-form .main-frame sp-underlay + sp-dialog { position: fixed; top: 50%; left: 50%; @@ -56,21 +56,21 @@ min-width: 480px; } -.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog h1 { +.series-creation-form .main-frame sp-underlay + sp-dialog h1 { font-size: var(--type-heading-s-size); } -.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog p { +.series-creation-form .main-frame sp-underlay + sp-dialog p { font-size: var(--type-body-s-size); } -.series-creation-form .main-frame sp-theme sp-underlay + sp-dialog .button-container { +.series-creation-form .main-frame sp-underlay + sp-dialog .button-container { display: flex; justify-content: flex-end; gap: 16px; } -.series-creation-form .main-frame sp-theme sp-underlay:not([open]) + sp-dialog { +.series-creation-form .main-frame sp-underlay:not([open]) + sp-dialog { display: none; } @@ -91,6 +91,10 @@ .series-creation-form .main-frame { flex-grow: 1; min-height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; } .series-creation-form .series-creation-form-ctas-panel { @@ -254,14 +258,6 @@ opacity: 0.5; } -.series-creation-form .main-frame sp-theme { - min-height: 100%; - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; -} - .series-creation-form .main-frame .section .content { max-width: none; } @@ -290,7 +286,8 @@ } .series-creation-form .form-component > div:first-of-type > div > h2, -.series-creation-form .form-component > div:first-of-type > div > h3 { +.series-creation-form .form-component > div:first-of-type > div > h3, +.series-creation-form .form-component > div:first-of-type > div > h4 { display: flex; align-items: center; gap: 8px; @@ -356,8 +353,10 @@ .series-creation-form .form-component > div:first-of-type > div > h2 sp-action-button, .series-creation-form .form-component > div:first-of-type > div > h3 sp-action-button, +.series-creation-form .form-component > div:first-of-type > div > h4 sp-action-button, .series-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button, -.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button { +.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button, +.series-creation-form .section:not(:first-of-type) > div.content > h4 sp-action-button { padding: 0; background: none; border: none; @@ -366,8 +365,10 @@ .series-creation-form .form-component > div:first-of-type > div > h2 sp-action-button .icon-info, .series-creation-form .form-component > div:first-of-type > div > h3 sp-action-button .icon-info, +.series-creation-form .form-component > div:first-of-type > div > h4 sp-action-button .icon-info, .series-creation-form .section:not(:first-of-type) > div.content > h2 sp-action-button .icon-info, -.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button .icon-info { +.series-creation-form .section:not(:first-of-type) > div.content > h3 sp-action-button .icon-info, +.series-creation-form .section:not(:first-of-type) > div.content > h4 sp-action-button .icon-info { display: block; } diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 9f31ef62..299340a0 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -365,7 +365,7 @@ function decorateForm(el) { form.append(formDiv.parentElement); }); - const cols = formBodyRow.querySelectorAll(':scope > div'); + const cols = formBodyRow.querySelectorAll(':scope > div, :scope > sp-theme'); cols.forEach((col, i) => { if (i === 0) { diff --git a/ecc/blocks/series-details-component/series-details-component.css b/ecc/blocks/series-details-component/series-details-component.css index d07cc76a..e423dcca 100644 --- a/ecc/blocks/series-details-component/series-details-component.css +++ b/ecc/blocks/series-details-component/series-details-component.css @@ -1,311 +1,47 @@ -.event-info-component .info-field-wrapper { +.series-details-component .info-field-wrapper { margin-bottom: 24px; } -.event-info-component .attr-text { +.series-details-component .attr-text { font-size: var(--type-body-xs-size); text-align: right; } -.event-info-component sp-textfield { - width: 100%; -} - -.event-info-component sp-textfield#info-field-event-title sp-help-text { - position: absolute; - display: none; -} - -.event-info-component sp-textfield#info-field-event-title.show-negative-help-text sp-help-text { - display: flex; -} - -.event-info-component sp-textfield.textarea-input { - font-size: var(--type-body-m-size);; - width: 100%; - outline: none; - resize: vertical; -} - -.event-info-component label { - font-weight: 700; -} - -.event-info-component .date-time-row { - display: flex; - align-items: flex-start; - flex-direction: column; - gap: 24px; - margin-bottom: 24px; -} - -.event-info-component .date-time-row > .date-picker { - position: relative; - border-bottom: 1px solid var(--color-black); - min-width: 300px; - display: flex; - align-items: center; - gap: 8px; - margin-top: 16px; - cursor: pointer; -} - -.event-info-component .date-time-row > .date-picker input::placeholder { - font-size: var(--type-heading-m-size); - font-weight: 700; - font-family: var(--body-font-family); -} - -.event-info-component .date-time-row > div:last-of-type { - flex-grow: 1; -} - -.event-info-component .date-time-row input { - border: none; - width: 100%; - max-width: 360px; - pointer-events: none; -} - -.event-info-component .date-time-row .icon { - display: block; - cursor: pointer; -} - -.event-info-component .date-time-row > p { - font-size: var(--type-heading-m-size); - font-weight: 700; -} - -.event-info-component .date-time-row > p::before { - content: ''; - display: inline-block; - background-image: url('../../icons/clock.svg'); - background-repeat: no-repeat; - background-position: center; - width: 32px; - height: 16px; -} - -.event-info-component .time-inputs-wrapper { +.series-details-component .series-fields-wrapper { display: flex; - flex-direction: column; - gap: 24px; - width: 100%; + gap: 64px; } -.event-info-component .time-pickers { +.series-details-component .format-picker-wrapper { display: flex; - gap: 32px; + gap: 12px; } -.event-info-component .time-pickers .time-picker-wrapper { - flex: 1 1 0; +.series-details-component .series-fields-wrapper, +.series-details-component .text-field-row { + margin-bottom: 52px; } -.event-info-component .time-pickers .time-picker-wrapper .select-wrapper { - display: flex; - gap: 8px; -} - -.event-info-component .time-zone-picker sp-picker, -.event-info-component .time-pickers .time-picker-wrapper sp-picker { - flex-grow: 1; - border-radius: 4px; - padding: 4px 0; - height: 32px; +.series-details-component sp-textfield { width: 100%; - box-sizing: border-box; -} - -.event-info-component .time-pickers .time-picker-wrapper .select-wrapper sp-picker:last-of-type { - width: max-content; -} - -.event-info-component .time-pickers .time-picker-wrapper label { - display: block; - font-size: var(--type-body-xs-size); -} - -.event-info-component .time-zone-picker { - display: flex; - justify-content: flex-end; -} - -.event-info-component .date-input { - height: 40px; } -/* Calendar container styling */ -.event-info-component .calendar-container { - top: calc(100% + 8px); - right: 50%; - transform: translateX(50%); - padding: 16px; - width: 317px; +.series-details-component sp-textfield#info-field-series-name sp-help-text { position: absolute; - margin: auto; - background: white; - box-shadow: 0 4px 8px rgb(0 0 0 / 10%); - border-radius: 8px; - overflow: hidden; - font-size: var(--type-body-xs-size); - z-index: 2; -} - -/* Calendar header */ -.event-info-component .calendar-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - font-weight: 700; - font-size: var(--type-body-s-size); -} - -.event-info-component .calendar-header a.next-button, -.event-info-component .calendar-header a.prev-button { - color: var(--color-black); - font-weight: 400; - cursor: pointer; - user-select: none; -} - -.event-info-component .calendar-header a.next-button:hover, -.event-info-component .calendar-header a.prev-button:hover, -.event-info-component .calendar-header a.next-button:focus, -.event-info-component .calendar-header a.prev-button:focus { - text-decoration: none; -} - -.event-info-component .calendar-header .header-title { - cursor: pointer; -} - -/* Calendar grid */ -.event-info-component .calendar-grid, -.event-info-component .weekdays { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 8px; - text-align: center; - justify-items: center; -} - -.event-info-component .calendar-grid.month-view, -.event-info-component .calendar-grid.year-view { - grid-template-columns: repeat(4, 1fr); - height: calc(100% - 64px); -} - -.event-info-component .calendar-grid .calendar-month, -.event-info-component .calendar-grid .calendar-year { - align-content: center; - cursor: pointer; -} - -.event-info-component .weekdays .weekday { - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; + display: none; } -.event-info-component .calendar-day { - height: 32px; - width: 32px; - position: relative; +.series-details-component sp-textfield#info-field-series-name.show-negative-help-text sp-help-text { display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 50%; -} - -.event-info-component .calendar-day:last-child { - border-right: none; } -.event-info-component .calendar-day.empty { - cursor: default; -} - -.event-info-component .calendar-day.disabled, -.event-info-component .calendar-month.disabled, -.event-info-component .calendar-year.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Accessibility focus */ -.event-info-component .calendar-day:focus { - box-shadow: 0 0 0 2px rgb(0 123 255 / 50%); - outline: none; -} - -.event-info-component .calendar-day:hover:not(.empty), -.event-info-component .calendar-day:focus:not(.empty) { +.series-details-component sp-textfield.textarea-input { + font-size: var(--type-body-m-size);; + width: 100%; outline: none; + resize: vertical; } -.event-info-component .calendar-day.selected { - background-color: var(--link-color-dark); - color: white; -} - -.event-info-component .calendar-day.range { - background-color: var(--color-info-accent-light); - overflow: visible; - color: var(--color-black); -} - -.event-info-component .calendar-day.range::before { - content: ''; - position: absolute; - width: calc(100% + 16px); - height: 100%; - top: 0; - left: 50%; - transform: translateX(-50%); - background-color: var(--color-info-accent-light); - z-index: -1; -} - -.event-info-component .calendar-container.range-selected .calendar-day.start-date:not(.end-date)::before { - content: ''; - position: absolute; - width: calc(100% + 16px); - border-radius: 24px 0 0 24px; - height: 100%; - top: 0; - left: 0 ; - background-color: var(--color-info-accent-light); - z-index: -1; -} - -.event-info-component .calendar-container.range-selected .calendar-day.end-date:not(.start-date)::before { - content: ''; - position: absolute; - width: calc(100% + 16px); - border-radius: 0 24px 24px 0; - height: 100%; - top: 0; - right: 0 ; - background-color: var(--color-info-accent-light); - z-index: -1; -} - -@media screen and (min-width: 900px) { - .event-info-component .date-time-row { - flex-direction: row; - } - - .event-info-component .time-inputs-wrapper { - width: auto; - } - - .event-info-component .date-time-row > p { - margin-left: 24px; - } +.series-details-component label { + font-weight: 700; } diff --git a/ecc/blocks/series-details-component/series-details-component.js b/ecc/blocks/series-details-component/series-details-component.js index bfe6d70d..3f22de45 100644 --- a/ecc/blocks/series-details-component/series-details-component.js +++ b/ecc/blocks/series-details-component/series-details-component.js @@ -1,6 +1,5 @@ import { LIBS } from '../../scripts/scripts.js'; import { - getIcon, generateToolTip, decorateTextfield, decorateTextarea, @@ -9,86 +8,6 @@ import { const { createTag } = await import(`${LIBS}/utils/utils.js`); -function buildDatePicker(column) { - column.classList.add('date-picker'); - const datePicker = createTag('input', { id: 'event-info-date-picker', name: 'event-date', class: 'date-input', required: true, placeholder: column.textContent.trim() }); - const calendarIcon = getIcon('calendar-add'); - - column.innerHTML = ''; - column.append(datePicker, calendarIcon); -} - -function buildTimePicker(column, wrapper) { - column.classList.add('time-pickers'); - const header = column.querySelector(':scope > p'); - const rows = column.querySelectorAll('table tr'); - const timePickerWrappers = []; - - rows.forEach((r, i) => { - const timePickerWrapper = createTag('div', { class: 'time-picker-wrapper' }); - const cols = r.querySelectorAll('td'); - let pickerName; - let pickerHandle; - if (i === 0) pickerHandle = 'start-time'; - if (i === 1) pickerHandle = 'end-time'; - cols.forEach((c, j) => { - if (j === 0) { - pickerName = c.textContent.trim(); - - const label = createTag('label', { for: `time-picker-${pickerHandle}` }, pickerName); - timePickerWrapper.append(label); - } - - if (j === 1) { - const timeSlots = c.querySelectorAll('li'); - const selectWrapper = createTag('div', { class: 'select-wrapper' }); - const submitValueHolder = createTag('input', { type: 'hidden', name: `time-picker-${pickerHandle}`, id: `time-picker-${pickerHandle}-value`, value: '' }); - const timeSelect = createTag('sp-picker', { id: `time-picker-${pickerHandle}`, class: 'select-input', required: true, label: '-' }); - const ampmSelect = createTag('sp-picker', { id: `ampm-picker-${pickerHandle}`, class: 'select-input', required: true, label: '-' }); - - timeSlots.forEach((t) => { - const text = t.textContent.trim(); - const opt = createTag('sp-menu-item', { value: text }, text); - timeSelect.append(opt); - }); - - ['AM', 'PM'].forEach((t, ti) => { - const opt = createTag('sp-menu-item', { value: t }, t); - if (ti === 0) opt.selected = true; - ampmSelect.append(opt); - }); - - selectWrapper.append(timeSelect, ampmSelect, submitValueHolder); - timePickerWrapper.append(selectWrapper); - } - }); - - timePickerWrappers.push(timePickerWrapper); - }); - - column.innerHTML = ''; - if (header) wrapper.before(header); - timePickerWrappers.forEach((w) => { column.append(w); }); - - wrapper.append(column); -} - -function decorateTimeZoneSelect(cell, wrapper) { - const phText = cell.querySelector('p')?.textContent.trim(); - const select = createTag('sp-picker', { id: 'time-zone-select-input', class: 'select-input', required: true, label: phText }); - const timeZones = cell.querySelectorAll('li'); - timeZones.forEach((t) => { - const text = t.textContent.trim(); - const opt = createTag('sp-menu-item', { value: text.split(' - ')[1] }, text); - select.append(opt); - }); - cell.innerHTML = ''; - cell.className = 'time-zone-picker'; - cell.append(select); - - wrapper.append(cell); -} - async function decorateCloudTagSelect(column) { const phText = column.textContent.trim(); const buSelectWrapper = createTag('div', { class: 'bu-picker-wrapper' }); @@ -111,6 +30,25 @@ async function decorateCloudTagSelect(column) { select.pending = false; } +function decorateSeriesFormatSelect(cell) { + const formatSelectWrapper = createTag('div', { class: 'format-picker-wrapper' }); + const label = createTag('sp-field-label', { for: 'format-select-input' }, cell.textContent.trim()); + const select = createTag('sp-picker', { id: 'format-select-input', class: 'select-input', size: 'm', label: 'Format' }); + const options = [ + { id: 'InPerson', name: 'In-Person' }, + { id: 'Webinar', name: 'Webinar' }, + ]; + + options.forEach((o) => { + const opt = createTag('sp-menu-item', { value: o.id }, o.name); + select.append(opt); + }); + + cell.innerHTML = ''; + formatSelectWrapper.append(label, select); + cell.append(formatSelectWrapper); +} + export default function init(el) { el.classList.add('form-component'); @@ -125,8 +63,6 @@ export default function init(el) { cols.forEach(async (c, ci) => { if (ci === 0) decorateCloudTagSelect(c); if (ci === 1) decorateSeriesFormatSelect(c); - // if (ci === 2) decorateNewSeriesBtnAndModal(c); - // if (ci === 2) decorateCheckbox(c); }); } diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js new file mode 100644 index 00000000..40ef879d --- /dev/null +++ b/ecc/blocks/series-templates-component/controller.js @@ -0,0 +1,600 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-use-before-define */ +import { getEvents } from '../../scripts/esp-controller.js'; +import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { LIBS } from '../../scripts/scripts.js'; +import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; + +const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); + +function formatDate(date) { + let month = `${date.getMonth() + 1}`; + let day = `${date.getDate()}`; + const year = date.getFullYear(); + + if (month.length < 2) month = `0${month}`; + if (day.length < 2) day = `0${day}`; + + return [year, month, day].join('-'); +} + +function parseFormatedDate(string) { + if (!string) return null; + + const [year, month, day] = string.split('-'); + const date = new Date(year, +month - 1, day); + + return date; +} + +// Function to generate a calendar +function updateCalendar(component, parent, state) { + parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); + if (state.currentView === 'days') { + updateDayView(component, parent, state); + } else if (state.currentView === 'months') { + updateMonthView(component, parent, state); + } else if (state.currentView === 'years') { + updateYearView(component, parent, state); + } + + if (state.selectedStartDate && state.selectedEndDate) { + updateSelectedDates(state); + } +} + +function updateDayView(component, parent, state) { + state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; + const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); + weekdays.forEach((day) => { + createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); + }); + + const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); + const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); + const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); + const todayDate = new Date(); + + for (let i = 0; i < firstDayOfMonth; i += 1) { + createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); + } + for (let day = 1; day <= daysInMonth; day += 1) { + const date = new Date(state.currentYear, state.currentMonth, day); + const dayElement = createTag('div', { + class: 'calendar-day', + tabindex: '0', + 'data-date': formatDate(date), + }, day.toString(), { parent: calendarGrid }); + + if (date < todayDate) { + dayElement.classList.add('disabled'); + } else { + dayElement.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + selectDate(component, state, date); + event.preventDefault(); + } + }); + dayElement.addEventListener('click', () => selectDate(component, state, date)); + } + + // Mark today's date + if (date === todayDate) { + dayElement.classList.add('today'); + } + } +} + +function updateMonthView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear}`; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + months.forEach((month, index) => { + const monthElement = createTag('div', { + class: 'calendar-month', + 'data-month': index, + }, month, { parent: calendarGrid }); + + // Disable past months in the current year + if ((state.currentYear === currentYear && index < currentMonth) + || state.currentYear < currentYear) { + monthElement.classList.add('disabled'); + } else { + monthElement.addEventListener('click', () => { + state.currentMonth = index; + state.currentView = 'days'; + updateCalendar(component, parent, state); + }); + } + }); +} + +function isSameDay(date1, date2) { + return date1.getFullYear() === date2.getFullYear() + && date1.getMonth() === date2.getMonth() + && date1.getDate() === date2.getDate(); +} + +function updateYearView(component, parent, state) { + state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; + const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); + const currentYear = new Date().getFullYear(); + + for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { + const yearElement = createTag('div', { + class: 'calendar-year', + 'data-year': year, + }, year.toString(), { parent: calendarGrid }); + + // Disable past years + if (year < currentYear) { + yearElement.classList.add('disabled'); + } else { + yearElement.addEventListener('click', () => { + state.currentYear = year; + state.currentView = 'months'; + updateCalendar(component, parent, state); + }); + } + } +} + +function selectDate(component, state, date) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { + state.selectedStartDate = date; + state.selectedEndDate = date; + } else if ((state.selectedStartDate && !state.selectedEndDate) + || (state.selectedStartDate === state.selectedEndDate)) { + if (date < state.selectedStartDate) { + state.selectedEndDate = state.selectedStartDate; + state.selectedStartDate = date; + } else { + state.selectedEndDate = date; + } + } + + updateSelectedDates(state); + updateInput(component, state); + input.dispatchEvent(new Event('change')); +} + +function updateInput(component, state) { + const dateInput = component.querySelector('#event-info-date-picker'); + + if (dateInput) { + if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); + if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); + + if (dateInput.dataset.startDate && dateInput.dataset.endDate) { + const options = { year: 'numeric', month: 'long', day: 'numeric' }; + const dateLocale = getConfig().locale?.ietf || 'en-US'; + const startDateTime = state.selectedStartDate + .toLocaleDateString(dateLocale, options); + const endDateTime = state.selectedEndDate + .toLocaleDateString(dateLocale, options); + const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate + ? startDateTime : `${startDateTime} - ${endDateTime}`; + dateInput.value = dateValue; + } + } +} + +function updateSelectedDates(state) { + const { parent } = state; + parent.querySelectorAll('.calendar-day').forEach((dayElement) => { + if (!dayElement.getAttribute('data-date')) return; + + const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); + dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); + dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); + // Mark the start date and end date + + if (isSameDay(clickedDate, state.selectedStartDate) + && isSameDay(state.selectedStartDate, state.selectedEndDate)) { + dayElement.classList.add('start-date', 'end-date'); + } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { + dayElement.classList.remove('end-date'); + dayElement.classList.add('start-date'); + } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { + dayElement.classList.remove('start-date'); + dayElement.classList.add('end-date'); + } else { + dayElement.classList.remove('start-date', 'end-date'); + } + + parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); + }); +} + +function changeCalendarPage(component, state, delta) { + if (state.currentView === 'days') { + state.currentMonth += delta; + if (state.currentMonth < 0) { + state.currentMonth = 11; + state.currentYear -= 1; + } else if (state.currentMonth > 11) { + state.currentMonth = 0; + state.currentYear += 1; + } + } else if (state.currentView === 'months') { + state.currentYear += delta; + } else if (state.currentView === 'years') { + state.currentYear += delta * 10; + } + updateCalendar(component, state.parent, state); +} + +function initInputWatcher(input, onChange) { + const config = { attributes: true, childList: false, subtree: false }; + + const callback = (mutationList) => { + const [mutation] = mutationList; + if (mutation.target.disabled) { + onChange(); + } + }; + + const observer = new MutationObserver(callback); + observer.observe(input, config); +} + +function buildCalendar(component, parent) { + const input = component.querySelector('#event-info-date-picker'); + + if (!input) return; + + const state = { + currentView: 'days', + selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, + selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, + currentYear: new Date().getFullYear(), + currentMonth: new Date().getMonth(), + headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), + parent, + }; + + const header = createTag('div', { class: 'calendar-header' }, null, { parent }); + const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); + header.append(state.headerTitle); + const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); + + prevButton.onclick = () => changeCalendarPage(component, state, -1); + nextButton.onclick = () => changeCalendarPage(component, state, 1); + + state.headerTitle.addEventListener('click', () => { + // eslint-disable-next-line no-nested-ternary + state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; + updateCalendar(component, parent, state); + }); + + updateCalendar(component, parent, state); +} + +function initCalendar(component) { + let calendar; + const datePickerContainer = component.querySelector('.date-picker'); + const input = component.querySelector('#event-info-date-picker'); + + datePickerContainer.addEventListener('click', () => { + if (calendar || input.disabled) return; + calendar = createTag('div', { class: 'calendar-container' }); + datePickerContainer.append(calendar); + buildCalendar(component, calendar); + }); + + document.addEventListener('click', (e) => { + if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { + calendar.remove(); + calendar = ''; + } + }); + + initInputWatcher(input, () => { + calendar.remove(); + calendar = ''; + }); +} + +function dateTimeStringToTimestamp(dateString, timeString) { + const dateTimeString = `${dateString}T${timeString}`; + + const date = new Date(dateTimeString); + + return date.getTime(); +} + +export function onSubmit(component, props) { + if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const title = component.querySelector('#info-field-event-title').value; + const description = component.querySelector('#info-field-event-description').value; + const datePicker = component.querySelector('#event-info-date-picker'); + const localStartDate = datePicker.dataset.startDate; + const localEndDate = datePicker.dataset.endDate; + + const localStartTime = component.querySelector('#time-picker-start-time-value').value; + const localEndTime = component.querySelector('#time-picker-end-time-value').value; + + const timezone = component.querySelector('#time-zone-select-input').value; + + const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); + const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); + + const eventInfo = { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + localStartTimeMillis, + localEndTimeMillis, + timezone, + }; + + props.payload = { ...props.payload, ...eventInfo }; +} + +export async function onPayloadUpdate(component, props) { + // do nothing +} + +export async function onRespUpdate(_component, _props) { + // Do nothing +} + +function checkEventDuplication(event, compareMetrics) { + const titleMatch = event.title === compareMetrics.title; + const startDateMatch = event.localStartDate === compareMetrics.startDate; + const venueIdMatch = event.venue?.city === compareMetrics.city; + const eventIdNoMatch = event.eventId !== compareMetrics.eventId; + + return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; +} + +export default async function init(component, props) { + const allEventsResp = await getEvents(); + const allEvents = allEventsResp?.events; + const eventData = props.eventDataResp; + const sameSeriesEvents = allEvents?.filter((e) => { + const matchInPayload = e.seriesId === props.payload.seriesId; + const matchInResp = e.seriesId === eventData.seriesId; + return matchInPayload || matchInResp; + }) || []; + + const eventTitleInput = component.querySelector('#info-field-event-title'); + const startTimeInput = component.querySelector('#time-picker-start-time'); + const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); + const startAmpmInput = component.querySelector('#ampm-picker-start-time'); + const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); + const endTimeInput = component.querySelector('#time-picker-end-time'); + const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); + const endAmpmInput = component.querySelector('#ampm-picker-end-time'); + const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); + const startTime = component.querySelector('#time-picker-start-time-value'); + const endTime = component.querySelector('#time-picker-end-time-value'); + const datePicker = component.querySelector('#event-info-date-picker'); + + initCalendar(component); + + eventTitleInput.addEventListener('input', () => { + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); + }); + + const resetAllOptions = () => { + [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] + .forEach((options) => { + options.forEach((option) => { + option.disabled = false; + }); + }); + }; + + const sameDayEvent = () => datePicker.dataset.startDate + && datePicker.dataset.endDate + && datePicker.dataset.startDate === datePicker.dataset.endDate; + + const onEndTimeUpdate = () => { + if (endAmpmInput.value && endTimeInput.value) { + endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); + } else { + endTime.value = null; + } + + if (!sameDayEvent()) return; + + startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; + let onlyPossibleStartAmpm = startAmpmInput.value; + if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; + + if (startTimeInput.value) { + if (onlyPossibleStartAmpm) { + const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); + if (endAmpmInput.value) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= onlyPossibleStartTime; + }); + } + } + } + + if (endTime.value) { + if (onlyPossibleStartAmpm) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); + option.disabled = optionTime >= endTime.value; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= endTime.value; + }); + } + } + }; + + const onStartTimeUpdate = () => { + if (startAmpmInput.value && startTimeInput.value) { + startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); + } else { + startTime.value = null; + } + + if (!sameDayEvent()) return; + + endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; + let onlyPossibleEndAmpm = endAmpmInput.value; + if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; + + if (endTimeInput.value) { + if (onlyPossibleEndAmpm) { + const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); + if (startAmpmInput.value) { + allStartTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + + if (startTimeInput.value) { + startAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); + option.disabled = optionTime >= onlyPossibleEndTime; + }); + } + } + } + + if (startTime.value) { + if (onlyPossibleEndAmpm) { + allEndTimeOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); + option.disabled = optionTime <= startTime.value; + }); + } + + if (endTimeInput.value) { + endAmpmOptions.forEach((option) => { + const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); + option.disabled = optionTime <= startTime.value; + }); + } + } + }; + + const updateTimeOptionsBasedOnDate = () => { + if (!sameDayEvent()) { + resetAllOptions(); + } else { + startTimeInput.value = ''; + startAmpmInput.value = ''; + endTimeInput.value = ''; + endAmpmInput.value = ''; + startTime.value = null; + endTime.value = null; + + resetAllOptions(); + } + }; + + startTimeInput.addEventListener('change', onStartTimeUpdate); + endTimeInput.addEventListener('change', onEndTimeUpdate); + startAmpmInput.addEventListener('change', onStartTimeUpdate); + endAmpmInput.addEventListener('change', onEndTimeUpdate); + + datePicker.addEventListener('change', (e) => { + updateTimeOptionsBasedOnDate(e); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); + }); + + BlockMediator.subscribe('eventDupMetrics', (store) => { + const metrics = store.newValue; + const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); + + helpText.textContent = helpText.textContent + .replace('[[seriesName]]', metrics.seriesName) + .replace('[[eventName]]', metrics.title); + + const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); + if (isDup) { + props.el.classList.add('show-dup-event-error'); + eventTitleInput.invalid = true; + } else { + props.el.classList.remove('show-dup-event-error'); + eventTitleInput.invalid = false; + } + + eventTitleInput.dispatchEvent(new Event('change')); + }); + + const { + title, + description, + localStartDate, + localEndDate, + localStartTime, + localEndTime, + timezone, + } = eventData; + + if (title + && description + && localStartDate + && localEndDate + && localStartTime + && localEndTime + && timezone) { + const startTimePieces = parse24HourFormat(localStartTime); + const endTimePieces = parse24HourFormat(localEndTime); + + datePicker.dataset.startDate = localStartDate || ''; + datePicker.dataset.endDate = localEndDate || ''; + updateInput(component, { + selectedStartDate: parseFormatedDate(localStartDate), + selectedEndDate: parseFormatedDate(localEndDate), + }); + + component.querySelector('#info-field-event-title').value = title || ''; + component.querySelector('#info-field-event-description').value = description || ''; + changeInputValue(startTime, 'value', `${localStartTime}` || ''); + changeInputValue(endTime, 'value', `${localEndTime}` || ''); + changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); + changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); + changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); + changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); + changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); + + BlockMediator.set('eventDupMetrics', { + ...BlockMediator.get('eventDupMetrics'), + ...{ + title, + startDate: localStartDate, + eventId: eventData.eventId, + }, + }); + + component.classList.add('prefilled'); + } +} + +export function onEventUpdate(component, props) { + // Do nothing +} diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css new file mode 100644 index 00000000..b374909d --- /dev/null +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -0,0 +1,35 @@ +.series-templates-component .labeled-text-field-wrapper { + margin-bottom: 24px; +} + +.series-templates-component .attr-text { + font-size: var(--type-body-xs-size); + text-align: right; +} + +.series-templates-component sp-textfield { + width: 493px; +} + +.series-templates-component sp-textfield#info-field-event-title sp-help-text { + position: absolute; + display: none; +} + +.series-templates-component sp-textfield#info-field-event-title.show-negative-help-text sp-help-text { + display: flex; +} + +.series-templates-component sp-textfield.textarea-input { + font-size: var(--type-body-m-size);; + width: 100%; + outline: none; + resize: vertical; +} + +.series-templates-component .text-field-label { + width: 180px; + font-family: var(--body-font-family); + font-size: var(--type-body-s-size); + font-weight: 700; +} diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js new file mode 100644 index 00000000..9780bd6c --- /dev/null +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -0,0 +1,17 @@ +import { + generateToolTip, + decorateLabeledTextfield, +} from '../../scripts/utils.js'; + +export default function init(el) { + el.classList.add('form-component'); + + const rows = el.querySelectorAll(':scope > div'); + rows.forEach(async (r, ri) => { + if (ri === 0) generateToolTip(r); + + if (ri === 1) { + await decorateLabeledTextfield(r, { id: 'info-field-series-susi' }); + } + }); +} diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index 1d328bfd..f0e3770f 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -131,7 +131,7 @@ export function addTooltipToHeading(em, heading) { } export function generateToolTip(el) { - const heading = el.querySelector('h2, h3'); + const heading = el.querySelector('h2, h3, h4'); const em = el.querySelector('p > em'); if (heading && em) { @@ -182,6 +182,42 @@ function mergeOptions(defaultOptions, overrideOptions) { return combinedOptions; } +export async function decorateLabeledTextfield(cell, inputOpts = {}, labelOpts = {}) { + cell.classList.add('labeled-text-field-row'); + const cols = cell.querySelectorAll(':scope > div'); + if (!cols.length) return; + const [labelCol, placeholderCol] = cols; + + const text = labelCol?.textContent.trim(); + + const phText = placeholderCol?.textContent.trim(); + const maxCharNum = placeholderCol?.querySelector('strong')?.textContent.trim(); + const isRequired = text.endsWith('*'); + + const label = createTag('sp-field-label', mergeOptions({ + for: 'text-input', + 'side-aligned': 'start', + require: isRequired, + class: 'text-field-label', + }, labelOpts), text); + const input = createTag('sp-textfield', mergeOptions( + { + class: 'text-input', + placeholder: phText, + required: isRequired, + size: 'xl', + }, + inputOpts, + )); + + if (maxCharNum) input.setAttribute('maxlength', maxCharNum); + + const wrapper = createTag('div', { class: 'labeled-text-field-wrapper' }); + cell.innerHTML = ''; + wrapper.append(label, input); + cell.append(wrapper); +} + export async function decorateTextfield(cell, extraOptions, negativeHelperText = '') { cell.classList.add('text-field-row'); const cols = cell.querySelectorAll(':scope > div'); From f621b9906f9be2767682f33a280def8626dd97d1 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 2 Dec 2024 15:43:24 -0600 Subject: [PATCH 29/74] wip --- .../series-templates-component/controller.js | 612 ++---------------- .../series-templates-component.js | 31 +- 2 files changed, 78 insertions(+), 565 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 40ef879d..0a1c0954 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -1,346 +1,10 @@ /* eslint-disable no-unused-vars */ -/* eslint-disable no-use-before-define */ -import { getEvents } from '../../scripts/esp-controller.js'; -import BlockMediator from '../../scripts/deps/block-mediator.min.js'; import { LIBS } from '../../scripts/scripts.js'; -import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; -const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); - -function formatDate(date) { - let month = `${date.getMonth() + 1}`; - let day = `${date.getDate()}`; - const year = date.getFullYear(); - - if (month.length < 2) month = `0${month}`; - if (day.length < 2) day = `0${day}`; - - return [year, month, day].join('-'); -} - -function parseFormatedDate(string) { - if (!string) return null; - - const [year, month, day] = string.split('-'); - const date = new Date(year, +month - 1, day); - - return date; -} - -// Function to generate a calendar -function updateCalendar(component, parent, state) { - parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); - if (state.currentView === 'days') { - updateDayView(component, parent, state); - } else if (state.currentView === 'months') { - updateMonthView(component, parent, state); - } else if (state.currentView === 'years') { - updateYearView(component, parent, state); - } - - if (state.selectedStartDate && state.selectedEndDate) { - updateSelectedDates(state); - } -} - -function updateDayView(component, parent, state) { - state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; - const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); - weekdays.forEach((day) => { - createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); - }); - - const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); - const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); - const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); - const todayDate = new Date(); - - for (let i = 0; i < firstDayOfMonth; i += 1) { - createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); - } - for (let day = 1; day <= daysInMonth; day += 1) { - const date = new Date(state.currentYear, state.currentMonth, day); - const dayElement = createTag('div', { - class: 'calendar-day', - tabindex: '0', - 'data-date': formatDate(date), - }, day.toString(), { parent: calendarGrid }); - - if (date < todayDate) { - dayElement.classList.add('disabled'); - } else { - dayElement.addEventListener('keydown', (event) => { - if (event.key === 'Enter' || event.key === ' ') { - selectDate(component, state, date); - event.preventDefault(); - } - }); - dayElement.addEventListener('click', () => selectDate(component, state, date)); - } - - // Mark today's date - if (date === todayDate) { - dayElement.classList.add('today'); - } - } -} - -function updateMonthView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear}`; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); - const currentMonth = new Date().getMonth(); - const currentYear = new Date().getFullYear(); - - months.forEach((month, index) => { - const monthElement = createTag('div', { - class: 'calendar-month', - 'data-month': index, - }, month, { parent: calendarGrid }); - - // Disable past months in the current year - if ((state.currentYear === currentYear && index < currentMonth) - || state.currentYear < currentYear) { - monthElement.classList.add('disabled'); - } else { - monthElement.addEventListener('click', () => { - state.currentMonth = index; - state.currentView = 'days'; - updateCalendar(component, parent, state); - }); - } - }); -} - -function isSameDay(date1, date2) { - return date1.getFullYear() === date2.getFullYear() - && date1.getMonth() === date2.getMonth() - && date1.getDate() === date2.getDate(); -} - -function updateYearView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; - const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); - const currentYear = new Date().getFullYear(); - - for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { - const yearElement = createTag('div', { - class: 'calendar-year', - 'data-year': year, - }, year.toString(), { parent: calendarGrid }); - - // Disable past years - if (year < currentYear) { - yearElement.classList.add('disabled'); - } else { - yearElement.addEventListener('click', () => { - state.currentYear = year; - state.currentView = 'months'; - updateCalendar(component, parent, state); - }); - } - } -} - -function selectDate(component, state, date) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { - state.selectedStartDate = date; - state.selectedEndDate = date; - } else if ((state.selectedStartDate && !state.selectedEndDate) - || (state.selectedStartDate === state.selectedEndDate)) { - if (date < state.selectedStartDate) { - state.selectedEndDate = state.selectedStartDate; - state.selectedStartDate = date; - } else { - state.selectedEndDate = date; - } - } - - updateSelectedDates(state); - updateInput(component, state); - input.dispatchEvent(new Event('change')); -} - -function updateInput(component, state) { - const dateInput = component.querySelector('#event-info-date-picker'); - - if (dateInput) { - if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); - if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); - - if (dateInput.dataset.startDate && dateInput.dataset.endDate) { - const options = { year: 'numeric', month: 'long', day: 'numeric' }; - const dateLocale = getConfig().locale?.ietf || 'en-US'; - const startDateTime = state.selectedStartDate - .toLocaleDateString(dateLocale, options); - const endDateTime = state.selectedEndDate - .toLocaleDateString(dateLocale, options); - const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate - ? startDateTime : `${startDateTime} - ${endDateTime}`; - dateInput.value = dateValue; - } - } -} - -function updateSelectedDates(state) { - const { parent } = state; - parent.querySelectorAll('.calendar-day').forEach((dayElement) => { - if (!dayElement.getAttribute('data-date')) return; - - const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); - dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); - dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); - // Mark the start date and end date - - if (isSameDay(clickedDate, state.selectedStartDate) - && isSameDay(state.selectedStartDate, state.selectedEndDate)) { - dayElement.classList.add('start-date', 'end-date'); - } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { - dayElement.classList.remove('end-date'); - dayElement.classList.add('start-date'); - } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { - dayElement.classList.remove('start-date'); - dayElement.classList.add('end-date'); - } else { - dayElement.classList.remove('start-date', 'end-date'); - } - - parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); - }); -} - -function changeCalendarPage(component, state, delta) { - if (state.currentView === 'days') { - state.currentMonth += delta; - if (state.currentMonth < 0) { - state.currentMonth = 11; - state.currentYear -= 1; - } else if (state.currentMonth > 11) { - state.currentMonth = 0; - state.currentYear += 1; - } - } else if (state.currentView === 'months') { - state.currentYear += delta; - } else if (state.currentView === 'years') { - state.currentYear += delta * 10; - } - updateCalendar(component, state.parent, state); -} - -function initInputWatcher(input, onChange) { - const config = { attributes: true, childList: false, subtree: false }; - - const callback = (mutationList) => { - const [mutation] = mutationList; - if (mutation.target.disabled) { - onChange(); - } - }; - - const observer = new MutationObserver(callback); - observer.observe(input, config); -} - -function buildCalendar(component, parent) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - const state = { - currentView: 'days', - selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, - selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, - currentYear: new Date().getFullYear(), - currentMonth: new Date().getMonth(), - headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), - parent, - }; - - const header = createTag('div', { class: 'calendar-header' }, null, { parent }); - const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); - header.append(state.headerTitle); - const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); - - prevButton.onclick = () => changeCalendarPage(component, state, -1); - nextButton.onclick = () => changeCalendarPage(component, state, 1); - - state.headerTitle.addEventListener('click', () => { - // eslint-disable-next-line no-nested-ternary - state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; - updateCalendar(component, parent, state); - }); - - updateCalendar(component, parent, state); -} - -function initCalendar(component) { - let calendar; - const datePickerContainer = component.querySelector('.date-picker'); - const input = component.querySelector('#event-info-date-picker'); - - datePickerContainer.addEventListener('click', () => { - if (calendar || input.disabled) return; - calendar = createTag('div', { class: 'calendar-container' }); - datePickerContainer.append(calendar); - buildCalendar(component, calendar); - }); - - document.addEventListener('click', (e) => { - if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { - calendar.remove(); - calendar = ''; - } - }); - - initInputWatcher(input, () => { - calendar.remove(); - calendar = ''; - }); -} - -function dateTimeStringToTimestamp(dateString, timeString) { - const dateTimeString = `${dateString}T${timeString}`; - - const date = new Date(dateTimeString); - - return date.getTime(); -} +const { createTag } = await import(`${LIBS}/utils/utils.js`); export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; - - const title = component.querySelector('#info-field-event-title').value; - const description = component.querySelector('#info-field-event-description').value; - const datePicker = component.querySelector('#event-info-date-picker'); - const localStartDate = datePicker.dataset.startDate; - const localEndDate = datePicker.dataset.endDate; - - const localStartTime = component.querySelector('#time-picker-start-time-value').value; - const localEndTime = component.querySelector('#time-picker-end-time-value').value; - - const timezone = component.querySelector('#time-zone-select-input').value; - - const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); - const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); - - const eventInfo = { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - localStartTimeMillis, - localEndTimeMillis, - timezone, - }; - - props.payload = { ...props.payload, ...eventInfo }; } export async function onPayloadUpdate(component, props) { @@ -351,248 +15,68 @@ export async function onRespUpdate(_component, _props) { // Do nothing } -function checkEventDuplication(event, compareMetrics) { - const titleMatch = event.title === compareMetrics.title; - const startDateMatch = event.localStartDate === compareMetrics.startDate; - const venueIdMatch = event.venue?.city === compareMetrics.city; - const eventIdNoMatch = event.eventId !== compareMetrics.eventId; - - return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; -} - -export default async function init(component, props) { - const allEventsResp = await getEvents(); - const allEvents = allEventsResp?.events; - const eventData = props.eventDataResp; - const sameSeriesEvents = allEvents?.filter((e) => { - const matchInPayload = e.seriesId === props.payload.seriesId; - const matchInResp = e.seriesId === eventData.seriesId; - return matchInPayload || matchInResp; - }) || []; - - const eventTitleInput = component.querySelector('#info-field-event-title'); - const startTimeInput = component.querySelector('#time-picker-start-time'); - const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); - const startAmpmInput = component.querySelector('#ampm-picker-start-time'); - const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); - const endTimeInput = component.querySelector('#time-picker-end-time'); - const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); - const endAmpmInput = component.querySelector('#ampm-picker-end-time'); - const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); - const startTime = component.querySelector('#time-picker-start-time-value'); - const endTime = component.querySelector('#time-picker-end-time-value'); - const datePicker = component.querySelector('#event-info-date-picker'); +async function buildPreviewListOptionsFromSource(previewList, source, attr) { + const valueHolder = previewList.closest('.series-info-wrapper').querySelector(`.${attr.handle}-input`); + const previewListItems = previewList.querySelector('.preview-list-items'); + const previewListOverlay = previewList.querySelector('.preview-list-overlay'); - initCalendar(component); - - eventTitleInput.addEventListener('input', () => { - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); + const jsonResp = await fetch(source).then((res) => { + if (!res.ok) throw new Error('Failed to fetch series templates'); + return res.json(); }); - const resetAllOptions = () => { - [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] - .forEach((options) => { - options.forEach((option) => { - option.disabled = false; - }); - }); - }; - - const sameDayEvent = () => datePicker.dataset.startDate - && datePicker.dataset.endDate - && datePicker.dataset.startDate === datePicker.dataset.endDate; - - const onEndTimeUpdate = () => { - if (endAmpmInput.value && endTimeInput.value) { - endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); - } else { - endTime.value = null; - } - - if (!sameDayEvent()) return; - - startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; - let onlyPossibleStartAmpm = startAmpmInput.value; - if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; - - if (startTimeInput.value) { - if (onlyPossibleStartAmpm) { - const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); - if (endAmpmInput.value) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - } - } - - if (endTime.value) { - if (onlyPossibleStartAmpm) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); - option.disabled = optionTime >= endTime.value; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= endTime.value; - }); - } - } - }; - - const onStartTimeUpdate = () => { - if (startAmpmInput.value && startTimeInput.value) { - startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); - } else { - startTime.value = null; - } - - if (!sameDayEvent()) return; - - endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; - let onlyPossibleEndAmpm = endAmpmInput.value; - if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; - - if (endTimeInput.value) { - if (onlyPossibleEndAmpm) { - const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); - if (startAmpmInput.value) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - } - } - - if (startTime.value) { - if (onlyPossibleEndAmpm) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); - option.disabled = optionTime <= startTime.value; - }); - } + const options = jsonResp.data; + if (!options) return; - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= startTime.value; - }); - } - } - }; - - const updateTimeOptionsBasedOnDate = () => { - if (!sameDayEvent()) { - resetAllOptions(); - } else { - startTimeInput.value = ''; - startAmpmInput.value = ''; - endTimeInput.value = ''; - endAmpmInput.value = ''; - startTime.value = null; - endTime.value = null; - - resetAllOptions(); - } - }; - - startTimeInput.addEventListener('change', onStartTimeUpdate); - endTimeInput.addEventListener('change', onEndTimeUpdate); - startAmpmInput.addEventListener('change', onStartTimeUpdate); - endAmpmInput.addEventListener('change', onEndTimeUpdate); + if (options.length > 3) { + previewListItems.classList.add('show-3'); + } else { + previewListItems.classList.remove('show-3'); + } - datePicker.addEventListener('change', (e) => { - updateTimeOptionsBasedOnDate(e); - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); + options.forEach((option) => { + const previewListItem = createTag('div', { class: 'preview-list-item' }); + const previewListItemImage = createTag('img', { src: option['template-image'] }); + const previewListItemTitle = createTag('h5', {}, option['template-name']); + const selectItemBtn = createTag('a', { class: 'con-button blue select-item-btn' }, 'Select', { parent: previewListItem }); + previewListItem.append(previewListItemImage, previewListItemTitle, selectItemBtn); + previewListItems.append(previewListItem); + + selectItemBtn.addEventListener('click', () => { + valueHolder.value = option['template-path']; + previewListOverlay.classList.add('hidden'); + }); }); +} - BlockMediator.subscribe('eventDupMetrics', (store) => { - const metrics = store.newValue; - const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); - - helpText.textContent = helpText.textContent - .replace('[[seriesName]]', metrics.seriesName) - .replace('[[eventName]]', metrics.title); +function buildPreviewList(attrObj) { + const { optionsSource } = attrObj; - const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); - if (isDup) { - props.el.classList.add('show-dup-event-error'); - eventTitleInput.invalid = true; - } else { - props.el.classList.remove('show-dup-event-error'); - eventTitleInput.invalid = false; - } + const previewList = createTag('div', { class: 'preview-list' }); + const previewListTitle = createTag('h4', {}, 'Select a template'); + const previewListItems = createTag('div', { class: 'preview-list-items' }); + const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, 'Select'); + const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); + const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); + const previewListCloseBtn = createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); - eventTitleInput.dispatchEvent(new Event('change')); + previewListBtn.addEventListener('click', () => { + previewListOverlay.classList.remove('hidden'); + buildPreviewListOptionsFromSource(previewList, optionsSource, attrObj); }); - const { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - timezone, - } = eventData; - - if (title - && description - && localStartDate - && localEndDate - && localStartTime - && localEndTime - && timezone) { - const startTimePieces = parse24HourFormat(localStartTime); - const endTimePieces = parse24HourFormat(localEndTime); + previewListCloseBtn.addEventListener('click', () => { + previewListOverlay.classList.add('hidden'); + }); - datePicker.dataset.startDate = localStartDate || ''; - datePicker.dataset.endDate = localEndDate || ''; - updateInput(component, { - selectedStartDate: parseFormatedDate(localStartDate), - selectedEndDate: parseFormatedDate(localEndDate), - }); + previewListModal.append(previewListTitle, previewListItems); + previewList.append(previewListBtn, previewListOverlay); + return previewList; +} - component.querySelector('#info-field-event-title').value = title || ''; - component.querySelector('#info-field-event-description').value = description || ''; - changeInputValue(startTime, 'value', `${localStartTime}` || ''); - changeInputValue(endTime, 'value', `${localEndTime}` || ''); - changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); - changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); - changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); - changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); - changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); - BlockMediator.set('eventDupMetrics', { - ...BlockMediator.get('eventDupMetrics'), - ...{ - title, - startDate: localStartDate, - eventId: eventData.eventId, - }, - }); +export default async function init(component, props) { - component.classList.add('prefilled'); - } } export function onEventUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index 9780bd6c..cc8ebbc8 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -1,9 +1,34 @@ +import { LIBS } from '../../scripts/scripts.js'; import { generateToolTip, decorateLabeledTextfield, } from '../../scripts/utils.js'; -export default function init(el) { +async function buildPreviewList(row) { + const { createTag } = await import(`${LIBS}/utils/utils.js`); + + const [labelCol, sourseLinkCel] = row.querySelectorAll(':scope > div'); + + const label = labelCol?.textContent.trim() || 'Event template'; + const sourceLink = sourseLinkCel?.textContent.trim(); + + if (!label || !sourceLink) return; + + const previewList = createTag('div', { class: 'preview-list' }); + const previewListTitle = createTag('h4', {}, 'Select a template'); + const previewListItems = createTag('div', { class: 'preview-list-items' }); + const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, label); + const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); + const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); + + createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); + previewListModal.append(previewListTitle, previewListItems); + previewList.append(previewListBtn, previewListOverlay); + row.innerHTML = ''; + row.append(previewList); +} + +export default async function init(el) { el.classList.add('form-component'); const rows = el.querySelectorAll(':scope > div'); @@ -13,5 +38,9 @@ export default function init(el) { if (ri === 1) { await decorateLabeledTextfield(r, { id: 'info-field-series-susi' }); } + + if (ri === 2) { + await buildPreviewList(r); + } }); } From c187e99d925692766beaa57074168f4473012543 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 2 Dec 2024 21:43:22 -0600 Subject: [PATCH 30/74] wip --- .../controller.js | 586 +----------------- .../series-creation-form.js | 16 +- .../series-details-component/controller.js | 586 +----------------- .../series-templates-component/controller.js | 56 +- .../series-templates-component.css | 57 +- .../series-templates-component.js | 31 +- ecc/icons/close-circle.svg | 11 + .../form-component/controller.sample.js | 20 + 8 files changed, 146 insertions(+), 1217 deletions(-) create mode 100644 ecc/icons/close-circle.svg create mode 100644 ecc/samples/form-component/controller.sample.js diff --git a/ecc/blocks/series-additional-info-component/controller.js b/ecc/blocks/series-additional-info-component/controller.js index 40ef879d..e9b1f488 100644 --- a/ecc/blocks/series-additional-info-component/controller.js +++ b/ecc/blocks/series-additional-info-component/controller.js @@ -1,598 +1,18 @@ /* eslint-disable no-unused-vars */ -/* eslint-disable no-use-before-define */ -import { getEvents } from '../../scripts/esp-controller.js'; -import BlockMediator from '../../scripts/deps/block-mediator.min.js'; -import { LIBS } from '../../scripts/scripts.js'; -import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; - -const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); - -function formatDate(date) { - let month = `${date.getMonth() + 1}`; - let day = `${date.getDate()}`; - const year = date.getFullYear(); - - if (month.length < 2) month = `0${month}`; - if (day.length < 2) day = `0${day}`; - - return [year, month, day].join('-'); -} - -function parseFormatedDate(string) { - if (!string) return null; - - const [year, month, day] = string.split('-'); - const date = new Date(year, +month - 1, day); - - return date; -} - -// Function to generate a calendar -function updateCalendar(component, parent, state) { - parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); - if (state.currentView === 'days') { - updateDayView(component, parent, state); - } else if (state.currentView === 'months') { - updateMonthView(component, parent, state); - } else if (state.currentView === 'years') { - updateYearView(component, parent, state); - } - - if (state.selectedStartDate && state.selectedEndDate) { - updateSelectedDates(state); - } -} - -function updateDayView(component, parent, state) { - state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; - const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); - weekdays.forEach((day) => { - createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); - }); - - const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); - const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); - const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); - const todayDate = new Date(); - - for (let i = 0; i < firstDayOfMonth; i += 1) { - createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); - } - for (let day = 1; day <= daysInMonth; day += 1) { - const date = new Date(state.currentYear, state.currentMonth, day); - const dayElement = createTag('div', { - class: 'calendar-day', - tabindex: '0', - 'data-date': formatDate(date), - }, day.toString(), { parent: calendarGrid }); - - if (date < todayDate) { - dayElement.classList.add('disabled'); - } else { - dayElement.addEventListener('keydown', (event) => { - if (event.key === 'Enter' || event.key === ' ') { - selectDate(component, state, date); - event.preventDefault(); - } - }); - dayElement.addEventListener('click', () => selectDate(component, state, date)); - } - - // Mark today's date - if (date === todayDate) { - dayElement.classList.add('today'); - } - } -} - -function updateMonthView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear}`; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); - const currentMonth = new Date().getMonth(); - const currentYear = new Date().getFullYear(); - - months.forEach((month, index) => { - const monthElement = createTag('div', { - class: 'calendar-month', - 'data-month': index, - }, month, { parent: calendarGrid }); - - // Disable past months in the current year - if ((state.currentYear === currentYear && index < currentMonth) - || state.currentYear < currentYear) { - monthElement.classList.add('disabled'); - } else { - monthElement.addEventListener('click', () => { - state.currentMonth = index; - state.currentView = 'days'; - updateCalendar(component, parent, state); - }); - } - }); -} - -function isSameDay(date1, date2) { - return date1.getFullYear() === date2.getFullYear() - && date1.getMonth() === date2.getMonth() - && date1.getDate() === date2.getDate(); -} - -function updateYearView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; - const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); - const currentYear = new Date().getFullYear(); - - for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { - const yearElement = createTag('div', { - class: 'calendar-year', - 'data-year': year, - }, year.toString(), { parent: calendarGrid }); - - // Disable past years - if (year < currentYear) { - yearElement.classList.add('disabled'); - } else { - yearElement.addEventListener('click', () => { - state.currentYear = year; - state.currentView = 'months'; - updateCalendar(component, parent, state); - }); - } - } -} - -function selectDate(component, state, date) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { - state.selectedStartDate = date; - state.selectedEndDate = date; - } else if ((state.selectedStartDate && !state.selectedEndDate) - || (state.selectedStartDate === state.selectedEndDate)) { - if (date < state.selectedStartDate) { - state.selectedEndDate = state.selectedStartDate; - state.selectedStartDate = date; - } else { - state.selectedEndDate = date; - } - } - - updateSelectedDates(state); - updateInput(component, state); - input.dispatchEvent(new Event('change')); -} - -function updateInput(component, state) { - const dateInput = component.querySelector('#event-info-date-picker'); - - if (dateInput) { - if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); - if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); - - if (dateInput.dataset.startDate && dateInput.dataset.endDate) { - const options = { year: 'numeric', month: 'long', day: 'numeric' }; - const dateLocale = getConfig().locale?.ietf || 'en-US'; - const startDateTime = state.selectedStartDate - .toLocaleDateString(dateLocale, options); - const endDateTime = state.selectedEndDate - .toLocaleDateString(dateLocale, options); - const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate - ? startDateTime : `${startDateTime} - ${endDateTime}`; - dateInput.value = dateValue; - } - } -} - -function updateSelectedDates(state) { - const { parent } = state; - parent.querySelectorAll('.calendar-day').forEach((dayElement) => { - if (!dayElement.getAttribute('data-date')) return; - - const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); - dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); - dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); - // Mark the start date and end date - - if (isSameDay(clickedDate, state.selectedStartDate) - && isSameDay(state.selectedStartDate, state.selectedEndDate)) { - dayElement.classList.add('start-date', 'end-date'); - } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { - dayElement.classList.remove('end-date'); - dayElement.classList.add('start-date'); - } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { - dayElement.classList.remove('start-date'); - dayElement.classList.add('end-date'); - } else { - dayElement.classList.remove('start-date', 'end-date'); - } - - parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); - }); -} - -function changeCalendarPage(component, state, delta) { - if (state.currentView === 'days') { - state.currentMonth += delta; - if (state.currentMonth < 0) { - state.currentMonth = 11; - state.currentYear -= 1; - } else if (state.currentMonth > 11) { - state.currentMonth = 0; - state.currentYear += 1; - } - } else if (state.currentView === 'months') { - state.currentYear += delta; - } else if (state.currentView === 'years') { - state.currentYear += delta * 10; - } - updateCalendar(component, state.parent, state); -} - -function initInputWatcher(input, onChange) { - const config = { attributes: true, childList: false, subtree: false }; - - const callback = (mutationList) => { - const [mutation] = mutationList; - if (mutation.target.disabled) { - onChange(); - } - }; - - const observer = new MutationObserver(callback); - observer.observe(input, config); -} - -function buildCalendar(component, parent) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - const state = { - currentView: 'days', - selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, - selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, - currentYear: new Date().getFullYear(), - currentMonth: new Date().getMonth(), - headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), - parent, - }; - - const header = createTag('div', { class: 'calendar-header' }, null, { parent }); - const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); - header.append(state.headerTitle); - const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); - - prevButton.onclick = () => changeCalendarPage(component, state, -1); - nextButton.onclick = () => changeCalendarPage(component, state, 1); - - state.headerTitle.addEventListener('click', () => { - // eslint-disable-next-line no-nested-ternary - state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; - updateCalendar(component, parent, state); - }); - - updateCalendar(component, parent, state); -} - -function initCalendar(component) { - let calendar; - const datePickerContainer = component.querySelector('.date-picker'); - const input = component.querySelector('#event-info-date-picker'); - - datePickerContainer.addEventListener('click', () => { - if (calendar || input.disabled) return; - calendar = createTag('div', { class: 'calendar-container' }); - datePickerContainer.append(calendar); - buildCalendar(component, calendar); - }); - - document.addEventListener('click', (e) => { - if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { - calendar.remove(); - calendar = ''; - } - }); - - initInputWatcher(input, () => { - calendar.remove(); - calendar = ''; - }); -} - -function dateTimeStringToTimestamp(dateString, timeString) { - const dateTimeString = `${dateString}T${timeString}`; - - const date = new Date(dateTimeString); - - return date.getTime(); -} - export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; - - const title = component.querySelector('#info-field-event-title').value; - const description = component.querySelector('#info-field-event-description').value; - const datePicker = component.querySelector('#event-info-date-picker'); - const localStartDate = datePicker.dataset.startDate; - const localEndDate = datePicker.dataset.endDate; - - const localStartTime = component.querySelector('#time-picker-start-time-value').value; - const localEndTime = component.querySelector('#time-picker-end-time-value').value; - - const timezone = component.querySelector('#time-zone-select-input').value; - - const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); - const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); - - const eventInfo = { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - localStartTimeMillis, - localEndTimeMillis, - timezone, - }; - - props.payload = { ...props.payload, ...eventInfo }; } -export async function onPayloadUpdate(component, props) { - // do nothing +export async function onPayloadUpdate(_component, _props) { + // Do nothing } export async function onRespUpdate(_component, _props) { // Do nothing } -function checkEventDuplication(event, compareMetrics) { - const titleMatch = event.title === compareMetrics.title; - const startDateMatch = event.localStartDate === compareMetrics.startDate; - const venueIdMatch = event.venue?.city === compareMetrics.city; - const eventIdNoMatch = event.eventId !== compareMetrics.eventId; - - return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; -} - -export default async function init(component, props) { - const allEventsResp = await getEvents(); - const allEvents = allEventsResp?.events; - const eventData = props.eventDataResp; - const sameSeriesEvents = allEvents?.filter((e) => { - const matchInPayload = e.seriesId === props.payload.seriesId; - const matchInResp = e.seriesId === eventData.seriesId; - return matchInPayload || matchInResp; - }) || []; - - const eventTitleInput = component.querySelector('#info-field-event-title'); - const startTimeInput = component.querySelector('#time-picker-start-time'); - const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); - const startAmpmInput = component.querySelector('#ampm-picker-start-time'); - const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); - const endTimeInput = component.querySelector('#time-picker-end-time'); - const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); - const endAmpmInput = component.querySelector('#ampm-picker-end-time'); - const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); - const startTime = component.querySelector('#time-picker-start-time-value'); - const endTime = component.querySelector('#time-picker-end-time-value'); - const datePicker = component.querySelector('#event-info-date-picker'); - - initCalendar(component); - - eventTitleInput.addEventListener('input', () => { - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); - }); - - const resetAllOptions = () => { - [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] - .forEach((options) => { - options.forEach((option) => { - option.disabled = false; - }); - }); - }; - - const sameDayEvent = () => datePicker.dataset.startDate - && datePicker.dataset.endDate - && datePicker.dataset.startDate === datePicker.dataset.endDate; - - const onEndTimeUpdate = () => { - if (endAmpmInput.value && endTimeInput.value) { - endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); - } else { - endTime.value = null; - } - - if (!sameDayEvent()) return; - - startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; - let onlyPossibleStartAmpm = startAmpmInput.value; - if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; - - if (startTimeInput.value) { - if (onlyPossibleStartAmpm) { - const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); - if (endAmpmInput.value) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - } - } - - if (endTime.value) { - if (onlyPossibleStartAmpm) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); - option.disabled = optionTime >= endTime.value; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= endTime.value; - }); - } - } - }; - - const onStartTimeUpdate = () => { - if (startAmpmInput.value && startTimeInput.value) { - startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); - } else { - startTime.value = null; - } - - if (!sameDayEvent()) return; - - endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; - let onlyPossibleEndAmpm = endAmpmInput.value; - if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; - - if (endTimeInput.value) { - if (onlyPossibleEndAmpm) { - const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); - if (startAmpmInput.value) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - } - } - - if (startTime.value) { - if (onlyPossibleEndAmpm) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); - option.disabled = optionTime <= startTime.value; - }); - } - - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= startTime.value; - }); - } - } - }; - - const updateTimeOptionsBasedOnDate = () => { - if (!sameDayEvent()) { - resetAllOptions(); - } else { - startTimeInput.value = ''; - startAmpmInput.value = ''; - endTimeInput.value = ''; - endAmpmInput.value = ''; - startTime.value = null; - endTime.value = null; - - resetAllOptions(); - } - }; - - startTimeInput.addEventListener('change', onStartTimeUpdate); - endTimeInput.addEventListener('change', onEndTimeUpdate); - startAmpmInput.addEventListener('change', onStartTimeUpdate); - endAmpmInput.addEventListener('change', onEndTimeUpdate); - - datePicker.addEventListener('change', (e) => { - updateTimeOptionsBasedOnDate(e); - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); - }); - - BlockMediator.subscribe('eventDupMetrics', (store) => { - const metrics = store.newValue; - const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); - - helpText.textContent = helpText.textContent - .replace('[[seriesName]]', metrics.seriesName) - .replace('[[eventName]]', metrics.title); - - const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); - if (isDup) { - props.el.classList.add('show-dup-event-error'); - eventTitleInput.invalid = true; - } else { - props.el.classList.remove('show-dup-event-error'); - eventTitleInput.invalid = false; - } - - eventTitleInput.dispatchEvent(new Event('change')); - }); - - const { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - timezone, - } = eventData; - - if (title - && description - && localStartDate - && localEndDate - && localStartTime - && localEndTime - && timezone) { - const startTimePieces = parse24HourFormat(localStartTime); - const endTimePieces = parse24HourFormat(localEndTime); - - datePicker.dataset.startDate = localStartDate || ''; - datePicker.dataset.endDate = localEndDate || ''; - updateInput(component, { - selectedStartDate: parseFormatedDate(localStartDate), - selectedEndDate: parseFormatedDate(localEndDate), - }); - - component.querySelector('#info-field-event-title').value = title || ''; - component.querySelector('#info-field-event-description').value = description || ''; - changeInputValue(startTime, 'value', `${localStartTime}` || ''); - changeInputValue(endTime, 'value', `${localEndTime}` || ''); - changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); - changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); - changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); - changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); - changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); - - BlockMediator.set('eventDupMetrics', { - ...BlockMediator.get('eventDupMetrics'), - ...{ - title, - startDate: localStartDate, - eventId: eventData.eventId, - }, - }); +export default function init(component, props) { - component.classList.add('prefilled'); - } } export function onEventUpdate(component, props) { diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 299340a0..0cb0a2d1 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -35,19 +35,9 @@ const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); // list of controllers for the handler to load const VANILLA_COMPONENTS = [ - 'event-format', - 'event-info', - 'img-upload', - 'venue-info', - 'profile', - 'event-agenda', - 'event-community-link', - 'event-partners', - 'terms-conditions', - 'product-promotion', - 'event-topics', - 'registration-details', - 'registration-fields', + 'series-details', + 'series-templates', + 'series-additional-info', ]; const REQUIRED_INPUT_TYPES = [ diff --git a/ecc/blocks/series-details-component/controller.js b/ecc/blocks/series-details-component/controller.js index 40ef879d..e9b1f488 100644 --- a/ecc/blocks/series-details-component/controller.js +++ b/ecc/blocks/series-details-component/controller.js @@ -1,598 +1,18 @@ /* eslint-disable no-unused-vars */ -/* eslint-disable no-use-before-define */ -import { getEvents } from '../../scripts/esp-controller.js'; -import BlockMediator from '../../scripts/deps/block-mediator.min.js'; -import { LIBS } from '../../scripts/scripts.js'; -import { changeInputValue, parse24HourFormat, convertTo24HourFormat } from '../../scripts/utils.js'; - -const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); - -function formatDate(date) { - let month = `${date.getMonth() + 1}`; - let day = `${date.getDate()}`; - const year = date.getFullYear(); - - if (month.length < 2) month = `0${month}`; - if (day.length < 2) day = `0${day}`; - - return [year, month, day].join('-'); -} - -function parseFormatedDate(string) { - if (!string) return null; - - const [year, month, day] = string.split('-'); - const date = new Date(year, +month - 1, day); - - return date; -} - -// Function to generate a calendar -function updateCalendar(component, parent, state) { - parent.querySelectorAll('.calendar-grid, .weekdays').forEach((e) => e.remove()); - if (state.currentView === 'days') { - updateDayView(component, parent, state); - } else if (state.currentView === 'months') { - updateMonthView(component, parent, state); - } else if (state.currentView === 'years') { - updateYearView(component, parent, state); - } - - if (state.selectedStartDate && state.selectedEndDate) { - updateSelectedDates(state); - } -} - -function updateDayView(component, parent, state) { - state.headerTitle.textContent = `${new Date(state.currentYear, state.currentMonth).toLocaleString('default', { month: 'long' })} ${state.currentYear}`; - const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - const weekdaysRow = createTag('div', { class: 'weekdays' }, null, { parent }); - weekdays.forEach((day) => { - createTag('div', { class: 'weekday' }, day, { parent: weekdaysRow }); - }); - - const daysInMonth = new Date(state.currentYear, state.currentMonth + 1, 0).getDate(); - const firstDayOfMonth = new Date(state.currentYear, state.currentMonth, 1).getDay(); - const calendarGrid = createTag('div', { class: 'calendar-grid' }, null, { parent }); - const todayDate = new Date(); - - for (let i = 0; i < firstDayOfMonth; i += 1) { - createTag('div', { class: 'calendar-day empty' }, '', { parent: calendarGrid }); - } - for (let day = 1; day <= daysInMonth; day += 1) { - const date = new Date(state.currentYear, state.currentMonth, day); - const dayElement = createTag('div', { - class: 'calendar-day', - tabindex: '0', - 'data-date': formatDate(date), - }, day.toString(), { parent: calendarGrid }); - - if (date < todayDate) { - dayElement.classList.add('disabled'); - } else { - dayElement.addEventListener('keydown', (event) => { - if (event.key === 'Enter' || event.key === ' ') { - selectDate(component, state, date); - event.preventDefault(); - } - }); - dayElement.addEventListener('click', () => selectDate(component, state, date)); - } - - // Mark today's date - if (date === todayDate) { - dayElement.classList.add('today'); - } - } -} - -function updateMonthView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear}`; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - const calendarGrid = createTag('div', { class: 'calendar-grid month-view' }, null, { parent }); - const currentMonth = new Date().getMonth(); - const currentYear = new Date().getFullYear(); - - months.forEach((month, index) => { - const monthElement = createTag('div', { - class: 'calendar-month', - 'data-month': index, - }, month, { parent: calendarGrid }); - - // Disable past months in the current year - if ((state.currentYear === currentYear && index < currentMonth) - || state.currentYear < currentYear) { - monthElement.classList.add('disabled'); - } else { - monthElement.addEventListener('click', () => { - state.currentMonth = index; - state.currentView = 'days'; - updateCalendar(component, parent, state); - }); - } - }); -} - -function isSameDay(date1, date2) { - return date1.getFullYear() === date2.getFullYear() - && date1.getMonth() === date2.getMonth() - && date1.getDate() === date2.getDate(); -} - -function updateYearView(component, parent, state) { - state.headerTitle.textContent = `${state.currentYear - 10} - ${state.currentYear + 10}`; - const calendarGrid = createTag('div', { class: 'calendar-grid year-view' }, null, { parent }); - const currentYear = new Date().getFullYear(); - - for (let year = state.currentYear - 10; year <= state.currentYear + 10; year += 1) { - const yearElement = createTag('div', { - class: 'calendar-year', - 'data-year': year, - }, year.toString(), { parent: calendarGrid }); - - // Disable past years - if (year < currentYear) { - yearElement.classList.add('disabled'); - } else { - yearElement.addEventListener('click', () => { - state.currentYear = year; - state.currentView = 'months'; - updateCalendar(component, parent, state); - }); - } - } -} - -function selectDate(component, state, date) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - if (!state.selectedStartDate || (state.selectedStartDate !== state.selectedEndDate)) { - state.selectedStartDate = date; - state.selectedEndDate = date; - } else if ((state.selectedStartDate && !state.selectedEndDate) - || (state.selectedStartDate === state.selectedEndDate)) { - if (date < state.selectedStartDate) { - state.selectedEndDate = state.selectedStartDate; - state.selectedStartDate = date; - } else { - state.selectedEndDate = date; - } - } - - updateSelectedDates(state); - updateInput(component, state); - input.dispatchEvent(new Event('change')); -} - -function updateInput(component, state) { - const dateInput = component.querySelector('#event-info-date-picker'); - - if (dateInput) { - if (state.selectedStartDate) dateInput.dataset.startDate = formatDate(state.selectedStartDate); - if (state.selectedEndDate) dateInput.dataset.endDate = formatDate(state.selectedEndDate); - - if (dateInput.dataset.startDate && dateInput.dataset.endDate) { - const options = { year: 'numeric', month: 'long', day: 'numeric' }; - const dateLocale = getConfig().locale?.ietf || 'en-US'; - const startDateTime = state.selectedStartDate - .toLocaleDateString(dateLocale, options); - const endDateTime = state.selectedEndDate - .toLocaleDateString(dateLocale, options); - const dateValue = dateInput.dataset.startDate === dateInput.dataset.endDate - ? startDateTime : `${startDateTime} - ${endDateTime}`; - dateInput.value = dateValue; - } - } -} - -function updateSelectedDates(state) { - const { parent } = state; - parent.querySelectorAll('.calendar-day').forEach((dayElement) => { - if (!dayElement.getAttribute('data-date')) return; - - const clickedDate = parseFormatedDate(dayElement.getAttribute('data-date')); - dayElement.classList.toggle('selected', clickedDate >= state.selectedStartDate && clickedDate <= (state.selectedEndDate || state.selectedStartDate)); - dayElement.classList.toggle('range', clickedDate > state.selectedStartDate && clickedDate < (state.selectedEndDate || state.selectedStartDate)); - // Mark the start date and end date - - if (isSameDay(clickedDate, state.selectedStartDate) - && isSameDay(state.selectedStartDate, state.selectedEndDate)) { - dayElement.classList.add('start-date', 'end-date'); - } else if (state.selectedStartDate && isSameDay(clickedDate, state.selectedStartDate)) { - dayElement.classList.remove('end-date'); - dayElement.classList.add('start-date'); - } else if (state.selectedEndDate && isSameDay(clickedDate, state.selectedEndDate)) { - dayElement.classList.remove('start-date'); - dayElement.classList.add('end-date'); - } else { - dayElement.classList.remove('start-date', 'end-date'); - } - - parent.classList.toggle('range-selected', state.selectedStartDate && state.selectedEndDate); - }); -} - -function changeCalendarPage(component, state, delta) { - if (state.currentView === 'days') { - state.currentMonth += delta; - if (state.currentMonth < 0) { - state.currentMonth = 11; - state.currentYear -= 1; - } else if (state.currentMonth > 11) { - state.currentMonth = 0; - state.currentYear += 1; - } - } else if (state.currentView === 'months') { - state.currentYear += delta; - } else if (state.currentView === 'years') { - state.currentYear += delta * 10; - } - updateCalendar(component, state.parent, state); -} - -function initInputWatcher(input, onChange) { - const config = { attributes: true, childList: false, subtree: false }; - - const callback = (mutationList) => { - const [mutation] = mutationList; - if (mutation.target.disabled) { - onChange(); - } - }; - - const observer = new MutationObserver(callback); - observer.observe(input, config); -} - -function buildCalendar(component, parent) { - const input = component.querySelector('#event-info-date-picker'); - - if (!input) return; - - const state = { - currentView: 'days', - selectedStartDate: input.dataset.startDate ? parseFormatedDate(input.dataset.startDate) : null, - selectedEndDate: input.dataset.endDate ? parseFormatedDate(input.dataset.endDate) : null, - currentYear: new Date().getFullYear(), - currentMonth: new Date().getMonth(), - headerTitle: createTag('span', { class: 'header-title' }, '', { parent }), - parent, - }; - - const header = createTag('div', { class: 'calendar-header' }, null, { parent }); - const prevButton = createTag('a', { class: 'prev-button' }, '<', { parent: header }); - header.append(state.headerTitle); - const nextButton = createTag('a', { class: 'next-button' }, '>', { parent: header }); - - prevButton.onclick = () => changeCalendarPage(component, state, -1); - nextButton.onclick = () => changeCalendarPage(component, state, 1); - - state.headerTitle.addEventListener('click', () => { - // eslint-disable-next-line no-nested-ternary - state.currentView = state.currentView === 'days' ? 'months' : state.currentView === 'months' ? 'years' : 'days'; - updateCalendar(component, parent, state); - }); - - updateCalendar(component, parent, state); -} - -function initCalendar(component) { - let calendar; - const datePickerContainer = component.querySelector('.date-picker'); - const input = component.querySelector('#event-info-date-picker'); - - datePickerContainer.addEventListener('click', () => { - if (calendar || input.disabled) return; - calendar = createTag('div', { class: 'calendar-container' }); - datePickerContainer.append(calendar); - buildCalendar(component, calendar); - }); - - document.addEventListener('click', (e) => { - if (!(e.target.closest('.date-picker') || e.target.parentElement?.classList.contains('calendar-grid')) && calendar) { - calendar.remove(); - calendar = ''; - } - }); - - initInputWatcher(input, () => { - calendar.remove(); - calendar = ''; - }); -} - -function dateTimeStringToTimestamp(dateString, timeString) { - const dateTimeString = `${dateString}T${timeString}`; - - const date = new Date(dateTimeString); - - return date.getTime(); -} - export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; - - const title = component.querySelector('#info-field-event-title').value; - const description = component.querySelector('#info-field-event-description').value; - const datePicker = component.querySelector('#event-info-date-picker'); - const localStartDate = datePicker.dataset.startDate; - const localEndDate = datePicker.dataset.endDate; - - const localStartTime = component.querySelector('#time-picker-start-time-value').value; - const localEndTime = component.querySelector('#time-picker-end-time-value').value; - - const timezone = component.querySelector('#time-zone-select-input').value; - - const localStartTimeMillis = dateTimeStringToTimestamp(localStartDate, localStartTime); - const localEndTimeMillis = dateTimeStringToTimestamp(localEndDate, localEndTime); - - const eventInfo = { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - localStartTimeMillis, - localEndTimeMillis, - timezone, - }; - - props.payload = { ...props.payload, ...eventInfo }; } -export async function onPayloadUpdate(component, props) { - // do nothing +export async function onPayloadUpdate(_component, _props) { + // Do nothing } export async function onRespUpdate(_component, _props) { // Do nothing } -function checkEventDuplication(event, compareMetrics) { - const titleMatch = event.title === compareMetrics.title; - const startDateMatch = event.localStartDate === compareMetrics.startDate; - const venueIdMatch = event.venue?.city === compareMetrics.city; - const eventIdNoMatch = event.eventId !== compareMetrics.eventId; - - return titleMatch && startDateMatch && venueIdMatch && eventIdNoMatch; -} - -export default async function init(component, props) { - const allEventsResp = await getEvents(); - const allEvents = allEventsResp?.events; - const eventData = props.eventDataResp; - const sameSeriesEvents = allEvents?.filter((e) => { - const matchInPayload = e.seriesId === props.payload.seriesId; - const matchInResp = e.seriesId === eventData.seriesId; - return matchInPayload || matchInResp; - }) || []; - - const eventTitleInput = component.querySelector('#info-field-event-title'); - const startTimeInput = component.querySelector('#time-picker-start-time'); - const allStartTimeOptions = startTimeInput.querySelectorAll('sp-menu-item'); - const startAmpmInput = component.querySelector('#ampm-picker-start-time'); - const startAmpmOptions = startAmpmInput.querySelectorAll('sp-menu-item'); - const endTimeInput = component.querySelector('#time-picker-end-time'); - const allEndTimeOptions = endTimeInput.querySelectorAll('sp-menu-item'); - const endAmpmInput = component.querySelector('#ampm-picker-end-time'); - const endAmpmOptions = endAmpmInput.querySelectorAll('sp-menu-item'); - const startTime = component.querySelector('#time-picker-start-time-value'); - const endTime = component.querySelector('#time-picker-end-time-value'); - const datePicker = component.querySelector('#event-info-date-picker'); - - initCalendar(component); - - eventTitleInput.addEventListener('input', () => { - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); - }); - - const resetAllOptions = () => { - [allEndTimeOptions, allStartTimeOptions, endAmpmOptions, startAmpmOptions] - .forEach((options) => { - options.forEach((option) => { - option.disabled = false; - }); - }); - }; - - const sameDayEvent = () => datePicker.dataset.startDate - && datePicker.dataset.endDate - && datePicker.dataset.startDate === datePicker.dataset.endDate; - - const onEndTimeUpdate = () => { - if (endAmpmInput.value && endTimeInput.value) { - endTime.value = convertTo24HourFormat(`${endTimeInput.value} ${endAmpmInput.value}`); - } else { - endTime.value = null; - } - - if (!sameDayEvent()) return; - - startAmpmOptions[1].disabled = endAmpmInput.value === 'AM'; - let onlyPossibleStartAmpm = startAmpmInput.value; - if (!onlyPossibleStartAmpm && startAmpmOptions[1].disabled) onlyPossibleStartAmpm = 'AM'; - - if (startTimeInput.value) { - if (onlyPossibleStartAmpm) { - const onlyPossibleStartTime = convertTo24HourFormat(`${startTimeInput.value} ${onlyPossibleStartAmpm}`); - if (endAmpmInput.value) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${endAmpmInput.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= onlyPossibleStartTime; - }); - } - } - } - - if (endTime.value) { - if (onlyPossibleStartAmpm) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleStartAmpm}`); - option.disabled = optionTime >= endTime.value; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= endTime.value; - }); - } - } - }; - - const onStartTimeUpdate = () => { - if (startAmpmInput.value && startTimeInput.value) { - startTime.value = convertTo24HourFormat(`${startTimeInput.value} ${startAmpmInput.value}`); - } else { - startTime.value = null; - } - - if (!sameDayEvent()) return; - - endAmpmOptions[0].disabled = startAmpmInput.value === 'PM'; - let onlyPossibleEndAmpm = endAmpmInput.value; - if (!onlyPossibleEndAmpm && endAmpmOptions[0].disabled) onlyPossibleEndAmpm = 'PM'; - - if (endTimeInput.value) { - if (onlyPossibleEndAmpm) { - const onlyPossibleEndTime = convertTo24HourFormat(`${endTimeInput.value} ${onlyPossibleEndAmpm}`); - if (startAmpmInput.value) { - allStartTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${startAmpmInput.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - - if (startTimeInput.value) { - startAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${startTimeInput.value} ${option.value}`); - option.disabled = optionTime >= onlyPossibleEndTime; - }); - } - } - } - - if (startTime.value) { - if (onlyPossibleEndAmpm) { - allEndTimeOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${option.value} ${onlyPossibleEndAmpm}`); - option.disabled = optionTime <= startTime.value; - }); - } - - if (endTimeInput.value) { - endAmpmOptions.forEach((option) => { - const optionTime = convertTo24HourFormat(`${endTimeInput.value} ${option.value}`); - option.disabled = optionTime <= startTime.value; - }); - } - } - }; - - const updateTimeOptionsBasedOnDate = () => { - if (!sameDayEvent()) { - resetAllOptions(); - } else { - startTimeInput.value = ''; - startAmpmInput.value = ''; - endTimeInput.value = ''; - endAmpmInput.value = ''; - startTime.value = null; - endTime.value = null; - - resetAllOptions(); - } - }; - - startTimeInput.addEventListener('change', onStartTimeUpdate); - endTimeInput.addEventListener('change', onEndTimeUpdate); - startAmpmInput.addEventListener('change', onStartTimeUpdate); - endAmpmInput.addEventListener('change', onEndTimeUpdate); - - datePicker.addEventListener('change', (e) => { - updateTimeOptionsBasedOnDate(e); - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), startDate: datePicker.dataset.startDate }); - }); - - BlockMediator.subscribe('eventDupMetrics', (store) => { - const metrics = store.newValue; - const helpText = component.querySelector('sp-textfield#info-field-event-title sp-help-text'); - - helpText.textContent = helpText.textContent - .replace('[[seriesName]]', metrics.seriesName) - .replace('[[eventName]]', metrics.title); - - const isDup = sameSeriesEvents?.some((e) => checkEventDuplication(e, metrics)); - if (isDup) { - props.el.classList.add('show-dup-event-error'); - eventTitleInput.invalid = true; - } else { - props.el.classList.remove('show-dup-event-error'); - eventTitleInput.invalid = false; - } - - eventTitleInput.dispatchEvent(new Event('change')); - }); - - const { - title, - description, - localStartDate, - localEndDate, - localStartTime, - localEndTime, - timezone, - } = eventData; - - if (title - && description - && localStartDate - && localEndDate - && localStartTime - && localEndTime - && timezone) { - const startTimePieces = parse24HourFormat(localStartTime); - const endTimePieces = parse24HourFormat(localEndTime); - - datePicker.dataset.startDate = localStartDate || ''; - datePicker.dataset.endDate = localEndDate || ''; - updateInput(component, { - selectedStartDate: parseFormatedDate(localStartDate), - selectedEndDate: parseFormatedDate(localEndDate), - }); - - component.querySelector('#info-field-event-title').value = title || ''; - component.querySelector('#info-field-event-description').value = description || ''; - changeInputValue(startTime, 'value', `${localStartTime}` || ''); - changeInputValue(endTime, 'value', `${localEndTime}` || ''); - changeInputValue(startTimeInput, 'value', `${startTimePieces.hours}:${startTimePieces.minutes}` || ''); - changeInputValue(startAmpmInput, 'value', startTimePieces.period || ''); - changeInputValue(endTimeInput, 'value', `${endTimePieces.hours}:${endTimePieces.minutes}` || ''); - changeInputValue(endAmpmInput, 'value', endTimePieces.period || ''); - changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); - - BlockMediator.set('eventDupMetrics', { - ...BlockMediator.get('eventDupMetrics'), - ...{ - title, - startDate: localStartDate, - eventId: eventData.eventId, - }, - }); +export default function init(component, props) { - component.classList.add('prefilled'); - } } export function onEventUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 0a1c0954..a7823cc2 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -15,8 +15,9 @@ export async function onRespUpdate(_component, _props) { // Do nothing } -async function buildPreviewListOptionsFromSource(previewList, source, attr) { - const valueHolder = previewList.closest('.series-info-wrapper').querySelector(`.${attr.handle}-input`); +async function buildPreviewListOptionsFromSource(component, source) { + const previewList = component.querySelector('.preview-list'); + const valueHolder = createTag('input', { type: 'hidden', name: 'series-template-input', value: '' }, '', { parent: previewList }); const previewListItems = previewList.querySelector('.preview-list-items'); const previewListOverlay = previewList.querySelector('.preview-list-overlay'); @@ -35,48 +36,43 @@ async function buildPreviewListOptionsFromSource(previewList, source, attr) { } options.forEach((option) => { + const itemRadio = createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }); const previewListItem = createTag('div', { class: 'preview-list-item' }); const previewListItemImage = createTag('img', { src: option['template-image'] }); const previewListItemTitle = createTag('h5', {}, option['template-name']); const selectItemBtn = createTag('a', { class: 'con-button blue select-item-btn' }, 'Select', { parent: previewListItem }); previewListItem.append(previewListItemImage, previewListItemTitle, selectItemBtn); previewListItems.append(previewListItem); - - selectItemBtn.addEventListener('click', () => { - valueHolder.value = option['template-path']; - previewListOverlay.classList.add('hidden'); - }); }); } -function buildPreviewList(attrObj) { - const { optionsSource } = attrObj; - - const previewList = createTag('div', { class: 'preview-list' }); - const previewListTitle = createTag('h4', {}, 'Select a template'); - const previewListItems = createTag('div', { class: 'preview-list-items' }); - const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, 'Select'); - const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); - const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); - const previewListCloseBtn = createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); +export default async function init(component, props) { + const previewList = component.querySelector('.preview-list'); + if (!previewList) return; - previewListBtn.addEventListener('click', () => { - previewListOverlay.classList.remove('hidden'); - buildPreviewListOptionsFromSource(previewList, optionsSource, attrObj); - }); + const templateSelectBtn = component.querySelector('.preview-list-btn'); + const closeBtn = component.querySelector('.preview-list-close-btn'); - previewListCloseBtn.addEventListener('click', () => { - previewListOverlay.classList.add('hidden'); - }); + buildPreviewListOptionsFromSource(component, previewList.getAttribute('data-source-link')); - previewListModal.append(previewListTitle, previewListItems); - previewList.append(previewListBtn, previewListOverlay); - return previewList; -} + if (templateSelectBtn) { + templateSelectBtn.addEventListener('click', async () => { + const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + if (previewListOverlay.classList.contains('hidden')) { + previewListOverlay.classList.remove('hidden'); + } else { + previewListOverlay.classList.add('hidden'); + } + }); + } -export default async function init(component, props) { - + if (closeBtn) { + closeBtn.addEventListener('click', () => { + const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + previewListOverlay.classList.add('hidden'); + }); + } } export function onEventUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index b374909d..fa3310df 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -7,6 +7,11 @@ text-align: right; } +.series-templates-component .con-button, +.series-templates-component .preview-list-close-btn { + cursor: pointer; +} + .series-templates-component sp-textfield { width: 493px; } @@ -27,9 +32,59 @@ resize: vertical; } -.series-templates-component .text-field-label { +.series-templates-component .text-field-label, +.series-templates-component .preview-list-label { width: 180px; font-family: var(--body-font-family); font-size: var(--type-body-s-size); font-weight: 700; } + +.series-templates-component .preview-list-label { + display: inline-block; + margin: 4px 12px 0 0; +} + +.series-templates-component .preview-list-overlay { + position: fixed; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2; +} + +.series-templates-component .preview-list-modal { + position: relative; + max-width: var(--grid-container-width); + max-height: 80vh; + padding: 56px 72px; + margin: auto; + background-color: var(--color-white); + border-radius: 24px; + overflow: hidden; +} + +.series-templates-component .preview-list-fieldset { + border: none; + padding: 0; + height: 100%; + display: block; +} + +.series-templates-component .preview-list-items { + height: 100%; + display: flex; + flex-wrap: wrap; + gap: 16px; + padding: 16px; + overflow: auto; +} + +.series-templates-component .preview-list-item { + +} diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index cc8ebbc8..8ebd96a5 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -2,6 +2,7 @@ import { LIBS } from '../../scripts/scripts.js'; import { generateToolTip, decorateLabeledTextfield, + getIcon, } from '../../scripts/utils.js'; async function buildPreviewList(row) { @@ -9,23 +10,39 @@ async function buildPreviewList(row) { const [labelCol, sourseLinkCel] = row.querySelectorAll(':scope > div'); - const label = labelCol?.textContent.trim() || 'Event template'; + const labelText = labelCol?.textContent.trim() || 'Event template'; const sourceLink = sourseLinkCel?.textContent.trim(); - if (!label || !sourceLink) return; + if (!labelText || !sourceLink) return; + const label = createTag('span', { class: 'preview-list-label' }, labelText); const previewList = createTag('div', { class: 'preview-list' }); const previewListTitle = createTag('h4', {}, 'Select a template'); - const previewListItems = createTag('div', { class: 'preview-list-items' }); - const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, label); + const previewListFieldset = createTag('fieldset', { class: 'preview-list-fieldset' }); + createTag('div', { class: 'preview-list-items' }, '', { parent: previewListFieldset }); + + const previewListBtn = createTag('a', { class: 'con-button fill preview-list-btn' }, 'Select'); const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); - createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); - previewListModal.append(previewListTitle, previewListItems); - previewList.append(previewListBtn, previewListOverlay); + createTag('sp-divider', { parent: previewListModal }); + + const actionArea = createTag('div', { class: 'preview-list-action-area' }, ''); + createTag('a', { class: 'preview-list-cancel-btn con-button outline' }, 'Cancel', { parent: actionArea }); + createTag('a', { class: 'preview-list-save-btn con-button fill' }, 'Save', { parent: actionArea }); + + createTag('a', { class: 'preview-list-close-btn' }, getIcon('close-circle'), { parent: previewListModal }); + previewListModal.append(previewListTitle, previewListFieldset, actionArea); + previewList.append(label, previewListBtn, previewListOverlay); + row.innerHTML = ''; row.append(previewList); + + try { + previewList.dataset.sourceLink = new URL(sourceLink).pathname; + } catch (e) { + window.lana?.log('Invalid template source link'); + } } export default async function init(el) { diff --git a/ecc/icons/close-circle.svg b/ecc/icons/close-circle.svg new file mode 100644 index 00000000..d12b9a25 --- /dev/null +++ b/ecc/icons/close-circle.svg @@ -0,0 +1,11 @@ + + + + + S CloseCircle 18 N + + \ No newline at end of file diff --git a/ecc/samples/form-component/controller.sample.js b/ecc/samples/form-component/controller.sample.js new file mode 100644 index 00000000..3a41249d --- /dev/null +++ b/ecc/samples/form-component/controller.sample.js @@ -0,0 +1,20 @@ +/* eslint-disable no-unused-vars */ +export function onSubmit(_component, _props) { + // Do nothing +} + +export async function onPayloadUpdate(_component, _props) { + // Do nothing +} + +export async function onRespUpdate(_component, _props) { + // Do nothing +} + +export default function init(_component, _props) { + // Do nothing +} + +export function onEventUpdate(_component, _props) { + // Do nothing +} From 3b60a6b319b95684f9ce36a621755f1d7d641d7d Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 2 Dec 2024 22:45:29 -0600 Subject: [PATCH 31/74] wip --- .../series-templates-component/controller.js | 8 +-- .../series-templates-component.css | 57 +++++++++++++++++-- .../series-templates-component.js | 7 +-- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index a7823cc2..7be724ee 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -36,12 +36,12 @@ async function buildPreviewListOptionsFromSource(component, source) { } options.forEach((option) => { - const itemRadio = createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }); + const radioLabel = createTag('label', { class: 'radio-label' }, option['template-name']); + createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); + const previewListItem = createTag('div', { class: 'preview-list-item' }); const previewListItemImage = createTag('img', { src: option['template-image'] }); - const previewListItemTitle = createTag('h5', {}, option['template-name']); - const selectItemBtn = createTag('a', { class: 'con-button blue select-item-btn' }, 'Select', { parent: previewListItem }); - previewListItem.append(previewListItemImage, previewListItemTitle, selectItemBtn); + previewListItem.append(radioLabel, previewListItemImage); previewListItems.append(previewListItem); }); } diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index fa3310df..3b859f63 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -12,6 +12,12 @@ cursor: pointer; } +.series-templates-component .preview-list-close-btn { + position: absolute; + top: 16px; + right: 16px; +} + .series-templates-component sp-textfield { width: 493px; } @@ -58,10 +64,14 @@ z-index: 2; } +.series-templates-component .preview-list-overlay.hidden { + display: none; +} + .series-templates-component .preview-list-modal { position: relative; max-width: var(--grid-container-width); - max-height: 80vh; + max-height: 72vh; padding: 56px 72px; margin: auto; background-color: var(--color-white); @@ -69,22 +79,59 @@ overflow: hidden; } +.series-templates-component .preview-list-modal h2 { + color: var(--color-red) +} + +.series-templates-component .preview-list-modal .preview-list-subtitle { + font-weight: 700; + font-size: var(--type-body-m-size); +} + +.series-templates-component .preview-list-modal .preview-list-action-area { + display: flex; + justify-content: flex-end; + gap: 1rem; + padding-top: 16px; + border-top: 1px solid var(--color-gray-500); +} + .series-templates-component .preview-list-fieldset { border: none; padding: 0; height: 100%; + width: 100%; display: block; } .series-templates-component .preview-list-items { height: 100%; + width: 100%; display: flex; - flex-wrap: wrap; - gap: 16px; - padding: 16px; + gap: 50px; overflow: auto; + margin-bottom: 40px; } .series-templates-component .preview-list-item { - + max-height: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.series-templates-component .preview-list-item img { + object-fit: contain; + object-position: left; + max-height: 600px; + min-width: 231px; +} + +.series-templates-component .preview-list-item label.radio-label { + font-family: var(--body-font-family); + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + align-items: center; + gap: 8px; } diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index 8ebd96a5..735364bd 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -17,7 +17,8 @@ async function buildPreviewList(row) { const label = createTag('span', { class: 'preview-list-label' }, labelText); const previewList = createTag('div', { class: 'preview-list' }); - const previewListTitle = createTag('h4', {}, 'Select a template'); + const previewListTitle = createTag('h2', {}, 'Template set up'); + const previewListSubtitle = createTag('p', { class: 'preview-list-subtitle' }, 'Select a template'); const previewListFieldset = createTag('fieldset', { class: 'preview-list-fieldset' }); createTag('div', { class: 'preview-list-items' }, '', { parent: previewListFieldset }); @@ -25,14 +26,12 @@ async function buildPreviewList(row) { const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); - createTag('sp-divider', { parent: previewListModal }); - const actionArea = createTag('div', { class: 'preview-list-action-area' }, ''); createTag('a', { class: 'preview-list-cancel-btn con-button outline' }, 'Cancel', { parent: actionArea }); createTag('a', { class: 'preview-list-save-btn con-button fill' }, 'Save', { parent: actionArea }); createTag('a', { class: 'preview-list-close-btn' }, getIcon('close-circle'), { parent: previewListModal }); - previewListModal.append(previewListTitle, previewListFieldset, actionArea); + previewListModal.append(previewListTitle, previewListSubtitle, previewListFieldset, actionArea); previewList.append(label, previewListBtn, previewListOverlay); row.innerHTML = ''; From f5037e868484db5089c89ef78f4e73369106eff4 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 10:17:08 -0600 Subject: [PATCH 32/74] added AX carousel --- .../series-templates-component/controller.js | 3 + .../series-templates-component.css | 10 +- ecc/scripts/features/carousel.css | 159 ++++++++++++++ ecc/scripts/features/carousel.js | 196 ++++++++++++++++++ 4 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 ecc/scripts/features/carousel.css create mode 100644 ecc/scripts/features/carousel.js diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 7be724ee..0b0e87c7 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -1,4 +1,5 @@ /* eslint-disable no-unused-vars */ +import buildCarousel from '../../scripts/features/carousel.js'; import { LIBS } from '../../scripts/scripts.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -44,6 +45,8 @@ async function buildPreviewListOptionsFromSource(component, source) { previewListItem.append(radioLabel, previewListItemImage); previewListItems.append(previewListItem); }); + + await buildCarousel('.preview-list-item', previewListItems); } export default async function init(component, props) { diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index 3b859f63..ddc77009 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -102,14 +102,12 @@ height: 100%; width: 100%; display: block; + min-inline-size: unset; } .series-templates-component .preview-list-items { height: 100%; width: 100%; - display: flex; - gap: 50px; - overflow: auto; margin-bottom: 40px; } @@ -129,9 +127,15 @@ .series-templates-component .preview-list-item label.radio-label { font-family: var(--body-font-family); + font-size: var(--type-body-s-size); display: flex; flex-direction: row-reverse; justify-content: flex-end; align-items: center; gap: 8px; + margin-bottom: 24px; +} + +.series-templates-component .carousel-platform { + gap: 50px; } diff --git a/ecc/scripts/features/carousel.css b/ecc/scripts/features/carousel.css new file mode 100644 index 00000000..b888806f --- /dev/null +++ b/ecc/scripts/features/carousel.css @@ -0,0 +1,159 @@ +main .carousel-container { + display: inline-block; + position: relative; + height: auto; + width: 100%; + margin-bottom: 24px; +} + +main .carousel-container .carousel-platform { + width: max-content; + max-width: 100%; + left: 0; + height: auto; + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + padding-top: 4px; + padding-bottom: 6px; +} + +main .carousel-container:not(.controls-hidden) .carousel-platform::-webkit-scrollbar, +main .carousel-container .carousel-platform.infinity-scroll-loaded::-webkit-scrollbar { + display: none; +} + +main .carousel-container .carousel-platform > * { + flex-shrink: 0; + scroll-snap-align: start; +} + +main .carousel-container .carousel-fader-left { + text-align: left; + display: flex; + position: absolute; + height: 100%; + width: 60px; + align-items: center; + top: 0; + left: 0; + transition: opacity 0.2s, display 0.2s; + cursor: pointer; + -webkit-tap-highlight-color: rgba(0,0,0,0); + z-index: 1; +} + +main .carousel-container .carousel-fader-right { + text-align: left; + display: flex; + position: absolute; + height: 100%; + width: 60px; + align-items: center; + top: 0; + right: 0; + flex-direction: row-reverse; + transition: opacity 0.2s; + cursor: pointer; + -webkit-tap-highlight-color: rgba(0,0,0,0); + z-index: 1; +} + +main .carousel-container a.button.carousel-arrow { + cursor: pointer; + display: block; + float: left; + width: 32px; + height: 32px; + margin: 0 7px; + background: var(--color-white); + box-shadow: 0 4px 8px 2px rgba(102, 102, 102, 0.1); + border-radius: 50%; + pointer-events: auto; +} + +main .carousel-container .carousel-fader-left.arrow-hidden, +main .carousel-container .carousel-fader-right.arrow-hidden { + opacity: 0; + pointer-events: none; +} + +main .carousel-container img, +main .carousel-container video { + pointer-events: none; +} + +main .carousel-container .carousel-fader-left.arrow-hidden a.button.carousel-arrow, +main .carousel-container .carousel-fader-right.arrow-hidden a.button.carousel-arrow { + pointer-events: none; +} + +main .carousel-container a.button.carousel-arrow::before { + content: ''; + position: absolute; + margin-top: 11px; + width: 8px; + height: 8px; + border-top: solid 2px var(--color-gray-700); + border-right: solid 2px var(--color-gray-700); +} + +main .carousel-container a.button.carousel-arrow-left::before { + margin-left: 13px; + transform: rotate(-135deg); +} + +main .carousel-container a.button.carousel-arrow-right { + float: right; +} + +main .carousel-container a.button.carousel-arrow-right::before { + margin-left: 9px; + transform: rotate(45deg); +} + +/* Remove snapping in mobile-breakpoint if they scrolled using drag */ +main .carousel-container.controls-hidden .carousel-platform { + scroll-snap-type: none; +} + +@media (min-width: 900px) { + main .carousel-container .carousel-platform::-webkit-scrollbar { + display: none; + } + + main .carousel-container a.button.carousel-arrow { + margin: 0 10px; + } + + main .carousel-container.controls-hidden .carousel-platform { + scroll-snap-type: x mandatory; + } +} + +main .carousel-container .carousel-platform .carousel-left-trigger, +main .carousel-container .carousel-platform .carousel-right-trigger{ + justify-self: stretch; + align-self:stretch; + width: 1px; +} + +main .carousel-container .carousel-platform.left-fader:not(.right-fader){ + -webkit-mask-image: linear-gradient(to right, transparent, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px)); + mask-image: linear-gradient(to right, transparent, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px)); +} +main .carousel-container .carousel-platform.right-fader:not(.left-fade){ + -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px), transparent); + mask-image: linear-gradient(to right, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px), transparent); +} +main .carousel-container .carousel-platform.left-fader.right-fader{ + -webkit-mask-image: linear-gradient(to right, transparent, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px), transparent); + mask-image: linear-gradient(to right, transparent, rgba(0,0,0,1) 60px, rgba(0,0,0,1) calc(100% - 60px), transparent); +} diff --git a/ecc/scripts/features/carousel.js b/ecc/scripts/features/carousel.js new file mode 100644 index 00000000..64d37a90 --- /dev/null +++ b/ecc/scripts/features/carousel.js @@ -0,0 +1,196 @@ +import { LIBS } from '../scripts.js'; + +function correctCenterAlignment(plat) { + if (plat.parentElement.offsetWidth <= plat.offsetWidth) return; + plat.parentElement.style.maxWidth = `${plat.offsetWidth}px`; +} + +function initToggleTriggers(parent) { + if (!parent) return; + + const isInHiddenSection = () => { + const parentSection = parent.closest('.section'); + if (!parentSection) return false; + + return parentSection.dataset.toggle && parentSection.style.display === 'none'; + }; + + const leftControl = parent.querySelector('.carousel-fader-left'); + const rightControl = parent.querySelector('.carousel-fader-right'); + const leftTrigger = parent.querySelector('.carousel-left-trigger'); + const rightTrigger = parent.querySelector('.carousel-right-trigger'); + const platform = parent.querySelector('.carousel-platform'); + + // If flex container has a gap, add negative margins to compensate + const gap = window.getComputedStyle(platform, null).getPropertyValue('gap'); + if (gap !== 'normal') { + const gapInt = parseInt(gap.replace('px', ''), 10); + leftTrigger.style.marginRight = `-${gapInt + 1}px`; + rightTrigger.style.marginLeft = `-${gapInt + 1}px`; + } + + // intersection observer to toggle right arrow and gradient + const onSlideIntersect = (entries) => { + if (isInHiddenSection()) return; + + entries.forEach((entry) => { + if (entry.target === leftTrigger) { + if (entry.isIntersecting) { + leftControl.classList.add('arrow-hidden'); + platform.classList.remove('left-fader'); + } else { + leftControl.classList.remove('arrow-hidden'); + platform.classList.add('left-fader'); + } + } + + if (entry.target === rightTrigger) { + if (entry.isIntersecting) { + rightControl.classList.add('arrow-hidden'); + platform.classList.remove('right-fader'); + } else { + rightControl.classList.remove('arrow-hidden'); + platform.classList.add('right-fader'); + } + } + }); + }; + + const options = { threshold: 0, root: parent }; + const slideObserver = new IntersectionObserver(onSlideIntersect, options); + slideObserver.observe(leftTrigger); + slideObserver.observe(rightTrigger); + // todo: should unobserve triggers where/when appropriate... +} + +export async function onCarouselCSSLoad(selector, parent, options) { + const { createTag } = await import(`${LIBS}/utils/utils.js`); + const carouselContent = selector ? parent.querySelectorAll(selector) : parent.querySelectorAll(':scope > *'); + + carouselContent.forEach((el) => el.classList.add('carousel-element')); + + const container = createTag('div', { class: 'carousel-container' }); + const platform = createTag('div', { class: 'carousel-platform' }); + + const faderLeft = createTag('div', { class: 'carousel-fader-left arrow-hidden' }); + const faderRight = createTag('div', { class: 'carousel-fader-right arrow-hidden' }); + + const arrowLeft = createTag('a', { class: 'button carousel-arrow carousel-arrow-left' }); + const arrowRight = createTag('a', { class: 'button carousel-arrow carousel-arrow-right' }); + arrowLeft.title = 'Carousel Left'; + arrowRight.title = 'Carousel Right'; + + platform.append(...carouselContent); + + if (!options.infinityScrollEnabled) { + const leftTrigger = createTag('div', { class: 'carousel-left-trigger' }); + const rightTrigger = createTag('div', { class: 'carousel-right-trigger' }); + + platform.prepend(leftTrigger); + platform.append(rightTrigger); + } + + container.append(platform, faderLeft, faderRight); + faderLeft.append(arrowLeft); + faderRight.append(arrowRight); + parent.append(container); + + // Scroll the carousel by clicking on the controls + const moveCarousel = (increment) => { + platform.scrollLeft -= increment; + }; + + faderLeft.addEventListener('click', () => { + const increment = Math.max((platform.offsetWidth / 4) * 3, 300); + moveCarousel(increment); + }); + faderRight.addEventListener('click', () => { + const increment = Math.max((platform.offsetWidth / 4) * 3, 300); + moveCarousel(-increment); + }); + + // Carousel loop functionality (if enabled) + const stopScrolling = () => { // To prevent safari shakiness + platform.style.overflowX = 'hidden'; + setTimeout(() => { + platform.style.removeProperty('overflow-x'); + }, 20); + }; + + const moveToCenterIfNearTheEdge = (e = null) => { + // Start at the center and snap back to center if the user scrolls to the edges + const scrollPos = platform.scrollLeft; + const maxScroll = platform.scrollWidth; + if ((scrollPos > (maxScroll / 5) * 4) || scrollPos < 30) { + if (e) e.preventDefault(); + stopScrolling(); + platform.scrollTo({ + left: ((maxScroll / 5) * 2), + behavior: 'instant', + }); + } + }; + + const infinityScroll = (children) => { + const duplicateContent = () => { + [...children].forEach((child) => { + const duplicate = child.cloneNode(true); + const duplicateLinks = duplicate.querySelectorAll('a'); + platform.append(duplicate); + if (duplicate.tagName.toLowerCase() === 'a') { + const linksPopulated = new CustomEvent('linkspopulated', { detail: [duplicate] }); + document.dispatchEvent(linksPopulated); + } + if (duplicateLinks) { + const linksPopulated = new CustomEvent('linkspopulated', { detail: duplicateLinks }); + document.dispatchEvent(linksPopulated); + } + }); + }; + + // Duplicate children to simulate smooth scrolling + for (let i = 0; i < 4; i += 1) { + duplicateContent(); + } + + platform.addEventListener('scroll', (e) => { + moveToCenterIfNearTheEdge(e); + }, { passive: false }); + }; + + // set initial states + const setInitialState = (scrollable, opts) => { + if (opts.infinityScrollEnabled) { + infinityScroll([...carouselContent]); + faderLeft.classList.remove('arrow-hidden'); + faderRight.classList.remove('arrow-hidden'); + platform.classList.add('left-fader', 'right-fader'); + } + + const onIntersect = ([entry], observer) => { + if (!entry.isIntersecting) return; + + if (opts.centerAlign) correctCenterAlignment(scrollable); + if (opts.startPosition === 'right') moveCarousel(-scrollable.scrollWidth); + if (!opts.infinityScrollEnabled) initToggleTriggers(container); + + observer.unobserve(scrollable); + }; + + const carouselObserver = new IntersectionObserver(onIntersect, { rootMargin: '1000px', threshold: 0 }); + carouselObserver.observe(scrollable); + }; + + setInitialState(platform, options); +} + +export default async function buildCarousel(selector, parent, options = {}) { + const { loadStyle, getConfig } = await import(`${LIBS}/utils/utils.js`); + // Load CSS then build carousel + return new Promise((resolve) => { + loadStyle(`${getConfig().codeRoot}/scripts/features/carousel.css`, async () => { + await onCarouselCSSLoad(selector, parent, options); + resolve(); + }); + }); +} From 0cbb332736be6cb409db9f019b91675f0b315d4c Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 11:32:13 -0600 Subject: [PATCH 33/74] interaction WIP --- .../series-templates-component/controller.js | 102 +++++++++++++----- .../series-templates-component.css | 14 ++- .../series-templates-component.js | 1 + ecc/scripts/utils.js | 8 +- 4 files changed, 93 insertions(+), 32 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 0b0e87c7..e914350d 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -18,9 +18,7 @@ export async function onRespUpdate(_component, _props) { async function buildPreviewListOptionsFromSource(component, source) { const previewList = component.querySelector('.preview-list'); - const valueHolder = createTag('input', { type: 'hidden', name: 'series-template-input', value: '' }, '', { parent: previewList }); const previewListItems = previewList.querySelector('.preview-list-items'); - const previewListOverlay = previewList.querySelector('.preview-list-overlay'); const jsonResp = await fetch(source).then((res) => { if (!res.ok) throw new Error('Failed to fetch series templates'); @@ -30,52 +28,98 @@ async function buildPreviewListOptionsFromSource(component, source) { const options = jsonResp.data; if (!options) return; - if (options.length > 3) { - previewListItems.classList.add('show-3'); - } else { - previewListItems.classList.remove('show-3'); - } - options.forEach((option) => { const radioLabel = createTag('label', { class: 'radio-label' }, option['template-name']); - createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); + const radio = createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); const previewListItem = createTag('div', { class: 'preview-list-item' }); const previewListItemImage = createTag('img', { src: option['template-image'] }); previewListItem.append(radioLabel, previewListItemImage); previewListItems.append(previewListItem); + + previewListItem.addEventListener('click', async () => { + if (radio && !radio.checked) { + radio.checked = true; + radio.dispatchEvent(new Event('change')); + } + }); + + radio.addEventListener('change', () => { + const saveBtn = component.querySelector('.preview-list-save-btn'); + saveBtn.classList.toggle('disabled', !radio.checked); + }); }); await buildCarousel('.preview-list-item', previewListItems); } -export default async function init(component, props) { +function initInteractions(component) { const previewList = component.querySelector('.preview-list'); - if (!previewList) return; - - const templateSelectBtn = component.querySelector('.preview-list-btn'); + const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + const previewListBtn = component.querySelector('.preview-list-btn'); const closeBtn = component.querySelector('.preview-list-close-btn'); + const cancelBtn = component.querySelector('.preview-list-cancel-btn'); + const saveBtn = component.querySelector('.preview-list-save-btn'); + const valueInput = component.querySelector('input[name="series-template-input"]'); + + if ( + !previewList + || !previewListOverlay + || !previewListBtn + || !closeBtn + || !cancelBtn + || !saveBtn + || !valueInput + ) return; + + const resetPreviewList = () => { + previewList.querySelector('input[name="series-template"]:checked')?.removeAttribute('checked'); + previewListOverlay.classList.add('hidden'); + saveBtn.classList.add('disabled'); + }; + + previewListBtn.addEventListener('click', () => { + previewListOverlay.classList.remove('hidden'); + }); - buildPreviewListOptionsFromSource(component, previewList.getAttribute('data-source-link')); + closeBtn.addEventListener('click', () => { + resetPreviewList(); + }); - if (templateSelectBtn) { - templateSelectBtn.addEventListener('click', async () => { - const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + cancelBtn.addEventListener('click', () => { + resetPreviewList(); + }); - if (previewListOverlay.classList.contains('hidden')) { - previewListOverlay.classList.remove('hidden'); - } else { - previewListOverlay.classList.add('hidden'); - } - }); - } + saveBtn.addEventListener('click', () => { + if (saveBtn.classList.contains('disabled')) return; + previewListOverlay.classList.add('hidden'); + valueInput.value = previewList.querySelector('input[name="series-template"]:checked').value; + }); + + valueInput.addEventListener('change', () => { + if (valueInput.value) { + valueInput.type = 'text'; + } else { + valueInput.type = 'hidden'; + } + }); - if (closeBtn) { - closeBtn.addEventListener('click', () => { - const previewListOverlay = previewList.querySelector('.preview-list-overlay'); + saveBtn.classList.toggle('disabled', !valueInput.value); + + previewListOverlay.addEventListener('click', (e) => { + if (e.target === previewListOverlay) { previewListOverlay.classList.add('hidden'); - }); - } + } + }); +} + +export default async function init(component, props) { + const previewList = component.querySelector('.preview-list'); + if (!previewList) return; + + buildPreviewListOptionsFromSource(component, previewList.getAttribute('data-source-link')); + + initInteractions(component); } export function onEventUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index ddc77009..8061ec87 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -12,6 +12,11 @@ cursor: pointer; } +.series-templates-component .preview-list-btn { + padding: 10px 20px; + border-radius: 20px; +} + .series-templates-component .preview-list-close-btn { position: absolute; top: 16px; @@ -76,7 +81,7 @@ margin: auto; background-color: var(--color-white); border-radius: 24px; - overflow: hidden; + overflow: auto; } .series-templates-component .preview-list-modal h2 { @@ -116,6 +121,7 @@ display: flex; flex-direction: column; align-items: flex-start; + cursor: pointer; } .series-templates-component .preview-list-item img { @@ -134,8 +140,14 @@ align-items: center; gap: 8px; margin-bottom: 24px; + user-select: none; } .series-templates-component .carousel-platform { gap: 50px; } + +.series-templates-component .preview-list-save-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index 735364bd..616ae778 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -21,6 +21,7 @@ async function buildPreviewList(row) { const previewListSubtitle = createTag('p', { class: 'preview-list-subtitle' }, 'Select a template'); const previewListFieldset = createTag('fieldset', { class: 'preview-list-fieldset' }); createTag('div', { class: 'preview-list-items' }, '', { parent: previewListFieldset }); + createTag('input', { type: 'hidden', name: 'series-template-input', value: '' }, '', { parent: previewList }); const previewListBtn = createTag('a', { class: 'con-button fill preview-list-btn' }, 'Select'); const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index f0e3770f..f5f86e02 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -197,19 +197,23 @@ export async function decorateLabeledTextfield(cell, inputOpts = {}, labelOpts = const label = createTag('sp-field-label', mergeOptions({ for: 'text-input', 'side-aligned': 'start', - require: isRequired, class: 'text-field-label', }, labelOpts), text); + const input = createTag('sp-textfield', mergeOptions( { class: 'text-input', placeholder: phText, - required: isRequired, size: 'xl', }, inputOpts, )); + if (isRequired) { + input.required = true; + label.required = true; + } + if (maxCharNum) input.setAttribute('maxlength', maxCharNum); const wrapper = createTag('div', { class: 'labeled-text-field-wrapper' }); From 7cc43e81429bebab08693fa49220f8c30b35c088 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 13:10:10 -0600 Subject: [PATCH 34/74] series template inputs done --- .../series-additional-info-component.js | 1 + .../series-templates-component/controller.js | 20 +++++++++++++------ .../series-templates-component.css | 11 +++++++++- .../series-templates-component.js | 13 +++++++++--- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/ecc/blocks/series-additional-info-component/series-additional-info-component.js b/ecc/blocks/series-additional-info-component/series-additional-info-component.js index eb7ddb3e..4a057821 100644 --- a/ecc/blocks/series-additional-info-component/series-additional-info-component.js +++ b/ecc/blocks/series-additional-info-component/series-additional-info-component.js @@ -7,6 +7,7 @@ export default function init(el) { el.classList.add('form-component'); const rows = el.querySelectorAll(':scope > div'); + rows.forEach(async (r, ri) => { if (ri === 0) generateToolTip(r); diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index e914350d..1b2e881e 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -6,6 +6,15 @@ const { createTag } = await import(`${LIBS}/utils/utils.js`); export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const eventTemplateInput = component.querySelector('input[name="series-template-input"]'); + const emailTemplateInput = component.querySelector('sp-textfield[name="series-email-template"]'); + + props.payload = { + ...props.payload, + eventTemplate: eventTemplateInput.value, + emailTemplate: emailTemplateInput.value, + }; } export async function onPayloadUpdate(component, props) { @@ -60,7 +69,8 @@ function initInteractions(component) { const closeBtn = component.querySelector('.preview-list-close-btn'); const cancelBtn = component.querySelector('.preview-list-cancel-btn'); const saveBtn = component.querySelector('.preview-list-save-btn'); - const valueInput = component.querySelector('input[name="series-template-input"]'); + const valueInput = component.querySelector('input.series-template-input'); + const nameInput = component.querySelector('sp-textfield.series-template-name'); if ( !previewList @@ -94,14 +104,12 @@ function initInteractions(component) { if (saveBtn.classList.contains('disabled')) return; previewListOverlay.classList.add('hidden'); valueInput.value = previewList.querySelector('input[name="series-template"]:checked').value; + nameInput.value = previewList.querySelector('input[name="series-template"]:checked').parentElement.textContent.trim(); + valueInput.dispatchEvent(new Event('change')); }); valueInput.addEventListener('change', () => { - if (valueInput.value) { - valueInput.type = 'text'; - } else { - valueInput.type = 'hidden'; - } + previewList.classList.toggle('selected', valueInput.value); }); saveBtn.classList.toggle('disabled', !valueInput.value); diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index 8061ec87..a0bdf540 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -12,6 +12,15 @@ cursor: pointer; } +.series-templates-component .preview-list .series-template-name { + display: none; + margin-right: 24px; +} + +.series-templates-component .preview-list.selected .series-template-name { + display: inline-block; +} + .series-templates-component .preview-list-btn { padding: 10px 20px; border-radius: 20px; @@ -81,7 +90,7 @@ margin: auto; background-color: var(--color-white); border-radius: 24px; - overflow: auto; + overflow: hidden auto; } .series-templates-component .preview-list-modal h2 { diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index 616ae778..3247b1d3 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -21,7 +21,8 @@ async function buildPreviewList(row) { const previewListSubtitle = createTag('p', { class: 'preview-list-subtitle' }, 'Select a template'); const previewListFieldset = createTag('fieldset', { class: 'preview-list-fieldset' }); createTag('div', { class: 'preview-list-items' }, '', { parent: previewListFieldset }); - createTag('input', { type: 'hidden', name: 'series-template-input', value: '' }, '', { parent: previewList }); + const previewListInput = createTag('input', { type: 'hidden', name: 'series-template-input', class: 'series-template-input' }); + const templateNameInput = createTag('sp-textfield', { class: 'series-template-name', size: 'xl', readonly: true }); const previewListBtn = createTag('a', { class: 'con-button fill preview-list-btn' }, 'Select'); const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); @@ -33,7 +34,13 @@ async function buildPreviewList(row) { createTag('a', { class: 'preview-list-close-btn' }, getIcon('close-circle'), { parent: previewListModal }); previewListModal.append(previewListTitle, previewListSubtitle, previewListFieldset, actionArea); - previewList.append(label, previewListBtn, previewListOverlay); + previewList.append( + label, + templateNameInput, + previewListInput, + previewListBtn, + previewListOverlay, + ); row.innerHTML = ''; row.append(previewList); @@ -53,7 +60,7 @@ export default async function init(el) { if (ri === 0) generateToolTip(r); if (ri === 1) { - await decorateLabeledTextfield(r, { id: 'info-field-series-susi' }); + await decorateLabeledTextfield(r, { id: 'series-email-template' }); } if (ri === 2) { From 3633253404eebb07d71028a90a5cc799685e69ee Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 15:10:18 -0600 Subject: [PATCH 35/74] form handler WIP --- .../series-creation-form/data-handler.js | 114 +-------- .../series-creation-form.css | 1 - .../series-creation-form.js | 230 ++---------------- .../series-details-component.css | 4 + 4 files changed, 24 insertions(+), 325 deletions(-) diff --git a/ecc/blocks/series-creation-form/data-handler.js b/ecc/blocks/series-creation-form/data-handler.js index 40dd55c2..a18d7f3a 100644 --- a/ecc/blocks/series-creation-form/data-handler.js +++ b/ecc/blocks/series-creation-form/data-handler.js @@ -5,33 +5,12 @@ let payloadCache = {}; const submissionFilter = [ // from payload and response - 'agenda', - 'topics', - 'eventType', + 'seriesName', + 'externalThemeId', 'cloudType', - 'seriesId', 'templateId', - 'communityTopicUrl', - 'title', - 'description', - 'localStartDate', - 'localEndDate', - 'localStartTime', - 'localEndTime', - 'timezone', - 'showAgendaPostEvent', - 'showVenuePostEvent', - 'showVenueImage', - 'showSponsors', - 'rsvpFormFields', - 'relatedProducts', - 'rsvpDescription', - 'attendeeLimit', - 'allowWaitlisting', - 'hostEmail', - 'eventId', - 'published', - 'creationTime', + 'relatedDomain', + 'emailTemplate', 'modificationTime', ]; @@ -69,91 +48,6 @@ export function getFilteredCachedResponse() { return responseCache; } -/** - * Recursively compares two values to determine if they are different. - * - * @param {*} value1 - The first value to compare. - * @param {*} value2 - The second value to compare. - * @returns {boolean} - Returns true if the values are different, otherwise false. - */ -export function compareObjects(value1, value2, lengthOnly = false) { - if ( - typeof value1 === 'object' - && value1 !== null - && !Array.isArray(value1) - && typeof value2 === 'object' - && value2 !== null - && !Array.isArray(value2) - ) { - if (hasContentChanged(value1, value2)) { - return true; - } - } else if (Array.isArray(value1) && Array.isArray(value2)) { - if (value1.length !== value2.length) { - // Change detected due to different array lengths - return true; - } - - if (!lengthOnly) { - for (let i = 0; i < value1.length; i += 1) { - if (compareObjects(value1[i], value2[i])) { - return true; - } - } - } - } else if (value1 !== value2) { - // Change detected - return true; - } - return false; -} - -/** - * Determines if the content of two objects has changed. - * - * @param {Object} oldData - The original object. - * @param {Object} newData - The updated object. - * @returns {boolean} - Returns true if content has changed, otherwise false. - * @throws {TypeError} - Throws error if inputs are not objects. - */ -export function hasContentChanged(oldData, newData) { - // Ensure both inputs are objects - if ( - typeof oldData !== 'object' - || oldData === null - || typeof newData !== 'object' - || newData === null - ) { - throw new TypeError('Both oldData and newData must be objects'); - } - - const ignoreList = [ - 'modificationTime', - 'status', - 'platform', - 'platformCode', - 'liveUpdate', - ]; - - // Checking keys counts - const oldDataKeys = Object.keys(oldData).filter((key) => !ignoreList.includes(key)); - const newDataKeys = Object.keys(newData).filter((key) => !ignoreList.includes(key)); - - if (oldDataKeys.length !== newDataKeys.length) { - // Change detected due to different key counts - return true; - } - - // Check for differences in the actual values - return oldDataKeys.some( - (key) => { - const lengthOnly = key === 'speakers' && !oldData[key].ordinal; - - return !ignoreList.includes(key) && compareObjects(oldData[key], newData[key], lengthOnly); - }, - ); -} - export default function getJoinedData() { const filteredResponse = getFilteredCachedResponse(); const filteredPayload = getFilteredCachedPayload(); diff --git a/ecc/blocks/series-creation-form/series-creation-form.css b/ecc/blocks/series-creation-form/series-creation-form.css index 8507378c..af1a044c 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.css +++ b/ecc/blocks/series-creation-form/series-creation-form.css @@ -13,7 +13,6 @@ --mod-textfield-border-color-invalid-hover: #000; --mod-textfield-border-color-invalid-keyboard-focus: #000; --mod-textfield-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; - --mod-textfield-font-weight: 700; --mod-textfield-spacing-block-start: 8px; } diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 0cb0a2d1..ef630e9c 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -10,7 +10,7 @@ import { getDevToken, } from '../../scripts/utils.js'; import { - createEvent, + createSeries, updateEvent, publishEvent, getEvent, @@ -26,7 +26,7 @@ import ProductSelector from '../../components/product-selector/product-selector. import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; -import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; +import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { CustomSearch } from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; @@ -276,7 +276,7 @@ async function gatherValues(props) { await Promise.all(allComponentPromises); } -async function handleEventUpdate(props) { +async function handleSeriesUpdate(props) { const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); if (!mappedComponents.length) return {}; @@ -421,7 +421,7 @@ function updateDashboardLink(props) { dashboardLink.href = url.toString(); } -async function saveEvent(props, toPublish = false) { +async function saveSeries(props, toPublish = false) { try { await gatherValues(props); } catch (e) { @@ -430,8 +430,8 @@ async function saveEvent(props, toPublish = false) { let resp; - const onEventSave = async () => { - if (resp?.eventId) await handleEventUpdate(props); + const onSeriesSave = async () => { + if (resp?.eventId) await handleSeriesUpdate(props); if (!resp.error) { showSaveSuccessMessage(props); @@ -439,24 +439,24 @@ async function saveEvent(props, toPublish = false) { }; if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { - resp = await createEvent(quickFilter(props.payload)); + resp = await createSeries(quickFilter(props.payload)); props.eventDataResp = { ...props.eventDataResp, ...resp }; updateDashboardLink(props); - await onEventSave(); + await onSeriesSave(); } else if (props.currentStep <= props.maxStep && !toPublish) { resp = await updateEvent( getFilteredCachedResponse().eventId, getJoinedData(), ); props.eventDataResp = { ...props.eventDataResp, ...resp }; - await onEventSave(); + await onSeriesSave(); } else if (toPublish) { resp = await publishEvent( getFilteredCachedResponse().eventId, getJoinedData(), ); props.eventDataResp = { ...props.eventDataResp, ...resp }; - if (resp?.eventId) await handleEventUpdate(props); + if (resp?.eventId) await handleSeriesUpdate(props); } return resp; @@ -512,189 +512,6 @@ function navigateForm(props, stepIndex) { updateRequiredFields(props); } -function closeDialog(props) { - const spTheme = props.el.querySelector('#form-app'); - if (!spTheme) return; - - const underlay = spTheme.querySelector('sp-underlay'); - const dialog = spTheme.querySelector('sp-dialog'); - - if (underlay) underlay.open = false; - if (dialog) dialog.innerHTML = ''; -} - -function buildPreviewLoadingDialog(props) { - const spTheme = props.el.querySelector('#form-app'); - if (!spTheme) return null; - - const underlay = spTheme.querySelector('sp-underlay'); - const dialog = spTheme.querySelector('sp-dialog'); - - if (!underlay || !dialog) return null; - - underlay.open = false; - dialog.innerHTML = ''; - - createTag('h1', { slot: 'heading' }, 'Generating your preview...', { parent: dialog }); - createTag('p', {}, 'This usually takes 10-30 seconds, but it might take up to 10 minutes in rare cases. Please wait, and the preview will open in a new tab when it’s ready.', { parent: dialog }); - createTag('p', {}, 'Note: Please make sure pop-up is allowed in your browser settings.', { parent: dialog }); - const style = createTag('style', {}, ` - @keyframes progress-bar-indeterminate { - 0% { - transform: translateX(-100%); - } - 50% { - transform: translateX(0%); - } - 100% { - transform: translateX(200%); - } - } - `); - - // Create the progress bar container - const progressBar = createTag('div', { - style: ` - position: relative; - width: 100%; - height: 8px; - background: #e6e6e6; - border-radius: 4px; - overflow: hidden; - margin-bottom: 1rem; - `, - }); - - // Create the progress bar indicator - const progressBarIndicator = createTag('div', { - style: ` - position: absolute; - top: 0; - left: 0; - width: 50%; - height: 100%; - background: #1473e6; - transform: translateX(0); - animation: progress-bar-indeterminate 1.5s linear infinite; - `, - }); - - // Append the elements to the shadow root - progressBar.appendChild(progressBarIndicator); - dialog.appendChild(style); - dialog.appendChild(progressBar); - const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); - createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'Cancel', { parent: buttonContainer }); - - underlay.open = true; - - return dialog; -} - -function buildPreviewLoadingFailedDialog(props) { - const spTheme = props.el.querySelector('#form-app'); - if (!spTheme) return; - - const underlay = spTheme.querySelector('sp-underlay'); - const dialog = spTheme.querySelector('sp-dialog'); - - if (!underlay || !dialog) return; - - underlay.open = false; - dialog.innerHTML = ''; - - createTag('h1', { slot: 'heading' }, 'Preview generation failed.', { parent: dialog }); - createTag('p', {}, 'Your changes have been saved. Our system is working in the background to update the page.', { parent: dialog }); - const slackLink = createTag('a', { href: 'https://adobe.enterprise.slack.com/archives/C07KPJYA760' }, 'Slack'); - const emailLink = createTag('a', { href: 'mailto:Grp-acom-milo-events-support@adobe.com' }, 'Grp-acom-milo-events-support@adobe.com'); - createTag('p', {}, `Please try again later. If the issue persists, please feel free to contact us on ${slackLink.outerHTML} or email ${emailLink.outerHTML}`, { parent: dialog }); - const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); - const cancelButton = createTag('sp-button', { variant: 'cta', slot: 'button', id: 'cancel-preview' }, 'OK', { parent: buttonContainer }); - - underlay.open = true; - - cancelButton.addEventListener('click', () => { - closeDialog(props); - dialog.innerHTML = ''; - }); -} - -async function getNonProdPreviewDataById(props) { - if (!props.eventDataResp) return null; - - const { eventId } = props.eventDataResp; - - if (!eventId) return null; - - const esEnv = getEventServiceEnv(); - const resp = await fetch(`${getEventPageHost()}/events/default/${esEnv === 'prod' ? '' : `${esEnv}/`}metadata-preview.json`); - if (resp.ok) { - const json = await resp.json(); - const pageData = json.data.find((d) => d['event-id'] === eventId); - - if (pageData) return pageData; - - window.lana?.log('Failed to find non-prod metadata for current page'); - return null; - } - - window.lana?.log('Failed to fetch non-prod metadata:', resp); - return null; -} - -async function validatePreview(props, oldResp, cta) { - let retryCount = 0; - - const currentData = { ...props.eventDataResp }; - const oldData = { ...oldResp }; - - if (!hasContentChanged(currentData, oldData) || !Object.keys(oldData).length) { - window.open(cta.href); - return Promise.resolve(); - } - - const modificationTimeMatch = (metadataObj) => { - const metadataModTimestamp = new Date(metadataObj['modification-time']).getTime(); - return metadataModTimestamp === props.eventDataResp.modificationTime; - }; - - return new Promise((resolve) => { - const interval = setInterval(async () => { - try { - retryCount += 1; - const metadataJson = await getNonProdPreviewDataById(props); - - if (metadataJson && modificationTimeMatch(metadataJson)) { - clearInterval(interval); - closeDialog(props); - window.open(cta.href); - resolve(); - } else if (retryCount >= 30) { - clearInterval(interval); - buildPreviewLoadingFailedDialog(props); - window.lana?.log('Error: Failed to match metadata after 30 retries'); - resolve(); - } - } catch (error) { - window.lana?.log('Error in interval fetch:', error); - clearInterval(interval); - resolve(); - } - }, Math.floor(Math.random() * (2000 - 1000 + 1)) + 1000); - - const dialog = buildPreviewLoadingDialog(props, interval); - - if (dialog) { - const cancelButton = dialog.querySelector('#cancel-preview'); - cancelButton.addEventListener('click', () => { - closeDialog(props); - if (interval) clearInterval(interval); - resolve(); - }); - } - }); -} - function initFormCtas(props) { const ctaRow = props.el.querySelector(':scope > div:last-of-type'); decorateButtons(ctaRow, 'button-l'); @@ -722,23 +539,10 @@ function initFormCtas(props) { }); }; - let oldResp = { ...props.eventDataResp }; ctas.forEach((cta) => { if (cta.href) { const ctaUrl = new URL(cta.href); - if (['#pre-event', '#post-event'].includes(ctaUrl.hash)) { - cta.classList.add('fill', 'preview-btns', 'preview-not-ready', ctaUrl.hash.replace('#', '')); - cta.addEventListener('click', async (e) => { - e.preventDefault(); - toggleBtnsSubmittingState(true); - if (cta.classList.contains('preview-not-ready')) return; - validatePreview(props, oldResp, cta).then(() => { - toggleBtnsSubmittingState(false); - }); - }); - } - if (['#save', '#next'].includes(ctaUrl.hash)) { if (ctaUrl.hash === '#next') { cta.classList.add('next-button'); @@ -758,11 +562,9 @@ function initFormCtas(props) { if (ctaUrl.hash === '#next') { let resp; if (props.currentStep === props.maxStep) { - oldResp = { ...props.eventDataResp }; - resp = await saveEvent(props, true); + resp = await saveSeries(props, true); } else { - oldResp = { ...props.eventDataResp }; - resp = await saveEvent(props); + resp = await saveSeries(props); } if (resp?.error) { @@ -797,7 +599,7 @@ function initFormCtas(props) { } } else { oldResp = { ...props.eventDataResp }; - const resp = await saveEvent(props); + const resp = await saveSeries(props); if (resp?.error) { buildErrorMessage(props, resp); } @@ -812,7 +614,7 @@ function initFormCtas(props) { backBtn.addEventListener('click', async () => { toggleBtnsSubmittingState(true); oldResp = { ...props.eventDataResp }; - const resp = await saveEvent(props); + const resp = await saveSeries(props); if (resp?.error) { buildErrorMessage(props, resp); } else { @@ -868,7 +670,7 @@ function initNavigation(props) { if (!nav.disabled && !sideMenu.classList.contains('disabled')) { sideMenu.classList.add('disabled'); - const resp = await saveEvent(props); + const resp = await saveSeries(props); if (resp?.error) { buildErrorMessage(props, resp); } else { @@ -1019,7 +821,7 @@ function buildLoadingScreen(el) { el.classList.add('loading'); const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); - createTag('sp-field-label', {}, 'Loading Adobe Event Creation Console form...', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Adobe event series creation form...', { parent: loadingScreen }); el.prepend(loadingScreen); } diff --git a/ecc/blocks/series-details-component/series-details-component.css b/ecc/blocks/series-details-component/series-details-component.css index e423dcca..e7c41461 100644 --- a/ecc/blocks/series-details-component/series-details-component.css +++ b/ecc/blocks/series-details-component/series-details-component.css @@ -1,3 +1,7 @@ +.series-details-component { + --mod-textfield-font-weight: 700; +} + .series-details-component .info-field-wrapper { margin-bottom: 24px; } From 7fc576a9b948898b4df7851bab2cc762bbc316e0 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 15:43:54 -0600 Subject: [PATCH 36/74] renaming onTargetUpdate --- .../event-agenda-component/controller.js | 2 +- .../controller.js | 2 +- .../event-creation-form.css | 1 + .../event-creation-form.js | 4 +- .../event-format-component/controller.js | 2 +- ecc/blocks/event-info-component/controller.js | 2 +- .../event-partners-component/controller.js | 2 +- .../event-topics-component/controller.js | 2 +- ecc/blocks/form-handler/form-handler.js | 4 +- ecc/blocks/img-upload-component/controller.js | 2 +- .../product-promotion-component/controller.js | 2 +- ecc/blocks/profile-component/controller.js | 2 +- .../controller.js | 2 +- .../controller.js | 2 +- .../controller.js | 2 +- .../series-creation-form.css | 1 + .../series-creation-form.js | 114 ++++++---------- .../series-details-component/controller.js | 2 +- .../series-templates-component/controller.js | 2 +- .../terms-conditions-component/controller.js | 2 +- ecc/blocks/venue-info-component/controller.js | 2 +- .../form-component/controller.sample.js | 2 +- ecc/scripts/esp-controller.js | 125 ++++++++++++++++++ 23 files changed, 191 insertions(+), 92 deletions(-) diff --git a/ecc/blocks/event-agenda-component/controller.js b/ecc/blocks/event-agenda-component/controller.js index 097c77d2..ae692471 100644 --- a/ecc/blocks/event-agenda-component/controller.js +++ b/ecc/blocks/event-agenda-component/controller.js @@ -38,6 +38,6 @@ export default function init(component, props) { showAgendaPostEvent.checked = eventData.showAgendaPostEvent; } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/event-community-link-component/controller.js b/ecc/blocks/event-community-link-component/controller.js index 645db3a8..309370b1 100644 --- a/ecc/blocks/event-community-link-component/controller.js +++ b/ecc/blocks/event-community-link-component/controller.js @@ -47,6 +47,6 @@ export default function init(component, props) { updateInputState(); } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/event-creation-form/event-creation-form.css b/ecc/blocks/event-creation-form/event-creation-form.css index 20c4591b..e38ec2a9 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.css +++ b/ecc/blocks/event-creation-form/event-creation-form.css @@ -186,6 +186,7 @@ .event-creation-form .side-menu ul li a { color: var(--color-black); + text-decoration: none; } .event-creation-form .side-menu ul li a, diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index 43d61569..7850df6d 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -292,8 +292,8 @@ async function handleEventUpdate(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onEventUpdate } = await import(`../${comp}-component/controller.js`); - return onEventUpdate(component, props); + const { onTargetUpdate } = await import(`../${comp}-component/controller.js`); + return onTargetUpdate(component, props); }); return Promise.all(promises); diff --git a/ecc/blocks/event-format-component/controller.js b/ecc/blocks/event-format-component/controller.js index 8401ed9a..dfe6679c 100644 --- a/ecc/blocks/event-format-component/controller.js +++ b/ecc/blocks/event-format-component/controller.js @@ -164,6 +164,6 @@ export function onSubmit(component, props) { props.payload = { ...props.payload, ...eventFormat }; } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/event-info-component/controller.js b/ecc/blocks/event-info-component/controller.js index 40ef879d..6f29aaa1 100644 --- a/ecc/blocks/event-info-component/controller.js +++ b/ecc/blocks/event-info-component/controller.js @@ -595,6 +595,6 @@ export default async function init(component, props) { } } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/event-partners-component/controller.js b/ecc/blocks/event-partners-component/controller.js index e2529c11..308fc14e 100644 --- a/ecc/blocks/event-partners-component/controller.js +++ b/ecc/blocks/event-partners-component/controller.js @@ -178,6 +178,6 @@ export default async function init(component, props) { partnerVisible.checked = eventData.showSponsors; } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/event-topics-component/controller.js b/ecc/blocks/event-topics-component/controller.js index f0de27df..cf5fdc7b 100644 --- a/ecc/blocks/event-topics-component/controller.js +++ b/ecc/blocks/event-topics-component/controller.js @@ -37,6 +37,6 @@ export default function init(component, props) { } } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 7988b4e3..c8c941d0 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -292,8 +292,8 @@ async function handleEventUpdate(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onEventUpdate } = await import(`../${comp}-component/controller.js`); - return onEventUpdate(component, props); + const { onTargetUpdate } = await import(`../${comp}-component/controller.js`); + return onTargetUpdate(component, props); }); return Promise.all(promises); diff --git a/ecc/blocks/img-upload-component/controller.js b/ecc/blocks/img-upload-component/controller.js index 56b0fcbc..07ebf771 100644 --- a/ecc/blocks/img-upload-component/controller.js +++ b/ecc/blocks/img-upload-component/controller.js @@ -179,6 +179,6 @@ export default async function init(component, props) { } } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/product-promotion-component/controller.js b/ecc/blocks/product-promotion-component/controller.js index 5822897e..aacd23a4 100644 --- a/ecc/blocks/product-promotion-component/controller.js +++ b/ecc/blocks/product-promotion-component/controller.js @@ -126,6 +126,6 @@ export default async function init(component, props) { } } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/profile-component/controller.js b/ecc/blocks/profile-component/controller.js index 846f839d..30770932 100644 --- a/ecc/blocks/profile-component/controller.js +++ b/ecc/blocks/profile-component/controller.js @@ -146,6 +146,6 @@ export default async function init(component, props) { component.classList.add('prefilled'); } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/registration-details-component/controller.js b/ecc/blocks/registration-details-component/controller.js index 97f52334..78f4adc8 100644 --- a/ecc/blocks/registration-details-component/controller.js +++ b/ecc/blocks/registration-details-component/controller.js @@ -77,6 +77,6 @@ export default function init(component, props) { prefillFields(component, props); } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/registration-fields-component/controller.js b/ecc/blocks/registration-fields-component/controller.js index 353d6bb5..c9097866 100644 --- a/ecc/blocks/registration-fields-component/controller.js +++ b/ecc/blocks/registration-fields-component/controller.js @@ -56,6 +56,6 @@ export default function init(component, props) { }); } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/series-additional-info-component/controller.js b/ecc/blocks/series-additional-info-component/controller.js index e9b1f488..6be51403 100644 --- a/ecc/blocks/series-additional-info-component/controller.js +++ b/ecc/blocks/series-additional-info-component/controller.js @@ -15,6 +15,6 @@ export default function init(component, props) { } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/series-creation-form/series-creation-form.css b/ecc/blocks/series-creation-form/series-creation-form.css index af1a044c..205f800e 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.css +++ b/ecc/blocks/series-creation-form/series-creation-form.css @@ -189,6 +189,7 @@ .series-creation-form .side-menu ul li a { color: var(--color-black); + text-decoration: none; } .series-creation-form .side-menu ul li a, diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index ef630e9c..2e8c00cc 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -4,16 +4,15 @@ import { buildNoAccessScreen, generateToolTip, camelToSentenceCase, - getEventPageHost, signIn, getEventServiceEnv, getDevToken, } from '../../scripts/utils.js'; import { createSeries, - updateEvent, - publishEvent, - getEvent, + updateSeries, + publishSeries, + getSeries, } from '../../scripts/esp-controller.js'; import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; import { Profile } from '../../components/profile/profile.js'; @@ -96,10 +95,10 @@ export function buildErrorMessage(props, resp) { }, { once: true }); }); } else if (errorMessage) { - if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Event update invalid, event has been modified since last fetch"}') { - const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The event has been updated by a different session since your last save.', { parent: toastArea }); + if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Series update invalid. The series has been modified since last fetch"}') { + const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The series has been updated by a different session since your last save.', { parent: toastArea }); const url = new URL(window.location.href); - url.searchParams.set('eventId', getFilteredCachedResponse().eventId); + url.searchParams.set('seriesId', getFilteredCachedResponse().seriesId); createTag('sp-button', { slot: 'action', @@ -186,7 +185,7 @@ function initRequiredFieldsValidation(props) { function enableSideNavForEditFlow(props) { const frags = props.el.querySelectorAll('.fragment'); - const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component:not(.event-agenda-component)')) + const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component')) .every((fc) => fc.classList.contains('prefilled')); if (!completeFirstStep) return; @@ -217,18 +216,18 @@ function initCustomLitComponents() { customElements.define('custom-search', CustomSearch); } -async function loadEventData(props) { +async function loadData(props) { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); - const eventId = urlParams.get('eventId'); + const seriesId = urlParams.get('seriesId'); - if (eventId) { + if (seriesId) { setTimeout(() => { - if (!props.eventDataResp.eventId) { + if (!props.response.seriesId) { const toastArea = props.el.querySelector('.toast-area'); if (!toastArea) return; - const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the eventId URL Param is valid.', { parent: toastArea }); + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the seriesId URL Param is valid.', { parent: toastArea }); toast.addEventListener('close', () => { toast.remove(); }); @@ -236,8 +235,8 @@ async function loadEventData(props) { }, 5000); props.el.classList.add('disabled'); - const eventData = await getEvent(eventId); - props.eventDataResp = { ...props.eventDataResp, ...eventData }; + const data = await getSeries(seriesId); + props.response = { ...props.response, ...data }; props.el.classList.remove('disabled'); } } @@ -282,8 +281,8 @@ async function handleSeriesUpdate(props) { if (!mappedComponents.length) return {}; const promises = Array.from(mappedComponents).map(async (component) => { - const { onEventUpdate } = await import(`../${comp}-component/controller.js`); - return onEventUpdate(component, props); + const { onTargetUpdate } = await import(`../${comp}-component/controller.js`); + return onTargetUpdate(component, props); }); return Promise.all(promises); @@ -408,16 +407,16 @@ function showSaveSuccessMessage(props, detail = { message: 'Edits saved successf function updateDashboardLink(props) { // FIXME: presuming first link is dashboard link is not good. - if (!getFilteredCachedResponse().eventId) return; + if (!getFilteredCachedResponse().seriesId) return; const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); if (!dashboardLink) return; const url = new URL(dashboardLink.href); - if (url.searchParams.has('eventId')) return; + if (url.searchParams.has('seriesId')) return; - url.searchParams.set('newEventId', getFilteredCachedResponse().eventId); + url.searchParams.set('newEventId', getFilteredCachedResponse().seriesId); dashboardLink.href = url.toString(); } @@ -431,32 +430,32 @@ async function saveSeries(props, toPublish = false) { let resp; const onSeriesSave = async () => { - if (resp?.eventId) await handleSeriesUpdate(props); + if (resp?.seriesId) await handleSeriesUpdate(props); if (!resp.error) { showSaveSuccessMessage(props); } }; - if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { + if (props.currentStep === 0 && !getFilteredCachedResponse().seriesId) { resp = await createSeries(quickFilter(props.payload)); - props.eventDataResp = { ...props.eventDataResp, ...resp }; + props.response = { ...props.response, ...resp }; updateDashboardLink(props); await onSeriesSave(); } else if (props.currentStep <= props.maxStep && !toPublish) { - resp = await updateEvent( - getFilteredCachedResponse().eventId, + resp = await updateSeries( + getFilteredCachedResponse().seriesId, getJoinedData(), ); - props.eventDataResp = { ...props.eventDataResp, ...resp }; + props.response = { ...props.response, ...resp }; await onSeriesSave(); } else if (toPublish) { - resp = await publishEvent( - getFilteredCachedResponse().eventId, + resp = await publishSeries( + getFilteredCachedResponse().seriesId, getJoinedData(), ); - props.eventDataResp = { ...props.eventDataResp, ...resp }; - if (resp?.eventId) await handleSeriesUpdate(props); + props.response = { ...props.response, ...resp }; + if (resp?.seriesId) await handleSeriesUpdate(props); } return resp; @@ -486,7 +485,7 @@ function renderFormNavigation(props, prevStep, currentStep) { frags[currentStep].classList.remove('hidden'); if (props.currentStep === props.maxStep) { - if (props.eventDataResp.published) { + if (props.response.published) { nextBtn.textContent = nextBtn.dataset.republishStateText; } else { nextBtn.textContent = nextBtn.dataset.finalStateText; @@ -521,7 +520,7 @@ function initFormCtas(props) { const forwardActionsWrappers = ctaRow.querySelectorAll(':scope > div'); const panelWrapper = createTag('div', { class: 'series-creation-form-panel-wrapper' }, '', { parent: ctaRow }); - const backwardWrapper = createTag('div', { class: 'series-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); + createTag('div', { class: 'series-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); const forwardWrapper = createTag('div', { class: 'series-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); forwardActionsWrappers.forEach((w) => { @@ -529,12 +528,8 @@ function initFormCtas(props) { forwardWrapper.append(w); }); - const backBtn = createTag('a', { class: 'back-btn' }, getIcon('chev-left-white')); - - backwardWrapper.append(backBtn); - const toggleBtnsSubmittingState = (submitting) => { - [...ctas, backBtn].forEach((c) => { + ctas.forEach((c) => { c.classList.toggle('submitting', submitting); }); }; @@ -575,7 +570,7 @@ function initFormCtas(props) { cta.classList.add('disabled'); if (toastArea) { - const toast = createTag('sp-toast', { open: true, variant: 'positive' }, 'Success! This event has been published.', { parent: toastArea }); + const toast = createTag('sp-toast', { open: true, variant: 'positive' }, 'Success! This series has been published.', { parent: toastArea }); const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); createTag( @@ -598,7 +593,6 @@ function initFormCtas(props) { navigateForm(props); } } else { - oldResp = { ...props.eventDataResp }; const resp = await saveSeries(props); if (resp?.error) { buildErrorMessage(props, resp); @@ -610,37 +604,15 @@ function initFormCtas(props) { } } }); - - backBtn.addEventListener('click', async () => { - toggleBtnsSubmittingState(true); - oldResp = { ...props.eventDataResp }; - const resp = await saveSeries(props); - if (resp?.error) { - buildErrorMessage(props, resp); - } else { - props.currentStep -= 1; - } - - toggleBtnsSubmittingState(false); - }); } function updateCtas(props) { const formCtas = props.el.querySelectorAll('.series-creation-form-ctas-panel a'); - const { eventDataResp } = props; formCtas.forEach((a) => { - if (a.classList.contains('preview-btns')) { - const testTime = a.classList.contains('pre-event') ? +props.eventDataResp.localEndTimeMillis - 10 : +props.eventDataResp.localEndTimeMillis + 10; - if (eventDataResp.detailPagePath) { - a.href = `${getEventPageHost()}${eventDataResp.detailPagePath}?previewMode=true&cachebuster=${Date.now()}&timing=${testTime}`; - a.classList.remove('preview-not-ready'); - } - } - if (a.classList.contains('next-button')) { if (props.currentStep === props.maxStep) { - if (props.eventDataResp.published) { + if (props.response.published) { a.textContent = a.dataset.republishStateText; } else { a.textContent = a.dataset.finalStateText; @@ -698,22 +670,22 @@ function initDeepLink(props) { } function updateStatusTag(props) { - const { eventDataResp } = props; + const { response } = props; - if (eventDataResp?.published === undefined) return; + if (response?.published === undefined) return; const currentFragment = getCurrentFragment(props); const headingSection = currentFragment.querySelector(':scope > .section:first-child'); - const eixstingStatusTag = headingSection.querySelector('.event-status-tag'); + const eixstingStatusTag = headingSection.querySelector('.status-tag'); if (eixstingStatusTag) eixstingStatusTag.remove(); const heading = headingSection.querySelector('h2', 'h3', 'h3', 'h4'); const headingWrapper = createTag('div', { class: 'step-heading-wrapper' }); - const dot = eventDataResp.published ? getIcon('dot-purple') : getIcon('dot-green'); - const text = eventDataResp.published ? 'Published' : 'Draft'; - const statusTag = createTag('span', { class: 'event-status-tag' }); + const dot = response.published ? getIcon('dot-purple') : getIcon('dot-green'); + const text = response.published ? 'Published' : 'Draft'; + const statusTag = createTag('span', { class: 'status-tag' }); statusTag.append(dot, text); heading.parentElement?.replaceChild(headingWrapper, heading); @@ -727,7 +699,7 @@ async function buildForm(el) { farthestStep: 0, maxStep: el.querySelectorAll('.fragment').length - 1, payload: {}, - eventDataResp: {}, + response: {}, }; const dataHandler = { @@ -761,7 +733,7 @@ async function buildForm(el) { break; } - case 'eventDataResp': { + case 'response': { setResponseCache(value); updateComponentsOnRespChange(target); updateCtas(target); @@ -795,7 +767,7 @@ async function buildForm(el) { }); }); - await loadEventData(proxyProps); + await loadData(proxyProps); initFormCtas(proxyProps); initNavigation(proxyProps); await initComponents(proxyProps); diff --git a/ecc/blocks/series-details-component/controller.js b/ecc/blocks/series-details-component/controller.js index e9b1f488..6be51403 100644 --- a/ecc/blocks/series-details-component/controller.js +++ b/ecc/blocks/series-details-component/controller.js @@ -15,6 +15,6 @@ export default function init(component, props) { } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 1b2e881e..8277a817 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -130,6 +130,6 @@ export default async function init(component, props) { initInteractions(component); } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/terms-conditions-component/controller.js b/ecc/blocks/terms-conditions-component/controller.js index 4fccb2d4..4a9f6e51 100644 --- a/ecc/blocks/terms-conditions-component/controller.js +++ b/ecc/blocks/terms-conditions-component/controller.js @@ -81,6 +81,6 @@ export function onSubmit(_component, _props) { // Do nothing } -export function onEventUpdate(component, props) { +export function onTargetUpdate(component, props) { // Do nothing } diff --git a/ecc/blocks/venue-info-component/controller.js b/ecc/blocks/venue-info-component/controller.js index 482191c0..3dff5ad1 100644 --- a/ecc/blocks/venue-info-component/controller.js +++ b/ecc/blocks/venue-info-component/controller.js @@ -266,7 +266,7 @@ export default async function init(component, props) { } } -export async function onEventUpdate(component, props) { +export async function onTargetUpdate(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; const venueData = getVenueDataInForm(component); diff --git a/ecc/samples/form-component/controller.sample.js b/ecc/samples/form-component/controller.sample.js index 3a41249d..d149ef26 100644 --- a/ecc/samples/form-component/controller.sample.js +++ b/ecc/samples/form-component/controller.sample.js @@ -15,6 +15,6 @@ export default function init(_component, _props) { // Do nothing } -export function onEventUpdate(_component, _props) { +export function onTargetUpdate(_component, _props) { // Do nothing } diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 93bfc3a0..ae4cc50a 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -785,6 +785,131 @@ export async function getSeries() { } } +export async function getSeriesById(seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const options = await constructRequestOptions('GET'); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to fetch series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to fetch series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function createSeries(seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify(seriesData); + const options = await constructRequestOptions('POST', raw); + + try { + const response = await fetch(`${host}/v1/series`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log('Failed to create series. Status:', response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log('Failed to create series. Error:', error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function updateSeries(seriesData, seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to update series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to update series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function publishSeries(seriesId, seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'published' }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to publish series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to publish series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function unpublishSeries(seriesId, seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'unpublished' }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to unpublish series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to unpublish series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + +export async function archiveSeries(seriesId, seriesData) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const raw = JSON.stringify({ ...seriesData, seriesId, status: 'archived' }); + const options = await constructRequestOptions('PUT', raw); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + const data = await response.json(); + + if (!response.ok) { + window.lana?.log(`Failed to archive series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + return data; + } catch (error) { + window.lana?.log(`Failed to archive series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + export async function createAttendee(eventId, attendeeData) { if (!eventId || !attendeeData) return false; From 25f0c7a244f3854228a33c442217266f95a9f7d2 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 3 Dec 2024 20:08:10 -0600 Subject: [PATCH 37/74] Update controller.js --- ecc/blocks/series-templates-component/controller.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 8277a817..7f6f843c 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -71,6 +71,7 @@ function initInteractions(component) { const saveBtn = component.querySelector('.preview-list-save-btn'); const valueInput = component.querySelector('input.series-template-input'); const nameInput = component.querySelector('sp-textfield.series-template-name'); + const allRadioInputs = previewList.querySelectorAll('input[name="series-template"]'); if ( !previewList @@ -83,13 +84,20 @@ function initInteractions(component) { ) return; const resetPreviewList = () => { - previewList.querySelector('input[name="series-template"]:checked')?.removeAttribute('checked'); + allRadioInputs.forEach((radio) => { + radio.checked = false; + }); + previewListOverlay.classList.add('hidden'); saveBtn.classList.add('disabled'); }; previewListBtn.addEventListener('click', () => { previewListOverlay.classList.remove('hidden'); + if (valueInput.value) { + const selectedRadio = previewList.querySelector(`input[type='radio'][value="${valueInput.value}"]`); + if (selectedRadio) selectedRadio.checked = true; + } }); closeBtn.addEventListener('click', () => { @@ -116,7 +124,7 @@ function initInteractions(component) { previewListOverlay.addEventListener('click', (e) => { if (e.target === previewListOverlay) { - previewListOverlay.classList.add('hidden'); + resetPreviewList(); } }); } From a6f82c1b49e7767ecf32cd79fbf42c75ae397178 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 4 Dec 2024 14:29:34 -0600 Subject: [PATCH 38/74] WIP --- .../series-details-component.js | 23 +---- .../series-templates-component/controller.js | 63 +++++++------ .../series-templates-component.css | 46 ++++++---- .../series-templates-component.js | 88 +++++++++++-------- 4 files changed, 109 insertions(+), 111 deletions(-) diff --git a/ecc/blocks/series-details-component/series-details-component.js b/ecc/blocks/series-details-component/series-details-component.js index 3f22de45..b17b6ca7 100644 --- a/ecc/blocks/series-details-component/series-details-component.js +++ b/ecc/blocks/series-details-component/series-details-component.js @@ -19,8 +19,7 @@ async function decorateCloudTagSelect(column) { // FIXME: cloulds shouldn't be hardcoded // const clouds = await getClouds(); - // const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }]; - const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }]; + const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }]; Object.entries(clouds).forEach(([, val]) => { const opt = createTag('sp-menu-item', { value: val.id }, val.name); @@ -30,25 +29,6 @@ async function decorateCloudTagSelect(column) { select.pending = false; } -function decorateSeriesFormatSelect(cell) { - const formatSelectWrapper = createTag('div', { class: 'format-picker-wrapper' }); - const label = createTag('sp-field-label', { for: 'format-select-input' }, cell.textContent.trim()); - const select = createTag('sp-picker', { id: 'format-select-input', class: 'select-input', size: 'm', label: 'Format' }); - const options = [ - { id: 'InPerson', name: 'In-Person' }, - { id: 'Webinar', name: 'Webinar' }, - ]; - - options.forEach((o) => { - const opt = createTag('sp-menu-item', { value: o.id }, o.name); - select.append(opt); - }); - - cell.innerHTML = ''; - formatSelectWrapper.append(label, select); - cell.append(formatSelectWrapper); -} - export default function init(el) { el.classList.add('form-component'); @@ -62,7 +42,6 @@ export default function init(el) { cols.forEach(async (c, ci) => { if (ci === 0) decorateCloudTagSelect(c); - if (ci === 1) decorateSeriesFormatSelect(c); }); } diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 7f6f843c..bbe6000b 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -26,8 +26,7 @@ export async function onRespUpdate(_component, _props) { } async function buildPreviewListOptionsFromSource(component, source) { - const previewList = component.querySelector('.preview-list'); - const previewListItems = previewList.querySelector('.preview-list-items'); + const pickerItems = component.querySelector('.picker-items'); const jsonResp = await fetch(source).then((res) => { if (!res.ok) throw new Error('Failed to fetch series templates'); @@ -41,12 +40,12 @@ async function buildPreviewListOptionsFromSource(component, source) { const radioLabel = createTag('label', { class: 'radio-label' }, option['template-name']); const radio = createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); - const previewListItem = createTag('div', { class: 'preview-list-item' }); - const previewListItemImage = createTag('img', { src: option['template-image'] }); - previewListItem.append(radioLabel, previewListItemImage); - previewListItems.append(previewListItem); + const pickerItem = createTag('div', { class: 'picker-item' }); + const pickerItemImage = createTag('img', { src: option['template-image'] }); + pickerItem.append(radioLabel, pickerItemImage); + pickerItems.append(pickerItem); - previewListItem.addEventListener('click', async () => { + pickerItem.addEventListener('click', async () => { if (radio && !radio.checked) { radio.checked = true; radio.dispatchEvent(new Event('change')); @@ -54,29 +53,29 @@ async function buildPreviewListOptionsFromSource(component, source) { }); radio.addEventListener('change', () => { - const saveBtn = component.querySelector('.preview-list-save-btn'); + const saveBtn = component.querySelector('.picker-save-btn'); saveBtn.classList.toggle('disabled', !radio.checked); }); }); - await buildCarousel('.preview-list-item', previewListItems); + await buildCarousel('.picker-item', pickerItems); } function initInteractions(component) { - const previewList = component.querySelector('.preview-list'); - const previewListOverlay = previewList.querySelector('.preview-list-overlay'); - const previewListBtn = component.querySelector('.preview-list-btn'); - const closeBtn = component.querySelector('.preview-list-close-btn'); - const cancelBtn = component.querySelector('.preview-list-cancel-btn'); - const saveBtn = component.querySelector('.preview-list-save-btn'); + const picker = component.querySelector('.picker'); + const pickerOverlay = component.querySelector('.picker-overlay'); + const pickerBtn = component.querySelector('.picker-btn'); + const closeBtn = component.querySelector('.picker-close-btn'); + const cancelBtn = component.querySelector('.picker-cancel-btn'); + const saveBtn = component.querySelector('.picker-save-btn'); const valueInput = component.querySelector('input.series-template-input'); const nameInput = component.querySelector('sp-textfield.series-template-name'); - const allRadioInputs = previewList.querySelectorAll('input[name="series-template"]'); + const allRadioInputs = component.querySelectorAll('input[name="series-template"]'); if ( - !previewList - || !previewListOverlay - || !previewListBtn + !picker + || !pickerOverlay + || !pickerBtn || !closeBtn || !cancelBtn || !saveBtn @@ -88,14 +87,14 @@ function initInteractions(component) { radio.checked = false; }); - previewListOverlay.classList.add('hidden'); + pickerOverlay.classList.add('hidden'); saveBtn.classList.add('disabled'); }; - previewListBtn.addEventListener('click', () => { - previewListOverlay.classList.remove('hidden'); + pickerBtn.addEventListener('click', () => { + pickerOverlay.classList.remove('hidden'); if (valueInput.value) { - const selectedRadio = previewList.querySelector(`input[type='radio'][value="${valueInput.value}"]`); + const selectedRadio = component.querySelector(`input[type='radio'][value="${valueInput.value}"]`); if (selectedRadio) selectedRadio.checked = true; } }); @@ -110,30 +109,30 @@ function initInteractions(component) { saveBtn.addEventListener('click', () => { if (saveBtn.classList.contains('disabled')) return; - previewListOverlay.classList.add('hidden'); - valueInput.value = previewList.querySelector('input[name="series-template"]:checked').value; - nameInput.value = previewList.querySelector('input[name="series-template"]:checked').parentElement.textContent.trim(); + pickerOverlay.classList.add('hidden'); + valueInput.value = component.querySelector('input[name="series-template"]:checked').value; + nameInput.value = component.querySelector('input[name="series-template"]:checked').parentElement.textContent.trim(); valueInput.dispatchEvent(new Event('change')); }); valueInput.addEventListener('change', () => { - previewList.classList.toggle('selected', valueInput.value); + picker.classList.toggle('selected', valueInput.value); }); saveBtn.classList.toggle('disabled', !valueInput.value); - previewListOverlay.addEventListener('click', (e) => { - if (e.target === previewListOverlay) { + pickerOverlay.addEventListener('click', (e) => { + if (e.target === pickerOverlay) { resetPreviewList(); } }); } export default async function init(component, props) { - const previewList = component.querySelector('.preview-list'); - if (!previewList) return; + const picker = component.querySelector('.picker'); + if (!picker) return; - buildPreviewListOptionsFromSource(component, previewList.getAttribute('data-source-link')); + buildPreviewListOptionsFromSource(component, picker.getAttribute('data-source-link')); initInteractions(component); } diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index a0bdf540..a95e85a6 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -8,25 +8,25 @@ } .series-templates-component .con-button, -.series-templates-component .preview-list-close-btn { +.series-templates-component .picker-close-btn { cursor: pointer; } -.series-templates-component .preview-list .series-template-name { +.series-templates-component .picker .series-template-name { display: none; margin-right: 24px; } -.series-templates-component .preview-list.selected .series-template-name { +.series-templates-component .picker.selected .series-template-name { display: inline-block; } -.series-templates-component .preview-list-btn { +.series-templates-component .picker-btn { padding: 10px 20px; border-radius: 20px; } -.series-templates-component .preview-list-close-btn { +.series-templates-component .picker-close-btn { position: absolute; top: 16px; right: 16px; @@ -53,19 +53,19 @@ } .series-templates-component .text-field-label, -.series-templates-component .preview-list-label { +.series-templates-component .picker-label { width: 180px; font-family: var(--body-font-family); font-size: var(--type-body-s-size); font-weight: 700; } -.series-templates-component .preview-list-label { +.series-templates-component .picker-label { display: inline-block; margin: 4px 12px 0 0; } -.series-templates-component .preview-list-overlay { +.series-templates-component .picker-overlay { position: fixed; display: flex; justify-content: center; @@ -78,11 +78,11 @@ z-index: 2; } -.series-templates-component .preview-list-overlay.hidden { +.series-templates-component .picker-overlay.hidden { display: none; } -.series-templates-component .preview-list-modal { +.series-templates-component .picker-modal { position: relative; max-width: var(--grid-container-width); max-height: 72vh; @@ -93,16 +93,16 @@ overflow: hidden auto; } -.series-templates-component .preview-list-modal h2 { +.series-templates-component .picker-modal h2 { color: var(--color-red) } -.series-templates-component .preview-list-modal .preview-list-subtitle { +.series-templates-component .picker-modal .picker-subtitle { font-weight: 700; font-size: var(--type-body-m-size); } -.series-templates-component .preview-list-modal .preview-list-action-area { +.series-templates-component .picker-modal .picker-action-area { display: flex; justify-content: flex-end; gap: 1rem; @@ -110,7 +110,7 @@ border-top: 1px solid var(--color-gray-500); } -.series-templates-component .preview-list-fieldset { +.series-templates-component .picker-fieldset { border: none; padding: 0; height: 100%; @@ -119,13 +119,13 @@ min-inline-size: unset; } -.series-templates-component .preview-list-items { +.series-templates-component .picker-items { height: 100%; width: 100%; margin-bottom: 40px; } -.series-templates-component .preview-list-item { +.series-templates-component .picker-item { max-height: 100%; display: flex; flex-direction: column; @@ -133,14 +133,14 @@ cursor: pointer; } -.series-templates-component .preview-list-item img { +.series-templates-component .picker-item img { object-fit: contain; object-position: left; max-height: 600px; min-width: 231px; } -.series-templates-component .preview-list-item label.radio-label { +.series-templates-component .picker-item label.radio-label { font-family: var(--body-font-family); font-size: var(--type-body-s-size); display: flex; @@ -152,11 +152,19 @@ user-select: none; } +.series-templates-component .picker-item[aria-selected="true"] label.radio-label { + font-weight: 700; +} + +.series-templates-component .picker-item label.radio-label input[type="radio"] { + display: none; +} + .series-templates-component .carousel-platform { gap: 50px; } -.series-templates-component .preview-list-save-btn.disabled { +.series-templates-component .picker-save-btn.disabled { opacity: 0.5; cursor: not-allowed; } diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index 3247b1d3..a679009e 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -1,52 +1,68 @@ import { LIBS } from '../../scripts/scripts.js'; import { generateToolTip, - decorateLabeledTextfield, getIcon, } from '../../scripts/utils.js'; -async function buildPreviewList(row) { +async function buildPickerRow(row) { const { createTag } = await import(`${LIBS}/utils/utils.js`); const [labelCol, sourseLinkCel] = row.querySelectorAll(':scope > div'); - const labelText = labelCol?.textContent.trim() || 'Event template'; const sourceLink = sourseLinkCel?.textContent.trim(); + const labelText = labelCol?.textContent.trim() || 'Event template'; + + const templatePicker = createTag('div', { class: 'picker' }); + + if (labelText && sourceLink) { + const label = createTag('span', { class: 'picker-label' }, labelText); + + const pickerInput = createTag('input', { type: 'hidden', name: 'series-template-input', class: 'series-template-input' }); + const templateNameInput = createTag('sp-textfield', { class: 'series-template-name', size: 'xl', readonly: true }); + + const pickerBtn = createTag('a', { class: 'con-button fill picker-btn' }, 'Select'); + + templatePicker.append( + label, + templateNameInput, + pickerInput, + pickerBtn, + ); + } + + return templatePicker; +} - if (!labelText || !sourceLink) return; - - const label = createTag('span', { class: 'preview-list-label' }, labelText); - const previewList = createTag('div', { class: 'preview-list' }); - const previewListTitle = createTag('h2', {}, 'Template set up'); - const previewListSubtitle = createTag('p', { class: 'preview-list-subtitle' }, 'Select a template'); - const previewListFieldset = createTag('fieldset', { class: 'preview-list-fieldset' }); - createTag('div', { class: 'preview-list-items' }, '', { parent: previewListFieldset }); - const previewListInput = createTag('input', { type: 'hidden', name: 'series-template-input', class: 'series-template-input' }); - const templateNameInput = createTag('sp-textfield', { class: 'series-template-name', size: 'xl', readonly: true }); - - const previewListBtn = createTag('a', { class: 'con-button fill preview-list-btn' }, 'Select'); - const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); - const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); - - const actionArea = createTag('div', { class: 'preview-list-action-area' }, ''); - createTag('a', { class: 'preview-list-cancel-btn con-button outline' }, 'Cancel', { parent: actionArea }); - createTag('a', { class: 'preview-list-save-btn con-button fill' }, 'Save', { parent: actionArea }); - - createTag('a', { class: 'preview-list-close-btn' }, getIcon('close-circle'), { parent: previewListModal }); - previewListModal.append(previewListTitle, previewListSubtitle, previewListFieldset, actionArea); - previewList.append( - label, - templateNameInput, - previewListInput, - previewListBtn, - previewListOverlay, - ); +async function buildPickerOverlay() { + const { createTag } = await import(`${LIBS}/utils/utils.js`); + + const pickerOverlay = createTag('div', { class: 'picker-overlay hidden' }); + const pickerModal = createTag('div', { class: 'picker-modal' }, '', { parent: pickerOverlay }); + const pickerTitle = createTag('h2', {}, 'Select a template'); + const pickerFieldset = createTag('fieldset', { class: 'picker-fieldset' }); + const actionArea = createTag('div', { class: 'picker-action-area' }, ''); + + createTag('div', { class: 'picker-items' }, '', { parent: pickerFieldset }); + createTag('a', { class: 'picker-close-btn' }, getIcon('close-circle'), { parent: pickerModal }); + createTag('a', { class: 'picker-cancel-btn con-button outline' }, 'Cancel', { parent: actionArea }); + createTag('a', { class: 'picker-save-btn con-button fill' }, 'Save', { parent: actionArea }); + + pickerModal.append(pickerTitle, pickerFieldset, actionArea); + + return pickerOverlay; +} + +async function buildTemplatesPicker(row) { + const pickerOverlay = await buildPickerOverlay(); + const templatePicker = await buildPickerRow(row); + const [, sourseLinkCel] = row.querySelectorAll(':scope > div'); + const sourceLink = sourseLinkCel?.textContent.trim(); row.innerHTML = ''; - row.append(previewList); + row.append(templatePicker, pickerOverlay); try { - previewList.dataset.sourceLink = new URL(sourceLink).pathname; + templatePicker.dataset.sourceLink = new URL(sourceLink).pathname; } catch (e) { window.lana?.log('Invalid template source link'); } @@ -60,11 +76,7 @@ export default async function init(el) { if (ri === 0) generateToolTip(r); if (ri === 1) { - await decorateLabeledTextfield(r, { id: 'series-email-template' }); - } - - if (ri === 2) { - await buildPreviewList(r); + await buildTemplatesPicker(r, el); } }); } From 182008987bdfb853dc6f3191d5840d61dc367c20 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 4 Dec 2024 15:37:31 -0600 Subject: [PATCH 39/74] preview layout WIP --- .../series-templates-component/controller.js | 15 +++++++++++++-- .../series-templates-component.css | 18 ++++++++++++------ .../series-templates-component.js | 19 +++++++++++++++++-- ecc/icons/zoom-in.svg | 12 ++++++++++++ ecc/icons/zoom-out.svg | 12 ++++++++++++ 5 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 ecc/icons/zoom-in.svg create mode 100644 ecc/icons/zoom-out.svg diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index bbe6000b..42231dff 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -42,10 +42,10 @@ async function buildPreviewListOptionsFromSource(component, source) { const pickerItem = createTag('div', { class: 'picker-item' }); const pickerItemImage = createTag('img', { src: option['template-image'] }); - pickerItem.append(radioLabel, pickerItemImage); + pickerItem.append(pickerItemImage, radioLabel); pickerItems.append(pickerItem); - pickerItem.addEventListener('click', async () => { + pickerItem.addEventListener('click', () => { if (radio && !radio.checked) { radio.checked = true; radio.dispatchEvent(new Event('change')); @@ -53,8 +53,14 @@ async function buildPreviewListOptionsFromSource(component, source) { }); radio.addEventListener('change', () => { + const alItems = component.querySelectorAll('.picker-item'); const saveBtn = component.querySelector('.picker-save-btn'); + saveBtn.classList.toggle('disabled', !radio.checked); + alItems.forEach((item) => { + if (pickerItem !== item) item.setAttribute('aria-selected', 'false'); + }); + pickerItem.setAttribute('aria-selected', radio.checked); }); }); @@ -70,6 +76,7 @@ function initInteractions(component) { const saveBtn = component.querySelector('.picker-save-btn'); const valueInput = component.querySelector('input.series-template-input'); const nameInput = component.querySelector('sp-textfield.series-template-name'); + const pickerItems = component.querySelectorAll('.picker-item'); const allRadioInputs = component.querySelectorAll('input[name="series-template"]'); if ( @@ -83,6 +90,10 @@ function initInteractions(component) { ) return; const resetPreviewList = () => { + pickerItems.forEach((item) => { + item.setAttribute('aria-selected', 'false'); + }); + allRadioInputs.forEach((radio) => { radio.checked = false; }); diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index a95e85a6..ec29178d 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -84,7 +84,7 @@ .series-templates-component .picker-modal { position: relative; - max-width: var(--grid-container-width); + max-width: 1000px; max-height: 72vh; padding: 56px 72px; margin: auto; @@ -134,10 +134,17 @@ } .series-templates-component .picker-item img { - object-fit: contain; + object-fit: cover; object-position: left; - max-height: 600px; - min-width: 231px; + max-height: 220px; + min-width: 154px; + margin-bottom: 8px; + border: 1px solid var(--color-gray-400); +} + +.series-templates-component .picker-item[aria-selected="true"] img { + border-radius: 4px; + border: 3px solid var(--color-accent); } .series-templates-component .picker-item label.radio-label { @@ -148,7 +155,6 @@ justify-content: flex-end; align-items: center; gap: 8px; - margin-bottom: 24px; user-select: none; } @@ -161,7 +167,7 @@ } .series-templates-component .carousel-platform { - gap: 50px; + gap: 24px; } .series-templates-component .picker-save-btn.disabled { diff --git a/ecc/blocks/series-templates-component/series-templates-component.js b/ecc/blocks/series-templates-component/series-templates-component.js index a679009e..40a99d5c 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.js +++ b/ecc/blocks/series-templates-component/series-templates-component.js @@ -39,15 +39,30 @@ async function buildPickerOverlay() { const pickerOverlay = createTag('div', { class: 'picker-overlay hidden' }); const pickerModal = createTag('div', { class: 'picker-modal' }, '', { parent: pickerOverlay }); const pickerTitle = createTag('h2', {}, 'Select a template'); + + const previewContainer = createTag('div', { class: 'picker-preview-container' }); + const previewHeading = createTag('h3', { class: 'picker-preview-heading' }, 'Preview'); + const previewFrame = createTag('div', { class: 'picker-preview-frame' }); + const previewImageHolder = createTag('div', { class: 'picker-preview-image-holder' }); + const previewImage = createTag('img', { class: 'picker-preview-image' }); + const previewActions = createTag('div', { class: 'picker-preview-actions' }); + const zoomInBtn = createTag('button', { class: 'picker-zoom-in-btn' }, getIcon('zoom-in')); + const zoomOutBtn = createTag('button', { class: 'picker-zoom-out-btn' }, getIcon('zoom-out')); + const pickerFieldset = createTag('fieldset', { class: 'picker-fieldset' }); - const actionArea = createTag('div', { class: 'picker-action-area' }, ''); + + const actionArea = createTag('div', { class: 'picker-action-area' }); createTag('div', { class: 'picker-items' }, '', { parent: pickerFieldset }); createTag('a', { class: 'picker-close-btn' }, getIcon('close-circle'), { parent: pickerModal }); createTag('a', { class: 'picker-cancel-btn con-button outline' }, 'Cancel', { parent: actionArea }); createTag('a', { class: 'picker-save-btn con-button fill' }, 'Save', { parent: actionArea }); - pickerModal.append(pickerTitle, pickerFieldset, actionArea); + pickerModal.append(pickerTitle, previewContainer, pickerFieldset, actionArea); + previewContainer.append(previewHeading, previewFrame); + previewFrame.append(previewImageHolder, previewActions); + previewActions.append(zoomInBtn, zoomOutBtn); + previewImageHolder.append(previewImage); return pickerOverlay; } diff --git a/ecc/icons/zoom-in.svg b/ecc/icons/zoom-in.svg new file mode 100644 index 00000000..dbe52171 --- /dev/null +++ b/ecc/icons/zoom-in.svg @@ -0,0 +1,12 @@ + + + + + S ZoomIn 18 N + + + \ No newline at end of file diff --git a/ecc/icons/zoom-out.svg b/ecc/icons/zoom-out.svg new file mode 100644 index 00000000..0e73a1a5 --- /dev/null +++ b/ecc/icons/zoom-out.svg @@ -0,0 +1,12 @@ + + + + + S ZoomOut 18 N + + + \ No newline at end of file From 0b42bce5e484cf9fabfd2e6344dc77cfa0a2c0da Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 4 Dec 2024 17:06:38 -0600 Subject: [PATCH 40/74] zooming semi-working --- .../series-templates-component/controller.js | 67 +++++++++++++------ .../series-templates-component.css | 50 +++++++++++++- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 42231dff..58041a17 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -38,36 +38,18 @@ async function buildPreviewListOptionsFromSource(component, source) { options.forEach((option) => { const radioLabel = createTag('label', { class: 'radio-label' }, option['template-name']); - const radio = createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); + createTag('input', { type: 'radio', name: 'series-template', value: option['template-path'] }, '', { parent: radioLabel }); const pickerItem = createTag('div', { class: 'picker-item' }); const pickerItemImage = createTag('img', { src: option['template-image'] }); pickerItem.append(pickerItemImage, radioLabel); pickerItems.append(pickerItem); - - pickerItem.addEventListener('click', () => { - if (radio && !radio.checked) { - radio.checked = true; - radio.dispatchEvent(new Event('change')); - } - }); - - radio.addEventListener('change', () => { - const alItems = component.querySelectorAll('.picker-item'); - const saveBtn = component.querySelector('.picker-save-btn'); - - saveBtn.classList.toggle('disabled', !radio.checked); - alItems.forEach((item) => { - if (pickerItem !== item) item.setAttribute('aria-selected', 'false'); - }); - pickerItem.setAttribute('aria-selected', radio.checked); - }); }); await buildCarousel('.picker-item', pickerItems); } -function initInteractions(component) { +function initPicker(component) { const picker = component.querySelector('.picker'); const pickerOverlay = component.querySelector('.picker-overlay'); const pickerBtn = component.querySelector('.picker-btn'); @@ -78,6 +60,9 @@ function initInteractions(component) { const nameInput = component.querySelector('sp-textfield.series-template-name'); const pickerItems = component.querySelectorAll('.picker-item'); const allRadioInputs = component.querySelectorAll('input[name="series-template"]'); + const previewImage = component.querySelector('.picker-preview-image'); + const zoomInBtn = component.querySelector('.picker-zoom-in-btn'); + const zoomOutBtn = component.querySelector('.picker-zoom-out-btn'); if ( !picker @@ -100,6 +85,7 @@ function initInteractions(component) { pickerOverlay.classList.add('hidden'); saveBtn.classList.add('disabled'); + previewImage.src = ''; }; pickerBtn.addEventListener('click', () => { @@ -137,15 +123,52 @@ function initInteractions(component) { resetPreviewList(); } }); + + zoomInBtn.addEventListener('click', () => { + if (!previewImage.src) return; + + const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; + previewImage.style.transform = `scale(${parseFloat(scale) + 0.5})`; + }); + + zoomOutBtn.addEventListener('click', () => { + if (!previewImage.src) return; + + const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; + previewImage.style.transform = `scale(${parseFloat(scale) - 0.5})`; + }); + + pickerItems.forEach((pickerItem) => { + const radio = pickerItem.querySelector('input[type="radio"]'); + + pickerItem.addEventListener('click', () => { + if (radio && !radio.checked) { + radio.checked = true; + radio.dispatchEvent(new Event('change')); + } + }); + + radio.addEventListener('change', () => { + saveBtn.classList.toggle('disabled', !radio.checked); + pickerItems.forEach((item) => { + if (pickerItem !== item) item.setAttribute('aria-selected', 'false'); + }); + pickerItem.setAttribute('aria-selected', radio.checked); + + if (previewImage) { + previewImage.src = pickerItem.querySelector('img')?.src; + } + }); + }); } export default async function init(component, props) { const picker = component.querySelector('.picker'); if (!picker) return; - buildPreviewListOptionsFromSource(component, picker.getAttribute('data-source-link')); + await buildPreviewListOptionsFromSource(component, picker.getAttribute('data-source-link')); - initInteractions(component); + initPicker(component); } export function onTargetUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index ec29178d..f86cc4d4 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -85,6 +85,7 @@ .series-templates-component .picker-modal { position: relative; max-width: 1000px; + box-sizing: border-box; max-height: 72vh; padding: 56px 72px; margin: auto; @@ -97,9 +98,52 @@ color: var(--color-red) } -.series-templates-component .picker-modal .picker-subtitle { - font-weight: 700; - font-size: var(--type-body-m-size); +.series-templates-component .picker-preview-container { + margin-bottom: 64px; +} + +.series-templates-component .picker-preview-frame { + width: 100%; + max-width: 892px; + height: 539px; + background-color: var(--color-gray-100); + position: relative; +} + +.series-templates-component .picker-preview-image-holder { + width: 100%; + height: 100%; + overflow: auto; +} + +.series-templates-component .picker-preview-image-holder img.picker-preview-image { + display: block; + margin: auto; + width: 50%; + transform-origin: top left; + transition: transform 0.2s; +} + +.series-templates-component .picker-preview-actions { + position: absolute; + top: 0; + left: 0; + display: flex; + width: max-content; + flex-direction: column; + align-items: center; + padding: 16px 8px; + gap: 8px; +} + +.series-templates-component .picker-preview-actions button { + background: none; + border: none; + cursor: pointer; +} + +.series-templates-component .picker-preview-actions button:hover { + opacity: 0.8; } .series-templates-component .picker-modal .picker-action-area { From 0d08ae5e85893569beef6a95d7b98e29506f4295 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 4 Dec 2024 21:00:35 -0600 Subject: [PATCH 41/74] zooming somewhat working --- .../series-templates-component/controller.js | 26 ++++++++++++++++--- .../series-templates-component.css | 13 ++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 58041a17..478c74ce 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -49,6 +49,12 @@ async function buildPreviewListOptionsFromSource(component, source) { await buildCarousel('.picker-item', pickerItems); } +function resizeImage(image, newScale) { + const scale = newScale; + + image.style.transform = `scale(${scale})`; +} + function initPicker(component) { const picker = component.querySelector('.picker'); const pickerOverlay = component.querySelector('.picker-overlay'); @@ -92,7 +98,10 @@ function initPicker(component) { pickerOverlay.classList.remove('hidden'); if (valueInput.value) { const selectedRadio = component.querySelector(`input[type='radio'][value="${valueInput.value}"]`); - if (selectedRadio) selectedRadio.checked = true; + if (selectedRadio) { + selectedRadio.checked = true; + selectedRadio.dispatchEvent(new Event('change')); + } } }); @@ -128,14 +137,24 @@ function initPicker(component) { if (!previewImage.src) return; const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; - previewImage.style.transform = `scale(${parseFloat(scale) + 0.5})`; + + const newScale = parseFloat(scale) + 0.25; + + resizeImage(previewImage, newScale); + + if (parseFloat(newScale) > 0.5) zoomOutBtn.disabled = false; }); zoomOutBtn.addEventListener('click', () => { if (!previewImage.src) return; const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; - previewImage.style.transform = `scale(${parseFloat(scale) - 0.5})`; + + const newScale = parseFloat(scale) - 0.25; + + resizeImage(previewImage, newScale); + + if (parseFloat(newScale) <= 0.5) zoomOutBtn.disabled = true; }); pickerItems.forEach((pickerItem) => { @@ -157,6 +176,7 @@ function initPicker(component) { if (previewImage) { previewImage.src = pickerItem.querySelector('img')?.src; + previewImage.style.transform = 'scale(1)'; } }); }); diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index f86cc4d4..4cc36037 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -117,11 +117,14 @@ } .series-templates-component .picker-preview-image-holder img.picker-preview-image { + padding: 16px; + transform: scale(1); + width: 50%; display: block; margin: auto; - width: 50%; transform-origin: top left; - transition: transform 0.2s; + transition: transform 0.3s; + will-change: transform; } .series-templates-component .picker-preview-actions { @@ -140,12 +143,18 @@ background: none; border: none; cursor: pointer; + filter: drop-shadow(1px 1px 1px #ffffff); } .series-templates-component .picker-preview-actions button:hover { opacity: 0.8; } +.series-templates-component .picker-preview-actions button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + .series-templates-component .picker-modal .picker-action-area { display: flex; justify-content: flex-end; From 202e20839551842d761901e4a0d46bb17137b8d5 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 5 Dec 2024 14:03:16 -0600 Subject: [PATCH 42/74] preview prototype done --- .../series-templates-component/controller.js | 63 +++++-------- .../series-templates-component.css | 26 ++++-- .../series-templates-component/utils.js | 88 +++++++++++++++++++ ecc/scripts/utils.js | 7 +- 4 files changed, 131 insertions(+), 53 deletions(-) create mode 100644 ecc/blocks/series-templates-component/utils.js diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 478c74ce..995eb522 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -1,5 +1,6 @@ /* eslint-disable no-unused-vars */ import buildCarousel from '../../scripts/features/carousel.js'; +import initPreviewFrame, { resetPreviewFrame } from './utils.js'; import { LIBS } from '../../scripts/scripts.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -49,12 +50,6 @@ async function buildPreviewListOptionsFromSource(component, source) { await buildCarousel('.picker-item', pickerItems); } -function resizeImage(image, newScale) { - const scale = newScale; - - image.style.transform = `scale(${scale})`; -} - function initPicker(component) { const picker = component.querySelector('.picker'); const pickerOverlay = component.querySelector('.picker-overlay'); @@ -66,9 +61,9 @@ function initPicker(component) { const nameInput = component.querySelector('sp-textfield.series-template-name'); const pickerItems = component.querySelectorAll('.picker-item'); const allRadioInputs = component.querySelectorAll('input[name="series-template"]'); + const previewFrame = component.querySelector('.picker-preview-frame'); const previewImage = component.querySelector('.picker-preview-image'); - const zoomInBtn = component.querySelector('.picker-zoom-in-btn'); - const zoomOutBtn = component.querySelector('.picker-zoom-out-btn'); + const pickerPreviewHeading = component.querySelector('.picker-preview-heading'); if ( !picker @@ -80,7 +75,7 @@ function initPicker(component) { || !valueInput ) return; - const resetPreviewList = () => { + const resetPreview = () => { pickerItems.forEach((item) => { item.setAttribute('aria-selected', 'false'); }); @@ -92,6 +87,10 @@ function initPicker(component) { pickerOverlay.classList.add('hidden'); saveBtn.classList.add('disabled'); previewImage.src = ''; + previewFrame.classList.remove('has-image'); + pickerPreviewHeading.textContent = 'Preview'; + + resetPreviewFrame(); }; pickerBtn.addEventListener('click', () => { @@ -106,11 +105,11 @@ function initPicker(component) { }); closeBtn.addEventListener('click', () => { - resetPreviewList(); + resetPreview(); }); cancelBtn.addEventListener('click', () => { - resetPreviewList(); + resetPreview(); }); saveBtn.addEventListener('click', () => { @@ -127,35 +126,7 @@ function initPicker(component) { saveBtn.classList.toggle('disabled', !valueInput.value); - pickerOverlay.addEventListener('click', (e) => { - if (e.target === pickerOverlay) { - resetPreviewList(); - } - }); - - zoomInBtn.addEventListener('click', () => { - if (!previewImage.src) return; - - const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; - - const newScale = parseFloat(scale) + 0.25; - - resizeImage(previewImage, newScale); - - if (parseFloat(newScale) > 0.5) zoomOutBtn.disabled = false; - }); - - zoomOutBtn.addEventListener('click', () => { - if (!previewImage.src) return; - - const scale = previewImage.style.transform?.match(/scale\((\d+(\.\d+)?)\)/)?.[1] || 1; - - const newScale = parseFloat(scale) - 0.25; - - resizeImage(previewImage, newScale); - - if (parseFloat(newScale) <= 0.5) zoomOutBtn.disabled = true; - }); + initPreviewFrame(component); pickerItems.forEach((pickerItem) => { const radio = pickerItem.querySelector('input[type="radio"]'); @@ -172,11 +143,19 @@ function initPicker(component) { pickerItems.forEach((item) => { if (pickerItem !== item) item.setAttribute('aria-selected', 'false'); }); - pickerItem.setAttribute('aria-selected', radio.checked); + pickerItem.setAttribute('aria-selected', radio.checked); + pickerPreviewHeading.textContent = `Preview ${radio.parentElement.textContent.trim()}`; if (previewImage) { + previewImage.src = ''; + previewFrame.classList.remove('has-image'); + previewImage.src = pickerItem.querySelector('img')?.src; - previewImage.style.transform = 'scale(1)'; + resetPreviewFrame(); + + previewImage.addEventListener('load', () => { + previewFrame.classList.add('has-image'); + }); } }); }); diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index 4cc36037..3f4f4af0 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -113,25 +113,31 @@ .series-templates-component .picker-preview-image-holder { width: 100%; height: 100%; - overflow: auto; + overflow: hidden; /* Prevent default scrollbars */ + position: relative; + cursor: grab; /* Indicate draggable behavior */ } .series-templates-component .picker-preview-image-holder img.picker-preview-image { - padding: 16px; - transform: scale(1); - width: 50%; display: block; - margin: auto; - transform-origin: top left; - transition: transform 0.3s; + transform-origin: center; /* Zoom from the center */ + transition: transform 0.2s ease-out; /* Smooth zoom effect */ will-change: transform; + height: auto; + padding: 24px; + width: 50%; + margin: auto; +} + +.series-templates-component .picker-preview-image-holder img.picker-preview-image.grabbing { + transition: none; } .series-templates-component .picker-preview-actions { position: absolute; top: 0; left: 0; - display: flex; + display: none; width: max-content; flex-direction: column; align-items: center; @@ -139,6 +145,10 @@ gap: 8px; } +.series-templates-component .picker-preview-frame.has-image .picker-preview-actions { + display: flex; +} + .series-templates-component .picker-preview-actions button { background: none; border: none; diff --git a/ecc/blocks/series-templates-component/utils.js b/ecc/blocks/series-templates-component/utils.js new file mode 100644 index 00000000..2bb908e6 --- /dev/null +++ b/ecc/blocks/series-templates-component/utils.js @@ -0,0 +1,88 @@ +let container; +let image; +let zoomInBtn; +let zoomOutBtn; + +let scale = 1; // Initial scale +let posX = 0; // Initial horizontal offset +let posY = 0; // Initial vertical offset + +// Dragging state +let isDragging = false; +let startX; +let startY; + +// Handle zooming +function zoom(scaleFactor) { + if (scale + scaleFactor < 0.5) return; + + scale += scaleFactor; + image.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; + + zoomOutBtn.disabled = scale <= 0.5; +} + +// Handle dragging start +function onDragStart(event) { + image.classList.add('grabbing'); + event.preventDefault(); + isDragging = true; + startX = event.clientX || event.touches?.[0].clientX; + startY = event.clientY || event.touches?.[0].clientY; + + container.style.cursor = 'grabbing'; +} + +// Handle dragging move +function onDragMove(event) { + if (!isDragging) return; + + event.preventDefault(); + + const currentX = event.clientX || event.touches?.[0].clientX; + const currentY = event.clientY || event.touches?.[0].clientY; + + const deltaX = currentX - startX; + const deltaY = currentY - startY; + + posX += deltaX; + posY += deltaY; + + // Apply transformations + image.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; + + // Update start positions for next move + startX = currentX; + startY = currentY; +} + +// Handle dragging end +function onDragEnd() { + isDragging = false; + image.classList.remove('grabbing'); + container.style.cursor = 'grab'; +} + +export default function initPreviewFrame(component) { + container = component.querySelector('.picker-preview-image-holder'); + image = component.querySelector('.picker-preview-image'); + zoomInBtn = component.querySelector('.picker-zoom-in-btn'); + zoomOutBtn = component.querySelector('.picker-zoom-out-btn'); + + zoomInBtn?.addEventListener('click', () => zoom(0.25)); + zoomOutBtn?.addEventListener('click', () => zoom(-0.25)); + container.addEventListener('mousedown', onDragStart); + container.addEventListener('mousemove', onDragMove); + container.addEventListener('mouseup', onDragEnd); + container.addEventListener('mouseleave', onDragEnd); + container.addEventListener('touchstart', onDragStart); + container.addEventListener('touchmove', onDragMove); + container.addEventListener('touchend', onDragEnd); +} + +export function resetPreviewFrame() { + scale = 1; + posX = 0; + posY = 0; + image.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; +} diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index f5f86e02..f4aaf4cd 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -243,13 +243,14 @@ export async function decorateTextfield(cell, extraOptions, negativeHelperText = { class: 'text-input', placeholder: text, - required: isRequired, quiet: true, size: 'xl', }, extraOptions, )); + if (isRequired) input.required = true; + if (negativeHelperText) { createTag('sp-help-text', { variant: 'negative', slot: 'negative-help-text' }, negativeHelperText, { parent: input }); } @@ -289,14 +290,14 @@ export async function decorateTextarea(cell, extraOptions) { { multiline: true, class: 'textarea-input', - // quiet: true, placeholder: text, - required: isRequired, ...extraOptions, }, extraOptions, )); + if (isRequired) input.required = true; + if (maxCharNum) input.setAttribute('maxlength', maxCharNum); const wrapper = createTag('div', { class: 'info-field-wrapper' }); From 06d767643011db1a94e6393a38b90838225a698b Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 5 Dec 2024 14:03:51 -0600 Subject: [PATCH 43/74] clean up --- ecc/blocks/series-templates-component/utils.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ecc/blocks/series-templates-component/utils.js b/ecc/blocks/series-templates-component/utils.js index 2bb908e6..63f51712 100644 --- a/ecc/blocks/series-templates-component/utils.js +++ b/ecc/blocks/series-templates-component/utils.js @@ -3,16 +3,14 @@ let image; let zoomInBtn; let zoomOutBtn; -let scale = 1; // Initial scale -let posX = 0; // Initial horizontal offset -let posY = 0; // Initial vertical offset +let scale = 1; +let posX = 0; +let posY = 0; -// Dragging state let isDragging = false; let startX; let startY; -// Handle zooming function zoom(scaleFactor) { if (scale + scaleFactor < 0.5) return; @@ -22,7 +20,6 @@ function zoom(scaleFactor) { zoomOutBtn.disabled = scale <= 0.5; } -// Handle dragging start function onDragStart(event) { image.classList.add('grabbing'); event.preventDefault(); @@ -33,7 +30,6 @@ function onDragStart(event) { container.style.cursor = 'grabbing'; } -// Handle dragging move function onDragMove(event) { if (!isDragging) return; @@ -48,15 +44,12 @@ function onDragMove(event) { posX += deltaX; posY += deltaY; - // Apply transformations image.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; - // Update start positions for next move startX = currentX; startY = currentY; } -// Handle dragging end function onDragEnd() { isDragging = false; image.classList.remove('grabbing'); From 62599207c80c1de27b73e358816398800fbe9560 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 5 Dec 2024 15:11:38 -0600 Subject: [PATCH 44/74] series-creation-form submission done --- .../event-creation-form.js | 4 ++ .../controller.js | 24 ++++++++++++ .../series-additional-info-component.js | 3 +- .../series-creation-form/data-handler.js | 3 +- .../series-creation-form.js | 38 +++++++++++++------ .../series-details-component/controller.js | 24 ++++++++++++ .../series-templates-component/controller.js | 4 +- .../series-templates-component.css | 4 ++ ecc/scripts/esp-controller.js | 6 +-- 9 files changed, 91 insertions(+), 19 deletions(-) diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index 7850df6d..b4d4a3fc 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -763,6 +763,10 @@ function initFormCtas(props) { cta.dataset.republishStateText = republishStateText; } + if (ctaUrl.hash === '#save') { + cta.classList.add('save-button'); + } + cta.addEventListener('click', async (e) => { e.preventDefault(); toggleBtnsSubmittingState(true); diff --git a/ecc/blocks/series-additional-info-component/controller.js b/ecc/blocks/series-additional-info-component/controller.js index 6be51403..4cabcca7 100644 --- a/ecc/blocks/series-additional-info-component/controller.js +++ b/ecc/blocks/series-additional-info-component/controller.js @@ -1,6 +1,18 @@ /* eslint-disable no-unused-vars */ export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const susiContextId = component.querySelector('#info-field-series-susi'); + const relatedDomain = component.querySelector('#info-field-series-related-domain'); + const externalThemeId = component.querySelector('#info-field-series-ext-id'); + + const seriesInfo = {}; + + if (susiContextId.value) seriesInfo.susiContextId = susiContextId.value; + if (relatedDomain.value) seriesInfo.relatedDomain = relatedDomain.value; + if (externalThemeId.value) seriesInfo.externalThemeId = externalThemeId.value; + + props.payload = { ...props.payload, ...seriesInfo }; } export async function onPayloadUpdate(_component, _props) { @@ -12,7 +24,19 @@ export async function onRespUpdate(_component, _props) { } export default function init(component, props) { + const data = props.resp; + + if (data) { + const susiContextId = component.querySelector('#info-field-series-susi'); + const relatedDomain = component.querySelector('#info-field-series-related-domain'); + const externalThemeId = component.querySelector('#info-field-series-ext-id'); + + susiContextId.value = data.susiContextId || ''; + relatedDomain.value = data.relatedDomain || ''; + externalThemeId.value = data.externalThemeId || ''; + component.classList.add('prefilled'); + } } export function onTargetUpdate(component, props) { diff --git a/ecc/blocks/series-additional-info-component/series-additional-info-component.js b/ecc/blocks/series-additional-info-component/series-additional-info-component.js index 4a057821..8455000f 100644 --- a/ecc/blocks/series-additional-info-component/series-additional-info-component.js +++ b/ecc/blocks/series-additional-info-component/series-additional-info-component.js @@ -1,3 +1,4 @@ +import { LINK_REGEX } from '../../constants/constants.js'; import { generateToolTip, decorateLabeledTextfield, @@ -16,7 +17,7 @@ export default function init(el) { } if (ri === 2) { - await decorateLabeledTextfield(r, { id: 'info-field-series-related-domain' }); + await decorateLabeledTextfield(r, { id: 'info-field-series-related-domain', pattern: LINK_REGEX }); } if (ri === 3) { diff --git a/ecc/blocks/series-creation-form/data-handler.js b/ecc/blocks/series-creation-form/data-handler.js index a18d7f3a..2e4112c0 100644 --- a/ecc/blocks/series-creation-form/data-handler.js +++ b/ecc/blocks/series-creation-form/data-handler.js @@ -6,11 +6,12 @@ let payloadCache = {}; const submissionFilter = [ // from payload and response 'seriesName', + 'seriesDescription', + 'susiContextId', 'externalThemeId', 'cloudType', 'templateId', 'relatedDomain', - 'emailTemplate', 'modificationTime', ]; diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 2e8c00cc..a90a4526 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -70,6 +70,12 @@ const SPECTRUM_COMPONENTS = [ 'progress-circle', ]; +const PUBLISHABLE_ATTRS = [ + 'seriesName', + 'cloudType', + 'templateId', +]; + export function buildErrorMessage(props, resp) { if (!resp) return; @@ -144,24 +150,18 @@ function getCurrentFragment(props) { return currentFrag; } -function validateRequiredFields(fields) { +function validateFields(fields) { return fields.length === 0 || Array.from(fields).every((f) => f.value && !f.invalid); } function onStepValidate(props) { - return function updateCtaStatus() { + return function updateSaveCtaStatus() { const currentFrag = getCurrentFragment(props); - const stepValid = validateRequiredFields(props[`required-fields-in-${currentFrag.id}`]); - const ctas = props.el.querySelectorAll('.series-creation-form-panel-wrapper a'); + const stepValid = validateFields(props[`required-fields-in-${currentFrag.id}`]); + const saveButton = props.el.querySelector('.series-creation-form-ctas-panel .save-button'); const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); - ctas.forEach((cta) => { - if (cta.classList.contains('back-btn')) { - cta.classList.toggle('disabled', props.currentStep === 0); - } else { - cta.classList.toggle('disabled', !stepValid); - } - }); + saveButton.classList.toggle('disabled', !stepValid); sideNavs.forEach((nav, i) => { if (i !== props.currentStep) { @@ -183,6 +183,14 @@ function initRequiredFieldsValidation(props) { inputValidationCB(); } +function validatePublishFields(props) { + const publishAttributesFilled = PUBLISHABLE_ATTRS.every((attr) => props.payload[attr]); + console.log('publishAttributesFilled', publishAttributesFilled); + const publishButton = props.el.querySelector('.series-creation-form-ctas-panel .next-button'); + + publishButton.classList.toggle('disabled', !publishAttributesFilled); +} + function enableSideNavForEditFlow(props) { const frags = props.el.querySelectorAll('.fragment'); const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component')) @@ -489,6 +497,7 @@ function renderFormNavigation(props, prevStep, currentStep) { nextBtn.textContent = nextBtn.dataset.republishStateText; } else { nextBtn.textContent = nextBtn.dataset.finalStateText; + nextBtn.prepend(getIcon('golden-rocket')); } } else { nextBtn.textContent = nextBtn.dataset.finalStateText; @@ -550,6 +559,10 @@ function initFormCtas(props) { cta.dataset.republishStateText = republishStateText; } + if (ctaUrl.hash === '#save') { + cta.classList.add('save-button'); + } + cta.addEventListener('click', async (e) => { e.preventDefault(); toggleBtnsSubmittingState(true); @@ -616,6 +629,7 @@ function updateCtas(props) { a.textContent = a.dataset.republishStateText; } else { a.textContent = a.dataset.finalStateText; + a.prepend(getIcon('golden-rocket')); } } else { a.textContent = a.dataset.finalStateText; @@ -730,6 +744,7 @@ async function buildForm(el) { setPayloadCache(value); updateComponentsOnPayloadChange(target); initRequiredFieldsValidation(target); + validatePublishFields(target); break; } @@ -771,6 +786,7 @@ async function buildForm(el) { initFormCtas(proxyProps); initNavigation(proxyProps); await initComponents(proxyProps); + validatePublishFields(proxyProps); updateRequiredFields(proxyProps); enableSideNavForEditFlow(proxyProps); initDeepLink(proxyProps); diff --git a/ecc/blocks/series-details-component/controller.js b/ecc/blocks/series-details-component/controller.js index 6be51403..65ade393 100644 --- a/ecc/blocks/series-details-component/controller.js +++ b/ecc/blocks/series-details-component/controller.js @@ -1,6 +1,18 @@ /* eslint-disable no-unused-vars */ export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; + + const cloudType = component.querySelector('#bu-select-input'); + const seriesName = component.querySelector('#info-field-series-name'); + const seriesDescription = component.querySelector('#info-field-series-description'); + + const seriesInfo = {}; + + if (cloudType.value) seriesInfo.cloudType = cloudType.value; + if (seriesName.value) seriesInfo.seriesName = seriesName.value; + if (seriesDescription.value) seriesInfo.seriesDescription = seriesDescription.value; + + props.payload = { ...props.payload, ...seriesInfo }; } export async function onPayloadUpdate(_component, _props) { @@ -12,7 +24,19 @@ export async function onRespUpdate(_component, _props) { } export default function init(component, props) { + const data = props.resp; + + if (data) { + const cloudType = component.querySelector('#bu-select-input'); + const seriesName = component.querySelector('#info-field-series-name'); + const seriesDescription = component.querySelector('#info-field-series-description'); + + cloudType.value = data.cloudType; + seriesName.value = data.seriesName; + seriesDescription.value = data.seriesDescription; + component.classList.add('prefilled'); + } } export function onTargetUpdate(component, props) { diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index 995eb522..f3385745 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -9,12 +9,10 @@ export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; const eventTemplateInput = component.querySelector('input[name="series-template-input"]'); - const emailTemplateInput = component.querySelector('sp-textfield[name="series-email-template"]'); props.payload = { ...props.payload, - eventTemplate: eventTemplateInput.value, - emailTemplate: emailTemplateInput.value, + templateId: eventTemplateInput.value, }; } diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index 3f4f4af0..d10addaa 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -1,3 +1,7 @@ +.series-templates-component { + --mod-textfield-font-weight: 700; +} + .series-templates-component .labeled-text-field-wrapper { margin-bottom: 24px; } diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index ae4cc50a..d7931890 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -849,7 +849,7 @@ export async function updateSeries(seriesData, seriesId) { export async function publishSeries(seriesId, seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const raw = JSON.stringify({ ...seriesData, seriesId, status: 'published' }); + const raw = JSON.stringify({ ...seriesData, seriesId, seriesStatus: 'published' }); const options = await constructRequestOptions('PUT', raw); try { @@ -870,7 +870,7 @@ export async function publishSeries(seriesId, seriesData) { export async function unpublishSeries(seriesId, seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const raw = JSON.stringify({ ...seriesData, seriesId, status: 'unpublished' }); + const raw = JSON.stringify({ ...seriesData, seriesId, seriesStatus: 'draft' }); const options = await constructRequestOptions('PUT', raw); try { @@ -891,7 +891,7 @@ export async function unpublishSeries(seriesId, seriesData) { export async function archiveSeries(seriesId, seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const raw = JSON.stringify({ ...seriesData, seriesId, status: 'archived' }); + const raw = JSON.stringify({ ...seriesData, seriesId, seriesStatus: 'archived' }); const options = await constructRequestOptions('PUT', raw); try { From 941beba36d6359ad32c92b40e9f99e93bfcbbfbd Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 9 Dec 2024 11:07:35 -0600 Subject: [PATCH 45/74] Update series-dashboard.js --- .../series-dashboard/series-dashboard.js | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 4ed20250..ea98051c 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -13,6 +13,7 @@ import { readBlockConfig, signIn, getEventServiceEnv, + getDevToken, } from '../../scripts/utils.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; @@ -162,7 +163,7 @@ function initMoreOptions(props, config, seriesObj, row) { if (seriesObj.published) { const unpub = buildTool(toolBox, 'Unpublish', 'publish-remove'); - if (seriesObj.status === 'archived') unpub.classList.add('disabled'); + if (seriesObj.seriesStatus === 'archived') unpub.classList.add('disabled'); unpub.addEventListener('click', async (e) => { e.preventDefault(); toolBox.remove(); @@ -175,7 +176,7 @@ function initMoreOptions(props, config, seriesObj, row) { }); } else { const pub = buildTool(toolBox, 'Publish', 'publish-rocket'); - if (seriesObj.status === 'archived') pub.classList.add('disabled'); + if (seriesObj.seriesStatus === 'archived') pub.classList.add('disabled'); pub.addEventListener('click', async (e) => { e.preventDefault(); toolBox.remove(); @@ -189,11 +190,11 @@ function initMoreOptions(props, config, seriesObj, row) { }); } - const viewTemplate = buildTool(toolBox, 'View Template', 'preview-eye'); + // const viewTemplate = buildTool(toolBox, 'View Template', 'preview-eye'); const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); const clone = buildTool(toolBox, 'Clone', 'clone'); const archive = buildTool(toolBox, 'Archive', 'archive'); - const verHistory = buildTool(toolBox, 'Version History', 'version-history'); + // const verHistory = buildTool(toolBox, 'Version History', 'version-history'); // edit const url = new URL(`${window.location.origin}${config['create-form-url']}`); @@ -295,7 +296,7 @@ function initMoreOptions(props, config, seriesObj, row) { function buildStatusTag(series) { let dot; - switch (series.status) { + switch (series.seriesStatus) { case 'published': dot = getIcon('dot-purple'); break; @@ -309,7 +310,7 @@ function buildStatusTag(series) { } const statusTag = createTag('div', { class: 'status' }); - statusTag.append(dot, series.status); + statusTag.append(dot, series.seriesStatus || 'unknown'); return statusTag; } @@ -321,6 +322,11 @@ function buildSeriesNameTag(config, seriesObj) { } function buildEventsCountTag(series, events) { + if (!events) { + const eventsCountTag = createTag('span', { class: 'events-count' }, 'N/A'); + return eventsCountTag; + } + const seriesEvents = getSeriesEvents(series.seriesId, events); const eventsCountTag = createTag('span', { class: 'events-count' }, seriesEvents.length); @@ -356,7 +362,7 @@ async function populateRow(props, config, index) { if (series.seriesId === sp.get('newEventId')) { if (!props.el.classList.contains('toast-shown')) { - showToast(props, buildToastMsgWithEventTitle(series.seriesName, config['new-toast-msg']), { variant: 'positive' }); + showToast(props, buildToastMsgWithEventTitle(series.seriesName, config['new-creation-msg']), { variant: 'positive' }); props.el.classList.add('toast-shown'); } @@ -499,7 +505,7 @@ function buildDashboardHeader(props, config) { const textContainer = createTag('div', { class: 'dashboard-header-text' }); const actionsContainer = createTag('div', { class: 'dashboard-actions-container' }); - createTag('h1', { class: 'dashboard-header-heading' }, 'All Events', { parent: textContainer }); + createTag('h1', { class: 'dashboard-header-heading' }, 'All Event series', { parent: textContainer }); createTag('p', { class: 'dashboard-header-series-count' }, `(${props.data.length} series)`, { parent: textContainer }); const searchInputWrapper = createTag('div', { class: 'search-input-wrapper' }, '', { parent: actionsContainer }); @@ -512,7 +518,7 @@ function buildDashboardHeader(props, config) { props.el.prepend(dashboardHeader); } -function updateEventsCount(props) { +function updateDataCount(props) { const seriesCount = props.el.querySelector('.dashboard-header-series-count'); seriesCount.textContent = `(${props.data.length} series)`; } @@ -536,13 +542,13 @@ function buildDashboardTable(props, config) { } } -function buildNoEventScreen(el, config) { +function buildNoDataScreen(el, config) { el.classList.add('no-data'); - const h1 = createTag('h1', {}, 'All Events'); + const h1 = createTag('h1', {}, 'All Event series'); const area = createTag('div', { class: 'no-data-area' }); - const noEventHeading = createTag('h2', {}, config['no-heading']); - const noEventDescription = createTag('p', {}, config['no-description']); + const noEventHeading = createTag('h2', {}, config['no-data-heading']); + const noEventDescription = createTag('p', {}, config['no-data-description']); const cta = createTag('a', { class: 'con-button blue', href: config['create-form-url'] }, config['create-cta-text']); el.append(h1, area); @@ -563,7 +569,7 @@ async function buildDashboard(el, config) { const [{ series }, { events }] = await Promise.all([getAllSeries(), getEvents()]); if (!series?.length) { - buildNoEventScreen(el, config); + buildNoDataScreen(el, config); } else { props.events = events; props.data = series; @@ -574,7 +580,7 @@ async function buildDashboard(el, config) { set(target, prop, value, receiver) { target[prop] = value; populateTable(receiver, config); - updateEventsCount(receiver); + updateDataCount(receiver); return true; }, }; @@ -613,8 +619,7 @@ export default async function init(el) { el.innerHTML = ''; buildLoadingScreen(el); - const sp = new URLSearchParams(window.location.search); - const devToken = sp.get('devToken'); + const devToken = getDevToken(); if (devToken && getEventServiceEnv() === 'local') { buildDashboard(el, config); return; From 4aede70694634433cbc84cd05ece1362202d0b60 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 9 Dec 2024 12:08:20 -0600 Subject: [PATCH 46/74] Update form-handler.js --- ecc/blocks/form-handler/form-handler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 0f1ea1b1..6121d54b 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -254,7 +254,6 @@ async function loadEventData(props) { } else { buildNoAccessScreen(props.el); props.el.classList.remove('loading'); - return; } } } From aca5a5456cad1c55c387a006cd0e09de73d63015 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 9 Dec 2024 12:09:47 -0600 Subject: [PATCH 47/74] correct import path --- ecc/blocks/series-dashboard/series-dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index ea98051c..504559ce 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -15,7 +15,7 @@ import { getEventServiceEnv, getDevToken, } from '../../scripts/utils.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); From c590903f4ac356ab72d447708cf204797bad6761 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 9 Dec 2024 15:13:54 -0600 Subject: [PATCH 48/74] Linting, sample blocks creation and Lit component import refactor --- .../attendee-management-table.js | 4 +- .../event-creation-form.css | 13 +- .../event-creation-form.js | 16 +- ecc/blocks/form-handler/form-handler.js | 12 +- .../series-creation-form.css | 22 +- .../series-creation-form.js | 26 +- ecc/components/custom-search/custom-search.js | 3 +- .../custom-textfield/custom-textfield.js | 4 +- ecc/components/filter-menu/filter-menu.js | 3 +- .../image-dropzone/image-dropzone.js | 3 +- .../profile-container/profile-container.js | 3 +- ecc/components/profile/profile.js | 3 +- ecc/components/repeater/repeater.js | 4 +- .../searchable-picker/searchable-picker.js | 3 +- .../controller.sample.js | 0 .../sample-form-component.css | 1 + .../sample-form-component.js | 15 + ecc/samples/sample-form/data-handler.js | 54 ++ ecc/samples/sample-form/sample-form.css | 478 ++++++++++ ecc/samples/sample-form/sample-form.js | 836 ++++++++++++++++++ 20 files changed, 1413 insertions(+), 90 deletions(-) rename ecc/samples/{form-component => sample-form-component}/controller.sample.js (100%) create mode 100644 ecc/samples/sample-form-component/sample-form-component.css create mode 100644 ecc/samples/sample-form-component/sample-form-component.js create mode 100644 ecc/samples/sample-form/data-handler.js create mode 100644 ecc/samples/sample-form/sample-form.css create mode 100644 ecc/samples/sample-form/sample-form.js diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index f235d48f..768e162b 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -10,8 +10,8 @@ import { getEventServiceEnv, getDevToken, } from '../../scripts/utils.js'; -import { SearchablePicker } from '../../components/searchable-picker/searchable-picker.js'; -import { FilterMenu } from '../../components/filter-menu/filter-menu.js'; +import SearchablePicker from '../../components/searchable-picker/searchable-picker.js'; +import FilterMenu from '../../components/filter-menu/filter-menu.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/event-creation-form/event-creation-form.css b/ecc/blocks/event-creation-form/event-creation-form.css index e38ec2a9..16655746 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.css +++ b/ecc/blocks/event-creation-form/event-creation-form.css @@ -311,7 +311,7 @@ gap: 16px; } -.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag { +.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag { padding: 0 8px; background-color: var(--color-white); border-radius: 4px; @@ -337,15 +337,6 @@ border-bottom: 3px solid var(--stroke-color-divider); } -.event-creation-form .event-heading-tooltip-wrapper .event-heading-tooltip-icon { - height: 16px; - width: 16px; - display: flex; - align-items: center; - justify-content: center; - cursor: help; -} - .event-creation-form .section:not(:first-of-type) > div.content > h2, .event-creation-form .section:not(:first-of-type) > div.content > h3 { display: flex; @@ -397,7 +388,7 @@ width: 20px; } -.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag .icon { +.event-creation-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag .icon { margin-right: 4px; } diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index b4d4a3fc..af02d337 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -15,19 +15,19 @@ import { publishEvent, getEvent, } from '../../scripts/esp-controller.js'; -import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; -import { Profile } from '../../components/profile/profile.js'; -import { Repeater } from '../../components/repeater/repeater.js'; +import ImageDropzone from '../../components/image-dropzone/image-dropzone.js'; +import Profile from '../../components/profile/profile.js'; +import Repeater from '../../components/repeater/repeater.js'; import AgendaFieldset from '../../components/agenda-fieldset/agenda-fieldset.js'; import AgendaFieldsetGroup from '../../components/agenda-fieldset-group/agenda-fieldset-group.js'; -import { ProfileContainer } from '../../components/profile-container/profile-container.js'; -import { CustomTextfield } from '../../components/custom-textfield/custom-textfield.js'; +import ProfileContainer from '../../components/profile-container/profile-container.js'; +import CustomTextfield from '../../components/custom-textfield/custom-textfield.js'; import ProductSelector from '../../components/product-selector/product-selector.js'; import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; -import { CustomSearch } from '../../components/custom-search/custom-search.js'; +import CustomSearch from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -921,14 +921,14 @@ function updateStatusTag(props) { const headingSection = currentFragment.querySelector(':scope > .section:first-child'); - const eixstingStatusTag = headingSection.querySelector('.event-status-tag'); + const eixstingStatusTag = headingSection.querySelector('.status-tag'); if (eixstingStatusTag) eixstingStatusTag.remove(); const heading = headingSection.querySelector('h2', 'h3', 'h3', 'h4'); const headingWrapper = createTag('div', { class: 'step-heading-wrapper' }); const dot = eventDataResp.published ? getIcon('dot-purple') : getIcon('dot-green'); const text = eventDataResp.published ? 'Published' : 'Draft'; - const statusTag = createTag('span', { class: 'event-status-tag' }); + const statusTag = createTag('span', { class: 'status-tag' }); statusTag.append(dot, text); heading.parentElement?.replaceChild(headingWrapper, heading); diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index c8c941d0..bee8ab17 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -15,19 +15,19 @@ import { publishEvent, getEvent, } from '../../scripts/esp-controller.js'; -import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; -import { Profile } from '../../components/profile/profile.js'; -import { Repeater } from '../../components/repeater/repeater.js'; +import ImageDropzone from '../../components/image-dropzone/image-dropzone.js'; +import Profile from '../../components/profile/profile.js'; +import Repeater from '../../components/repeater/repeater.js'; import AgendaFieldset from '../../components/agenda-fieldset/agenda-fieldset.js'; import AgendaFieldsetGroup from '../../components/agenda-fieldset-group/agenda-fieldset-group.js'; -import { ProfileContainer } from '../../components/profile-container/profile-container.js'; -import { CustomTextfield } from '../../components/custom-textfield/custom-textfield.js'; +import ProfileContainer from '../../components/profile-container/profile-container.js'; +import CustomTextfield from '../../components/custom-textfield/custom-textfield.js'; import ProductSelector from '../../components/product-selector/product-selector.js'; import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; -import { CustomSearch } from '../../components/custom-search/custom-search.js'; +import CustomSearch from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/series-creation-form/series-creation-form.css b/ecc/blocks/series-creation-form/series-creation-form.css index 205f800e..8958dff4 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.css +++ b/ecc/blocks/series-creation-form/series-creation-form.css @@ -78,15 +78,6 @@ --mod-textfield-border-color-invalid-default: unset; } -.series-creation-form.show-dup-event-error #info-field-event-title { - --mod-textfield-icon-size-invalid: 16px; - --mod-textfield-border-color-invalid-default: unset; -} - -.series-creation-form.show-dup-event-error #info-field-event-title sp-help-text { - display: flex; -} - .series-creation-form .main-frame { flex-grow: 1; min-height: 100%; @@ -307,7 +298,7 @@ gap: 16px; } -.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag { +.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag { padding: 0 8px; background-color: var(--color-white); border-radius: 4px; @@ -333,15 +324,6 @@ border-bottom: 3px solid var(--stroke-color-divider); } -.series-creation-form .event-heading-tooltip-wrapper .event-heading-tooltip-icon { - height: 16px; - width: 16px; - display: flex; - align-items: center; - justify-content: center; - cursor: help; -} - .series-creation-form .section:not(:first-of-type) > div.content > h2, .series-creation-form .section:not(:first-of-type) > div.content > h3 { display: flex; @@ -397,7 +379,7 @@ width: 20px; } -.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .event-status-tag .icon { +.series-creation-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag .icon { margin-right: 4px; } diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index a90a4526..662cd4f7 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -14,19 +14,7 @@ import { publishSeries, getSeries, } from '../../scripts/esp-controller.js'; -import { ImageDropzone } from '../../components/image-dropzone/image-dropzone.js'; -import { Profile } from '../../components/profile/profile.js'; -import { Repeater } from '../../components/repeater/repeater.js'; -import AgendaFieldset from '../../components/agenda-fieldset/agenda-fieldset.js'; -import AgendaFieldsetGroup from '../../components/agenda-fieldset-group/agenda-fieldset-group.js'; -import { ProfileContainer } from '../../components/profile-container/profile-container.js'; -import { CustomTextfield } from '../../components/custom-textfield/custom-textfield.js'; -import ProductSelector from '../../components/product-selector/product-selector.js'; -import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; -import PartnerSelector from '../../components/partner-selector/partner-selector.js'; -import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; -import { CustomSearch } from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -185,7 +173,6 @@ function initRequiredFieldsValidation(props) { function validatePublishFields(props) { const publishAttributesFilled = PUBLISHABLE_ATTRS.every((attr) => props.payload[attr]); - console.log('publishAttributesFilled', publishAttributesFilled); const publishButton = props.el.querySelector('.series-creation-form-ctas-panel .next-button'); publishButton.classList.toggle('disabled', !publishAttributesFilled); @@ -210,18 +197,7 @@ function enableSideNavForEditFlow(props) { } function initCustomLitComponents() { - customElements.define('image-dropzone', ImageDropzone); - customElements.define('profile-ui', Profile); - customElements.define('repeater-element', Repeater); - customElements.define('partner-selector', PartnerSelector); - customElements.define('partner-selector-group', PartnerSelectorGroup); - customElements.define('agenda-fieldset', AgendaFieldset); - customElements.define('agenda-fieldset-group', AgendaFieldsetGroup); - customElements.define('product-selector', ProductSelector); - customElements.define('product-selector-group', ProductSelectorGroup); - customElements.define('profile-container', ProfileContainer); - customElements.define('custom-textfield', CustomTextfield); - customElements.define('custom-search', CustomSearch); + // no-op } async function loadData(props) { diff --git a/ecc/components/custom-search/custom-search.js b/ecc/components/custom-search/custom-search.js index 11e43813..5aa830c7 100644 --- a/ecc/components/custom-search/custom-search.js +++ b/ecc/components/custom-search/custom-search.js @@ -5,8 +5,7 @@ const { LitElement, html, repeat, nothing } = await import(`${LIBS}/deps/lit-all const SEARCH_TIMEOUT_MS = 500; -// eslint-disable-next-line import/prefer-default-export -export class CustomSearch extends LitElement { +export default class CustomSearch extends LitElement { static properties = { identifier: { type: String }, searchMap: { type: Object }, diff --git a/ecc/components/custom-textfield/custom-textfield.js b/ecc/components/custom-textfield/custom-textfield.js index 97fec30f..6e793268 100644 --- a/ecc/components/custom-textfield/custom-textfield.js +++ b/ecc/components/custom-textfield/custom-textfield.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable class-methods-use-this */ import { LIBS } from '../../scripts/scripts.js'; import { style } from './custom-textfield.css.js'; @@ -17,7 +15,7 @@ const defaultData = { helperText: '', }; -export class CustomTextfield extends LitElement { +export default class CustomTextfield extends LitElement { static properties = { config: { type: Object, reflect: true }, fielddata: { type: Object, reflect: true }, diff --git a/ecc/components/filter-menu/filter-menu.js b/ecc/components/filter-menu/filter-menu.js index e372cca8..d01fd831 100644 --- a/ecc/components/filter-menu/filter-menu.js +++ b/ecc/components/filter-menu/filter-menu.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ /* eslint-disable max-len */ import { LIBS } from '../../scripts/scripts.js'; import { camelToSentenceCase } from '../../scripts/utils.js'; @@ -6,7 +5,7 @@ import { style } from './filter-menu.css.js'; const { LitElement, html, repeat } = await import(`${LIBS}/deps/lit-all.min.js`); -export class FilterMenu extends LitElement { +export default class FilterMenu extends LitElement { static styles = style; static properties = { diff --git a/ecc/components/image-dropzone/image-dropzone.js b/ecc/components/image-dropzone/image-dropzone.js index 42a571f1..f9cbda7b 100644 --- a/ecc/components/image-dropzone/image-dropzone.js +++ b/ecc/components/image-dropzone/image-dropzone.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ /* eslint-disable class-methods-use-this */ import { isImageTypeValid, isImageSizeValid } from '../../scripts/image-validator.js'; import { LIBS } from '../../scripts/scripts.js'; @@ -6,7 +5,7 @@ import { style } from './image-dropzone.css.js'; const { LitElement, html } = await import(`${LIBS}/deps/lit-all.min.js`); -export class ImageDropzone extends LitElement { +export default class ImageDropzone extends LitElement { static properties = { file: { type: Object, reflect: true }, handleImage: { type: Function }, diff --git a/ecc/components/profile-container/profile-container.js b/ecc/components/profile-container/profile-container.js index 4e6a3449..1e577d2c 100644 --- a/ecc/components/profile-container/profile-container.js +++ b/ecc/components/profile-container/profile-container.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ /* eslint-disable class-methods-use-this */ import { getSpeakers } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; @@ -9,7 +8,7 @@ const { LitElement, html, repeat, nothing } = await import(`${LIBS}/deps/lit-all const defaultProfile = { socialMedia: [{ link: '' }], isPlaceholder: true }; -export class ProfileContainer extends LitElement { +export default class ProfileContainer extends LitElement { static properties = { fieldlabels: { type: Object, reflect: true }, profiles: { type: Array, reflect: true }, diff --git a/ecc/components/profile/profile.js b/ecc/components/profile/profile.js index 55ce2efe..67095a4e 100644 --- a/ecc/components/profile/profile.js +++ b/ecc/components/profile/profile.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ /* eslint-disable class-methods-use-this */ import { LIBS } from '../../scripts/scripts.js'; import { style } from './profile.css.js'; @@ -23,7 +22,7 @@ const DEFAULT_FIELD_LABELS = { const SPEAKER_TYPE = ['Host', 'Speaker', 'Judge']; const SUPPORTED_SOCIAL = ['YouTube', 'LinkedIn', 'Web', 'Twitter', 'X', 'TikTok', 'Instagram', 'Facebook', 'Pinterest']; -export class Profile extends LitElement { +export default class Profile extends LitElement { static properties = { seriesId: { type: String }, fieldlabels: { type: Object, reflect: true }, diff --git a/ecc/components/repeater/repeater.js b/ecc/components/repeater/repeater.js index 54d2d652..1d2752bb 100644 --- a/ecc/components/repeater/repeater.js +++ b/ecc/components/repeater/repeater.js @@ -1,11 +1,9 @@ -/* eslint-disable import/prefer-default-export */ -/* eslint-disable class-methods-use-this */ import { LIBS } from '../../scripts/scripts.js'; import { style } from './repeater.css.js'; const { LitElement, html } = await import(`${LIBS}/deps/lit-all.min.js`); -export class Repeater extends LitElement { +export default class Repeater extends LitElement { static properties = { text: { type: String, reflect: true } }; static styles = style; diff --git a/ecc/components/searchable-picker/searchable-picker.js b/ecc/components/searchable-picker/searchable-picker.js index 83a43162..04604012 100644 --- a/ecc/components/searchable-picker/searchable-picker.js +++ b/ecc/components/searchable-picker/searchable-picker.js @@ -4,8 +4,7 @@ import { style } from './searchable-picker.css.js'; const { LitElement, html, repeat } = await import(`${LIBS}/deps/lit-all.min.js`); -// eslint-disable-next-line import/prefer-default-export -export class SearchablePicker extends LitElement { +export default class SearchablePicker extends LitElement { static styles = style; static properties = { diff --git a/ecc/samples/form-component/controller.sample.js b/ecc/samples/sample-form-component/controller.sample.js similarity index 100% rename from ecc/samples/form-component/controller.sample.js rename to ecc/samples/sample-form-component/controller.sample.js diff --git a/ecc/samples/sample-form-component/sample-form-component.css b/ecc/samples/sample-form-component/sample-form-component.css new file mode 100644 index 00000000..ef777318 --- /dev/null +++ b/ecc/samples/sample-form-component/sample-form-component.css @@ -0,0 +1 @@ +/* write any CSS you need for your component here */ diff --git a/ecc/samples/sample-form-component/sample-form-component.js b/ecc/samples/sample-form-component/sample-form-component.js new file mode 100644 index 00000000..ed05b6a5 --- /dev/null +++ b/ecc/samples/sample-form-component/sample-form-component.js @@ -0,0 +1,15 @@ +/* eslint-disable no-unused-vars */ +// import { LIBS } from '../../scripts/scripts.js'; +// import { handlize, generateToolTip } from '../../scripts/utils.js'; + +// const { createTag } = await import(`${LIBS}/utils/utils.js`); + +export default function init(el) { + el.classList.add('form-component'); + // generateToolTip(el); + + const rows = el.querySelectorAll(':scope > div'); + rows.forEach((_r, _i) => { + // perform decoration based on the index of the row + }); +} diff --git a/ecc/samples/sample-form/data-handler.js b/ecc/samples/sample-form/data-handler.js new file mode 100644 index 00000000..4f805bf9 --- /dev/null +++ b/ecc/samples/sample-form/data-handler.js @@ -0,0 +1,54 @@ +/* eslint-disable no-use-before-define */ +// FIXME: Needs better implementation overall + +let responseCache = {}; +let payloadCache = {}; + +const submissionFilter = [ + // list attribute keys submittable by this form +]; + +function isValidAttribute(attr) { + return attr !== undefined && attr !== null; +} + +export function quickFilter(obj) { + const output = {}; + + submissionFilter.forEach((attr) => { + if (isValidAttribute(obj[attr])) { + output[attr] = obj[attr]; + } + }); + + return output; +} + +export function setPayloadCache(payload) { + if (!payload) return; + payloadCache = quickFilter(payload); +} + +export function getFilteredCachedPayload() { + return payloadCache; +} + +export function setResponseCache(response) { + if (!response) return; + responseCache = quickFilter(response); +} + +export function getFilteredCachedResponse() { + return responseCache; +} + +export default function getJoinedData() { + const filteredResponse = getFilteredCachedResponse(); + const filteredPayload = getFilteredCachedPayload(); + + return { + ...filteredResponse, + ...filteredPayload, + modificationTime: filteredResponse.modificationTime, + }; +} diff --git a/ecc/samples/sample-form/sample-form.css b/ecc/samples/sample-form/sample-form.css new file mode 100644 index 00000000..82c70a95 --- /dev/null +++ b/ecc/samples/sample-form/sample-form.css @@ -0,0 +1,478 @@ +.sample-form { + display: block; + padding: 0 40px; + + --mod-textfield-icon-size-invalid: 0; + --stroke-color-divider: #6E6E6E; + --color-red: #EB1000; + --mod-textfield-focus-indicator-width: 0; + --mod-textfield-text-color-disabled: #000; + --mod-textfield-border-color-invalid-default: #000; + --mod-textfield-border-color-invalid-focus: #000; + --mod-textfield-border-color-invalid-focus-hover: #000; + --mod-textfield-border-color-invalid-hover: #000; + --mod-textfield-border-color-invalid-keyboard-focus: #000; + --mod-textfield-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; + --mod-textfield-spacing-block-start: 8px; +} + +.sample-form .loading-screen { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 20; + background-color: var(--color-white); + opacity: 1; +} + +.sample-form .loading-screen sp-field-label { + font-size: var(--type-body-s-size); +} + +.sample-form .img-upload-text p { + margin: 0; + font-size: var(--type-body-xs-size); + line-height: normal; +} + +.sample-form .main-frame sp-underlay { + z-index: 2; +} + +.sample-form .main-frame sp-underlay + sp-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 3; + background: var(--spectrum-gray-100); + min-width: 480px; +} + +.sample-form .main-frame sp-underlay + sp-dialog h1 { + font-size: var(--type-heading-s-size); +} + +.sample-form .main-frame sp-underlay + sp-dialog p { + font-size: var(--type-body-s-size); +} + +.sample-form .main-frame sp-underlay + sp-dialog .button-container { + display: flex; + justify-content: flex-end; + gap: 16px; +} + +.sample-form .main-frame sp-underlay:not([open]) + sp-dialog { + display: none; +} + +.sample-form.show-error { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.sample-form .main-frame { + flex-grow: 1; + min-height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.sample-form .sample-form-ctas-panel { + position: sticky; + transform: translateX(-40px); + box-sizing: border-box; + bottom: 0; + padding: 16px 60px; + background-color: var(--color-red); + width: calc(100% + 80px); + z-index: 1; + display: flex; + justify-content: center; +} + +.sample-form .side-menu, +.sample-form .main-frame, +.sample-form .sample-form-ctas-panel, +.sample-form .loading-screen { + transition: opacity 0.5s; +} + +.sample-form.disabled .main-frame, +.sample-form.disabled .sample-form-ctas-panel { + pointer-events: none; +} + +.sample-form .side-menu { + transition: opacity 0.2s; +} + +.sample-form.loading div:first-of-type, +.sample-form.loading .side-menu, +.sample-form.loading .main-frame, +.sample-form.loading .sample-form-ctas-panel { + opacity: 0; +} + +.sample-form .side-menu button { + font-family: var(--body-font-family); +} + +.sample-form sp-textfield { + outline: none; +} + +.sample-form sp-textfield[quiet]:not(:read-only):focus { + outline: 1px var(--color-gray-500) solid; + border-radius: 4px; +} + +.sample-form > div.form-body { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 203px); +} + +.sample-form .side-menu.disabled { + opacity: 0.5; + pointer-events: none; +} + +.sample-form .side-menu h3 { + font-size: var(--type-body-xs-size); + color: var(--color-gray-400); + margin-bottom: 0; + margin-top: 24px; + padding: 0 24px; +} + +.sample-form .side-menu ul { + margin-top: 0; + padding: 0; +} + +.sample-form .side-menu ul li { + list-style: none; + font-size: var(--type-body-xs-size); + line-height: normal; + border-radius: 8px; + padding-left: 24px; + padding-right: 24px; +} + +.sample-form .sample-form-ctas-panel a { + font-size: var(--type-body-s-size); + display: inline-flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s, filter 0.2s; +} + +.sample-form .side-menu ul li a { + color: var(--color-black); + text-decoration: none; +} + +.sample-form .side-menu ul li a, +.sample-form .side-menu ul li button { + text-align: left; + border: none; + background-color: transparent; + padding-left: 24px; + padding-right: 24px; + width: 100%; +} + +.sample-form .side-menu ul li:not(:has(ul)) { + padding-left: 0; + padding-right: 0; + margin: 12px 0; + display: flex; +} + +.sample-form .side-menu ul li ul { + margin-top: 12px; +} + +.sample-form .side-menu ul li ul li:not(:has(ul)) { + margin: 4px 0; +} + +.sample-form .side-menu ul li:not(:has(ul)) a, +.sample-form .side-menu ul li:not(:has(ul)) button { + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; +} + +.sample-form .side-menu ul li:not(:has(ul)):has(a):hover, +.sample-form .side-menu ul li:not(:has(ul)):has(a).active, +.sample-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover, +.sample-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active { + background-color: var(--color-red); + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.sample-form .side-menu ul li:not(:has(ul)):has(a):hover a, +.sample-form .side-menu ul li:not(:has(ul)):has(a).active a, +.sample-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)).active button, +.sample-form .side-menu ul li:not(:has(ul)):has(button:not(:disabled)):hover button { + color: var(--color-white); + font-weight: 700; + letter-spacing: -0.02em +} + +.sample-form .side-menu .nav-item { + cursor: pointer; +} + +.sample-form .side-menu .nav-item:disabled { + pointer-events: none; + cursor: unset; +} + +.sample-form .side-menu .nav-item.disabled { + pointer-events: none; + cursor: unset; + opacity: 0.5; +} + +.sample-form .main-frame .section .content { + max-width: none; +} + +.sample-form .main-frame .section:first-of-type .content { + margin: 16px 24px; + max-width: none; + display: grid; + align-items: center; + justify-content: space-between; + grid-template-columns: 1fr 1fr; +} + +.sample-form .main-frame .section:first-of-type .content p { + font-size: var(--type-body-xs-size); +} + +.sample-form .main-frame .section:first-of-type .content p:first-of-type { + display: flex; + flex-direction: row-reverse; +} + +.sample-form .form-component > div:first-of-type > div > h2 { + font-size: var(--type-heading-xl-size); + line-height: var(--type-heading-xl-lh); +} + +.sample-form .form-component > div:first-of-type > div > h2, +.sample-form .form-component > div:first-of-type > div > h3, +.sample-form .form-component > div:first-of-type > div > h4 { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + margin-bottom: 32px; +} + +.sample-form .main-frame .section:first-of-type h2 { + margin: 0; + font-weight: 900; + color: var(--color-red); +} + +.sample-form .main-frame .section:first-of-type .step-heading-wrapper { + display: flex; + align-items: center; + gap: 16px; +} + +.sample-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag { + padding: 0 8px; + background-color: var(--color-white); + border-radius: 4px; +} + +.sample-form .main-frame .section:not(:first-of-type) { + padding: 24px 56px; + border-radius: 10px; + margin: 24px; + box-shadow: 0 3px 6px 0 rgb(0 0 0 / 16%); + background-color: var(--color-white); +} + +.sample-form .fragment.hidden { + display: none; +} + +.sample-form .form-component { + padding: 32px 12px; +} + +.sample-form .form-component:not(:last-of-type):not(.no-divider) { + border-bottom: 3px solid var(--stroke-color-divider); +} + +.sample-form .section:not(:first-of-type) > div.content > h2, +.sample-form .section:not(:first-of-type) > div.content > h3 { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 0; + padding: 0 12px; +} + +.sample-form .form-component > div:first-of-type > div > h2 sp-action-button, +.sample-form .form-component > div:first-of-type > div > h3 sp-action-button, +.sample-form .form-component > div:first-of-type > div > h4 sp-action-button, +.sample-form .section:not(:first-of-type) > div.content > h2 sp-action-button, +.sample-form .section:not(:first-of-type) > div.content > h3 sp-action-button, +.sample-form .section:not(:first-of-type) > div.content > h4 sp-action-button { + padding: 0; + background: none; + border: none; + cursor: help +} + +.sample-form .form-component > div:first-of-type > div > h2 sp-action-button .icon-info, +.sample-form .form-component > div:first-of-type > div > h3 sp-action-button .icon-info, +.sample-form .form-component > div:first-of-type > div > h4 sp-action-button .icon-info, +.sample-form .section:not(:first-of-type) > div.content > h2 sp-action-button .icon-info, +.sample-form .section:not(:first-of-type) > div.content > h3 sp-action-button .icon-info, +.sample-form .section:not(:first-of-type) > div.content > h4 sp-action-button .icon-info { + display: block; +} + +.sample-form .sample-form-ctas-panel .sample-form-panel-wrapper { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 1440px; +} + +.sample-form .sample-form-ctas-panel .sample-form-panel-wrapper > div { + display: flex; + align-items: center; +} + +.sample-form .sample-form-ctas-panel .sample-form-backward-wrapper .back-btn { + padding: 8px; + border: 2px solid var(--color-white); + border-radius: 24px; + cursor: pointer; +} + +.sample-form .sample-form-ctas-panel .sample-form-backward-wrapper .back-btn .icon { + display: block; + height: 20px; + width: 20px; +} + +.sample-form .main-frame .section:first-of-type .step-heading-wrapper .status-tag .icon { + margin-right: 4px; +} + +.sample-form .sample-form-ctas-panel .sample-form-forward-wrapper > div:first-of-type { + padding-right: 64px; + border-right: 1px solid var(--color-black); + margin-right: 104px; +} + +.sample-form .sample-form-ctas-panel .sample-form-forward-wrapper .action-area { + display: flex; + align-items: center; + gap: 16px; +} + +.sample-form .sample-form-ctas-panel a.disabled, +.sample-form .sample-form-ctas-panel a.preview-not-ready, +.sample-form .sample-form-ctas-panel a.submitting { + pointer-events: none; + opacity: 0.5; +} + +.sample-form .sample-form-ctas-panel a.next-button { + background-color: var(--color-gray-800); + border-color: var(--color-gray-800); +} + +.sample-form .sample-form-ctas-panel a.next-button:hover { + background-color: var(--color-black) +} + +.sample-form .sample-form-ctas-panel a.fill { + background-color: var(--color-gray-200); + color: var(--color-black); + font-weight: 700; + border-radius: 20px; + line-height: 20px; + min-height: 21px; + padding: 7px 18px 8px; + border: 2px solid var(--color-white); +} + +.sample-form .sample-form-ctas-panel a.fill:hover { + text-decoration: none; + filter: invert(); +} + +.sample-form .sample-form-ctas-panel a.preview-btns svg { + height: 20px; + width: 28px; +} + +.sample-form .sample-form-ctas-panel .sample-form-panel-wrapper .con-button.outline { + color: var(--color-white); + border-color: var(--color-white); +} + +.sample-form .sample-form-ctas-panel .sample-form-panel-wrapper .con-button.outline:hover { + background-color: var(--color-white); + color: var(--color-red); +} + +.sample-form.hidden, +.sample-form .hidden { + display: none; +} + +.sample-form:not(.loading) .loading-screen { + opacity: 0; + z-index: -1; +} + +.sample-form .toast-parent { + position: absolute; + bottom: 100%; + right: 60px; +} + +.sample-form .toast-area { + margin-bottom: 16px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 16px; +} + +@media screen and (min-width: 900px) { + .sample-form > div.form-body { + flex-direction: row; + } + + .sample-form .main-frame { + max-width: var(--grid-container-width); + } +} diff --git a/ecc/samples/sample-form/sample-form.js b/ecc/samples/sample-form/sample-form.js new file mode 100644 index 00000000..8bc49469 --- /dev/null +++ b/ecc/samples/sample-form/sample-form.js @@ -0,0 +1,836 @@ +import { LIBS } from '../../scripts/scripts.js'; +import { + getIcon, + buildNoAccessScreen, + generateToolTip, + camelToSentenceCase, + signIn, + getEventServiceEnv, + getDevToken, +} from '../../scripts/utils.js'; +import { + createSeries, + updateSeries, + publishSeries, + getSeries, +} from '../../scripts/esp-controller.js'; +import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; +import { initProfileLogicTree } from '../../scripts/event-apis.js'; + +const { createTag } = await import(`${LIBS}/utils/utils.js`); +const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); + +// list of controllers for the handler to load +const VANILLA_COMPONENTS = [ + 'series-details', + 'series-templates', + 'series-additional-info', +]; + +const REQUIRED_INPUT_TYPES = [ + 'input[required]', + 'select[required]', + 'textarea[required]', + 'sp-textfield[required]', + 'sp-checkbox[required]', + 'sp-picker[required]', +]; + +const SPECTRUM_COMPONENTS = [ + 'theme', + 'textfield', + 'picker', + 'menu', + 'checkbox', + 'field-label', + 'divider', + 'button', + 'progress-circle', + 'overlay', + 'dialog', + 'button-group', + 'tooltip', + 'popover', + 'search', + 'toast', + 'icon', + 'action-button', + 'progress-circle', +]; + +const CUSTOM_LIT_COMPONENTS = [ + 'custom-textfield', +]; + +const PUBLISHABLE_ATTRS = [ + 'seriesName', + 'cloudType', + 'templateId', +]; + +export function buildErrorMessage(props, resp) { + if (!resp) return; + + const toastArea = resp.targetEl ? resp.targetEl.querySelector('.toast-area') : props.el.querySelector('.toast-area'); + + if (resp.error) { + const messages = []; + const errorBag = resp.error.errors; + const errorMessage = resp.error.message; + + if (errorBag) { + errorBag.forEach((error) => { + const errorPathSegments = error.path.split('/'); + const text = `${camelToSentenceCase(errorPathSegments[errorPathSegments.length - 1])} ${error.message}`; + messages.push(text); + }); + + messages.forEach((msg, i) => { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 + (i * 3000) }, msg, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + }); + } else if (errorMessage) { + if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Series update invalid. The series has been modified since last fetch"}') { + const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The series has been updated by a different session since your last save.', { parent: toastArea }); + const url = new URL(window.location.href); + url.searchParams.set('seriesId', getFilteredCachedResponse().seriesId); + + createTag('sp-button', { + slot: 'action', + variant: 'overBackground', + href: `${url.toString()}`, + }, 'See the latest version', { parent: toast }); + + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } else { + const toast = createTag('sp-toast', { open: true, variant: 'negative', timeout: 6000 }, errorMessage, { parent: toastArea }); + toast.addEventListener('close', (e) => { + e.stopPropagation(); + toast.remove(); + }, { once: true }); + } + } + } +} + +function replaceAnchorWithButton(anchor) { + if (!anchor || anchor.tagName !== 'A') { + return null; + } + + const attributes = {}; + for (let i = 0; i < anchor.attributes.length; i += 1) { + const attr = anchor.attributes[i]; + attributes[attr.name] = attr.value; + } + + const button = createTag('button', attributes, anchor.innerHTML); + + anchor.parentNode.replaceChild(button, anchor); + return button; +} + +function getCurrentFragment(props) { + const frags = props.el.querySelectorAll('.fragment'); + const currentFrag = frags[props.currentStep]; + return currentFrag; +} + +function validateFields(fields) { + return fields.length === 0 || Array.from(fields).every((f) => f.value && !f.invalid); +} + +function onStepValidate(props) { + return function updateSaveCtaStatus() { + const currentFrag = getCurrentFragment(props); + const stepValid = validateFields(props[`required-fields-in-${currentFrag.id}`]); + const saveButton = props.el.querySelector('.series-creation-form-ctas-panel .save-button'); + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + saveButton.classList.toggle('disabled', !stepValid); + + sideNavs.forEach((nav, i) => { + if (i !== props.currentStep) { + nav.disabled = !stepValid; + } + }); + }; +} + +function initRequiredFieldsValidation(props) { + const currentFrag = getCurrentFragment(props); + + const inputValidationCB = onStepValidate(props); + props[`required-fields-in-${currentFrag.id}`].forEach((field) => { + field.removeEventListener('change', inputValidationCB); + field.addEventListener('change', inputValidationCB, { bubbles: true }); + }); + + inputValidationCB(); +} + +function validatePublishFields(props) { + const publishAttributesFilled = PUBLISHABLE_ATTRS.every((attr) => props.payload[attr]); + const publishButton = props.el.querySelector('.series-creation-form-ctas-panel .next-button'); + + publishButton.classList.toggle('disabled', !publishAttributesFilled); +} + +function enableSideNavForEditFlow(props) { + const frags = props.el.querySelectorAll('.fragment'); + const completeFirstStep = Array.from(frags[0].querySelectorAll('.form-component')) + .every((fc) => fc.classList.contains('prefilled')); + + if (!completeFirstStep) return; + + frags.forEach((frag, i) => { + const prefilledOtherSteps = i !== 0 && frag.querySelector('.form-component.prefilled'); + + if (completeFirstStep || prefilledOtherSteps) { + props.farthestStep = Math.max(props.farthestStep, i); + } + }); + + initRequiredFieldsValidation(props); +} + +async function initCustomLitComponents() { + // aync import all custom lit components + const promises = CUSTOM_LIT_COMPONENTS.map(async (componentName) => { + const { default: component } = await import(`../components/${componentName}/${componentName}.js`); + customElements.define(componentName, component); + }); + + await Promise.all(promises); +} + +async function loadData(props) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const seriesId = urlParams.get('seriesId'); + + if (seriesId) { + setTimeout(() => { + if (!props.response.seriesId) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the seriesId URL Param is valid.', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); + } + }, 5000); + + props.el.classList.add('disabled'); + const data = await getSeries(seriesId); + props.response = { ...props.response, ...data }; + props.el.classList.remove('disabled'); + } +} + +async function initComponents(props) { + await initCustomLitComponents(); + + const componentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents?.length) return; + + const componentInitPromises = Array.from(mappedComponents).map(async (component) => { + const { default: initComponent } = await import(`../${comp}-component/controller.js`); + await initComponent(component, props); + }); + + await Promise.all(componentInitPromises); + }); + + await Promise.all(componentPromises); +} + +async function gatherValues(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onSubmit } = await import(`../${comp}-component/controller.js`); + return onSubmit(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function handleSeriesUpdate(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onTargetUpdate } = await import(`../${comp}-component/controller.js`); + return onTargetUpdate(component, props); + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnPayloadChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onPayloadUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onPayloadUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +async function updateComponentsOnRespChange(props) { + const allComponentPromises = VANILLA_COMPONENTS.map(async (comp) => { + const mappedComponents = props.el.querySelectorAll(`.${comp}-component`); + if (!mappedComponents.length) return {}; + + const promises = Array.from(mappedComponents).map(async (component) => { + const { onRespUpdate } = await import(`../${comp}-component/controller.js`); + const componentPayload = await onRespUpdate(component, props); + return componentPayload; + }); + + return Promise.all(promises); + }); + + await Promise.all(allComponentPromises); +} + +function decorateForm(el) { + const ctaRow = el.querySelector(':scope > div:last-of-type'); + const formBodyRow = el.querySelector(':scope > div:first-of-type'); + + if (ctaRow) { + const toastParent = createTag('sp-theme', { class: 'toast-parent', color: 'light', scale: 'medium' }, '', { parent: ctaRow }); + createTag('div', { class: 'toast-area' }, '', { parent: toastParent }); + } + + if (!formBodyRow) return; + + formBodyRow.classList.add('form-body'); + + const app = createTag('sp-theme', { color: 'light', scale: 'medium', id: 'form-app' }); + createTag('sp-underlay', {}, '', { parent: app }); + createTag('sp-dialog', { size: 's' }, '', { parent: app }); + const form = createTag('form', {}, '', { parent: app }); + const formDivs = el.querySelectorAll('.fragment'); + + if (!formDivs.length) { + el.remove(); + return; + } + + formDivs.forEach((formDiv) => { + formDiv.parentElement.parentElement.replaceChild(app, formDiv.parentElement); + form.append(formDiv.parentElement); + }); + + const cols = formBodyRow.querySelectorAll(':scope > div, :scope > sp-theme'); + + cols.forEach((col, i) => { + if (i === 0) { + col.classList.add('side-menu'); + const navItems = col.querySelectorAll('a[href*="#"]'); + navItems.forEach((nav, index) => { + const btn = replaceAnchorWithButton(nav); + btn.classList.add('nav-item'); + + if (index !== 0) { + btn.disabled = true; + } else { + btn.closest('li')?.classList.add('active'); + } + }); + } + + if (i === 1) { + col.classList.add('main-frame'); + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + const fragPathSegments = frag.dataset.path.split('/'); + const fragId = `form-step-${fragPathSegments[fragPathSegments.length - 1]}`; + frag.id = fragId; + }); + } + }); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + }); +} + +function showSaveSuccessMessage(props, detail = { message: 'Edits saved successfully' }) { + const toastArea = props.el.querySelector('.toast-area'); + if (!toastArea) return; + + const previousMsgs = toastArea.querySelectorAll('.save-success-msg'); + + previousMsgs.forEach((msg) => { + msg.remove(); + }); + + const toast = createTag('sp-toast', { class: 'save-success-msg', open: true, variant: 'positive', timeout: 6000 }, detail.message || 'Edits saved successfully', { parent: toastArea }); + toast.addEventListener('close', () => { + toast.remove(); + }); +} + +function updateDashboardLink(props) { + // FIXME: presuming first link is dashboard link is not good. + if (!getFilteredCachedResponse().seriesId) return; + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + if (!dashboardLink) return; + + const url = new URL(dashboardLink.href); + + if (url.searchParams.has('seriesId')) return; + + url.searchParams.set('newEventId', getFilteredCachedResponse().seriesId); + dashboardLink.href = url.toString(); +} + +async function saveSeries(props, toPublish = false) { + try { + await gatherValues(props); + } catch (e) { + return { error: { message: e.message } }; + } + + let resp; + + const onSeriesSave = async () => { + if (resp?.seriesId) await handleSeriesUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } + }; + + if (props.currentStep === 0 && !getFilteredCachedResponse().seriesId) { + resp = await createSeries(quickFilter(props.payload)); + props.response = { ...props.response, ...resp }; + updateDashboardLink(props); + await onSeriesSave(); + } else if (props.currentStep <= props.maxStep && !toPublish) { + resp = await updateSeries( + getFilteredCachedResponse().seriesId, + getJoinedData(), + ); + props.response = { ...props.response, ...resp }; + await onSeriesSave(); + } else if (toPublish) { + resp = await publishSeries( + getFilteredCachedResponse().seriesId, + getJoinedData(), + ); + props.response = { ...props.response, ...resp }; + if (resp?.seriesId) await handleSeriesUpdate(props); + } + + return resp; +} + +function updateSideNav(props) { + const sideNavs = props.el.querySelectorAll('.side-menu .nav-item'); + + sideNavs.forEach((n, i) => { + n.closest('li')?.classList.remove('active'); + if (i <= props.farthestStep) n.disabled = false; + if (i === props.currentStep) n.closest('li')?.classList.add('active'); + }); +} + +function updateRequiredFields(props) { + const currentFrag = getCurrentFragment(props); + props[`required-fields-in-${currentFrag.id}`] = currentFrag.querySelectorAll(REQUIRED_INPUT_TYPES.join()); +} + +function renderFormNavigation(props, prevStep, currentStep) { + const nextBtn = props.el.querySelector('.series-creation-form-ctas-panel .next-button'); + const backBtn = props.el.querySelector('.series-creation-form-ctas-panel .back-btn'); + const frags = props.el.querySelectorAll('.fragment'); + + frags[prevStep].classList.add('hidden'); + frags[currentStep].classList.remove('hidden'); + + if (props.currentStep === props.maxStep) { + if (props.response.published) { + nextBtn.textContent = nextBtn.dataset.republishStateText; + } else { + nextBtn.textContent = nextBtn.dataset.finalStateText; + nextBtn.prepend(getIcon('golden-rocket')); + } + } else { + nextBtn.textContent = nextBtn.dataset.finalStateText; + nextBtn.prepend(getIcon('golden-rocket')); + } + + backBtn.classList.toggle('disabled', currentStep === 0); +} + +function navigateForm(props, stepIndex) { + const index = stepIndex || stepIndex === 0 ? stepIndex : props.currentStep + 1; + const frags = props.el.querySelectorAll('.fragment'); + + if (index >= frags.length || index < 0) return; + + props.currentStep = index; + props.farthestStep = Math.max(props.farthestStep, index); + + window.scrollTo(0, 0); + updateRequiredFields(props); +} + +function initFormCtas(props) { + const ctaRow = props.el.querySelector(':scope > div:last-of-type'); + decorateButtons(ctaRow, 'button-l'); + const ctas = ctaRow.querySelectorAll('a'); + ctaRow.classList.add('series-creation-form-ctas-panel'); + + const forwardActionsWrappers = ctaRow.querySelectorAll(':scope > div'); + + const panelWrapper = createTag('div', { class: 'series-creation-form-panel-wrapper' }, '', { parent: ctaRow }); + createTag('div', { class: 'series-creation-form-backward-wrapper' }, '', { parent: panelWrapper }); + const forwardWrapper = createTag('div', { class: 'series-creation-form-forward-wrapper' }, '', { parent: panelWrapper }); + + forwardActionsWrappers.forEach((w) => { + w.classList.add('action-area'); + forwardWrapper.append(w); + }); + + const toggleBtnsSubmittingState = (submitting) => { + ctas.forEach((c) => { + c.classList.toggle('submitting', submitting); + }); + }; + + ctas.forEach((cta) => { + if (cta.href) { + const ctaUrl = new URL(cta.href); + + if (['#save', '#next'].includes(ctaUrl.hash)) { + if (ctaUrl.hash === '#next') { + cta.classList.add('next-button'); + const [finalStateText, doneStateText, republishStateText] = cta.textContent.split('||'); + + cta.textContent = finalStateText; + cta.prepend(getIcon('golden-rocket')); + cta.dataset.finalStateText = finalStateText; + cta.dataset.doneStateText = doneStateText; + cta.dataset.republishStateText = republishStateText; + } + + if (ctaUrl.hash === '#save') { + cta.classList.add('save-button'); + } + + cta.addEventListener('click', async (e) => { + e.preventDefault(); + toggleBtnsSubmittingState(true); + + if (ctaUrl.hash === '#next') { + let resp; + if (props.currentStep === props.maxStep) { + resp = await saveSeries(props, true); + } else { + resp = await saveSeries(props); + } + + if (resp?.error) { + buildErrorMessage(props, resp); + } else if (props.currentStep === props.maxStep) { + const toastArea = props.el.querySelector('.toast-area'); + cta.textContent = cta.dataset.doneStateText; + cta.classList.add('disabled'); + + if (toastArea) { + const toast = createTag('sp-toast', { open: true, variant: 'positive' }, 'Success! This series has been published.', { parent: toastArea }); + const dashboardLink = props.el.querySelector('.side-menu > ul > li > a'); + + createTag( + 'sp-button', + { + slot: 'action', + variant: 'overBackground', + treatment: 'outline', + href: dashboardLink.href, + }, + 'Go to dashboard', + { parent: toast }, + ); + + toast.addEventListener('close', () => { + toast.remove(); + }); + } + } else { + navigateForm(props); + } + } else { + const resp = await saveSeries(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } + } + + toggleBtnsSubmittingState(false); + }); + } + } + }); +} + +function updateCtas(props) { + const formCtas = props.el.querySelectorAll('.series-creation-form-ctas-panel a'); + + formCtas.forEach((a) => { + if (a.classList.contains('next-button')) { + if (props.currentStep === props.maxStep) { + if (props.response.published) { + a.textContent = a.dataset.republishStateText; + } else { + a.textContent = a.dataset.finalStateText; + a.prepend(getIcon('golden-rocket')); + } + } else { + a.textContent = a.dataset.finalStateText; + a.prepend(getIcon('golden-rocket')); + } + } + }); +} + +function initNavigation(props) { + const frags = props.el.querySelectorAll('.fragment'); + const sideMenu = props.el.querySelector('.side-menu'); + const navItems = sideMenu.querySelectorAll('.nav-item'); + + frags.forEach((frag, i) => { + if (i !== 0) { + frag.classList.add('hidden'); + } + }); + + navItems.forEach((nav, i) => { + nav.addEventListener('click', async () => { + if (nav.closest('li').classList.contains('active')) return; + if (!nav.disabled && !sideMenu.classList.contains('disabled')) { + sideMenu.classList.add('disabled'); + + const resp = await saveSeries(props); + if (resp?.error) { + buildErrorMessage(props, resp); + } else { + navigateForm(props, i); + } + + sideMenu.classList.remove('disabled'); + } + }); + }); +} + +function initDeepLink(props) { + const { hash } = window.location; + + if (hash) { + const frags = props.el.querySelectorAll('.fragment'); + + const targetFragindex = Array.from(frags).findIndex((frag) => `#${frag.id}` === hash); + + if (targetFragindex && targetFragindex <= props.farthestStep) { + navigateForm(props, targetFragindex); + } + } +} + +function updateStatusTag(props) { + const { response } = props; + + if (response?.published === undefined) return; + + const currentFragment = getCurrentFragment(props); + + const headingSection = currentFragment.querySelector(':scope > .section:first-child'); + + const eixstingStatusTag = headingSection.querySelector('.status-tag'); + if (eixstingStatusTag) eixstingStatusTag.remove(); + + const heading = headingSection.querySelector('h2', 'h3', 'h3', 'h4'); + const headingWrapper = createTag('div', { class: 'step-heading-wrapper' }); + const dot = response.published ? getIcon('dot-purple') : getIcon('dot-green'); + const text = response.published ? 'Published' : 'Draft'; + const statusTag = createTag('span', { class: 'status-tag' }); + + statusTag.append(dot, text); + heading.parentElement?.replaceChild(headingWrapper, heading); + headingWrapper.append(heading, statusTag); +} + +async function buildForm(el) { + const props = { + el, + currentStep: 0, + farthestStep: 0, + maxStep: el.querySelectorAll('.fragment').length - 1, + payload: {}, + response: {}, + }; + + const dataHandler = { + set(target, prop, value) { + const oldValue = target[prop]; + target[prop] = value; + + if (prop.startsWith('required-fields-in-')) { + initRequiredFieldsValidation(target); + } + + switch (prop) { + case 'currentStep': + { + renderFormNavigation(target, oldValue, value); + updateSideNav(target); + initRequiredFieldsValidation(target); + updateStatusTag(target); + break; + } + + case 'farthestStep': { + updateSideNav(target); + break; + } + + case 'payload': { + setPayloadCache(value); + updateComponentsOnPayloadChange(target); + initRequiredFieldsValidation(target); + validatePublishFields(target); + break; + } + + case 'response': { + setResponseCache(value); + updateComponentsOnRespChange(target); + updateCtas(target); + if (value.error) { + props.el.classList.add('show-error'); + } else { + props.el.classList.remove('show-error'); + } + break; + } + + default: + break; + } + + return true; + }, + }; + + const proxyProps = new Proxy(props, dataHandler); + + decorateForm(el); + + const frags = el.querySelectorAll('.fragment'); + + frags.forEach((frag) => { + props[`required-fields-in-${frag.id}`] = []; + + frag.querySelectorAll(':scope > .section > .content').forEach((c) => { + generateToolTip(c); + }); + }); + + await loadData(proxyProps); + initFormCtas(proxyProps); + initNavigation(proxyProps); + await initComponents(proxyProps); + validatePublishFields(proxyProps); + updateRequiredFields(proxyProps); + enableSideNavForEditFlow(proxyProps); + initDeepLink(proxyProps); + updateStatusTag(proxyProps); + + el.addEventListener('show-error-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + buildErrorMessage(proxyProps, e.detail); + }); + + el.addEventListener('show-success-toast', (e) => { + e.stopPropagation(); + e.preventDefault(); + showSaveSuccessMessage(proxyProps, e.detail); + }); +} + +function buildLoadingScreen(el) { + el.classList.add('loading'); + const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); + createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading Adobe event series creation form...', { parent: loadingScreen }); + + el.prepend(loadingScreen); +} + +export default async function init(el) { + buildLoadingScreen(el); + const miloLibs = LIBS; + const promises = Array.from(SPECTRUM_COMPONENTS).map(async (component) => { + await import(`${miloLibs}/features/spectrum-web-components/dist/${component}.js`); + }); + await Promise.all([ + import(`${miloLibs}/deps/lit-all.min.js`), + ...promises, + ]); + + const devToken = getDevToken(); + if (devToken && getEventServiceEnv() === 'local') { + buildForm(el).then(() => { + el.classList.remove('loading'); + }); + return; + } + + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { + buildNoAccessScreen(el); + el.classList.remove('loading'); + }, + validProfile: () => { + buildForm(el).then(() => { + el.classList.remove('loading'); + }); + }, + }); +} From cc5ea4086d66f86a7a09173bada68692cefabe12 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Mon, 9 Dec 2024 19:45:59 -0600 Subject: [PATCH 49/74] Update series-creation-form.js --- ecc/blocks/series-creation-form/series-creation-form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 662cd4f7..b3961782 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -12,7 +12,7 @@ import { createSeries, updateSeries, publishSeries, - getSeries, + getSeriesById, } from '../../scripts/esp-controller.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { initProfileLogicTree } from '../../scripts/event-apis.js'; @@ -219,7 +219,7 @@ async function loadData(props) { }, 5000); props.el.classList.add('disabled'); - const data = await getSeries(seriesId); + const data = await getSeriesById(seriesId); props.response = { ...props.response, ...data }; props.el.classList.remove('disabled'); } From c5997cdc3125a24aa9ba7051f2058b85665e4d72 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 09:47:18 -0600 Subject: [PATCH 50/74] remove POC block --- ecc/blocks/series-console/series-console.css | 127 -------- ecc/blocks/series-console/series-console.js | 294 ------------------- 2 files changed, 421 deletions(-) delete mode 100644 ecc/blocks/series-console/series-console.css delete mode 100644 ecc/blocks/series-console/series-console.js diff --git a/ecc/blocks/series-console/series-console.css b/ecc/blocks/series-console/series-console.css deleted file mode 100644 index 68637376..00000000 --- a/ecc/blocks/series-console/series-console.css +++ /dev/null @@ -1,127 +0,0 @@ -.series-console { - width: var(--grid-container-width); - margin: 40px auto; - font-family: var(--body-font-family); -} - -.series-console.loading { - opacity: 0.5; - pointer-events: none; -} - -.series-console .new-series-form { - padding: 0 0 20px; - display: flex; - align-items: center; - gap: 1rem; -} - -.series-console label { - font-size: var(--type-body-xs-size); - width: max-content; -} - -.series-console input, -.series-console select { - font-family: var(--body-font-family); - padding: 8px; - border: none; - border-bottom: 1px solid var(--color-black); - flex-grow: 1; - max-width: 240px; -} - -.series-console input:disabled { - cursor: not-allowed; - border-bottom: 1px solid var(--color-gray-300); - background: none; - color: var(--color-black); -} - -.series-console a { - cursor: pointer; - text-decoration: none; -} - -.series-console .series-info-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 2rem; -} - -.series-console a:not(:any-link):not(.con-button) { - color: var(--color-primary); -} - -.series-console .series-info-wrapper { - padding: 16px; - border: 1px solid var(--color-gray-300); - border-radius: 8px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 1rem; -} - -.series-console .series-info-wrapper .actions-wrapper { - display: flex; - gap: 1rem; -} - -.series-console .field-wrapper { - max-width: 100%; - width: 100%; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 1rem; -} - -.series-console .preview-list-overlay { - position: fixed; - display: flex; - align-items: center; - justify-content: center; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgb(0 0 0 / 40%); - z-index: 1; -} - -.series-console .preview-list-modal { - position: relative; - background-color: var(--color-white); - padding: 40px; - border-radius: 24px; - max-height: 80%; - width: 800px; - max-width: 80%; -} - -.series-console .preview-list-modal img { - max-height: 600px; -} - -.series-console .preview-list-overlay.hidden { - display: none; -} - -.series-console .preview-list-items { - max-height: 600px; - display: grid; - overflow: auto; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 20px; -} - -.series-console sp-theme.toast-area { - position: fixed; - right: calc((100% - var(--grid-container-width)) / 2); - bottom: 67px; - display: flex; - flex-direction: column; - gap: 16px; - z-index: 9; -} diff --git a/ecc/blocks/series-console/series-console.js b/ecc/blocks/series-console/series-console.js deleted file mode 100644 index a4166109..00000000 --- a/ecc/blocks/series-console/series-console.js +++ /dev/null @@ -1,294 +0,0 @@ -import { createSeries, deleteSeries, getAllSeries, updateSeries } from '../../scripts/esp-controller.js'; -import { LIBS } from '../../scripts/scripts.js'; - -const { createTag } = await import(`${LIBS}/utils/utils.js`); - -const ATTR_MAP = { - seriesId: { - handle: 'series-id', - label: 'Series ID', - type: 'string', - readonly: true, - }, - seriesName: { - handle: 'series-name', - label: 'Series Name', - type: 'string', - readonly: false, - }, - externalThemeId: { - handle: 'external-theme-id', - label: 'External Theme ID', - type: 'string', - readonly: true, - }, - cloudType: { - handle: 'cloud-type', - label: 'Cloud Type', - type: 'list', - readonly: false, - staticOptions: ['CreativeCloud', 'DX'], - }, - templateId: { - handle: 'template-id', - label: 'Events Template', - type: 'preview-list', - readonly: false, - optionsSource: '/ecc/system/series-templates.json', - }, - relatedDomain: { - handle: 'related-domain', - label: 'Related Domain', - type: 'string', - readonly: false, - }, - emailTemplate: { - handle: 'email-template', - label: 'Email Template', - type: 'string', - readonly: false, - }, - modificationTime: { - handle: 'modification-time', - label: 'Last Modified', - type: 'timestamp', - readonly: true, - }, - creationTime: { - handle: 'creation-time', - label: 'Creation Time', - type: 'timestamp', - readonly: true, - }, -}; - -function buildSeriesInfoWrapper(props) { - const seriesInfoContainer = createTag('div', { class: 'series-info-container' }); - props.el.append(seriesInfoContainer); -} - -function showToast(props, msg, options = {}) { - const toastArea = props.el.querySelector('sp-theme.toast-area'); - const toast = createTag('sp-toast', { open: true, ...options }, msg, { parent: toastArea }); - toast.addEventListener('close', () => { - toast.remove(); - }); -} - -async function buildPreviewListOptionsFromSource(previewList, source, attr) { - const valueHolder = previewList.closest('.series-info-wrapper').querySelector(`.${attr.handle}-input`); - const previewListItems = previewList.querySelector('.preview-list-items'); - const previewListOverlay = previewList.querySelector('.preview-list-overlay'); - - const jsonResp = await fetch(source).then((res) => { - if (!res.ok) throw new Error('Failed to fetch series templates'); - return res.json(); - }); - - const options = jsonResp.data; - if (!options) return; - - if (options.length > 3) { - previewListItems.classList.add('show-3'); - } else { - previewListItems.classList.remove('show-3'); - } - - options.forEach((option) => { - const previewListItem = createTag('div', { class: 'preview-list-item' }); - const previewListItemImage = createTag('img', { src: option['template-image'] }); - const previewListItemTitle = createTag('h5', {}, option['template-name']); - const selectItemBtn = createTag('a', { class: 'con-button blue select-item-btn' }, 'Select', { parent: previewListItem }); - previewListItem.append(previewListItemImage, previewListItemTitle, selectItemBtn); - previewListItems.append(previewListItem); - - selectItemBtn.addEventListener('click', () => { - valueHolder.value = option['template-path']; - previewListOverlay.classList.add('hidden'); - }); - }); -} - -function buildPreviewList(attrObj) { - const { optionsSource } = attrObj; - - const previewList = createTag('div', { class: 'preview-list' }); - const previewListTitle = createTag('h4', {}, 'Select a template'); - const previewListItems = createTag('div', { class: 'preview-list-items' }); - const previewListBtn = createTag('a', { class: 'con-button preview-list-btn' }, 'Select'); - const previewListOverlay = createTag('div', { class: 'preview-list-overlay hidden' }); - const previewListModal = createTag('div', { class: 'preview-list-modal' }, '', { parent: previewListOverlay }); - const previewListCloseBtn = createTag('a', { class: 'preview-list-close-btn' }, '✕', { parent: previewListModal }); - - previewListBtn.addEventListener('click', () => { - previewListOverlay.classList.remove('hidden'); - buildPreviewListOptionsFromSource(previewList, optionsSource, attrObj); - }); - - previewListCloseBtn.addEventListener('click', () => { - previewListOverlay.classList.add('hidden'); - }); - - previewListModal.append(previewListTitle, previewListItems); - previewList.append(previewListBtn, previewListOverlay); - return previewList; -} - -function listAllSeries(props) { - const seriesInfoContainer = props.el.querySelector('.series-info-container'); - if (!seriesInfoContainer) return; - seriesInfoContainer.innerHTML = ''; - - props.data.forEach((series) => { - const seriesInfoWrapper = createTag('div', { class: 'series-info-wrapper' }); - - Object.keys(ATTR_MAP).forEach(async (attr) => { - const fieldWrapper = createTag('div', { class: 'field-wrapper' }); - const attrValue = series[attr] || ''; - const attrType = ATTR_MAP[attr].type; - const attrReadonly = ATTR_MAP[attr].readonly; - const attrHandle = ATTR_MAP[attr].handle; - const attrSentence = ATTR_MAP[attr].label; - - if (attrType === 'list') { - const attrOptions = ATTR_MAP[attr].staticOptions || series[attr]; - const attrSelect = createTag('select', { class: `${attrHandle}-select` }); - if (attrReadonly) attrSelect.disabled = true; - attrOptions.forEach((option) => { - const opt = createTag('option', { value: option }, option); - attrSelect.append(opt); - }); - - fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrSelect); - } - - if (attrType === 'timestamp') { - const attrInput = createTag('input', { class: `${attrHandle}-input`, value: new Date(attrValue).toLocaleString() }); - attrInput.disabled = true; - fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrInput); - } - - if (attrType === 'preview-list') { - const label = createTag('label', {}, `${attrSentence}:`); - const valueHolder = createTag('input', { class: `${attrHandle}-input`, value: attrValue, disabled: true }); - const previewList = buildPreviewList(ATTR_MAP[attr]); - fieldWrapper.append(label, valueHolder, previewList); - } - - if (attrType === 'string') { - const attrInput = createTag('input', { class: `${attrHandle}-input`, value: attrValue }); - if (attrReadonly) attrInput.disabled = true; - fieldWrapper.append(createTag('label', {}, `${attrSentence}:`), attrInput); - } - - seriesInfoWrapper.append(fieldWrapper); - }); - - const actionsWrapper = createTag('div', { class: 'actions-wrapper' }); - const updateSeriesBtn = createTag('a', { class: 'con-button fill update-series-btn' }, 'Update Series'); - const deleteSeriesBtn = createTag('a', { class: 'con-button fill delete-series-btn' }, 'Delete Series'); - actionsWrapper.append(updateSeriesBtn, deleteSeriesBtn); - seriesInfoWrapper.append(actionsWrapper); - - updateSeriesBtn.addEventListener('click', async (e) => { - e.preventDefault(); - const updatedSeries = {}; - - Object.keys(ATTR_MAP).forEach((attr) => { - const readOnly = ATTR_MAP[attr].readonly; - - if (readOnly) return; - - const attrType = ATTR_MAP[attr].type; - const attrHandle = ATTR_MAP[attr].handle; - - if (attrType === 'list') { - const attrSelect = seriesInfoWrapper.querySelector(`.${attrHandle}-select`); - if (attrSelect && attrSelect.value) updatedSeries[attr] = attrSelect.value; - } else { - const attrInput = seriesInfoWrapper.querySelector(`.${attrHandle}-input`); - if (attrInput && attrInput.value) updatedSeries[attr] = attrInput.value; - } - }); - - props.el.classList.add('loading'); - const resp = await updateSeries( - { ...updatedSeries, modificationTime: series.modificationTime }, - series.seriesId, - ); - - if (!resp.error) { - props.data = await getAllSeries(); - showToast(props, 'Series updated', { variant: 'positive', timeout: 6000 }); - } else { - showToast(props, 'Update action failed. Please try again later.', { variant: 'negative', timeout: 6000 }); - } - props.el.classList.remove('loading'); - }); - - deleteSeriesBtn.addEventListener('click', async (e) => { - e.preventDefault(); - props.el.classList.add('loading'); - const { seriesId } = series; - const resp = await deleteSeries(seriesId); - - if (!resp.error) { - props.data = await getAllSeries(); - showToast(props, 'Series deleted', { variant: 'positive', timeout: 6000 }); - } else { - showToast(props, 'Delete action failed. Please try again later.', { variant: 'negative', timeout: 6000 }); - } - props.el.classList.remove('loading'); - }); - - seriesInfoContainer.append(seriesInfoWrapper); - }); -} - -function buildNewSeriesForm(props) { - const newSeriesForm = createTag('div', { class: 'new-series-form' }); - const newSeriesNameInput = createTag('input', { class: 'new-series-name-input', placeholder: 'Enter new series name' }, '', { parent: newSeriesForm }); - const createNewSeriesBtn = createTag('a', { class: 'con-button fill create-new-series-btn' }, 'Create New Series', { parent: newSeriesForm }); - - createNewSeriesBtn.addEventListener('click', async () => { - const newSeriesName = newSeriesNameInput.value; - if (!newSeriesName) return; - - const newSeries = await createSeries({ seriesName: newSeriesName }); - if (!newSeries) return; - - props.data = await getAllSeries(); - }); - - props.el.prepend(newSeriesForm); -} - -export default async function init(el) { - const miloLibs = LIBS; - await Promise.all([ - import(`${miloLibs}/deps/lit-all.min.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/theme.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/toast.js`), - ]); - createTag('sp-theme', { color: 'light', scale: 'medium', class: 'toast-area' }, '', { parent: el }); - - const allSeries = await getAllSeries(); - const props = { - el, - data: allSeries, - }; - - buildSeriesInfoWrapper(props); - - const dataHandler = { - set(target, prop, value, receiver) { - target[prop] = value; - listAllSeries(receiver); - return true; - }, - }; - - const proxyProps = new Proxy(props, dataHandler); - buildNewSeriesForm(proxyProps); - listAllSeries(proxyProps); -} From 2375c3e580096edcd43706966eb9c685dab457b8 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 10:27:59 -0600 Subject: [PATCH 51/74] series creation form resume working now --- .../controller.js | 2 +- .../series-creation-form.js | 12 --------- .../series-details-component/controller.js | 22 +++++++++------ .../series-templates-component/controller.js | 20 ++++++++++++++ ecc/samples/sample-form/sample-form.js | 27 +++++++------------ 5 files changed, 44 insertions(+), 39 deletions(-) diff --git a/ecc/blocks/series-additional-info-component/controller.js b/ecc/blocks/series-additional-info-component/controller.js index 4cabcca7..16398693 100644 --- a/ecc/blocks/series-additional-info-component/controller.js +++ b/ecc/blocks/series-additional-info-component/controller.js @@ -24,7 +24,7 @@ export async function onRespUpdate(_component, _props) { } export default function init(component, props) { - const data = props.resp; + const data = props.response; if (data) { const susiContextId = component.querySelector('#info-field-series-susi'); diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index b3961782..0f13924e 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -206,18 +206,6 @@ async function loadData(props) { const seriesId = urlParams.get('seriesId'); if (seriesId) { - setTimeout(() => { - if (!props.response.seriesId) { - const toastArea = props.el.querySelector('.toast-area'); - if (!toastArea) return; - - const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the seriesId URL Param is valid.', { parent: toastArea }); - toast.addEventListener('close', () => { - toast.remove(); - }); - } - }, 5000); - props.el.classList.add('disabled'); const data = await getSeriesById(seriesId); props.response = { ...props.response, ...data }; diff --git a/ecc/blocks/series-details-component/controller.js b/ecc/blocks/series-details-component/controller.js index 65ade393..5ee40aeb 100644 --- a/ecc/blocks/series-details-component/controller.js +++ b/ecc/blocks/series-details-component/controller.js @@ -24,16 +24,22 @@ export async function onRespUpdate(_component, _props) { } export default function init(component, props) { - const data = props.resp; + const data = props.response; if (data) { - const cloudType = component.querySelector('#bu-select-input'); - const seriesName = component.querySelector('#info-field-series-name'); - const seriesDescription = component.querySelector('#info-field-series-description'); - - cloudType.value = data.cloudType; - seriesName.value = data.seriesName; - seriesDescription.value = data.seriesDescription; + const { + cloudType, + seriesName, + seriesDescription, + } = data; + + const cloudTypeEl = component.querySelector('#bu-select-input'); + const seriesNameEl = component.querySelector('#info-field-series-name'); + const seriesDescriptionEl = component.querySelector('#info-field-series-description'); + + if (cloudType) cloudTypeEl.value = cloudType; + if (seriesName) seriesNameEl.value = seriesName; + if (seriesDescription) seriesDescriptionEl.value = seriesDescription; component.classList.add('prefilled'); } diff --git a/ecc/blocks/series-templates-component/controller.js b/ecc/blocks/series-templates-component/controller.js index f3385745..bcfd936c 100644 --- a/ecc/blocks/series-templates-component/controller.js +++ b/ecc/blocks/series-templates-component/controller.js @@ -161,11 +161,31 @@ function initPicker(component) { export default async function init(component, props) { const picker = component.querySelector('.picker'); + const data = props.response; + if (!picker) return; await buildPreviewListOptionsFromSource(component, picker.getAttribute('data-source-link')); initPicker(component); + + if (data) { + const { templateId } = data; + + if (templateId) { + const selectedRadio = component.querySelector(`input[type='radio'][value="${templateId}"]`); + const valueInput = picker.querySelector('input.series-template-input'); + const nameInput = picker.querySelector('sp-textfield.series-template-name'); + + if (selectedRadio) { + picker.classList.add('selected'); + selectedRadio.checked = true; + selectedRadio.dispatchEvent(new Event('change')); + valueInput.value = templateId; + nameInput.value = selectedRadio.parentElement?.textContent; + } + } + } } export function onTargetUpdate(component, props) { diff --git a/ecc/samples/sample-form/sample-form.js b/ecc/samples/sample-form/sample-form.js index 8bc49469..ad680f4e 100644 --- a/ecc/samples/sample-form/sample-form.js +++ b/ecc/samples/sample-form/sample-form.js @@ -213,26 +213,17 @@ async function initCustomLitComponents() { async function loadData(props) { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); - const seriesId = urlParams.get('seriesId'); + // use more specific query param if available + const id = urlParams.get('id'); - if (seriesId) { - setTimeout(() => { - if (!props.response.seriesId) { - const toastArea = props.el.querySelector('.toast-area'); - if (!toastArea) return; + if (!id) return; + + // fetch data to prefill the form - const toast = createTag('sp-toast', { open: true, timeout: 10000 }, 'Event data is taking longer than usual to load. Please check if the Adobe corp. VPN is connected or if the seriesId URL Param is valid.', { parent: toastArea }); - toast.addEventListener('close', () => { - toast.remove(); - }); - } - }, 5000); - - props.el.classList.add('disabled'); - const data = await getSeries(seriesId); - props.response = { ...props.response, ...data }; - props.el.classList.remove('disabled'); - } + // props.el.classList.add('disabled'); + // const data = await getSeries(id); + // props.response = { ...props.response, ...data }; + // props.el.classList.remove('disabled'); } async function initComponents(props) { From 7ab03edd88d7540dd3f91030f8b9a9c2b6ebcb9e Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 10:30:46 -0600 Subject: [PATCH 52/74] Update series-creation-form.js --- ecc/blocks/series-creation-form/series-creation-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 0f13924e..dbf4ad92 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -409,7 +409,7 @@ async function saveSeries(props, toPublish = false) { } }; - if (props.currentStep === 0 && !getFilteredCachedResponse().seriesId) { + if (!getFilteredCachedResponse().seriesId) { resp = await createSeries(quickFilter(props.payload)); props.response = { ...props.response, ...resp }; updateDashboardLink(props); From fc2d268719dbe3a30ed1844fb86d10f45cbe672c Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 10:34:00 -0600 Subject: [PATCH 53/74] Update series-creation-form.js --- ecc/blocks/series-creation-form/series-creation-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index dbf4ad92..ded66cfe 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -414,7 +414,7 @@ async function saveSeries(props, toPublish = false) { props.response = { ...props.response, ...resp }; updateDashboardLink(props); await onSeriesSave(); - } else if (props.currentStep <= props.maxStep && !toPublish) { + } else if (!toPublish) { resp = await updateSeries( getFilteredCachedResponse().seriesId, getJoinedData(), From d3892176224c886e7d9e672ccbda3988ce70e042 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 14:23:28 -0600 Subject: [PATCH 54/74] update working --- .../event-creation-form.js | 24 +++++++++---------- .../series-creation-form.js | 24 +++++++++---------- .../series-dashboard/series-dashboard.js | 3 ++- ecc/scripts/esp-controller.js | 23 +++++++++++++++++- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index af02d337..d0337f17 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -431,16 +431,16 @@ function updateDashboardLink(props) { dashboardLink.href = url.toString(); } -async function saveEvent(props, toPublish = false) { +async function save(props, toPublish = false) { try { await gatherValues(props); } catch (e) { return { error: { message: e.message } }; } - let resp; + let resp = props.response; - const onEventSave = async () => { + const onSave = async () => { if (resp?.eventId) await handleEventUpdate(props); if (!resp.error) { @@ -448,18 +448,18 @@ async function saveEvent(props, toPublish = false) { } }; - if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { + if (!resp.eventId) { resp = await createEvent(quickFilter(props.payload)); props.eventDataResp = { ...props.eventDataResp, ...resp }; updateDashboardLink(props); - await onEventSave(); - } else if (props.currentStep <= props.maxStep && !toPublish) { + await onSave(); + } else if (!toPublish) { resp = await updateEvent( getFilteredCachedResponse().eventId, getJoinedData(), ); props.eventDataResp = { ...props.eventDataResp, ...resp }; - await onEventSave(); + await onSave(); } else if (toPublish) { resp = await publishEvent( getFilteredCachedResponse().eventId, @@ -775,10 +775,10 @@ function initFormCtas(props) { let resp; if (props.currentStep === props.maxStep) { oldResp = { ...props.eventDataResp }; - resp = await saveEvent(props, true); + resp = await save(props, true); } else { oldResp = { ...props.eventDataResp }; - resp = await saveEvent(props); + resp = await save(props); } if (resp?.error) { @@ -813,7 +813,7 @@ function initFormCtas(props) { } } else { oldResp = { ...props.eventDataResp }; - const resp = await saveEvent(props); + const resp = await save(props); if (resp?.error) { buildErrorMessage(props, resp); } @@ -828,7 +828,7 @@ function initFormCtas(props) { backBtn.addEventListener('click', async () => { toggleBtnsSubmittingState(true); oldResp = { ...props.eventDataResp }; - const resp = await saveEvent(props); + const resp = await save(props); if (resp?.error) { buildErrorMessage(props, resp); } else { @@ -885,7 +885,7 @@ function initNavigation(props) { if (!nav.disabled && !sideMenu.classList.contains('disabled')) { sideMenu.classList.add('disabled'); - const resp = await saveEvent(props); + const resp = await save(props); if (resp?.error) { buildErrorMessage(props, resp); } else { diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index ded66cfe..efa331dc 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -392,16 +392,16 @@ function updateDashboardLink(props) { dashboardLink.href = url.toString(); } -async function saveSeries(props, toPublish = false) { +async function save(props, toPublish = false) { try { await gatherValues(props); } catch (e) { return { error: { message: e.message } }; } - let resp; + let resp = props.response; - const onSeriesSave = async () => { + const onSave = async () => { if (resp?.seriesId) await handleSeriesUpdate(props); if (!resp.error) { @@ -409,21 +409,21 @@ async function saveSeries(props, toPublish = false) { } }; - if (!getFilteredCachedResponse().seriesId) { + if (!resp.seriesId) { resp = await createSeries(quickFilter(props.payload)); props.response = { ...props.response, ...resp }; updateDashboardLink(props); - await onSeriesSave(); + await onSave(); } else if (!toPublish) { resp = await updateSeries( - getFilteredCachedResponse().seriesId, + resp.seriesId, getJoinedData(), ); props.response = { ...props.response, ...resp }; - await onSeriesSave(); + await onSave(); } else if (toPublish) { resp = await publishSeries( - getFilteredCachedResponse().seriesId, + resp.seriesId, getJoinedData(), ); props.response = { ...props.response, ...resp }; @@ -534,9 +534,9 @@ function initFormCtas(props) { if (ctaUrl.hash === '#next') { let resp; if (props.currentStep === props.maxStep) { - resp = await saveSeries(props, true); + resp = await save(props, true); } else { - resp = await saveSeries(props); + resp = await save(props); } if (resp?.error) { @@ -570,7 +570,7 @@ function initFormCtas(props) { navigateForm(props); } } else { - const resp = await saveSeries(props); + const resp = await save(props); if (resp?.error) { buildErrorMessage(props, resp); } @@ -620,7 +620,7 @@ function initNavigation(props) { if (!nav.disabled && !sideMenu.classList.contains('disabled')) { sideMenu.classList.add('disabled'); - const resp = await saveSeries(props); + const resp = await save(props); if (resp?.error) { buildErrorMessage(props, resp); } else { diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index ea98051c..b9822272 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -5,6 +5,7 @@ import { unpublishSeries, archiveSeries, getEvents, + deleteSeries, } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; import { @@ -253,7 +254,7 @@ function initMoreOptions(props, config, seriesObj, row) { underlay.open = false; dialog.innerHTML = ''; row.classList.add('pending'); - const resp = await archiveSeries(seriesObj.seriesId); + const resp = await deleteSeries(seriesObj.seriesId); if (resp.error) { row.classList.remove('pending'); diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 6cbf7341..e2cb7f9c 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -829,7 +829,7 @@ export async function createSeries(seriesData) { } } -export async function updateSeries(seriesData, seriesId) { +export async function updateSeries(seriesId, seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify({ ...seriesData, seriesId }); const options = await constructRequestOptions('PUT', raw); @@ -913,6 +913,27 @@ export async function archiveSeries(seriesId, seriesData) { } } +export async function deleteSeries(seriesId) { + const { host } = API_CONFIG.esp[getEventServiceEnv()]; + const options = await constructRequestOptions('DELETE'); + + try { + const response = await fetch(`${host}/v1/series/${seriesId}`, options); + + if (!response.ok) { + const data = await response.json(); + window.lana?.log(`Failed to delete series ${seriesId}. Status:`, response.status, 'Error:', data); + return { status: response.status, error: data }; + } + + // 204 no content. Return true if no error. + return true; + } catch (error) { + window.lana?.log(`Failed to delete series ${seriesId}. Error:`, error); + return { status: 'Network Error', error: error.message }; + } +} + export async function createAttendee(eventId, attendeeData) { if (!eventId || !attendeeData) return false; From dcad5ffdebfff656543a2ec85bfe025675117c79 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 14:38:30 -0600 Subject: [PATCH 55/74] Update profile.js --- ecc/scripts/profile.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index a23725f6..61824aa5 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -87,19 +87,40 @@ export async function getUser() { export function userHasAccessToBU(user, bu) { if (!user) return false; - const businessUnits = user['business-units'].split(',').map((b) => b.trim()); + + const userBU = user['business-units']; + + if (!userBU) return false; + + if (userBU === 'all') return true; + + const businessUnits = userBU.split(',').map((b) => b.trim()); return businessUnits.length === 0 || businessUnits.includes(bu); } export function userHasAccessToSeries(user, seriesId) { if (!user) return false; - const series = user.series.split(',').map((b) => b.trim()); + + const userSeries = user.series; + + if (!userSeries) return false; + + if (userSeries === 'all') return true; + + const series = userSeries.split(',').map((b) => b.trim()); return series.length === 0 || series.includes(seriesId); } export function userHasAccessToEvent(user, eventId) { if (!user) return false; - const events = user.events.split(',').map((b) => b.trim()); + + const userEvents = user.events; + + if (!userEvents) return false; + + if (userEvents === 'all') return true; + + const events = userEvents.split(',').map((b) => b.trim()); return events.length === 0 || events.includes(eventId); } From 9e3e49d54c443712ef6f666bbeb027972478c80a Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 14:59:31 -0600 Subject: [PATCH 56/74] corrected archive language --- ecc/blocks/series-dashboard/series-dashboard.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index b9822272..71182f1c 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -5,7 +5,6 @@ import { unpublishSeries, archiveSeries, getEvents, - deleteSeries, } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; import { @@ -241,20 +240,20 @@ function initMoreOptions(props, config, seriesObj, row) { const underlay = spTheme.querySelector('sp-underlay'); const dialog = spTheme.querySelector('sp-dialog'); - createTag('h1', { slot: 'heading' }, 'You are deleting this series.', { parent: dialog }); + createTag('h1', { slot: 'heading' }, 'You are archiving this series.', { parent: dialog }); createTag('p', {}, 'Are you sure you want to do this? This cannot be undone.', { parent: dialog }); const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); - const dialogDeleteBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to delete this series', { parent: buttonContainer }); - const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not delete', { parent: buttonContainer }); + const dialogArchiveBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to archive this series', { parent: buttonContainer }); + const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not archive', { parent: buttonContainer }); underlay.open = true; - dialogDeleteBtn.addEventListener('click', async () => { + dialogArchiveBtn.addEventListener('click', async () => { toolBox.remove(); underlay.open = false; dialog.innerHTML = ''; row.classList.add('pending'); - const resp = await deleteSeries(seriesObj.seriesId); + const resp = await archiveSeries(seriesObj.seriesId); if (resp.error) { row.classList.remove('pending'); From 8e8fa18f18e34e435b7b29ccc88251403fbc1826 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 15:13:35 -0600 Subject: [PATCH 57/74] lint error fix --- .../attendee-management-table/attendee-management-table.js | 4 ++-- ecc/blocks/event-creation-form/event-creation-form.js | 2 +- ecc/blocks/series-creation-form/series-creation-form.js | 2 +- ecc/samples/sample-form/sample-form.js | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index 402e242a..e57bd229 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -10,8 +10,8 @@ import { getEventServiceEnv, getDevToken, } from '../../scripts/utils.js'; -import { SearchablePicker } from '../../components/searchable-picker/searchable-picker.js'; -import { FilterMenu } from '../../components/filter-menu/filter-menu.js'; +import SearchablePicker from '../../components/searchable-picker/searchable-picker.js'; +import FilterMenu from '../../components/filter-menu/filter-menu.js'; import { getUser, initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index d0337f17..09b38a8e 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -28,7 +28,7 @@ import PartnerSelector from '../../components/partner-selector/partner-selector. import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; import CustomSearch from '../../components/custom-search/custom-search.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index efa331dc..484abdcd 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -15,7 +15,7 @@ import { getSeriesById, } from '../../scripts/esp-controller.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); diff --git a/ecc/samples/sample-form/sample-form.js b/ecc/samples/sample-form/sample-form.js index ad680f4e..07da2b1d 100644 --- a/ecc/samples/sample-form/sample-form.js +++ b/ecc/samples/sample-form/sample-form.js @@ -12,10 +12,9 @@ import { createSeries, updateSeries, publishSeries, - getSeries, } from '../../scripts/esp-controller.js'; import getJoinedData, { getFilteredCachedResponse, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; -import { initProfileLogicTree } from '../../scripts/event-apis.js'; +import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); const { decorateButtons } = await import(`${LIBS}/utils/decorate.js`); From 92159c3ded482c8564de4fe692bc4b1542bddc45 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 15:31:03 -0600 Subject: [PATCH 58/74] added block level user access control --- .../attendee-management-table.js | 2 +- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 2 +- .../event-creation-form.js | 2 +- ecc/blocks/form-handler/form-handler.js | 2 +- .../series-dashboard/series-dashboard.js | 2 +- ecc/scripts/profile.js | 42 ++++++++++++++++++- 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index e57bd229..a35ea821 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -717,7 +717,7 @@ export default async function init(el) { return; } - await initProfileLogicTree({ + await initProfileLogicTree('attendee-management-table', { noProfile: () => { signIn(); }, diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index 18e0c944..f9e22cac 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -729,7 +729,7 @@ export default async function init(el) { return; } - await initProfileLogicTree({ + await initProfileLogicTree('events-dashboard', { noProfile: () => { signIn(); }, diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index 09b38a8e..e8eff792 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -1060,7 +1060,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + initProfileLogicTree('event-creation-form', { noProfile: () => { signIn(); }, diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 21d98b48..981311c0 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -1062,7 +1062,7 @@ export default async function init(el) { return; } - await initProfileLogicTree({ + await initProfileLogicTree('event-creation-form', { noProfile: () => { signIn(); }, diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 28b16ebd..f0c30d12 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -625,7 +625,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + initProfileLogicTree('series-dashboard', { noProfile: () => { signIn(); }, diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 61824aa5..33dd11d9 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -124,7 +124,45 @@ export function userHasAccessToEvent(user, eventId) { return events.length === 0 || events.includes(eventId); } -export async function initProfileLogicTree(callbacks) { +export function userHasAccessToView(user, blockName) { + const { role } = user; + const managerAccess = [ + 'events-dashboard', + 'event-creation-form', + 'series-dashboard', + 'series-creation-form', + ]; + + const creatorAccess = [ + 'events-dashboard', + 'event-creation-form', + ]; + + const editorAccess = [ + 'events-dashboard', + 'event-creation-form', + ]; + + if (!role) return false; + + if (role === 'admin') return true; + + if (role === 'manager') { + return managerAccess.includes(blockName); + } + + if (role === 'creator') { + return creatorAccess.includes(blockName); + } + + if (role === 'editor') { + return editorAccess.includes(blockName); + } + + return false; +} + +export async function initProfileLogicTree(blockName, callbacks) { const { noProfile, noAccessProfile, validProfile } = callbacks; const profile = BlockMediator.get('imsProfile'); @@ -134,7 +172,7 @@ export async function initProfileLogicTree(callbacks) { user = await getUser(); if (profile.noProfile) { noProfile(); - } else if (!user) { + } else if (!user || !userHasAccessToView(user, blockName)) { noAccessProfile(); } else { validProfile(profile); From 2dc3521adeb7dedbc4b55adab7d14ff928f2de66 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 15:33:26 -0600 Subject: [PATCH 59/74] Update profile.js --- ecc/scripts/profile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 33dd11d9..cd4ca637 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -92,7 +92,7 @@ export function userHasAccessToBU(user, bu) { if (!userBU) return false; - if (userBU === 'all') return true; + if (userBU.toLowerCase() === 'all') return true; const businessUnits = userBU.split(',').map((b) => b.trim()); return businessUnits.length === 0 || businessUnits.includes(bu); @@ -105,7 +105,7 @@ export function userHasAccessToSeries(user, seriesId) { if (!userSeries) return false; - if (userSeries === 'all') return true; + if (userSeries.toLowerCase() === 'all') return true; const series = userSeries.split(',').map((b) => b.trim()); return series.length === 0 || series.includes(seriesId); @@ -118,7 +118,7 @@ export function userHasAccessToEvent(user, eventId) { if (!userEvents) return false; - if (userEvents === 'all') return true; + if (userEvents.toLowerCase() === 'all') return true; const events = userEvents.split(',').map((b) => b.trim()); return events.length === 0 || events.includes(eventId); From 16708a6a5c0c6a245ccf9c3edc337a93ab6622ea Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 15:39:54 -0600 Subject: [PATCH 60/74] debugging --- ecc/blocks/series-creation-form/series-creation-form.js | 2 +- ecc/samples/sample-form/sample-form.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index 484abdcd..5a19a1cd 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -797,7 +797,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + initProfileLogicTree('series-creation-form', { noProfile: () => { signIn(); }, diff --git a/ecc/samples/sample-form/sample-form.js b/ecc/samples/sample-form/sample-form.js index 07da2b1d..180fc31a 100644 --- a/ecc/samples/sample-form/sample-form.js +++ b/ecc/samples/sample-form/sample-form.js @@ -809,7 +809,7 @@ export default async function init(el) { return; } - initProfileLogicTree({ + initProfileLogicTree('sample-form', { noProfile: () => { signIn(); }, From 932a5cde1676fb14b1dfde08570322dc12824617 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 16:04:36 -0600 Subject: [PATCH 61/74] small refactor --- .../event-creation-form/event-creation-form.js | 18 +++++++++--------- .../series-creation-form.js | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index d0337f17..3cb3f620 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -440,26 +440,26 @@ async function save(props, toPublish = false) { let resp = props.response; - const onSave = async () => { + if (!resp.eventId) { + resp = await createEvent(quickFilter(props.payload)); + props.eventDataResp = { ...props.eventDataResp, ...resp }; + updateDashboardLink(props); if (resp?.eventId) await handleEventUpdate(props); if (!resp.error) { showSaveSuccessMessage(props); } - }; - - if (!resp.eventId) { - resp = await createEvent(quickFilter(props.payload)); - props.eventDataResp = { ...props.eventDataResp, ...resp }; - updateDashboardLink(props); - await onSave(); } else if (!toPublish) { resp = await updateEvent( getFilteredCachedResponse().eventId, getJoinedData(), ); props.eventDataResp = { ...props.eventDataResp, ...resp }; - await onSave(); + if (resp?.eventId) await handleEventUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } } else if (toPublish) { resp = await publishEvent( getFilteredCachedResponse().eventId, diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index efa331dc..cea3ce6d 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -402,25 +402,31 @@ async function save(props, toPublish = false) { let resp = props.response; const onSave = async () => { - if (resp?.seriesId) await handleSeriesUpdate(props); - if (!resp.error) { - showSaveSuccessMessage(props); - } }; if (!resp.seriesId) { resp = await createSeries(quickFilter(props.payload)); props.response = { ...props.response, ...resp }; updateDashboardLink(props); - await onSave(); + + if (resp?.seriesId) await handleSeriesUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } } else if (!toPublish) { resp = await updateSeries( resp.seriesId, getJoinedData(), ); props.response = { ...props.response, ...resp }; - await onSave(); + + if (resp?.seriesId) await handleSeriesUpdate(props); + + if (!resp.error) { + showSaveSuccessMessage(props); + } } else if (toPublish) { resp = await publishSeries( resp.seriesId, From cd10f3302b465f94e67fc3d854abcdae67534781 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 16:11:51 -0600 Subject: [PATCH 62/74] set status to draft on POST --- ecc/scripts/esp-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index e2cb7f9c..ab13c302 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -810,7 +810,7 @@ export async function getSeriesById(seriesId) { export async function createSeries(seriesData) { const { host } = API_CONFIG.esp[getEventServiceEnv()]; - const raw = JSON.stringify(seriesData); + const raw = JSON.stringify({ ...seriesData, seriesStatus: 'draft' }); const options = await constructRequestOptions('POST', raw); try { From 7c51e430ab084327e7f887ceba751dfe75240721 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 11 Dec 2024 20:32:24 -0600 Subject: [PATCH 63/74] No edit for archived series --- .../series-dashboard/series-dashboard.js | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 71182f1c..a3c85cc9 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -161,50 +161,104 @@ function initMoreOptions(props, config, seriesObj, row) { moreOptionIcon.addEventListener('click', () => { const toolBox = createTag('div', { class: 'dashboard-tool-box' }); - if (seriesObj.published) { - const unpub = buildTool(toolBox, 'Unpublish', 'publish-remove'); - if (seriesObj.seriesStatus === 'archived') unpub.classList.add('disabled'); - unpub.addEventListener('click', async (e) => { - e.preventDefault(); - toolBox.remove(); - row.classList.add('pending'); - const resp = await unpublishSeries(seriesObj.seriesId, seriesObj); - updateDashboardData(resp, props); - - sortData(props, config, { resort: true }); - showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['unpublished-msg']), { variant: 'positive' }); - }); - } else { - const pub = buildTool(toolBox, 'Publish', 'publish-rocket'); - if (seriesObj.seriesStatus === 'archived') pub.classList.add('disabled'); - pub.addEventListener('click', async (e) => { - e.preventDefault(); - toolBox.remove(); - row.classList.add('pending'); - const resp = await publishSeries(seriesObj.seriesId, seriesObj); - updateDashboardData(resp, props); - - sortData(props, config, { resort: true }); - - showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['published-msg']), { variant: 'positive' }); - }); + const { seriesStatus } = seriesObj; + + if (seriesStatus && seriesStatus !== 'archived') { + if (seriesStatus === 'published') { + const unpub = buildTool(toolBox, 'Unpublish', 'publish-remove'); + if (seriesObj.seriesStatus === 'archived') unpub.classList.add('disabled'); + unpub.addEventListener('click', async (e) => { + e.preventDefault(); + toolBox.remove(); + row.classList.add('pending'); + const resp = await unpublishSeries(seriesObj.seriesId, seriesObj); + updateDashboardData(resp, props); + + sortData(props, config, { resort: true }); + showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['unpublished-msg']), { variant: 'positive' }); + }); + } else { + const pub = buildTool(toolBox, 'Publish', 'publish-rocket'); + if (seriesObj.seriesStatus === 'archived') pub.classList.add('disabled'); + pub.addEventListener('click', async (e) => { + e.preventDefault(); + toolBox.remove(); + row.classList.add('pending'); + const resp = await publishSeries(seriesObj.seriesId, seriesObj); + updateDashboardData(resp, props); + + sortData(props, config, { resort: true }); + + showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['published-msg']), { variant: 'positive' }); + }); + } } // const viewTemplate = buildTool(toolBox, 'View Template', 'preview-eye'); - const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); + const clone = buildTool(toolBox, 'Clone', 'clone'); - const archive = buildTool(toolBox, 'Archive', 'archive'); - // const verHistory = buildTool(toolBox, 'Version History', 'version-history'); + + if (seriesStatus && seriesStatus !== 'archived') { + const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); - // edit - const url = new URL(`${window.location.origin}${config['create-form-url']}`); - url.searchParams.set('seriesId', seriesObj.seriesId); - edit.href = url.toString(); + const url = new URL(`${window.location.origin}${config['create-form-url']}`); + url.searchParams.set('seriesId', seriesObj.seriesId); + edit.href = url.toString(); + + const archive = buildTool(toolBox, 'Archive', 'archive'); + + archive.addEventListener('click', async (e) => { + e.preventDefault(); + + const spTheme = props.el.querySelector('sp-theme.toast-area'); + if (!spTheme) return; + + const underlay = spTheme.querySelector('sp-underlay'); + const dialog = spTheme.querySelector('sp-dialog'); + createTag('h1', { slot: 'heading' }, 'You are archiving this series.', { parent: dialog }); + createTag('p', {}, 'Are you sure you want to do this? This cannot be undone.', { parent: dialog }); + const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); + const dialogArchiveBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to archive this series', { parent: buttonContainer }); + const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not archive', { parent: buttonContainer }); + + underlay.open = true; + + dialogArchiveBtn.addEventListener('click', async () => { + toolBox.remove(); + underlay.open = false; + dialog.innerHTML = ''; + row.classList.add('pending'); + const resp = await archiveSeries(seriesObj.seriesId); + + if (resp.error) { + row.classList.remove('pending'); + showToast(props, resp.error, { variant: 'negative' }); + return; + } + + const newJson = await getAllSeries(); + props.data = newJson.series; + props.filteredData = newJson.series; + props.paginatedData = newJson.series; + + sortData(props, config, { resort: true }); + showToast(props, config['delete-toast-msg']); + }); + + dialogCancelBtn.addEventListener('click', () => { + toolBox.remove(); + underlay.open = false; + dialog.innerHTML = ''; + }); + }); + } + // const verHistory = buildTool(toolBox, 'Version History', 'version-history'); + // clone clone.addEventListener('click', async (e) => { e.preventDefault(); - const payload = { ...seriesObj }; + const payload = { ...quickFilter(seriesObj), seriesStatus: 'draft' }; payload.title = `${seriesObj.title} - copy`; toolBox.remove(); row.classList.add('pending'); @@ -231,52 +285,6 @@ function initMoreOptions(props, config, seriesObj, row) { showToast(props, buildToastMsgWithEventTitle(newSeriesObj.seriesName, config['clone-toast-msg']), { variant: 'info' }); }); - // archive - archive.addEventListener('click', async (e) => { - e.preventDefault(); - - const spTheme = props.el.querySelector('sp-theme.toast-area'); - if (!spTheme) return; - - const underlay = spTheme.querySelector('sp-underlay'); - const dialog = spTheme.querySelector('sp-dialog'); - createTag('h1', { slot: 'heading' }, 'You are archiving this series.', { parent: dialog }); - createTag('p', {}, 'Are you sure you want to do this? This cannot be undone.', { parent: dialog }); - const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); - const dialogArchiveBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to archive this series', { parent: buttonContainer }); - const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not archive', { parent: buttonContainer }); - - underlay.open = true; - - dialogArchiveBtn.addEventListener('click', async () => { - toolBox.remove(); - underlay.open = false; - dialog.innerHTML = ''; - row.classList.add('pending'); - const resp = await archiveSeries(seriesObj.seriesId); - - if (resp.error) { - row.classList.remove('pending'); - showToast(props, resp.error, { variant: 'negative' }); - return; - } - - const newJson = await getAllSeries(); - props.data = newJson.series; - props.filteredData = newJson.series; - props.paginatedData = newJson.series; - - sortData(props, config, { resort: true }); - showToast(props, config['delete-toast-msg']); - }); - - dialogCancelBtn.addEventListener('click', () => { - toolBox.remove(); - underlay.open = false; - dialog.innerHTML = ''; - }); - }); - if (!moreOptionsCell.querySelector('.dashboard-tool-box')) { moreOptionsCell.append(toolBox); } @@ -300,7 +308,7 @@ function buildStatusTag(series) { case 'published': dot = getIcon('dot-purple'); break; - case 'unpublished': + case 'draft': dot = getIcon('dot-green'); break; case 'archived': From 5d3680a05aab0d414307cf70669868d1fe82c4d5 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 10:03:34 -0600 Subject: [PATCH 64/74] Fixing linting error --- .../series-creation-form.js | 6 +--- .../series-dashboard/series-dashboard.js | 28 +++++++++---------- ecc/samples/sample-form/sample-form.js | 11 ++++---- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/ecc/blocks/series-creation-form/series-creation-form.js b/ecc/blocks/series-creation-form/series-creation-form.js index efa873d4..bda0702b 100644 --- a/ecc/blocks/series-creation-form/series-creation-form.js +++ b/ecc/blocks/series-creation-form/series-creation-form.js @@ -401,15 +401,11 @@ async function save(props, toPublish = false) { let resp = props.response; - const onSave = async () => { - - }; - if (!resp.seriesId) { resp = await createSeries(quickFilter(props.payload)); props.response = { ...props.response, ...resp }; updateDashboardLink(props); - + if (resp?.seriesId) await handleSeriesUpdate(props); if (!resp.error) { diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 45332a50..6e217e9e 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -16,6 +16,7 @@ import { getDevToken, } from '../../scripts/utils.js'; import { initProfileLogicTree } from '../../scripts/profile.js'; +import { quickFilter } from '../series-creation-form/data-handler.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -173,7 +174,7 @@ function initMoreOptions(props, config, seriesObj, row) { row.classList.add('pending'); const resp = await unpublishSeries(seriesObj.seriesId, seriesObj); updateDashboardData(resp, props); - + sortData(props, config, { resort: true }); showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['unpublished-msg']), { variant: 'positive' }); }); @@ -186,9 +187,9 @@ function initMoreOptions(props, config, seriesObj, row) { row.classList.add('pending'); const resp = await publishSeries(seriesObj.seriesId, seriesObj); updateDashboardData(resp, props); - + sortData(props, config, { resort: true }); - + showToast(props, buildToastMsgWithEventTitle(seriesObj.title, config['published-msg']), { variant: 'positive' }); }); } @@ -197,7 +198,7 @@ function initMoreOptions(props, config, seriesObj, row) { // const viewTemplate = buildTool(toolBox, 'View Template', 'preview-eye'); const clone = buildTool(toolBox, 'Clone', 'clone'); - + if (seriesStatus && seriesStatus !== 'archived') { const edit = buildTool(toolBox, 'Edit', 'edit-pencil'); @@ -205,15 +206,14 @@ function initMoreOptions(props, config, seriesObj, row) { url.searchParams.set('seriesId', seriesObj.seriesId); edit.href = url.toString(); - const archive = buildTool(toolBox, 'Archive', 'archive'); archive.addEventListener('click', async (e) => { e.preventDefault(); - + const spTheme = props.el.querySelector('sp-theme.toast-area'); if (!spTheme) return; - + const underlay = spTheme.querySelector('sp-underlay'); const dialog = spTheme.querySelector('sp-dialog'); createTag('h1', { slot: 'heading' }, 'You are archiving this series.', { parent: dialog }); @@ -221,31 +221,31 @@ function initMoreOptions(props, config, seriesObj, row) { const buttonContainer = createTag('div', { class: 'button-container' }, '', { parent: dialog }); const dialogArchiveBtn = createTag('sp-button', { variant: 'secondary', slot: 'button' }, 'Yes, I want to archive this series', { parent: buttonContainer }); const dialogCancelBtn = createTag('sp-button', { variant: 'cta', slot: 'button' }, 'Do not archive', { parent: buttonContainer }); - + underlay.open = true; - + dialogArchiveBtn.addEventListener('click', async () => { toolBox.remove(); underlay.open = false; dialog.innerHTML = ''; row.classList.add('pending'); const resp = await archiveSeries(seriesObj.seriesId); - + if (resp.error) { row.classList.remove('pending'); showToast(props, resp.error, { variant: 'negative' }); return; } - + const newJson = await getAllSeries(); props.data = newJson.series; props.filteredData = newJson.series; props.paginatedData = newJson.series; - + sortData(props, config, { resort: true }); showToast(props, config['delete-toast-msg']); }); - + dialogCancelBtn.addEventListener('click', () => { toolBox.remove(); underlay.open = false; @@ -254,7 +254,7 @@ function initMoreOptions(props, config, seriesObj, row) { }); } // const verHistory = buildTool(toolBox, 'Version History', 'version-history'); - + // clone clone.addEventListener('click', async (e) => { e.preventDefault(); diff --git a/ecc/samples/sample-form/sample-form.js b/ecc/samples/sample-form/sample-form.js index 180fc31a..206e5767 100644 --- a/ecc/samples/sample-form/sample-form.js +++ b/ecc/samples/sample-form/sample-form.js @@ -216,13 +216,14 @@ async function loadData(props) { const id = urlParams.get('id'); if (!id) return; - + // fetch data to prefill the form - // props.el.classList.add('disabled'); - // const data = await getSeries(id); - // props.response = { ...props.response, ...data }; - // props.el.classList.remove('disabled'); + props.el.classList.add('disabled'); + // const data = await get(id); + const data = {}; + props.response = { ...props.response, ...data }; + props.el.classList.remove('disabled'); } async function initComponents(props) { From a3c9f93ed7d801f46065b220bd277d846001185d Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 10:05:39 -0600 Subject: [PATCH 65/74] linting --- .../product-promotion-component.css | 1 + .../series-templates-component/series-templates-component.css | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ecc/blocks/product-promotion-component/product-promotion-component.css b/ecc/blocks/product-promotion-component/product-promotion-component.css index e69de29b..1c29adcd 100644 --- a/ecc/blocks/product-promotion-component/product-promotion-component.css +++ b/ecc/blocks/product-promotion-component/product-promotion-component.css @@ -0,0 +1 @@ +/* Wrapper block. No CSS rules are applied to the .product-promotion-component block. */ diff --git a/ecc/blocks/series-templates-component/series-templates-component.css b/ecc/blocks/series-templates-component/series-templates-component.css index d10addaa..ceae992a 100644 --- a/ecc/blocks/series-templates-component/series-templates-component.css +++ b/ecc/blocks/series-templates-component/series-templates-component.css @@ -78,7 +78,7 @@ left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgb(0 0 0 / 50%); z-index: 2; } @@ -157,7 +157,7 @@ background: none; border: none; cursor: pointer; - filter: drop-shadow(1px 1px 1px #ffffff); + filter: drop-shadow(1px 1px 1px #fff); } .series-templates-component .picker-preview-actions button:hover { diff --git a/package.json b/package.json index 4cd69260..8574cda9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test:watch": "npm test -- --watch", "lint": "npm run lint:js && npm run lint:css", "lint:js": "eslint .", - "lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css'" + "lint:css": "stylelint 'ecc/blocks/**/*.css' 'ecc/styles/*.css'" }, "repository": { "type": "git", From 3fee93112efbaa0b94c34a62ab58c7e6a5a3ce44 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 10:09:11 -0600 Subject: [PATCH 66/74] rename access reference --- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 2 +- ecc/scripts/profile.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index f9e22cac..fe4486e4 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -729,7 +729,7 @@ export default async function init(el) { return; } - await initProfileLogicTree('events-dashboard', { + await initProfileLogicTree('ecc-dashboard', { noProfile: () => { signIn(); }, diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index cd4ca637..9af467c7 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -127,19 +127,19 @@ export function userHasAccessToEvent(user, eventId) { export function userHasAccessToView(user, blockName) { const { role } = user; const managerAccess = [ - 'events-dashboard', + 'ecc-dashboard', 'event-creation-form', 'series-dashboard', 'series-creation-form', ]; const creatorAccess = [ - 'events-dashboard', + 'ecc-dashboard', 'event-creation-form', ]; const editorAccess = [ - 'events-dashboard', + 'ecc-dashboard', 'event-creation-form', ]; From 72ee0a63619b29c003a5ded060e17be46b0fa5b6 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 10:39:02 -0600 Subject: [PATCH 67/74] Fixed access --- ecc/scripts/profile.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 9af467c7..07ec37b1 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -66,7 +66,21 @@ export function lazyCaptureProfile() { export async function getUser() { const profile = BlockMediator.get('imsProfile'); - if (!profile || profile.noProfile) return null; + + if (!profile || profile.noProfile) { + const devToken = sessionStorage.getItem('devToken'); + if (devToken && window.location.hostname === 'localhost') { + return { + role: 'admin', + email: 'admin@adobe.com', + 'business-units': 'all', + series: 'all', + events: 'all', + }; + } + + return null; + } const { email } = profile; From bf50c53697d1f6bef54b4c2959a66e7bbd377c89 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 13:51:11 -0600 Subject: [PATCH 68/74] Update esp-controller.js --- ecc/scripts/esp-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 3a0df942..b37d68b4 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -949,9 +949,9 @@ export async function getSeriesForUser() { if (!user) return []; - const series = await getAllSeries(); + const { series } = await getAllSeries(); - if (!series.error) { + if (series) { const { role } = user; if (role === 'admin') return series; From 97819b588c5106be6e81f99706f0227fcb852f47 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Fri, 13 Dec 2024 15:09:07 -0600 Subject: [PATCH 69/74] refactor --- ecc/blocks/ecc-dashboard/ecc-dashboard.js | 2 +- ecc/blocks/event-creation-form/event-creation-form.js | 2 +- ecc/blocks/event-partners-component/controller.js | 2 +- .../form-handler/data-handler.js} | 8 +++++++- ecc/blocks/form-handler/form-handler.js | 2 +- ecc/blocks/img-upload-component/controller.js | 2 +- ecc/blocks/profile-component/controller.js | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) rename ecc/{scripts/event-data-handler.js => blocks/form-handler/data-handler.js} (95%) diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index fe4486e4..833eb3c3 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -17,7 +17,7 @@ import { getDevToken, } from '../../scripts/utils.js'; -import { quickFilter } from '../../scripts/event-data-handler.js'; +import { quickFilter } from '../event-creation-form/data-handler.js'; import { initProfileLogicTree } from '../../scripts/profile.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/event-creation-form/event-creation-form.js b/ecc/blocks/event-creation-form/event-creation-form.js index 6d9b6765..4fc05d14 100644 --- a/ecc/blocks/event-creation-form/event-creation-form.js +++ b/ecc/blocks/event-creation-form/event-creation-form.js @@ -26,7 +26,7 @@ import ProductSelector from '../../components/product-selector/product-selector. import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; -import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; +import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import CustomSearch from '../../components/custom-search/custom-search.js'; import { initProfileLogicTree } from '../../scripts/profile.js'; diff --git a/ecc/blocks/event-partners-component/controller.js b/ecc/blocks/event-partners-component/controller.js index 308fc14e..e5673da9 100644 --- a/ecc/blocks/event-partners-component/controller.js +++ b/ecc/blocks/event-partners-component/controller.js @@ -7,7 +7,7 @@ import { removeSponsorFromEvent, updateSponsorInEvent, } from '../../scripts/esp-controller.js'; -import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; +import { getFilteredCachedResponse } from '../event-creation-form/data-handler.js'; let PARTNERS_SERIES_ID; diff --git a/ecc/scripts/event-data-handler.js b/ecc/blocks/form-handler/data-handler.js similarity index 95% rename from ecc/scripts/event-data-handler.js rename to ecc/blocks/form-handler/data-handler.js index 40dd55c2..72e0be81 100644 --- a/ecc/scripts/event-data-handler.js +++ b/ecc/blocks/form-handler/data-handler.js @@ -4,7 +4,6 @@ let responseCache = {}; let payloadCache = {}; const submissionFilter = [ - // from payload and response 'agenda', 'topics', 'eventType', @@ -57,6 +56,13 @@ export function setPayloadCache(payload) { } export function getFilteredCachedPayload() { + const { topics } = payloadCache; + + if (topics) { + payloadCache.topics = Object.values(topics).reduce((acc, val) => acc.concat(val), []); + } + + console.log('payloadCache', payloadCache); return payloadCache; } diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index 981311c0..ee21092f 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -26,7 +26,7 @@ import ProductSelector from '../../components/product-selector/product-selector. import ProductSelectorGroup from '../../components/product-selector-group/product-selector-group.js'; import PartnerSelector from '../../components/partner-selector/partner-selector.js'; import PartnerSelectorGroup from '../../components/partner-selector-group/partner-selector-group.js'; -import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from '../../scripts/event-data-handler.js'; +import getJoinedData, { getFilteredCachedResponse, hasContentChanged, quickFilter, setPayloadCache, setResponseCache } from './data-handler.js'; import { getUser, initProfileLogicTree, userHasAccessToEvent } from '../../scripts/profile.js'; import CustomSearch from '../../components/custom-search/custom-search.js'; diff --git a/ecc/blocks/img-upload-component/controller.js b/ecc/blocks/img-upload-component/controller.js index 07ebf771..95060dd3 100644 --- a/ecc/blocks/img-upload-component/controller.js +++ b/ecc/blocks/img-upload-component/controller.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import { deleteImage, getEventImages, uploadImage } from '../../scripts/esp-controller.js'; import { LIBS } from '../../scripts/scripts.js'; -import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; +import { getFilteredCachedResponse } from '../event-creation-form/data-handler.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); diff --git a/ecc/blocks/profile-component/controller.js b/ecc/blocks/profile-component/controller.js index 30770932..237f3fe9 100644 --- a/ecc/blocks/profile-component/controller.js +++ b/ecc/blocks/profile-component/controller.js @@ -6,7 +6,7 @@ import { removeSpeakerFromEvent, getEventSpeaker, } from '../../scripts/esp-controller.js'; -import { getFilteredCachedResponse } from '../../scripts/event-data-handler.js'; +import { getFilteredCachedResponse } from '../event-creation-form/data-handler.js'; export async function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; From 660c6f31fee2863428f58caf91ce027ba9646df9 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 17 Dec 2024 12:40:46 -0600 Subject: [PATCH 70/74] always compare lowerCase --- ecc/scripts/profile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ecc/scripts/profile.js b/ecc/scripts/profile.js index 07ec37b1..3bcd1d39 100644 --- a/ecc/scripts/profile.js +++ b/ecc/scripts/profile.js @@ -84,6 +84,8 @@ export async function getUser() { const { email } = profile; + if (!email) return null; + if (usersCache.length === 0) { const resp = await fetch('/ecc/system/users.json') .then((r) => r) @@ -95,7 +97,7 @@ export async function getUser() { usersCache = json.data; } - const user = usersCache.find((s) => s.email === email); + const user = usersCache.find((s) => s.email.toLowerCase() === email.toLowerCase()); return user; } From c599e7e8318bf1481dd170098d54ad5d14af8a03 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 17 Dec 2024 13:19:21 -0600 Subject: [PATCH 71/74] more limiting series options --- ecc/blocks/event-format-component/controller.js | 2 ++ ecc/blocks/event-format-component/event-format-component.js | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/event-format-component/controller.js b/ecc/blocks/event-format-component/controller.js index a0ea3009..edc5549a 100644 --- a/ecc/blocks/event-format-component/controller.js +++ b/ecc/blocks/event-format-component/controller.js @@ -85,6 +85,8 @@ async function populateSeriesOptions(props, component) { } Object.values(series).forEach((val) => { + if (!val.seriesId || !val.seriesName) return; + if (!val.seriesStatus?.toLowerCase() === 'published') return; const opt = createTag('sp-menu-item', { value: val.seriesId }, val.seriesName); seriesSelect.append(opt); }); diff --git a/ecc/blocks/event-format-component/event-format-component.js b/ecc/blocks/event-format-component/event-format-component.js index 3cab23a9..8eff57e7 100644 --- a/ecc/blocks/event-format-component/event-format-component.js +++ b/ecc/blocks/event-format-component/event-format-component.js @@ -69,8 +69,6 @@ export default function init(el) { cols.forEach(async (c, ci) => { if (ci === 0) decorateCloudTagSelect(c); if (ci === 1) decorateSeriesSelect(c); - // if (ci === 2) decorateNewSeriesBtnAndModal(c); - // if (ci === 2) decorateCheckbox(c); }); } From 1cc04d48f946453bf1d027a44044c255c8e0d8b2 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 17 Dec 2024 13:26:40 -0600 Subject: [PATCH 72/74] Update controller.js --- ecc/blocks/event-format-component/controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecc/blocks/event-format-component/controller.js b/ecc/blocks/event-format-component/controller.js index edc5549a..aa5b94a7 100644 --- a/ecc/blocks/event-format-component/controller.js +++ b/ecc/blocks/event-format-component/controller.js @@ -86,7 +86,8 @@ async function populateSeriesOptions(props, component) { Object.values(series).forEach((val) => { if (!val.seriesId || !val.seriesName) return; - if (!val.seriesStatus?.toLowerCase() === 'published') return; + if (val.seriesStatus?.toLowerCase() !== 'published') return; + const opt = createTag('sp-menu-item', { value: val.seriesId }, val.seriesName); seriesSelect.append(opt); }); From 8567e048d3719f9c444f398a75545bfe025f3f84 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 17 Dec 2024 13:41:41 -0600 Subject: [PATCH 73/74] squeezing in more small fixes --- ecc/blocks/event-format-component/controller.js | 10 +++++++++- ecc/blocks/series-dashboard/series-dashboard.js | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ecc/blocks/event-format-component/controller.js b/ecc/blocks/event-format-component/controller.js index aa5b94a7..f14b5bb7 100644 --- a/ecc/blocks/event-format-component/controller.js +++ b/ecc/blocks/event-format-component/controller.js @@ -84,7 +84,15 @@ async function populateSeriesOptions(props, component) { return; } - Object.values(series).forEach((val) => { + Object.values(series).filter((s) => { + const hasRequiredVals = s.seriesId && s.seriesName; + const isPublished = s.seriesStatus?.toLowerCase() === 'published'; + + const currentCloud = props.eventDataResp.cloudType || props.payload.cloudType; + const isInCurrentCloud = s.cloudType === currentCloud; + + return hasRequiredVals && isPublished && isInCurrentCloud; + }).forEach((val) => { if (!val.seriesId || !val.seriesName) return; if (val.seriesStatus?.toLowerCase() !== 'published') return; diff --git a/ecc/blocks/series-dashboard/series-dashboard.js b/ecc/blocks/series-dashboard/series-dashboard.js index 6e217e9e..55bfe781 100644 --- a/ecc/blocks/series-dashboard/series-dashboard.js +++ b/ecc/blocks/series-dashboard/series-dashboard.js @@ -606,7 +606,7 @@ function buildLoadingScreen(el) { el.classList.add('loading'); const loadingScreen = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'loading-screen' }); createTag('sp-progress-circle', { size: 'l', indeterminate: true }, '', { parent: loadingScreen }); - createTag('sp-field-label', {}, 'Loading Series dashboard...', { parent: loadingScreen }); + createTag('sp-field-label', {}, 'Loading event series dashboard...', { parent: loadingScreen }); el.prepend(loadingScreen); } From 436b2bff720d444ef78dc838f0731663fb4bff3e Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Tue, 17 Dec 2024 13:44:40 -0600 Subject: [PATCH 74/74] extract cloudTypes --- ecc/blocks/event-format-component/event-format-component.js | 5 ++--- .../series-details-component/series-details-component.js | 4 ++-- ecc/constants/constants.js | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ecc/blocks/event-format-component/event-format-component.js b/ecc/blocks/event-format-component/event-format-component.js index 8eff57e7..15b71d84 100644 --- a/ecc/blocks/event-format-component/event-format-component.js +++ b/ecc/blocks/event-format-component/event-format-component.js @@ -1,4 +1,5 @@ /* eslint-disable max-len */ +import { SUPPORTED_CLOUDS } from '../../constants/constants.js'; import { LIBS } from '../../scripts/scripts.js'; import { generateToolTip } from '../../scripts/utils.js'; @@ -15,10 +16,8 @@ async function decorateCloudTagSelect(column) { // FIXME: cloulds shouldn't be hardcoded // const clouds = await getClouds(); - // const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }]; - const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }]; - Object.entries(clouds).forEach(([, val]) => { + Object.entries(SUPPORTED_CLOUDS).forEach(([, val]) => { const opt = createTag('sp-menu-item', { value: val.id }, val.name); select.append(opt); }); diff --git a/ecc/blocks/series-details-component/series-details-component.js b/ecc/blocks/series-details-component/series-details-component.js index b17b6ca7..68312b56 100644 --- a/ecc/blocks/series-details-component/series-details-component.js +++ b/ecc/blocks/series-details-component/series-details-component.js @@ -1,3 +1,4 @@ +import { SUPPORTED_CLOUDS } from '../../constants/constants.js'; import { LIBS } from '../../scripts/scripts.js'; import { generateToolTip, @@ -19,9 +20,8 @@ async function decorateCloudTagSelect(column) { // FIXME: cloulds shouldn't be hardcoded // const clouds = await getClouds(); - const clouds = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }]; - Object.entries(clouds).forEach(([, val]) => { + Object.entries(SUPPORTED_CLOUDS).forEach(([, val]) => { const opt = createTag('sp-menu-item', { value: val.id }, val.name); select.append(opt); }); diff --git a/ecc/constants/constants.js b/ecc/constants/constants.js index 939a98e8..fabd3d92 100644 --- a/ecc/constants/constants.js +++ b/ecc/constants/constants.js @@ -1,2 +1,3 @@ export const LINK_REGEX = '^https:\\/\\/[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,63}(:[0-9]{1,5})?(\\/.*)?$'; export const ALLOWED_ACCOUNT_TYPES = ['type3', 'type2e']; +export const SUPPORTED_CLOUDS = [{ id: 'CreativeCloud', name: 'Creative Cloud' }, { id: 'DX', name: 'Experience Cloud' }];