diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml new file mode 100644 index 00000000..386e1253 --- /dev/null +++ b/.github/workflows/sync-branches.yml @@ -0,0 +1,81 @@ +name: Sync 02 branches + +on: + push: + branches: + - dev + - stage + - main + +jobs: + sync-dev02: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/dev' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Check out dev02 branch + run: git checkout dev02 + + - name: Merge dev into dev02 with theirs strategy + run: git merge origin/dev --strategy-option theirs --allow-unrelated-histories + + - name: Push changes to dev02 + run: git push origin dev02 + + sync-stage02: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/stage' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Check out stage02 branch + run: git checkout stage02 + + - name: Merge stage into stage02 with theirs strategy + run: git merge origin/stage --strategy-option theirs --allow-unrelated-histories + + - name: Push changes to stage02 + run: git push origin stage02 + + sync-main02: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Check out main02 branch + run: git checkout main02 + + - name: Merge main into main02 with theirs strategy + run: git merge origin/main --strategy-option theirs --allow-unrelated-histories + + - name: Push changes to main02 + run: git push origin main02 diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.css b/ecc/blocks/attendee-management-table/attendee-management-table.css index e902ce51..a00941fa 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.css +++ b/ecc/blocks/attendee-management-table/attendee-management-table.css @@ -1,7 +1,12 @@ .attendee-management-table { font-family: var(--body-font-family); - padding: 40px; + max-width: 1440px; + padding: 0 40px 40px; margin: auto; + + --color-red: #EB1000; + --table-col-width: 120px; + --table-col-padding: 16px; } .attendee-management-table .loading-screen { @@ -22,10 +27,82 @@ font-size: var(--type-body-s-size); } -.attendee-management-table .dashboard-main-container { +.attendee-management-table .dashboard-header-heading { + display: flex; + align-items: center; + gap: 8px; +} + +.attendee-management-table .dashboard-header-heading sp-action-button { + padding: 0; + background: none; + border: none; + cursor: help; +} + +.attendee-management-table .dashboard-main-container .dashboard-body-container { margin-top: 24px; display: flex; - gap: 24px; + gap: 2rem; +} + +.attendee-management-table .dashboard-header-event-info { + display: flex; + align-items: center; + gap: 2rem; +} + +.attendee-management-table .event-info-container { + width: 100%; +} + +.attendee-management-table .dashboard-main-container .event-image { + max-height: 167px; + max-width: 224px; +} + +.attendee-management-table .dashboard-main-container .event-info-row { + display: flex; + justify-content: flex-start; + gap: 25%; + border-bottom: 1px solid var(--color-gray-500); +} + +.attendee-management-table .dashboard-main-container .event-info-label { + font-weight: 700; + margin-right: 1rem; +} + +.attendee-management-table .dashboard-main-container .event-stats-col-wrapper { + display: flex; + align-items: flex-start; +} + +.attendee-management-table .dashboard-main-container .event-stats-row { + height: 64px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 25%; + margin-top: 1rem; +} + +.attendee-management-table .dashboard-main-container .event-stats-label { + margin: 0; + margin-right: 1rem; + color: var(--color-gray-500) +} + +.attendee-management-table .dashboard-main-container .event-stats-value { + font-size: var(--type-heading-xl-size); + font-weight: 700; + color: var(--color-red); + margin: 0; +} + +.attendee-management-table .dashboard-main-container .event-stats-subtext { + font-size: var(--type-body-xxs-size); + margin: 0; } .attendee-management-table .dashboard-main-container .dashboard-side-panel { @@ -64,15 +141,8 @@ margin: 16px 0; } -.attendee-management-table .dashboard-header { - display: flex; - flex-direction: column; - gap: 16px; - justify-content: space-between; - align-items: flex-start; -} - .attendee-management-table .dashboard-table-container { + width: 100%; max-width: calc(100% - 250px); position: relative; } @@ -159,10 +229,6 @@ gap: 16px; } -.attendee-management-table .dashboard-header-text h1 { - margin: 0; -} - .attendee-management-table .dashboard-header-text p { margin: 6px 0; } @@ -171,6 +237,11 @@ display: flex; align-items: center; gap: 16px; + justify-content: flex-end; +} + +.attendee-management-table.no-results .dashboard-actions-container { + visibility: hidden; } .attendee-management-table .dashboard-actions-container .search-input-wrapper { @@ -205,20 +276,44 @@ } .attendee-management-table table .table-header-row { - height: 40; + height: 56px; border-bottom: 2px solid var(--color-gray-600); } +.attendee-management-table.no-results table .table-header-row { + display: none; +} + .attendee-management-table table .table-header-row th { - padding: 0 16px; + padding: 0 var(--table-col-padding); font-weight: 700; text-align: left; font-size: var(--type-body-xxs-size); color: var(--spectrum-color-gray-500); - width: 100px; + width: var(--table-col-width); white-space: nowrap; } +.attendee-management-table table .table-header-row th.sticky-right-1, +.attendee-management-table table .attendee-row td.sticky-right-1 { + position: sticky; + right: 0; + background-color: var(--color-gray-100); + max-width: var(--table-col-width); + min-width: var(--table-col-width); + z-index: 1; +} + +.attendee-management-table table .table-header-row th.sticky-right-2, +.attendee-management-table table .attendee-row td.sticky-right-2 { + position: sticky; + right: calc(var(--table-col-width) + (var(--table-col-padding) * 2)); + background-color: var(--color-gray-100); + max-width: var(--table-col-width); + min-width: var(--table-col-width); + z-index: 1; +} + .attendee-management-table table .table-header-row th span { white-space: nowrap; width: 60px; @@ -249,6 +344,7 @@ .attendee-management-table table .attendee-row { height: 40px; + border-bottom: 1px solid var(--color-gray-200); } .attendee-management-table table .no-search-results-row td { @@ -256,10 +352,6 @@ text-align: center; } -.attendee-management-table table .attendee-row:nth-of-type(even) { - background-color: var(--color-gray-100); -} - .attendee-management-table table .attendee-row .attendee-title-link { font-weight: 700; text-decoration: none; @@ -351,9 +443,3 @@ gap: 16px; z-index: 9; } - -@media screen and (min-width: 900px) { - .attendee-management-table .dashboard-header { - flex-direction: row; - } -} diff --git a/ecc/blocks/attendee-management-table/attendee-management-table.js b/ecc/blocks/attendee-management-table/attendee-management-table.js index fa4abc1b..106376ee 100644 --- a/ecc/blocks/attendee-management-table/attendee-management-table.js +++ b/ecc/blocks/attendee-management-table/attendee-management-table.js @@ -1,34 +1,96 @@ /* eslint-disable max-len */ import { getAllEventAttendees, getEvents } from '../../scripts/esp-controller.js'; -import { ALLOWED_ACCOUNT_TYPES } from '../../constants/constants.js'; import { LIBS } from '../../scripts/scripts.js'; import { getIcon, buildNoAccessScreen, camelToSentenceCase, readBlockConfig, - getECCEnv, + signIn, + getEventServiceEnv, } from '../../scripts/utils.js'; -import BlockMediator from '../../scripts/deps/block-mediator.min.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`); -const ATTENDEE_ATTR_KEYS = [ - 'attendeeId', - 'firstName', - 'lastName', - 'email', - 'companyName', - 'jobTitle', - 'mobilePhone', - 'industry', - 'productsOfInterest', - 'companySize', - 'age', - 'jobLevel', - 'contactMethod', +const ATTENDEE_ATTR_MAP = [ + { + key: 'firstName', + label: 'First name', + fallback: '', + }, + { + key: 'lastName', + label: 'Last name', + fallback: '', + }, + { + key: 'email', + label: 'Email', + fallback: '', + }, + { + key: 'companyName', + label: 'Company', + fallback: '', + }, + { + key: 'jobTitle', + label: 'Job title', + fallback: '', + }, + { + key: 'mobilePhone', + label: 'Mobile phone', + fallback: '', + }, + { + key: 'industry', + label: 'Industry', + fallback: '', + }, + { + key: 'productsOfInterest', + label: 'Products of interest', + fallback: '', + }, + { + key: 'companySize', + label: 'Company size', + fallback: '', + }, + { + key: 'age', + label: 'Age', + fallback: '', + }, + { + key: 'jobLevel', + label: 'Job level', + fallback: '', + }, + { + key: 'contactMethod', + label: 'Contact method', + fallback: '', + }, + { + key: 'registrationDate', + label: 'Registration date', + fallback: '', + }, + { + key: 'type', + label: 'RSVP status', + fallback: 'registered', + }, + { + key: 'checkedIn', + label: 'Checked in', + fallback: '-', + }, ]; const FILTER_MAP = { @@ -42,6 +104,23 @@ const FILTER_MAP = { contactMethod: [], }; +const SPECTRUM_COMPONENTS = [ + 'theme', + 'toast', + 'button', + 'dialog', + 'underlay', + 'progress-circle', + 'textfield', + 'picker', + 'divider', + 'overlay', + 'popover', + 'link', + 'tooltip', + 'action-button', +]; + function buildAllFilterMenues(props) { const sidePanel = props.el.querySelector('.dashboard-side-panel'); @@ -172,8 +251,11 @@ async function populateRow(props, index) { const row = createTag('tr', { class: 'attendee-row', 'data-attendee-id': attendee.attendeeId }, '', { parent: tBody }); - ATTENDEE_ATTR_KEYS.forEach((key) => { - createTag('td', {}, attendee[key] || '', { parent: row }); + ATTENDEE_ATTR_MAP.forEach(({ key, fallback }, i, arr) => { + const td = createTag('td', {}, attendee[key] || fallback, { parent: row }); + if (['type', 'checkedIn'].includes(key)) { + td.classList.add(`sticky-right-${arr.length - i}`, 'actions'); + } }); } @@ -236,20 +318,12 @@ function decoratePagination(props, config) { } function initSorting(props, config) { - const thead = props.el.querySelector('thead'); - const thRow = thead.querySelector('tr'); - - ATTENDEE_ATTR_KEYS.forEach((key) => { - const val = camelToSentenceCase(key).toUpperCase(); - const thText = createTag('span', {}, val); - const th = createTag('th', {}, thText, { parent: thRow }); - - th.append(getIcon('chev-down'), getIcon('chev-up')); - th.classList.add('sortable', key); + const sortables = props.el.querySelectorAll('th.sortable'); + sortables.forEach((th) => { th.addEventListener('click', () => { if (!props.filteredData.length) return; - thead.querySelectorAll('th').forEach((h) => { + sortables.forEach((h) => { if (th !== h) { h.classList.remove('active'); } @@ -257,28 +331,50 @@ function initSorting(props, config) { th.classList.add('active'); props.currentSort = { el: th, - field: key, + field: th.dataset.field, }; sortData(props, config); }); }); } -function buildNoResultsScreen(el, config) { +function buildTableHeaders(props, config) { + const thead = props.el.querySelector('thead'); + const thRow = thead.querySelector('tr'); + + ATTENDEE_ATTR_MAP.forEach(({ key, label }, i, arr) => { + const thText = createTag('span', {}, label.toUpperCase()); + const th = createTag('th', {}, thText, { parent: thRow }); + + th.append(getIcon('chev-down'), getIcon('chev-up')); + + if (['type', 'checkedIn'].includes(key)) th.classList.add('actions', `sticky-right-${arr.length - i}`); + th.classList.add('sortable'); + th.dataset.field = key; + }); + + initSorting(props, config); +} + +function buildNoResultsScreen(props, config) { + const tBody = props.el.querySelector('table.dashboard-table tbody'); + props.el.classList.add('no-results'); + const noSearchResultsRow = createTag('tr', { class: 'no-search-results-row' }); const noSearchResultsCol = createTag('td', { colspan: '100%' }, getIcon('empty-dashboard'), { parent: noSearchResultsRow }); createTag('h2', {}, config['no-attendee-results-heading'], { parent: noSearchResultsCol }); createTag('p', {}, config['no-attendee-results-text'], { parent: noSearchResultsCol }); - el.append(noSearchResultsRow); + tBody.append(noSearchResultsRow); } function populateTable(props, config) { const tBody = props.el.querySelector('table.dashboard-table tbody'); + props.el.classList.remove('no-results'); tBody.innerHTML = ''; if (!props.paginatedData.length) { - buildNoResultsScreen(tBody, config); + buildNoResultsScreen(props, config); } else { const endOfPage = Math.min(+config['page-size'], props.paginatedData.length); @@ -294,7 +390,7 @@ function populateTable(props, config) { function filterData(props, config) { const q = props.currentQuery.toLowerCase(); props.filteredData = props.data.filter((e) => { - const searchMatch = ATTENDEE_ATTR_KEYS.some((key) => e[key]?.toString().toLowerCase().includes(q)); + const searchMatch = ATTENDEE_ATTR_MAP.some(({ key }) => e[key]?.toString().toLowerCase().includes(q)); const appliedFilters = Object.entries(props.currentFilters).filter(([, val]) => val.length); const filterMatch = appliedFilters.every(([key, val]) => val.includes(e[key])); @@ -306,13 +402,84 @@ function filterData(props, config) { sortData(props, config, { resort: true }); } +function calculatePercentage(part, total) { + if (total === 0) { + return '0%'; + } + const percentage = (part / total) * 100; + return `${percentage.toFixed(2)}%`; +} + +function buildEventInfo(props) { + const eventInfoContainer = props.el.querySelector('.dashboard-header-event-info'); + if (!eventInfoContainer) return; + + eventInfoContainer.innerHTML = ''; + const eventInfo = props.events.find((e) => e.eventId === props.currentEventId); + + if (!eventInfo) return; + const heroImgObj = eventInfo.photos?.find((p) => p.imageKind === 'event-hero-image'); + + // build event image + createTag( + 'div', + { class: 'event-image-container' }, + createTag('img', { class: 'event-image', src: heroImgObj ? heroImgObj.sharepointUrl || heroImgObj.imageUrl : '' }), + { parent: eventInfoContainer }, + ); + + const infoContainer = createTag('div', { class: 'event-info-container' }, '', { parent: eventInfoContainer }); + const infoRow = createTag('div', { class: 'event-info-row' }, '', { parent: infoContainer }); + const statsRow = createTag('div', { class: 'event-stats-row' }, '', { parent: infoContainer }); + + [ + { + label: 'EVENT:', + value: eventInfo.title, + }, + { + label: 'WHEN:', + value: new Date(eventInfo.localStartTimeMillis).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), + }, + { + label: 'TYPE:', + value: eventInfo.eventType, + }, + ].forEach(({ label, value }) => { + const infoColWrapper = createTag('div', { class: 'event-stats-col-wrapper' }, '', { parent: infoRow }); + createTag('span', { class: 'event-info-label' }, label, { parent: infoColWrapper }); + createTag('span', { class: 'event-info-value' }, value, { parent: infoColWrapper }); + }); + + [ + { + label: 'RSVPs', + value: eventInfo.attendeeCount || '0', + subText: calculatePercentage(+eventInfo.attendeeCount, +eventInfo.attendeeLimit), + }, + ].forEach(({ label, value, subText }) => { + const statsColWrapper = createTag('div', { class: 'event-stats-col-wrapper' }, '', { parent: statsRow }); + createTag('h3', { class: 'event-stats-label' }, label, { parent: statsColWrapper }); + const statsValWrapper = createTag('div', { class: 'event-stats-value-wrapper' }, '', { parent: statsColWrapper }); + createTag('p', { class: 'event-stats-value' }, value, { parent: statsValWrapper }); + createTag('p', { class: 'event-stats-subtext' }, subText, { parent: statsValWrapper }); + }); +} + function buildDashboardHeader(props, config) { + const mainContainer = props.el.querySelector('.dashboard-main-container'); const dashboardHeader = createTag('div', { class: 'dashboard-header' }); - const textContainer = createTag('div', { class: 'dashboard-header-text' }); + const headingContainer = createTag('div', { class: 'dashboard-header-text' }); + const eventInfoContainer = createTag('div', { class: 'dashboard-header-event-info' }); const actionsContainer = createTag('div', { class: 'dashboard-actions-container' }); - createTag('h1', { class: 'dashboard-header-heading' }, 'All event attendees', { parent: textContainer }); - createTag('p', { class: 'dashboard-header-attendees-count' }, `(${props.data.length} attendees)`, { parent: textContainer }); + const heading = createTag('h1', { class: 'dashboard-header-heading' }, 'Event report', { parent: headingContainer }); + + if (config.tooltipText) { + const toolTipTrigger = createTag('sp-action-button', { size: 's' }, getIcon('info')); + createTag('sp-tooltip', { 'self-managed': true, variant: 'info' }, config.tooltipText, { parent: toolTipTrigger }); + heading.append(toolTipTrigger); + } const searchInputWrapper = createTag('div', { class: 'search-input-wrapper' }, '', { parent: actionsContainer }); const searchInput = createTag('input', { type: 'text', placeholder: 'Search' }, '', { parent: searchInputWrapper }); @@ -322,28 +489,23 @@ function buildDashboardHeader(props, config) { filterData(props, config); }); - dashboardHeader.append(textContainer, actionsContainer); - props.el.prepend(dashboardHeader); -} - -function updateDashboardHeader(props) { - const attendeesCount = props.el.querySelector('.dashboard-header-attendees-count'); - - if (attendeesCount) attendeesCount.textContent = `(${props.data.length} attendees)`; + dashboardHeader.append(headingContainer, eventInfoContainer, actionsContainer); + mainContainer.prepend(dashboardHeader); + buildEventInfo(props); } function buildDashboardTable(props, config) { - const mainContainer = props.el.querySelector('.dashboard-main-container'); + const dashboardBody = props.el.querySelector('.dashboard-body-container'); - if (!mainContainer) return; + if (!dashboardBody) return; - const tableContainer = createTag('div', { class: 'dashboard-table-container' }, '', { parent: mainContainer }); + const tableContainer = createTag('div', { class: 'dashboard-table-container' }, '', { parent: dashboardBody }); const tableWrapper = createTag('div', { class: 'dashboard-table-wrapper' }, '', { parent: tableContainer }); const table = createTag('table', { class: 'dashboard-table' }, '', { parent: tableWrapper }); const thead = createTag('thead', {}, '', { parent: table }); createTag('tbody', {}, '', { parent: table }); createTag('tr', { class: 'table-header-row' }, '', { parent: thead }); - initSorting(props, config); + buildTableHeaders(props, config); populateTable(props, config); } @@ -376,10 +538,10 @@ function buildEventPicker(props) { const sidePanel = props.el.querySelector('.dashboard-side-panel'); const eventsPickerWrapper = createTag('div', { class: 'events-picker-wrapper' }, '', { parent: sidePanel }); - createTag('sp-field-label', {}, 'Current event', { parent: eventsPickerWrapper }); + createTag('sp-field-label', {}, 'Search other events', { parent: eventsPickerWrapper }); const eventsPicker = createTag('searchable-picker', { class: 'events-picker', - label: 'Choose an event', + label: 'Event name', }, '', { parent: eventsPickerWrapper }); if (props.currentEventId) { @@ -424,11 +586,11 @@ function buildBackToDashboardBtn(props, config) { } function buildDashboardSidePanel(props, config) { - const mainContainer = props.el.querySelector('.dashboard-main-container'); + const dashboardBody = props.el.querySelector('.dashboard-body-container'); - if (!mainContainer) return; + if (!dashboardBody) return; - const sidePanel = createTag('div', { class: 'dashboard-side-panel' }, '', { parent: mainContainer }); + const sidePanel = createTag('div', { class: 'dashboard-side-panel' }, '', { parent: dashboardBody }); buildBackToDashboardBtn(props, config); buildEventPicker(props); createTag('sp-divider', {}, '', { parent: sidePanel }); @@ -459,7 +621,8 @@ 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 }); - createTag('sp-theme', { color: 'light', scale: 'medium', class: 'dashboard-main-container' }, '', { parent: el }); + const mainContainer = createTag('sp-theme', { color: 'light', scale: 'medium', class: 'dashboard-main-container' }, '', { parent: el }); + createTag('div', { class: 'dashboard-body-container' }, '', { parent: mainContainer }); const uspEventId = new URLSearchParams(window.location.search).get('eventId'); const events = await getEventsArray(); @@ -497,6 +660,7 @@ async function buildDashboard(el, config) { target.currentFilters = {}; updateFilterMap(receiver); buildFilters(receiver); + buildEventInfo(target); } if (prop === 'currentEventId') { @@ -508,7 +672,6 @@ async function buildDashboard(el, config) { filterData(target, config); } - updateDashboardHeader(target); populateTable(receiver, config); updateResetFilterBtnState(target); @@ -538,20 +701,12 @@ function buildLoadingScreen(el) { export default async function init(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`), - 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`), - import(`${miloLibs}/features/spectrum-web-components/dist/textfield.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/picker.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/divider.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/overlay.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/popover.js`), - import(`${miloLibs}/features/spectrum-web-components/dist/link.js`), + ...promises, ]); const config = readBlockConfig(el); @@ -560,32 +715,20 @@ export default async function init(el) { const sp = new URLSearchParams(window.location.search); const devToken = sp.get('devToken'); - if (devToken && getECCEnv() === 'dev') { + if (devToken && getEventServiceEnv() === 'dev') { buildDashboard(el, config); return; } - const profile = BlockMediator.get('imsProfile'); - - if (profile) { - if (profile.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(profile.account_type)) { + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { buildNoAccessScreen(el); - } else { + }, + validProfile: () => { buildDashboard(el, config); - } - - return; - } - - if (!profile) { - const unsubscribe = BlockMediator.subscribe('imsProfile', ({ newValue }) => { - if (newValue?.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(newValue.account_type)) { - buildNoAccessScreen(el); - } else { - buildDashboard(el, config); - } - - unsubscribe(); - }); - } + }, + }); } diff --git a/ecc/blocks/ecc-dashboard/ecc-dashboard.js b/ecc/blocks/ecc-dashboard/ecc-dashboard.js index 101868c0..fad59d6a 100644 --- a/ecc/blocks/ecc-dashboard/ecc-dashboard.js +++ b/ecc/blocks/ecc-dashboard/ecc-dashboard.js @@ -6,11 +6,17 @@ import { publishEvent, unpublishEvent, } from '../../scripts/esp-controller.js'; -import { ALLOWED_ACCOUNT_TYPES } from '../../constants/constants.js'; import { LIBS } from '../../scripts/scripts.js'; -import { getIcon, buildNoAccessScreen, getEventPageHost, readBlockConfig, getECCEnv } from '../../scripts/utils.js'; +import { + getIcon, + buildNoAccessScreen, + getEventPageHost, + readBlockConfig, + signIn, + getEventServiceEnv, +} from '../../scripts/utils.js'; import { quickFilter } from '../form-handler/data-handler.js'; -import BlockMediator from '../../scripts/deps/block-mediator.min.js'; +import { initProfileLogicTree } from '../../scripts/event-apis.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -602,6 +608,11 @@ function buildDashboardHeader(props, config) { props.el.prepend(dashboardHeader); } +function updateEventsCount(props) { + const eventsCount = props.el.querySelector('.dashboard-header-events-count'); + eventsCount.textContent = `(${props.data.length} events)`; +} + function buildDashboardTable(props, config) { const tableContainer = createTag('div', { class: 'dashboard-table-container' }, '', { parent: props.el }); const table = createTag('table', { class: 'dashboard-table' }, '', { parent: tableContainer }); @@ -667,7 +678,7 @@ async function buildDashboard(el, config) { set(target, prop, value, receiver) { target[prop] = value; populateTable(receiver, config); - + updateEventsCount(receiver); return true; }, }; @@ -708,32 +719,20 @@ export default async function init(el) { const sp = new URLSearchParams(window.location.search); const devToken = sp.get('devToken'); - if (devToken && getECCEnv() === 'dev') { + if (devToken && getEventServiceEnv() === 'dev') { buildDashboard(el, config); return; } - const profile = BlockMediator.get('imsProfile'); - - if (profile) { - if (profile.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(profile.account_type)) { + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { buildNoAccessScreen(el); - } else { + }, + validProfile: () => { buildDashboard(el, config); - } - - return; - } - - if (!profile) { - const unsubscribe = BlockMediator.subscribe('imsProfile', ({ newValue }) => { - if (newValue?.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(newValue.account_type)) { - buildNoAccessScreen(el); - } else { - buildDashboard(el, config); - } - - unsubscribe(); - }); - } + }, + }); } diff --git a/ecc/blocks/form-handler/controllers/event-agenda-component-controller.js b/ecc/blocks/form-handler/controllers/event-agenda-component-controller.js index 59759f2d..097c77d2 100644 --- a/ecc/blocks/form-handler/controllers/event-agenda-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-agenda-component-controller.js @@ -7,7 +7,7 @@ export function onSubmit(component, props) { let agenda = []; - if (agendaGroup) agenda = agendaGroup.getAgendas(); + if (agendaGroup) agenda = agendaGroup.getCompleteAgenda(); const agendaInfo = { showAgendaPostEvent, diff --git a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js b/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js index ac54ce5b..2156bc87 100644 --- a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js @@ -7,7 +7,7 @@ export function onSubmit(component, props) { const checkbox = component.querySelector('#checkbox-community'); if (checkbox.checked) { - const communityTopicUrl = component.querySelector('#community-url-details').value; + const communityTopicUrl = component.querySelector('#community-url-details')?.value?.trim(); props.payload = { ...props.payload, communityTopicUrl }; } else { const tempPayload = { ...props.payload }; diff --git a/ecc/blocks/form-handler/controllers/registration-details-component-controller.js b/ecc/blocks/form-handler/controllers/registration-details-component-controller.js index cde7c625..97f52334 100644 --- a/ecc/blocks/form-handler/controllers/registration-details-component-controller.js +++ b/ecc/blocks/form-handler/controllers/registration-details-component-controller.js @@ -41,20 +41,20 @@ function prefillFields(component, props) { export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; - const attendeeLimitVal = component.querySelector('#attendee-count-input')?.value; + const attendeeLimitVal = component.querySelector('#attendee-count-input')?.value?.trim(); const allowWaitlisting = component.querySelector('#registration-allow-waitlist')?.checked; const contactHost = component.querySelector('#registration-contact-host')?.checked; - const hostEmail = component.querySelector('#event-host-email-input')?.value; + const hostEmail = component.querySelector('#event-host-email-input')?.value?.trim(); const rsvpDescription = component.querySelector('#rsvp-form-detail-description')?.value; const attendeeLimit = Number.isNaN(+attendeeLimitVal) ? null : +attendeeLimitVal; const rsvpData = {}; - if (rsvpDescription) rsvpData.rsvpDescription = rsvpDescription; + rsvpData.rsvpDescription = rsvpDescription || ''; + rsvpData.allowWaitlisting = !!allowWaitlisting; if (contactHost && hostEmail) rsvpData.hostEmail = hostEmail; if (attendeeLimit) rsvpData.attendeeLimit = attendeeLimit; - if (allowWaitlisting) rsvpData.allowWaitlisting = allowWaitlisting; props.payload = { ...props.payload, ...rsvpData }; } diff --git a/ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js b/ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js index 9c9f7303..0d36d1f6 100644 --- a/ecc/blocks/form-handler/controllers/terms-conditions-component-controller.js +++ b/ecc/blocks/form-handler/controllers/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, getECCEnv } from '../../../scripts/utils.js'; +import { fetchThrottledMemoizedText, getEventServiceEnv } from '../../../scripts/utils.js'; const { createTag } = await import(`${LIBS}/utils/utils.js`); @@ -35,7 +35,7 @@ async function loadPreview(component, templateId) { let host; if (window.location.href.includes('.hlx.')) { - host = window.location.origin.replace(window.location.hostname, `${getECCEnv()}--events-milo--adobecom.hlx.page`); + host = window.location.origin.replace(window.location.hostname, `${getEventServiceEnv()}--events-milo--adobecom.hlx.page`); } else { host = window.location.origin; } 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 4b3a894b..02c96ee0 100644 --- a/ecc/blocks/form-handler/controllers/venue-info-component-controller.js +++ b/ecc/blocks/form-handler/controllers/venue-info-component-controller.js @@ -1,24 +1,26 @@ /* eslint-disable no-unused-vars */ import { createVenue, replaceVenue } from '../../../scripts/esp-controller.js'; import BlockMediator from '../../../scripts/deps/block-mediator.min.js'; -import { changeInputValue, getECCEnv, getSecret } from '../../../scripts/utils.js'; +import { changeInputValue, getEventServiceEnv, getSecret } from '../../../scripts/utils.js'; import { buildErrorMessage } from '../form-handler.js'; -function togglePrefillableFieldsHiddenState(component, showPrefilledFields) { - const addressInput = component.querySelector('#venue-info-venue-address'); - const cityInput = component.querySelector('#location-city'); - const stateInput = component.querySelector('#location-state'); - const postalCodeInput = component.querySelector('#location-zip-code'); - const countryInput = component.querySelector('#location-country'); +function togglePrefillableFieldsHiddenState(component) { + const address = component.querySelector('#venue-info-venue-address'); + const city = component.querySelector('#location-city'); + const state = component.querySelector('#location-state'); + const postal = component.querySelector('#location-zip-code'); + const county = component.querySelector('#location-country'); - [addressInput, cityInput, stateInput, postalCodeInput, countryInput].forEach((input) => { - input.classList.toggle('hidden', showPrefilledFields); + const hasUnfilledFields = [address, city, state, postal, county].some((input) => !input.value); + + [address, city, state, postal, county].forEach((input) => { + input.classList.toggle('hidden', hasUnfilledFields); }); } async function loadGoogleMapsAPI(callback) { const script = document.createElement('script'); - const apiKey = await getSecret(`${getECCEnv()}-google-places-api`); + const apiKey = await getSecret(`${getEventServiceEnv()}-google-places-api`); script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=onGoogleMapsApiLoaded`; script.async = true; script.defer = true; @@ -29,6 +31,109 @@ async function loadGoogleMapsAPI(callback) { document.head.appendChild(script); } +function resetAllFields(component) { + const venueNameInput = component.querySelector('#venue-info-venue-name'); + const addressInput = component.querySelector('#venue-info-venue-address'); + const cityInput = component.querySelector('#location-city'); + const stateInput = component.querySelector('#location-state'); + const stateCodeInput = component.querySelector('#location-state-code'); + const postalCodeInput = component.querySelector('#location-zip-code'); + const countryInput = component.querySelector('#location-country'); + const placeLatInput = component.querySelector('#google-place-lat'); + const placeLngInput = component.querySelector('#google-place-lng'); + const placeIdInput = component.querySelector('#google-place-id'); + const mapUrlInput = component.querySelector('#google-map-url'); + const gmtoffsetInput = component.querySelector('#google-place-gmt-offset'); + + venueNameInput.value = ''; + changeInputValue(addressInput, 'value', ''); + changeInputValue(cityInput, 'value', ''); + changeInputValue(stateInput, 'value', ''); + changeInputValue(stateCodeInput, 'value', ''); + changeInputValue(postalCodeInput, 'value', ''); + changeInputValue(countryInput, 'value', ''); + changeInputValue(placeLatInput, 'value', ''); + changeInputValue(placeLngInput, 'value', ''); + changeInputValue(placeIdInput, 'value', ''); + changeInputValue(mapUrlInput, 'value', ''); + changeInputValue(gmtoffsetInput, 'value', ''); +} + +function updateAllFields(venueData, component) { + const venueNameInput = component.querySelector('#venue-info-venue-name'); + const addressInput = component.querySelector('#venue-info-venue-address'); + const cityInput = component.querySelector('#location-city'); + const stateInput = component.querySelector('#location-state'); + const stateCodeInput = component.querySelector('#location-state-code'); + const postalCodeInput = component.querySelector('#location-zip-code'); + const countryInput = component.querySelector('#location-country'); + const placeLatInput = component.querySelector('#google-place-lat'); + const placeLngInput = component.querySelector('#google-place-lng'); + const placeIdInput = component.querySelector('#google-place-id'); + const mapUrlInput = component.querySelector('#google-map-url'); + const gmtoffsetInput = component.querySelector('#google-place-gmt-offset'); + + changeInputValue(venueNameInput, 'value', venueData.venueName); + changeInputValue(addressInput, 'value', venueData.address); + changeInputValue(cityInput, 'value', venueData.city); + changeInputValue(stateInput, 'value', venueData.state); + changeInputValue(stateCodeInput, 'value', venueData.statecode); + changeInputValue(postalCodeInput, 'value', venueData.postalCode); + changeInputValue(countryInput, 'value', venueData.country); + changeInputValue(placeLatInput, 'value', venueData.coordinates?.lat); + changeInputValue(placeLngInput, 'value', venueData.coordinates?.lon); + changeInputValue(placeIdInput, 'value', venueData.placeId); + changeInputValue(mapUrlInput, 'value', venueData.mapUrl); + changeInputValue(gmtoffsetInput, 'value', venueData.gmtOffset); +} + +function getVenueDataInForm(component) { + const venueNameInput = component.querySelector('#venue-info-venue-name'); + const addressInput = component.querySelector('#venue-info-venue-address'); + const cityInput = component.querySelector('#location-city'); + const stateInput = component.querySelector('#location-state'); + const stateCodeInput = component.querySelector('#location-state-code'); + const postalCodeInput = component.querySelector('#location-zip-code'); + const countryInput = component.querySelector('#location-country'); + const placeLatInput = component.querySelector('#google-place-lat'); + const placeLngInput = component.querySelector('#google-place-lng'); + const placeIdInput = component.querySelector('#google-place-id'); + const mapUrlInput = component.querySelector('#google-map-url'); + const gmtoffsetInput = component.querySelector('#google-place-gmt-offset'); + + const venueName = venueNameInput.value; + const address = addressInput.value; + const city = cityInput.value; + const state = stateInput.value; + const stateCode = stateCodeInput.value; + const postalCode = postalCodeInput.value; + const country = countryInput.value; + const placeId = placeIdInput.value; + const mapUrl = mapUrlInput.value; + const lat = +placeLatInput.value; + const lon = +placeLngInput.value; + const gmtOffset = +gmtoffsetInput.value; + + const venueData = { + venueName, + address, + city, + state, + stateCode, + postalCode, + country, + placeId, + mapUrl, + coordinates: { + lat, + lon, + }, + gmtOffset, + }; + + return venueData; +} + function initAutocomplete(el, props) { const venueName = el.querySelector('#venue-info-venue-name'); // eslint-disable-next-line no-undef @@ -86,6 +191,13 @@ function initAutocomplete(el, props) { } }); + if (Object.values(addressInfo).some((v) => !v)) { + el.dispatchEvent(new CustomEvent('show-error-toast', { detail: { error: { message: 'The selection is not a valid venue.' } }, bubbles: true, composed: true })); + resetAllFields(el); + togglePrefillableFieldsHiddenState(el); + return; + } + if (place.name) changeInputValue(venueName, 'value', place.name); changeInputValue(address, 'value', addressInfo.address); changeInputValue(city, 'value', addressInfo.city); @@ -96,7 +208,7 @@ function initAutocomplete(el, props) { changeInputValue(placeId, 'value', place.place_id); changeInputValue(mapUrl, 'value', place.url); - togglePrefillableFieldsHiddenState(el, false); + togglePrefillableFieldsHiddenState(el); BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), city: addressInfo.city }); } @@ -128,66 +240,23 @@ export default async function init(component, props) { const { venue, showVenuePostEvent } = eventData; const venueNameInput = component.querySelector('#venue-info-venue-name'); - const addressInput = component.querySelector('#venue-info-venue-address'); - const cityInput = component.querySelector('#location-city'); - const stateInput = component.querySelector('#location-state'); - const stateCodeInput = component.querySelector('#location-state-code'); - const postalCodeInput = component.querySelector('#location-zip-code'); - const countryInput = component.querySelector('#location-country'); - const placeLatInput = component.querySelector('#google-place-lat'); - const placeLngInput = component.querySelector('#google-place-lng'); - const placeIdInput = component.querySelector('#google-place-id'); - const mapUrlInput = component.querySelector('#google-map-url'); - const gmtoffsetInput = component.querySelector('#google-place-gmt-offset'); - togglePrefillableFieldsHiddenState(component, true); + togglePrefillableFieldsHiddenState(component); venueNameInput.addEventListener('change', () => { if (!venueNameInput.value) { - changeInputValue(addressInput, 'value', ''); - changeInputValue(cityInput, 'value', ''); - changeInputValue(stateInput, 'value', ''); - changeInputValue(stateCodeInput, 'value', ''); - changeInputValue(postalCodeInput, 'value', ''); - changeInputValue(countryInput, 'value', ''); - changeInputValue(placeLatInput, 'value', ''); - changeInputValue(placeLngInput, 'value', ''); - changeInputValue(placeIdInput, 'value', ''); - changeInputValue(mapUrlInput, 'value', ''); - changeInputValue(gmtoffsetInput, 'value', ''); + resetAllFields(component); + togglePrefillableFieldsHiddenState(component, true); } }); if (venue) { - const { - venueName, - address, - city, - state, - statecode, - postalCode, - country, - placeId, - mapUrl, - } = venue; - - changeInputValue(venueNameInput, 'value', venueName); - changeInputValue(addressInput, 'value', address); - changeInputValue(cityInput, 'value', city); - changeInputValue(stateInput, 'value', state); - changeInputValue(stateCodeInput, 'value', statecode); - changeInputValue(postalCodeInput, 'value', postalCode); - changeInputValue(countryInput, 'value', country); - changeInputValue(placeLatInput, 'value', venue.coordinates?.lat); - changeInputValue(placeLngInput, 'value', venue.coordinates?.lon); - changeInputValue(placeIdInput, 'value', placeId); - changeInputValue(mapUrlInput, 'value', mapUrl); - changeInputValue(gmtoffsetInput, 'value', venue.gmtOffset); - BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), city }); - - if (venueName) { + updateAllFields(venue, component); + BlockMediator.set('eventDupMetrics', { ...BlockMediator.get('eventDupMetrics'), city: venue.city }); + + if (venue.venueName) { component.classList.add('prefilled'); - togglePrefillableFieldsHiddenState(component, false); + togglePrefillableFieldsHiddenState(component); } } @@ -196,53 +265,6 @@ export default async function init(component, props) { } } -const getVenueDataInForm = (component) => { - const venueNameInput = component.querySelector('#venue-info-venue-name'); - const addressInput = component.querySelector('#venue-info-venue-address'); - const cityInput = component.querySelector('#location-city'); - const stateInput = component.querySelector('#location-state'); - const stateCodeInput = component.querySelector('#location-state-code'); - const postalCodeInput = component.querySelector('#location-zip-code'); - const countryInput = component.querySelector('#location-country'); - const placeLatInput = component.querySelector('#google-place-lat'); - const placeLngInput = component.querySelector('#google-place-lng'); - const placeIdInput = component.querySelector('#google-place-id'); - const mapUrlInput = component.querySelector('#google-map-url'); - const gmtoffsetInput = component.querySelector('#google-place-gmt-offset'); - - const venueName = venueNameInput.value; - const address = addressInput.value; - const city = cityInput.value; - const state = stateInput.value; - const stateCode = stateCodeInput.value; - const postalCode = postalCodeInput.value; - const country = countryInput.value; - const placeId = placeIdInput.value; - const mapUrl = mapUrlInput.value; - const lat = +placeLatInput.value; - const lon = +placeLngInput.value; - const gmtOffset = +gmtoffsetInput.value; - - const venueData = { - venueName, - address, - city, - state, - stateCode, - postalCode, - country, - placeId, - mapUrl, - coordinates: { - lat, - lon, - }, - gmtOffset, - }; - - return venueData; -}; - export async function onEventUpdate(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index ee73e6bd..67a5b0b3 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -1,4 +1,3 @@ -import { ALLOWED_ACCOUNT_TYPES } from '../../constants/constants.js'; import { LIBS } from '../../scripts/scripts.js'; import { getIcon, @@ -6,7 +5,8 @@ import { generateToolTip, camelToSentenceCase, getEventPageHost, - getECCEnv, + signIn, + getEventServiceEnv, } from '../../scripts/utils.js'; import { createEvent, @@ -26,8 +26,8 @@ import ProductSelectorGroup from '../../components/product-selector-group/produc 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 BlockMediator from '../../scripts/deps/block-mediator.min.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`); @@ -104,8 +104,8 @@ export function buildErrorMessage(props, resp) { }); }); } else if (errorMessage) { - if (resp.status === 409) { - const toast = createTag('sp-toast', { open: true, variant: 'negative' }, errorMessage, { parent: toastArea }); + 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); @@ -113,7 +113,7 @@ export function buildErrorMessage(props, resp) { slot: 'action', variant: 'overBackground', href: `${url.toString()}`, - }, 'See the latest version.', { parent: toast }); + }, 'See the latest version', { parent: toast }); toast.addEventListener('close', () => { toast.remove(); @@ -860,40 +860,25 @@ export default async function init(el) { const sp = new URLSearchParams(window.location.search); const devToken = sp.get('devToken'); - if (devToken && getECCEnv() === 'dev') { + if (devToken && getEventServiceEnv() === 'dev') { buildECCForm(el).then(() => { el.classList.remove('loading'); }); return; } - const profile = BlockMediator.get('imsProfile'); - - if (profile) { - if (profile.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(profile.account_type)) { + initProfileLogicTree({ + noProfile: () => { + signIn(); + }, + noAccessProfile: () => { buildNoAccessScreen(el); el.classList.remove('loading'); - } else { + }, + validProfile: () => { buildECCForm(el).then(() => { el.classList.remove('loading'); }); - } - - return; - } - - if (!profile) { - const unsubscribe = BlockMediator.subscribe('imsProfile', ({ newValue }) => { - if (newValue?.noProfile || !ALLOWED_ACCOUNT_TYPES.includes(newValue.account_type)) { - buildNoAccessScreen(el); - el.classList.remove('loading'); - unsubscribe(); - } else { - buildECCForm(el).then(() => { - el.classList.remove('loading'); - unsubscribe(); - }); - } - }); - } + }, + }); } diff --git a/ecc/blocks/registration-details-component/registration-details-component.js b/ecc/blocks/registration-details-component/registration-details-component.js index f5e6c550..41135343 100644 --- a/ecc/blocks/registration-details-component/registration-details-component.js +++ b/ecc/blocks/registration-details-component/registration-details-component.js @@ -65,6 +65,7 @@ function decorateAllCheckboxes(el) { id: 'event-host-email-input', class: 'text-input', placeholder: inputText, + type: 'email', size: 's', }); diff --git a/ecc/components/agenda-fieldset-group/agenda-fieldset-group.js b/ecc/components/agenda-fieldset-group/agenda-fieldset-group.js index 598074e2..5ded6fe3 100644 --- a/ecc/components/agenda-fieldset-group/agenda-fieldset-group.js +++ b/ecc/components/agenda-fieldset-group/agenda-fieldset-group.js @@ -40,8 +40,8 @@ export default class AgendaFieldsetGroup extends LitElement { this.agendaItems = this.agendaItems.map((agenda, i) => (i === index ? updatedAgenda : agenda)); } - getAgendas() { - return this.agendaItems.filter((o) => !(Object.keys(o).length === 0 && o.constructor === Object)); + getCompleteAgenda() { + return this.agendaItems.filter((o) => (o.startTime && o.description)); } hasOnlyEmptyAgendaLeft() { diff --git a/ecc/components/image-dropzone/image-dropzone.js b/ecc/components/image-dropzone/image-dropzone.js index 197bc8ce..42a571f1 100644 --- a/ecc/components/image-dropzone/image-dropzone.js +++ b/ecc/components/image-dropzone/image-dropzone.js @@ -1,5 +1,6 @@ /* 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'; import { style } from './image-dropzone.css.js'; @@ -22,16 +23,21 @@ export class ImageDropzone extends LitElement { this.handleDelete = this.handleDelete || null; } - setFile(files) { + async setFile(files) { const [file] = files; - if (file.size > 26214400) { + + if (!isImageSizeValid(file, 26214400)) { this.dispatchEvent(new CustomEvent('show-error-toast', { detail: { error: { message: 'File size should be less than 25MB' } }, bubbles: true, composed: true })); return; } - if (file.type.startsWith('image/')) { + + const isValid = await isImageTypeValid(file); + if (isValid) { this.file = file; this.file.url = URL.createObjectURL(file); this.requestUpdate(); + } else { + this.dispatchEvent(new CustomEvent('show-error-toast', { detail: { error: { message: 'Invalid file type. The image file should be in one of the following format: .jpeg, .jpg, .png, .svg' } }, bubbles: true, composed: true })); } } @@ -39,22 +45,22 @@ export class ImageDropzone extends LitElement { return this.file; } - handleImageDrop(e) { + async handleImageDrop(e) { e.preventDefault(); e.stopPropagation(); const { files } = e.dataTransfer; if (files.length > 0) { - this.setFile(files); + await this.setFile(files); this.handleImage(); } } - onImageChange(e) { + async onImageChange(e) { const { files } = e.currentTarget; if (files.length > 0) { - this.setFile(files); + await this.setFile(files); this.handleImage(); } diff --git a/ecc/components/partner-selector/partner-selector.js b/ecc/components/partner-selector/partner-selector.js index fe1227bf..d0829f6a 100644 --- a/ecc/components/partner-selector/partner-selector.js +++ b/ecc/components/partner-selector/partner-selector.js @@ -149,13 +149,13 @@ export default class PartnerSelector extends LitElement {
{ - this.updatePartner({ name: event.detail.value }); + this.updatePartner({ name: event.detail.value?.trim() }); }} @entry-selected=${this.handleAutocomplete} searchdata=${JSON.stringify(this.seriesPartners)} identifier='sponsorId'>
{ - this.updatePartner({ link: event.target.value }); + this.updatePartner({ link: event.target.value?.trim() }); // FIXME: I really shouldn't need to do this, but the pattern attribute doesn't reset properly. }} ?valid=${this.partner.link?.match(LINK_REGEX)}>
diff --git a/ecc/components/profile/profile.js b/ecc/components/profile/profile.js index df7db933..82498b3a 100644 --- a/ecc/components/profile/profile.js +++ b/ecc/components/profile/profile.js @@ -223,7 +223,7 @@ export class Profile extends LitElement { profile.socialMedia, (socialMedia, index) => html`
- this.updateSocialMedia(index, event.detail.value, shallow)}> + this.updateSocialMedia(index, event.detail.value?.trim(), shallow)}> ${profile.socialMedia?.length > 1 ? html`remove-repeater { profile.socialMedia.splice(index, 1); this.requestUpdate(); diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 4f5d31e7..5574f54b 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -1,4 +1,22 @@ -import { getECCEnv } from './utils.js'; +import { LIBS } from './scripts.js'; +import { getEventServiceEnv, getSecret } from './utils.js'; + +const API_CONFIG = { + esl: { + dev: { host: 'https://wcms-events-service-layer-deploy-ethos102-stage-va-9c3ecd.stage.cloud.adobe.io' }, + dev02: { host: 'https://wcms-events-service-layer-deploy-ethos102-stage-va-d5dc93.stage.cloud.adobe.io' }, + stage: { host: 'https://events-service-layer-stage.adobe.io' }, + stage02: { host: 'https://events-service-layer-stage02.adobe.io' }, + prod: { host: 'https://events-service-layer.adobe.io' }, + }, + esp: { + dev: { host: 'https://wcms-events-service-platform-deploy-ethos102-stage-caff5f.stage.cloud.adobe.io' }, + dev02: { host: 'https://wcms-events-service-platform-deploy-ethos102-stage-c81eb6.stage.cloud.adobe.io' }, + stage: { host: 'https://events-service-platform-stage.adobe.io' }, + stage02: { host: 'https://events-service-platform-stage02.adobe.io' }, + prod: { host: 'https://events-service-platform.adobe.io' }, + }, +}; export const getCaasTags = (() => { let cache; @@ -32,23 +50,6 @@ export const getCaasTags = (() => { }; })(); -function getAPIConfig() { - return { - esl: { - local: { host: 'http://localhost:8499' }, - dev: { host: 'https://wcms-events-service-layer-deploy-ethos102-stage-va-9c3ecd.stage.cloud.adobe.io' }, - stage: { host: 'https://events-service-layer-stage.adobe.io' }, - prod: { host: 'https://events-service-layer.adobe.io' }, - }, - esp: { - local: { host: 'http://localhost:8500' }, - dev: { host: 'https://wcms-events-service-platform-deploy-ethos102-stage-caff5f.stage.cloud.adobe.io' }, - stage: { host: 'https://events-service-platform-stage.adobe.io' }, - prod: { host: 'https://events-service-platform.adobe.io' }, - }, - }; -} - function waitForAdobeIMS() { return new Promise((resolve) => { const checkIMS = () => { @@ -63,18 +64,27 @@ function waitForAdobeIMS() { } export async function constructRequestOptions(method, body = null) { - await waitForAdobeIMS(); + const [ + { default: getUuid }, + clientIdentity, + ] = await Promise.all([ + import(`${LIBS}/utils/getUuid.js`), + getSecret(`${getEventServiceEnv()}-client-identity`), + waitForAdobeIMS(), + ]); const headers = new Headers(); const sp = new URLSearchParams(window.location.search); const devToken = sp.get('devToken'); - const authToken = devToken && getECCEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; + const authToken = devToken && getEventServiceEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; if (!authToken) window.lana?.log('Error: Failed to get Adobe IMS auth token'); headers.append('Authorization', `Bearer ${authToken}`); headers.append('x-api-key', 'acom_event_service'); headers.append('content-type', 'application/json'); + headers.append('x-request-id', await getUuid(new Date().getTime())); + headers.append('x-client-identity', clientIdentity); const options = { method, @@ -87,12 +97,20 @@ export async function constructRequestOptions(method, body = null) { } export async function uploadImage(file, configs, tracker, imageId = null) { - await waitForAdobeIMS(); - - const { host } = getAPIConfig().esp[getECCEnv()]; + const [ + { default: getUuid }, + clientIdentity, + ] = await Promise.all([ + import(`${LIBS}/utils/getUuid.js`), + getSecret(`${getEventServiceEnv()}-client-identity`), + waitForAdobeIMS(), + ]); + + 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 authToken = devToken && getECCEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; + const authToken = devToken && getEventServiceEnv() === 'dev' ? devToken : window.adobeIMS?.getAccessToken()?.token; let respJson = null; @@ -106,6 +124,8 @@ export async function uploadImage(file, configs, tracker, imageId = null) { xhr.setRequestHeader('x-image-kind', configs.type); xhr.setRequestHeader('x-api-key', 'acom_event_service'); xhr.setRequestHeader('Authorization', `Bearer ${authToken}`); + xhr.setRequestHeader('x-request-id', requestId); + xhr.setRequestHeader('x-client-identity', clientIdentity); if (tracker) { xhr.upload.onprogress = (event) => { @@ -181,7 +201,7 @@ function convertToSpeaker(speaker) { export async function deleteImage(configs, imageId) { await waitForAdobeIMS(); - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { @@ -202,7 +222,7 @@ export async function deleteImage(configs, imageId) { } export async function createVenue(eventId, venueData) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify(venueData); const options = await constructRequestOptions('POST', raw); @@ -215,7 +235,7 @@ export async function createVenue(eventId, venueData) { return { ok: response.ok, status: response.status, error: data }; } - return 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 }; @@ -223,7 +243,7 @@ export async function createVenue(eventId, venueData) { } export async function replaceVenue(eventId, venueId, venueData) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify(venueData); const options = await constructRequestOptions('PUT', raw); @@ -236,7 +256,7 @@ export async function replaceVenue(eventId, venueId, venueData) { return { ok: response.ok, status: response.status, error: data }; } - return 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 }; @@ -244,7 +264,7 @@ export async function replaceVenue(eventId, venueId, venueData) { } export async function createEvent(payload) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify({ ...payload, liveUpdate: false }); const options = await constructRequestOptions('POST', raw); @@ -257,7 +277,7 @@ export async function createEvent(payload) { return { ok: response.ok, status: response.status, error: data }; } - return 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 }; @@ -267,7 +287,7 @@ export async function createEvent(payload) { export async function createSpeaker(profile, seriesId) { const nSpeaker = convertToNSpeaker(profile); - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify({ ...nSpeaker, seriesId }); const options = await constructRequestOptions('POST', raw); @@ -288,7 +308,7 @@ export async function createSpeaker(profile, seriesId) { } export async function createSponsor(sponsorData, seriesId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(sponsorData); const options = await constructRequestOptions('POST', raw); @@ -309,7 +329,7 @@ export async function createSponsor(sponsorData, seriesId) { } export async function updateSponsor(sponsorData, sponsorId, seriesId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(sponsorData); const options = await constructRequestOptions('PUT', raw); @@ -330,7 +350,7 @@ export async function updateSponsor(sponsorData, sponsorId, seriesId) { } export async function addSponsorToEvent(sponsorData, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(sponsorData); const options = await constructRequestOptions('POST', raw); @@ -351,7 +371,7 @@ export async function addSponsorToEvent(sponsorData, eventId) { } export async function updateSponsorInEvent(sponsorData, sponsorId, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(sponsorData); const options = await constructRequestOptions('PUT', raw); @@ -372,7 +392,7 @@ export async function updateSponsorInEvent(sponsorData, sponsorId, eventId) { } export async function removeSponsorFromEvent(sponsorId, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { @@ -392,7 +412,7 @@ export async function removeSponsorFromEvent(sponsorId, eventId) { } export async function getSponsor(seriesId, sponsorId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -412,7 +432,7 @@ export async function getSponsor(seriesId, sponsorId) { } export async function getSponsors(seriesId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -432,7 +452,7 @@ export async function getSponsors(seriesId) { } export async function getSponsorImages(seriesId, sponsorId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -452,7 +472,7 @@ export async function getSponsorImages(seriesId, sponsorId) { } export async function addSpeakerToEvent(speakerData, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(speakerData); const options = await constructRequestOptions('POST', raw); @@ -473,7 +493,7 @@ export async function addSpeakerToEvent(speakerData, eventId) { } export async function updateSpeakerInEvent(speakerData, speakerId, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(speakerData); const options = await constructRequestOptions('PUT', raw); @@ -494,7 +514,7 @@ export async function updateSpeakerInEvent(speakerData, speakerId, eventId) { } export async function removeSpeakerFromEvent(speakerId, eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { @@ -515,7 +535,7 @@ export async function removeSpeakerFromEvent(speakerId, eventId) { export async function updateSpeaker(profile, seriesId) { const nSpeaker = convertToNSpeaker(profile); - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify({ ...nSpeaker, seriesId }); const options = await constructRequestOptions('PUT', raw); @@ -536,7 +556,7 @@ export async function updateSpeaker(profile, seriesId) { } export async function updateEvent(eventId, payload) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify({ ...payload, liveUpdate: false }); const options = await constructRequestOptions('PUT', raw); @@ -557,7 +577,7 @@ export async function updateEvent(eventId, payload) { } export async function publishEvent(eventId, payload) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify({ ...payload, published: true, liveUpdate: true }); const options = await constructRequestOptions('PUT', raw); @@ -578,7 +598,7 @@ export async function publishEvent(eventId, payload) { } export async function unpublishEvent(eventId, payload) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const raw = JSON.stringify({ ...payload, published: false, liveUpdate: true }); const options = await constructRequestOptions('PUT', raw); @@ -599,7 +619,7 @@ export async function unpublishEvent(eventId, payload) { } export async function deleteEvent(eventId) { - const { host } = getAPIConfig().esl[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { @@ -620,7 +640,7 @@ export async function deleteEvent(eventId) { } export async function getEvents() { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -640,7 +660,7 @@ export async function getEvents() { } export async function getEvent(eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -660,7 +680,7 @@ export async function getEvent(eventId) { } export async function getVenue(eventId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -680,7 +700,7 @@ export async function getVenue(eventId) { } export async function getSpeaker(seriesId, speakerId) { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -712,7 +732,7 @@ export async function getClouds() { } export async function getSeries() { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -734,7 +754,7 @@ export async function getSeries() { export async function createAttendee(eventId, attendeeData) { if (!eventId || !attendeeData) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(attendeeData); const options = await constructRequestOptions('POST', raw); @@ -757,7 +777,7 @@ export async function createAttendee(eventId, attendeeData) { export async function updateAttendee(eventId, attendeeId, attendeeData) { if (!eventId || !attendeeData) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const raw = JSON.stringify(attendeeData); const options = await constructRequestOptions('PUT', raw); @@ -777,10 +797,10 @@ export async function updateAttendee(eventId, attendeeId, attendeeData) { } } -export async function deleteAttendee(eventId, attendeeId) { +export async function removeAttendeeFromEvent(eventId, attendeeId) { if (!eventId || !attendeeId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esl[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { @@ -802,7 +822,7 @@ export async function deleteAttendee(eventId, attendeeId) { export async function getEventAttendees(eventId) { if (!eventId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -823,7 +843,7 @@ export async function getEventAttendees(eventId) { export async function getAllEventAttendees(eventId) { const recurGetAttendees = async (fullAttendeeArr = [], nextPageToken = null) => { - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); const fetchUrl = nextPageToken ? `${host}/v1/events/${eventId}/attendees?nextPageToken=${nextPageToken}` : `${host}/v1/events/${eventId}/attendees`; @@ -855,7 +875,7 @@ export async function getAllEventAttendees(eventId) { export async function getAttendee(eventId, attendeeId) { if (!eventId || !attendeeId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -877,7 +897,7 @@ export async function getAttendee(eventId, attendeeId) { export async function getSpeakers(seriesId) { if (!seriesId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -899,7 +919,7 @@ export async function getSpeakers(seriesId) { export async function getEventImages(eventId) { if (!eventId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('GET'); try { @@ -921,7 +941,7 @@ export async function getEventImages(eventId) { export async function deleteSpeakerImage(speakerId, seriesId, imageId) { if (!speakerId || !seriesId || !imageId) return false; - const { host } = getAPIConfig().esp[getECCEnv()]; + const { host } = API_CONFIG.esp[getEventServiceEnv()]; const options = await constructRequestOptions('DELETE'); try { diff --git a/ecc/scripts/event-apis.js b/ecc/scripts/event-apis.js index 0587a705..5bc41b3a 100644 --- a/ecc/scripts/event-apis.js +++ b/ecc/scripts/event-apis.js @@ -1,4 +1,5 @@ import BlockMediator from './deps/block-mediator.min.js'; +import { ALLOWED_ACCOUNT_TYPES } from '../constants/constants.js'; export async function getProfile() { const { feds, adobeProfile, fedsConfig, adobeIMS } = window; @@ -61,3 +62,37 @@ export function lazyCaptureProfile() { } }, 1000); } + +export function initProfileLogicTree(callbacks) { + const { noProfile, noAccessProfile, validProfile } = callbacks; + + const profile = BlockMediator.get('imsProfile'); + + if (profile) { + if (profile.noProfile) { + noProfile(); + } else if (!ALLOWED_ACCOUNT_TYPES.includes(profile.account_type)) { + noAccessProfile(); + } else { + validProfile(profile); + } + + return; + } + + if (!profile) { + const unsubscribe = BlockMediator.subscribe('imsProfile', ({ newValue }) => { + if (newValue) { + if (newValue.noProfile) { + noProfile(); + } else if (!ALLOWED_ACCOUNT_TYPES.includes(newValue.account_type)) { + noAccessProfile(); + } else { + validProfile(newValue); + } + } + + unsubscribe(); + }); + } +} diff --git a/ecc/scripts/image-validator.js b/ecc/scripts/image-validator.js new file mode 100644 index 00000000..54163770 --- /dev/null +++ b/ecc/scripts/image-validator.js @@ -0,0 +1,39 @@ +export async function isImageTypeValid(file) { + const validTypes = ['jpeg', 'jpg', 'png', 'svg']; + let currentFileType = ''; + + const blob = file.slice(0, 128); + + const arrayBuffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + + const signatures = { + jpeg: [0xFF, 0xD8, 0xFF], + png: [0x89, 0x50, 0x4E, 0x47], + }; + + if (signatures.jpeg.every((byte, i) => byte === bytes[i])) { + const extension = file.name.split('.').pop().toLowerCase(); + if (extension === 'jpg' || extension === 'jpeg') { + currentFileType = extension; + } + + currentFileType = 'jpg'; + } + + if (signatures.png.every((byte, i) => byte === bytes[i])) { + currentFileType = 'png'; + } + + const text = await blob.text(); + + if (text.trim().startsWith('