diff --git a/ecc/blocks/event-info-component/event-info-component.css b/ecc/blocks/event-info-component/event-info-component.css index 413e2229..a76ab279 100644 --- a/ecc/blocks/event-info-component/event-info-component.css +++ b/ecc/blocks/event-info-component/event-info-component.css @@ -11,6 +11,15 @@ 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%; @@ -33,7 +42,7 @@ .event-info-component .date-time-row > .date-picker { position: relative; border-bottom: 1px solid var(--color-black); - min-width: 280px; + min-width: 300px; display: flex; align-items: center; gap: 8px; diff --git a/ecc/blocks/event-info-component/event-info-component.js b/ecc/blocks/event-info-component/event-info-component.js index a938ee97..41db6aaf 100644 --- a/ecc/blocks/event-info-component/event-info-component.js +++ b/ecc/blocks/event-info-component/event-info-component.js @@ -1,5 +1,12 @@ import { LIBS } from '../../scripts/scripts.js'; -import { getIcon, generateToolTip, decorateTextfield, decorateTextarea, convertTo24HourFormat } from '../../scripts/utils.js'; +import { + getIcon, + generateToolTip, + decorateTextfield, + decorateTextarea, + convertTo24HourFormat, + miloReplaceKey, +} from '../../scripts/utils.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -94,7 +101,7 @@ export default function init(el) { generateToolTip(r); break; case 1: - await decorateTextfield(r, { id: 'info-field-event-title' }); + await decorateTextfield(r, { id: 'info-field-event-title' }, await miloReplaceKey('duplicate-event-title-error')); break; case 2: await decorateTextarea(r, { id: 'info-field-event-description', grows: true, quiet: true }); 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 d0f0951e..d12bd22b 100644 --- a/ecc/blocks/form-handler/controllers/event-format-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-format-component-controller.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ import { getSeries } from '../../../scripts/esp-controller.js'; -import { MILO_CONFIG, LIBS } from '../../../scripts/scripts.js'; +import { LIBS, BlockMediator } from '../../../scripts/scripts.js'; import { changeInputValue } from '../../../scripts/utils.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -67,7 +67,7 @@ export async function onUpdate(component, props) { } } -async function populateSeriesOptions(component) { +async function populateSeriesOptions(props, component) { const seriesSelect = component.querySelector('#series-select-input'); if (!seriesSelect) return; @@ -84,6 +84,19 @@ async function populateSeriesOptions(component) { }); seriesSelect.pending = false; + + seriesSelect.addEventListener('change', () => { + const seriesId = seriesSelect.value; + const seriesName = seriesSelect.querySelector(`[value="${seriesId}"]`).textContent; + + BlockMediator.set('eventDupMetrics', { + ...BlockMediator.get('eventDupMetrics'), + ...{ + seriesId, + seriesName, + }, + }); + }); } export default async function init(component, props) { @@ -104,7 +117,7 @@ export default async function init(component, props) { const eventData = props.eventDataResp; prepopulateTimeZone(component); initStepLock(component); - await populateSeriesOptions(component); + await populateSeriesOptions(props, component); const { cloudType, diff --git a/ecc/blocks/form-handler/controllers/event-info-component-controller.js b/ecc/blocks/form-handler/controllers/event-info-component-controller.js index 4b6e042c..0ea0bd9e 100644 --- a/ecc/blocks/form-handler/controllers/event-info-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-info-component-controller.js @@ -1,6 +1,7 @@ /* eslint-disable no-unused-vars */ /* eslint-disable no-use-before-define */ -import { LIBS } from '../../../scripts/scripts.js'; +import { getEvents } from '../../../scripts/esp-controller.js'; +import { BlockMediator, LIBS } from '../../../scripts/scripts.js'; import { changeInputValue } from '../../../scripts/utils.js'; const { createTag, getConfig } = await import(`${LIBS}/utils/utils.js`); @@ -345,14 +346,36 @@ export async function onUpdate(component, props) { // do nothing } -export default function init(component, props) { +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 endTimeInput = component.querySelector('#time-picker-end-time'); const datePicker = component.querySelector('#event-info-date-picker'); initCalendar(component); + eventTitleInput.addEventListener('input', () => { + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), title: eventTitleInput.value }); + }); + endTimeInput.addEventListener('change', () => { if (datePicker.dataset.startDate !== datePicker.dataset.endDate) return; const allOptions = startTimeInput.querySelectorAll('sp-menu-item'); @@ -386,6 +409,28 @@ export default function init(component, props) { option.disabled = false; }); } + + 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 { @@ -411,6 +456,15 @@ export default function init(component, props) { changeInputValue(endTimeInput, 'value', localEndTime || ''); changeInputValue(component.querySelector('#time-zone-select-input'), 'value', `${timezone}` || ''); + BlockMediator.set('eventDupMetrics', { + ...BlockMediator.get('eventDupMetrics'), + ...{ + title, + startDate: localStartDate, + eventId: eventData.eventId, + }, + }); + datePicker.dataset.startDate = localStartDate || ''; datePicker.dataset.endDate = localEndDate || ''; updateInput(component, { diff --git a/ecc/blocks/form-handler/controllers/venue-info-component-controller.js b/ecc/blocks/form-handler/controllers/venue-info-component-controller.js index 6d92b1d9..380ecd3e 100644 --- a/ecc/blocks/form-handler/controllers/venue-info-component-controller.js +++ b/ecc/blocks/form-handler/controllers/venue-info-component-controller.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ import { createVenue, replaceVenue } from '../../../scripts/esp-controller.js'; -import { ECC_ENV } from '../../../scripts/scripts.js'; +import { BlockMediator, ECC_ENV } from '../../../scripts/scripts.js'; import { changeInputValue, getSecret } from '../../../scripts/utils.js'; import { buildErrorMessage } from '../form-handler.js'; @@ -29,7 +29,7 @@ async function loadGoogleMapsAPI(callback) { document.head.appendChild(script); } -function initAutocomplete(el) { +function initAutocomplete(el, props) { const venueName = el.querySelector('#venue-info-venue-name'); // eslint-disable-next-line no-undef if (!google) return; @@ -97,6 +97,7 @@ function initAutocomplete(el) { changeInputValue(mapUrl, 'value', place.url); togglePrefillableFieldsHiddenState(el, false); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), city: addressInfo.city }); } if (place.geometry) { @@ -118,7 +119,7 @@ export async function onUpdate(component, props) { export default async function init(component, props) { const eventData = props.eventDataResp; - await loadGoogleMapsAPI(() => initAutocomplete(component)); + await loadGoogleMapsAPI(() => initAutocomplete(component, props)); const { venue, showVenuePostEvent } = eventData; @@ -178,6 +179,7 @@ export default async function init(component, props) { changeInputValue(placeIdInput, 'value', placeId); changeInputValue(mapUrlInput, 'value', mapUrl); changeInputValue(gmtoffsetInput, 'value', venue.gmtOffset); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), city }); if (venueName) { component.classList.add('prefilled'); diff --git a/ecc/blocks/form-handler/form-handler.css b/ecc/blocks/form-handler/form-handler.css index 14f230c9..efb38db6 100644 --- a/ecc/blocks/form-handler/form-handler.css +++ b/ecc/blocks/form-handler/form-handler.css @@ -76,6 +76,16 @@ .form-handler.show-error { --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.form-handler.show-dup-event-error #info-field-event-title { + --mod-textfield-icon-size-invalid: 16px; + --mod-textfield-border-color-invalid-default: unset; +} + +.form-handler.show-dup-event-error #info-field-event-title sp-help-text { + display: flex; } .form-handler .main-frame { @@ -96,7 +106,6 @@ justify-content: center; } - .form-handler .side-menu, .form-handler .main-frame, .form-handler .form-handler-ctas-panel, diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index eabe5b68..f2cb1756 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -151,7 +151,7 @@ function getCurrentFragment(props) { } function validateRequiredFields(fields) { - return fields.length === 0 || Array.from(fields).every((f) => f.value); + return fields.length === 0 || Array.from(fields).every((f) => f.value && !f.invalid); } function onStepValidate(props) { @@ -375,6 +375,21 @@ 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; + 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, options = { toPublish: false }) { try { await gatherValues(props); @@ -395,6 +410,7 @@ async function saveEvent(props, options = { toPublish: false }) { if (props.currentStep === 0 && !getFilteredCachedResponse().eventId) { resp = await createEvent(quickFilter(props.payload)); props.eventDataResp = { ...props.eventDataResp, ...resp }; + updateDashboardLink(props); onEventSave(); } else if (props.currentStep <= props.maxStep && !options.toPublish) { resp = await updateEvent( @@ -638,6 +654,7 @@ function initNavigation(props) { 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'); @@ -654,21 +671,6 @@ function initNavigation(props) { }); } -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(); -} - function initDeepLink(props) { const { hash } = window.location; @@ -750,7 +752,6 @@ async function buildECCForm(el) { case 'eventDataResp': { setResponseCache(value); updateCtas(target); - updateDashboardLink(target); if (value.error) { props.el.classList.add('show-error'); } else { diff --git a/ecc/scripts/utils.js b/ecc/scripts/utils.js index a52c60f4..01744da4 100644 --- a/ecc/scripts/utils.js +++ b/ecc/scripts/utils.js @@ -125,7 +125,7 @@ function mergeOptions(defaultOptions, overrideOptions) { return combinedOptions; } -export async function decorateTextfield(cell, extraOptions) { +export async function decorateTextfield(cell, extraOptions, negativeHelperText = '') { cell.classList.add('text-field-row'); const cols = cell.querySelectorAll(':scope > div'); if (!cols.length) return; @@ -153,6 +153,10 @@ export async function decorateTextfield(cell, extraOptions) { extraOptions, )); + if (negativeHelperText) { + createTag('sp-help-text', { variant: 'negative', slot: 'negative-help-text' }, negativeHelperText, { parent: input }); + } + if (maxCharNum) input.setAttribute('maxlength', maxCharNum); const wrapper = createTag('div', { class: 'info-field-wrapper' }); @@ -226,6 +230,24 @@ export function getServiceName(link) { return url.hostname.replace('.com', '').replace('www.', ''); } +export async function miloReplaceKey(key) { + try { + const [utils, placeholders] = await Promise.all([ + import(`${LIBS}/utils/utils.js`), + import(`${LIBS}/features/placeholders.js`), + ]); + + const { getConfig } = utils; + const { replaceKey } = placeholders; + const config = getConfig(); + + return await replaceKey(key, config); + } catch (error) { + window.lana?.log('Error trying to replace placeholder:', error); + return 'RSVP'; + } +} + export function toClassName(name) { return name && typeof name === 'string' ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-')